Compare commits

...
Author SHA1 Message Date
anmolsinghbhatia f0397ef2b5 feat: back redirection 2023-05-04 12:13:09 +05:30
pablohashescobarandGitHub a2825208b8 docs: update self hosting section in readme (#1002)
docs: update self hosting section in readme
2023-05-03 23:53:53 +05:30
pablohashescobarandGitHub c3387ba974 fix: environment variables for the services (#1001) 2023-05-03 21:31:06 +05:30
pablohashescobarandGitHub baa9c30449 fix: single docker file (#999) 2023-05-03 16:48:04 +05:30
pablohashescobarandGitHub 849e2d658a dev:: update docker file for frontend (#998) 2023-05-03 16:39:13 +05:30
c7f1090914 fix: docker setup (#987)
* removing dependencies from .env

* dev: Passing the arguments from docker compose to DockerWeb in nextjs to define base environment variables

* dev: removed env from docker-compose and taking the env from shell

* dev: Updated docker file and used console in signin to test the env from docker

* dev: Docker setting env variables via shell

* removed env variables and args

* Update Dockerfile.web

* Update Dockerfile.web

* Update signin.tsx

* .

* .

* dev: Added BASE_URL from docker

* dev: Updated docker config

* dev: scripts for replacing variable during runtime

* dev: entrypoint script

* dev: update replace env script and update docker entrypoint command for frontend

* dev: update replace env script to not update process.env

* dev: update docker file to add missing variables as well

* fix: updated docker compose yml and web

* dev: create start script to run docker and update script for replacing variables

* dev: update setup script and env example script to create variables in the root of the project

* .

* dev: update docker compose hub

* dev: update docker compose hub command

* dev: update docker compose yml and env example

* dev: update docker compose hub

* dev: single docker

---------

Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-05-03 13:36:55 +05:30
vamsi e3d43298df Merge branch 'stage-release' of https://github.com/makeplane/plane into stage-release 2023-05-03 00:57:04 +05:30
pablohashescobarandGitHub b36565298f Merge pull request #992 from makeplane/develop
promote: develop to stage-release
2023-05-03 00:55:14 +05:30
guru_sainathandGitHub b26e8bd956 update: UI for google auth button and loadin toggle in signin screen (#991) 2023-05-03 00:48:24 +05:30
pablohashescobarandGitHub e5703dbe70 Merge pull request #990 from makeplane/develop
promote: develop to stage-release
2023-05-02 23:37:25 +05:30
Anmol Singh BhatiaandGitHub 498d6d2b02 style: onboarding logo and style (#989) 2023-05-02 23:31:36 +05:30
pablohashescobarandGitHub 1d22817ede fix: import user invite emails (#986) 2023-05-02 20:09:54 +05:30
Anmol Singh BhatiaandGitHub 483c49d0ff fix: ui and bug fixes (#980)
* style: sub issue theming

* style: shortcut modal theming

* style: blocking and blocked by modal theming

* fix: filter labels dropdown width fix

* style: display properties

* chore: workspace invite

* style: invite co workers theming

* style: create workspace theming

* style: attachment theming

* style: workspace sidebar theming

* style: issue property theming

* style: create module modal lead icon

* style: label list modal theming

* delete attachment and member modal theming

* style: transfer issue modal

* style: delete estimate modal theming

* style: module form status

* style: delete state modal theming

* style: shortcut modal

* style: onboarding logo

* style: onboarding command menu
2023-05-02 20:00:33 +05:30
guru_sainathandGitHub 0fa9451633 fix: workspace integration github repos issue resolved (#988) 2023-05-02 18:15:24 +05:30
pablohashescobarandGitHub 46237c5431 Merge pull request #866 from makeplane/develop
promote: develop to stage-release
2023-05-02 01:57:04 +05:30
vamsi 20e400487f dev: migrations SlackProjectSync; new attributes to cycle, importer and module 2023-05-02 01:55:15 +05:30
pablohashescobarandGitHub 99fb3c9bfe chore: log cycle delete and cycle issue delete activities (#975)
* chore: log cycle delete and cycle issue delete activities

* dev: remove print logs
2023-05-02 01:21:48 +05:30
pablohashescobarandGitHub 88200a93bf feat: delete imported data as well when the import is deleted (#974) 2023-05-02 00:53:08 +05:30
pablohashescobarandGitHub b2ad071608 fix: bulk modules importer (#973) 2023-05-02 00:52:52 +05:30
pablohashescobarandGitHub 21992f540f chore: importer user invite email (#972) 2023-05-02 00:52:36 +05:30
pablohashescobarandGitHub 742731cbe6 chore: log module delete and module issue delete activities (#976) 2023-05-02 00:51:04 +05:30
pablohashescobarandGitHub c6878b9b0f fix: page access update (#977) 2023-05-02 00:50:41 +05:30
pablohashescobarandGitHub 887cac5612 feat: filter issues by estimate points (#978) 2023-05-02 00:50:10 +05:30
guru_sainathandGitHub 982566b5b4 Merge pull request #982 from makeplane/fix/slack-integration
fix: slack with project integration bug fixes
2023-05-02 00:47:36 +05:30
gurusainath 2dc5655886 fix: slack with project integration bug fixes 2023-05-02 00:40:47 +05:30
guru_sainathandGitHub 160b4a4390 Merge pull request #970 from makeplane/refactor/estimates_modal
refactor: estimates payload generate function
2023-04-29 14:17:29 +05:30
guru_sainathandGitHub f187d9512a Merge pull request #969 from makeplane/style/workspace_sidebar
style: workspace sidebar
2023-04-29 14:15:41 +05:30
Aaryan Khandelwal 53d3ea1979 refactor: estimates payload generate function 2023-04-29 03:27:03 +05:30
anmolsinghbhatia 13d76d4325 style: workspace sidebar 2023-04-28 20:51:47 +05:30
guru_sainathandGitHub 25338cc804 Merge pull request #968 from makeplane/style/onboarding_screens
style: onboarding screens, invitations page
2023-04-28 20:50:40 +05:30
Aaryan Khandelwal eaa750b025 fix: initial onboarding step 2023-04-28 20:47:29 +05:30
Aaryan Khandelwal 81d9c70026 style: onboarding screens, invitations page 2023-04-28 20:46:07 +05:30
guru_sainathandGitHub 93fb4fe1e9 Merge pull request #959 from makeplane/feat/product_update_modal
feat: product update modal
2023-04-28 19:38:33 +05:30
guru_sainathandGitHub b8e6d072cc Merge pull request #967 from makeplane/style/ui_fixes
style: command k item and cycle icon theming
2023-04-28 19:03:16 +05:30
anmolsinghbhatia 1220cebe50 style: command k item and cycle icon theming 2023-04-28 18:52:53 +05:30
anmolsinghbhatia 9464b5c00e style: product update modal 2023-04-28 18:29:01 +05:30
anmolsinghbhatia 3fae0f39c0 chore: product update interface 2023-04-28 18:28:43 +05:30
Kunal VishwakarmaandGitHub 73a757e337 fix: added the decoded url for slack integration popup (#958) 2023-04-28 17:55:57 +05:30
Kunal VishwakarmaandGitHub bb40b7feb5 chore: removed email span from invite member page (#956) 2023-04-28 17:49:38 +05:30
Aaryan KhandelwalandGitHub 3175ce9136 style: create project modal (#957) 2023-04-28 17:49:29 +05:30
Anmol Singh BhatiaandGitHub 0b9b4bb289 fix: cycle & module mutation , feat: cycle & module toast alerts (#962)
* fix: cycle remove issue mutation

* fix: module remove issue mutation

* feat: issue removed toast alert added
2023-04-28 17:49:16 +05:30
Anmol Singh BhatiaandGitHub f0f24b6fc4 style: module , cycle & icon styling (#963)
* style: target icon updated

* style: cycle card theming

* style: module card theming

* style: no current cycle message
2023-04-28 17:49:04 +05:30
Anmol Singh BhatiaandGitHub 429dffb055 fix: cycle & module sidebar default tab fix (#964) 2023-04-28 17:48:40 +05:30
guru_sainathandGitHub 6e5c85cd6e Merge pull request #961 from makeplane/style/issue_list_responsive
style: responsive design for issue list in 'list' view
2023-04-28 10:49:54 +05:30
Dakshesh Jain 2adcb163fb Merge branch 'develop' of https://github.com/makeplane/plane into style/issue_list_responsive 2023-04-25 19:44:19 +05:30
Dakshesh Jain 028a350cd1 style: issue list dropdown on next line 2023-04-25 19:43:52 +05:30
Anmol Singh BhatiaandGitHub d021a5696a fix: sub issues list fix (#960) 2023-04-25 19:00:20 +05:30
Dakshesh Jain a5c18e37c1 Merge branch 'develop' of https://github.com/makeplane/plane into style/issue_list_responsive 2023-04-25 18:36:24 +05:30
Dakshesh Jain 8e611664a8 style: made issue list view responsive 2023-04-25 18:35:58 +05:30
anmolsinghbhatia 3480b450f2 fix: build fix 2023-04-25 18:10:23 +05:30
anmolsinghbhatia a7d9591c44 feat: product updates tag and date added 2023-04-25 18:02:13 +05:30
anmolsinghbhatia 1364c842e0 feat: product updates button added 2023-04-25 17:33:53 +05:30
anmolsinghbhatia eb99b4adc9 feat: markdown to custom component 2023-04-25 16:54:12 +05:30
anmolsinghbhatia 6684dd4ab6 feat: product updates service added 2023-04-25 16:51:50 +05:30
Kunal VishwakarmaandGitHub 529ed4432c chore: fixed sidebar highlight not working (#952) 2023-04-25 12:10:05 +05:30
Kunal VishwakarmaandGitHub 8c1ad69f0c chore: changed pages empty state image (#954) 2023-04-25 12:09:29 +05:30
Kunal VishwakarmaandGitHub c23de32b03 chore: added create label inside of pages label select (#944)
* chore: added create label inside of pages label select

* chore: used footer instead of props
2023-04-25 12:08:56 +05:30
Rhea JainandGitHub 83ac1f4e4c chore: invite text update (#955) 2023-04-25 12:07:02 +05:30
Anmol Singh BhatiaandGitHub cee9695a4a feat: sub issues progress (#895) 2023-04-25 11:38:13 +05:30
Aaryan KhandelwalandGitHub c9f866e538 style: profile settings, activity theming (#951) 2023-04-24 18:53:30 +05:30
Aaryan KhandelwalandGitHub 7d96adcb70 chore: empty state for estimates (#950) 2023-04-24 18:53:18 +05:30
Aaryan KhandelwalandGitHub c5b034385f chore: added custom toggle switch everywhere (#949) 2023-04-24 18:53:07 +05:30
Aaryan KhandelwalandGitHub ff7f31c35b style: calendar view (#948)
* style: calendar view

* chore: add issue button positioning
2023-04-24 18:10:44 +05:30
Aaryan KhandelwalandGitHub 7234d6f68b style: workspace and project settings (#946) 2023-04-24 13:21:09 +05:30
Aaryan KhandelwalandGitHub d8a5b8d848 style: imports theming (#945) 2023-04-24 13:20:41 +05:30
Aaryan KhandelwalandGitHub 5412e09701 chore: empty input fields text (#943) 2023-04-24 11:28:05 +05:30
Aaryan KhandelwalandGitHub 7116acc331 style: auth pages theming (#942) 2023-04-24 11:20:14 +05:30
Aaryan KhandelwalandGitHub ae26b17cab style: filters list theming (#941) 2023-04-24 11:20:02 +05:30
Aaryan KhandelwalandGitHub 2ec8fbab34 style: modals theming (#940)
* style: existing issues list modal

* style: parent issues list modal

* style: issue modal

* style: cycle modal

* style: module modal

* style: view modal

* style: page modal

* style: delete modals
2023-04-24 11:19:53 +05:30
Aaryan KhandelwalandGitHub 213dc3f8e8 style: signin page theming (#938) 2023-04-24 11:19:43 +05:30
Aaryan KhandelwalandGitHub 4b02886c40 chore: remove outline button component (#937) 2023-04-23 01:43:58 +05:30
Aaryan KhandelwalandGitHub 8eddc4b304 fix: points not updating while editing estimate (#935) 2023-04-23 00:00:46 +05:30
Aaryan KhandelwalandGitHub 169a60723b style: project settings theming (#936)
* style: project and workspace members theming

* style: project features theming

* style: project settings states theming

* style: project settings labels theming

* style: project settings integrations theming
2023-04-22 23:46:19 +05:30
Kunal VishwakarmaandGitHub c80094581e feat: frontend slack integration (#932)
* feat: slack integration frontend

* feat: slack integration frontend complete

* Co-authored-by: Aaryan Khandelwal <aaryan610@users.noreply.github.com>
2023-04-22 21:54:50 +05:30
pablohashescobarandGitHub d99f669b89 dev: add s3 url for staging (#934) 2023-04-22 18:21:22 +05:30
Aaryan KhandelwalandGitHub 99dd1b9f0c chore: state delete validation (#930) 2023-04-22 18:19:35 +05:30
pablohashescobarandGitHub 48e77ea81b remove: state delete issue validation endpoint (#929) 2023-04-22 18:18:08 +05:30
pablohashescobarandGitHub 0be6738715 dev: back migration for integrations (#933) 2023-04-22 18:17:53 +05:30
Anmol Singh BhatiaandGitHub d041d8be6b fix: cycle date validation (#922) 2023-04-22 18:17:17 +05:30
pablohashescobarandGitHub e53847c59e feat: module view props (#882) 2023-04-22 18:16:16 +05:30
pablohashescobarandGitHub 16781a71fe feat: cycle view props (#881) 2023-04-22 18:16:05 +05:30
pablohashescobarandGitHub fb4535b294 feat: slack integration (#874)
* feat: init slack integration

* dev: create model and update existing view for slack

* dev: update slack sync model and create view to install slack

* dev: workspace integration query

* dev: update the metadata validation for access_token and team_id and save config to database

* dev: update validation for team_id

* dev: update validation

* dev: update validations

* dev: remove bot access token field from sync

* dev: handle integrity exception
2023-04-22 18:15:52 +05:30
pablohashescobarandGitHub 33a904bc3e chore: added estimate bulk endpoint for retrieving and updating (#919)
* chore: added estimate bulk endpoint for retrieving and updating

* chore: estimate endpoints

* fix: retrieve project estimate

* dev: handle integrity error check
2023-04-22 01:04:20 +05:30
pablohashescobarandGitHub 0d264838a9 remove: print exception instead use capture to log it (#926) 2023-04-22 01:04:07 +05:30
Aaryan KhandelwalandGitHub c638b6aba6 chore: new estimates workflow (#927)
* chore: new services for estimates

* chore: new estimates workflow
2023-04-22 00:59:57 +05:30
Aaryan KhandelwalandGitHub cb814dd68b chore: added new restricted workspace slugs (#928) 2023-04-22 00:57:16 +05:30
Aaryan KhandelwalandGitHub e17c824119 style: dashboard styling (#924) 2023-04-22 00:56:17 +05:30
Anmol Singh BhatiaandGitHub 68930c256f style: sidebar theming (#925)
* style: sidebar workspace dropdown theming

* style: progress chart and progress bar theming

* style: module and cycle sidebar theming
2023-04-22 00:15:45 +05:30
Anmol Singh BhatiaandGitHub e59137f6f2 style: cycle and module theming (#923)
* style: cycle theming

* style: module theming
2023-04-21 17:40:57 +05:30
Aaryan KhandelwalandGitHub 7f235739bd style: projects page theming (#921) 2023-04-21 17:40:49 +05:30
Vamsi KuramaandGitHub 20162050c3 Merge pull request #833 from kylewlacy/self-hosting-tweaks
Add a few more options for self-hosting
2023-04-21 16:47:36 +05:30
Aaryan KhandelwalandGitHub 06ad0d0f7a style: views theming (#920) 2023-04-21 16:30:16 +05:30
Aaryan KhandelwalandGitHub cfd7e1d154 fix: theming fixes (#914) 2023-04-21 15:48:06 +05:30
Anmol Singh BhatiaandGitHub fdf7ea1f82 feat: copy page link added (#915) 2023-04-21 15:46:28 +05:30
Anmol Singh BhatiaandGitHub 8f12d3d01b style: calendar theming (#918) 2023-04-21 15:45:57 +05:30
Aaryan KhandelwalandGitHub 62dc6c2f3f fix: comment box placeholder (#909) 2023-04-21 12:31:34 +05:30
Aaryan KhandelwalandGitHub 6f03022f65 chore: changed light mode colors (#913) 2023-04-21 10:46:04 +05:30
Aaryan KhandelwalandGitHub f2701a12ea style: pages theming (#912) 2023-04-21 02:15:21 +05:30
Anmol Singh BhatiaandGitHub 9129a6cde2 feat: calendar filters (#908)
* feat: hiding unnecessary filters for calendar view

* feat: filters for calendar view

* feat: module and cycle calendar view filters
2023-04-21 01:42:09 +05:30
Rhea JainandGitHub 2ba4594b29 chore: attachements button text update (#910) 2023-04-21 01:41:47 +05:30
Aaryan KhandelwalandGitHub 165d16e32b style: issue details page (#911) 2023-04-21 00:34:22 +05:30
Aaryan KhandelwalandGitHub 73388195ef chore: page block input (#907)
* fix: sidebar workspace dropdown logo

* chore: changed textarea to input for page block
2023-04-20 19:04:10 +05:30
pablohashescobarandGitHub 4dda4ec610 chore: single endpoint to create estimate and estimate points (#897) 2023-04-20 18:14:38 +05:30
pablohashescobarandGitHub e68a5382f9 feat: sub issue state distribution (#885)
* feat: sub issue state distribution

* dev: update the response structure to match consistency

* dev: update the query for sub issue distribution to include 0 issue groups as well
2023-04-20 18:14:24 +05:30
pablohashescobarandGitHub 5b6caadd6f chore: state delete validations endpoint (#880) 2023-04-20 18:14:05 +05:30
pablohashescobarandGitHub 73a8bbb31f feat: release information endpoint (#876)
* dev: init release notes

* feat: API endpoint to fetch get last 5 release information
2023-04-20 18:13:49 +05:30
Aaryan KhandelwalandGitHub 952d35dd79 style: list and kanban view theming (#906)
* fix: sidebar workspace dropdown logo

* style: list and kanban view theming
2023-04-20 18:13:21 +05:30
Anmol Singh BhatiaandGitHub 170b3d6eec fix: cycle and module sidebar fix (#905) 2023-04-20 18:09:01 +05:30
Anmol Singh BhatiaandGitHub d04a422054 feat: calendar add new issue (#901) 2023-04-20 14:11:11 +05:30
Aaryan KhandelwalandGitHub affc7655f7 fix: sidebar workspace dropdown logo (#903) 2023-04-20 13:56:09 +05:30
50c78628b3 feat: themes (#902)
* chore: add next theme and initial setup

* chore: add dark mode colors to layouts

* chore: user general setting page theming

* chore: dashboard theming

* chore: project page theming

* chore: workspace setting page theming

* chore: my issue page theming

* chore: cmdk theming

* chore: change hardcode bg and text color to theme

* chore: change color name according to the design

* style: fix card in the dashboard

* style: fix merge conflict design issues

* style: add light high contrast and dark high contrast

* style: fix cmd k menu color and selection

* feat: change theme from cmdk menu

* chore: add multiple theme field to custom theme

* chore: removed custom theming

* fix: build error

---------

Co-authored-by: Saheb Giri <iamsahebgiri@gmail.com>
2023-04-20 13:42:16 +05:30
3c2f5d12ed feat: themes (#902)
* chore: add next theme and initial setup

* chore: add dark mode colors to layouts

* chore: user general setting page theming

* chore: dashboard theming

* chore: project page theming

* chore: workspace setting page theming

* chore: my issue page theming

* chore: cmdk theming

* chore: change hardcode bg and text color to theme

* chore: change color name according to the design

* style: fix card in the dashboard

* style: fix merge conflict design issues

* style: add light high contrast and dark high contrast

* style: fix cmd k menu color and selection

* feat: change theme from cmdk menu

* chore: add multiple theme field to custom theme

* chore: removed custom theming

* fix: build error

---------

Co-authored-by: Saheb Giri <iamsahebgiri@gmail.com>
2023-04-20 13:41:24 +05:30
Rhea JainandGitHub 9f04933957 chore: upload button and github banner text update (#899)
* chore: upload button and github banner text update

* chore: attachments button text fix
2023-04-20 12:16:13 +05:30
Anmol Singh BhatiaandGitHub 8d37a3e58b feat: cycle list tab context (#900) 2023-04-20 12:11:33 +05:30
Aaryan KhandelwalandGitHub 4f61c5d552 fix: pages access mutation (#896) 2023-04-20 12:09:55 +05:30
Aaryan KhandelwalandGitHub 7149d20601 chore: change pages icon (#894) 2023-04-20 12:09:48 +05:30
Aaryan KhandelwalandGitHub d30a88832a fix: reset estimates modal form after create/update (#893) 2023-04-20 12:09:35 +05:30
Rhea JainandGitHub ebce364104 chore: changed loading text (#898) 2023-04-19 20:52:52 +05:30
Aaryan KhandelwalandGitHub c5206a7792 feat: separate filters for cycles and modules (#892) 2023-04-19 17:25:31 +05:30
Anmol Singh BhatiaandGitHub 390b837561 fix: invite button validation , style: workspace screen (#877)
* fix: invite button validation

* style: workspace screen tab height
2023-04-19 15:41:46 +05:30
Anmol Singh BhatiaandGitHub fb1932e309 fix : create issue modal (#875)
* fix: label list bug fix

* fix: assignee and label count removed

* fix: assignee and label fix
2023-04-19 15:41:17 +05:30
Dakshesh JainandGitHub ac125965eb fix: add tlds (#851) 2023-04-19 15:40:47 +05:30
Dakshesh JainandGitHub 63a36fb25d feat: jira importer (#879)
* feat: jira importer

* fix: yarn lock

* fix: displaying correct count of users that are been imported

* fix: showing workspace member in import users
2023-04-19 15:40:31 +05:30
pablohashescobarandGitHub 2b280935a1 chore: update state create endpoint to send error response on integrity error (#869)
* chore: update state create endpoint to send error response on integrity error

* dev: update status code for general exception
2023-04-18 12:25:22 +05:30
Kunal VishwakarmaandGitHub be5ef61428 fix: join project button (#873) 2023-04-18 10:55:32 +05:30
Aaryan KhandelwalandGitHub 1627a587ee refactor: workspace integrations code (#872) 2023-04-18 10:54:45 +05:30
Aaryan KhandelwalandGitHub 682a1477fb chore: create/update estimate points validation (#871) 2023-04-18 10:54:19 +05:30
Kyle Lacy ea87823478 Add $NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS to add domains for Next Image plugin 2023-04-17 16:30:36 -07:00
Kyle Lacy b8c06b3121 Add $AWS_S3_ENDPOINT_URL to .env.example 2023-04-17 14:08:47 -07:00
Kyle Lacy ca2366aa9b Use env var to set AWS_S3_ENDPOINT_URL setting 2023-04-17 14:07:00 -07:00
Aaryan KhandelwalandGitHub acff6396f9 chore: create/update state duplicate name error (#870) 2023-04-18 01:15:39 +05:30
Aaryan KhandelwalandGitHub fa5c994ddc chore: remove redundant console logs (#868) 2023-04-18 01:15:26 +05:30
Aaryan KhandelwalandGitHub 5f20e65ca6 style: list view styling reduced (#867) 2023-04-18 01:15:10 +05:30
Kyle Lacy 792162ae66 Remove redundant $REDIS_BROKER_SSL setting
A fix was already pushed upstream that made this setting unnecessary
2023-04-17 12:37:58 -07:00
Kyle Lacy e22f552ea0 Merge branch 'develop' into self-hosting-tweaks 2023-04-17 12:30:24 -07:00
pablohashescobarandGitHub 396fbc4ebb fix: redis url for docker in production settings (#865) 2023-04-17 22:38:52 +05:30
Vamsi KuramaandGitHub d2a58bf04a Merge pull request #862 from makeplane/stage-release
promote: staging to production v0.5
2023-04-17 19:08:10 +05:30
Vamsi KuramaandGitHub c51407c85e Merge branch 'master' into stage-release 2023-04-17 19:07:33 +05:30
Vamsi KuramaandGitHub 3ed937378f Merge pull request #861 from makeplane/develop
promote: patches from develop to stage-release
2023-04-17 18:48:21 +05:30
Dakshesh JainandGitHub 0fa3a8c3e3 fix: removed useEffect for project detail fetch error (#860) 2023-04-17 18:38:07 +05:30
Aaryan KhandelwalandGitHub bd0cfef02f fix: delete import (#859) 2023-04-17 18:34:18 +05:30
Dakshesh JainandGitHub 7aa9e0bba1 fix: project authorization api fetch keys (#858) 2023-04-17 18:22:04 +05:30
Vamsi KuramaandGitHub 718f62a898 Merge pull request #857 from makeplane/develop
promote: develop to stage-release
2023-04-17 17:47:19 +05:30
Kunal VishwakarmaandGitHub d26d01ace4 chore: filters spacing (#856) 2023-04-17 17:42:02 +05:30
Vamsi KuramaandGitHub 60e44fc1a2 Merge pull request #847 from makeplane/chore/my_issues_endpoint
chore: my issues endpoint to return attachment and link count
2023-04-17 17:39:38 +05:30
Anmol Singh BhatiaandGitHub 45eaa23ed0 style: dashboard content (#855) 2023-04-17 17:36:02 +05:30
Aaryan KhandelwalandGitHub 600fedd5ba fix: undefined block content (#854) 2023-04-17 17:21:15 +05:30
Aaryan KhandelwalandGitHub 6de54089cd fix: show proper issues list if no filters are present (#853) 2023-04-17 17:10:36 +05:30
Aaryan KhandelwalandGitHub dc53708109 chore: show workspace members (#852) 2023-04-17 16:41:16 +05:30
Aaryan KhandelwalandGitHub f5351e4419 fix: remove filters function (#846) 2023-04-17 15:03:56 +05:30
Aaryan KhandelwalandGitHub 819508d5fc fix: remirror placeholder (#849) 2023-04-17 15:03:44 +05:30
Kunal VishwakarmaandGitHub 0beb654069 chore: worked on making issue list padding consistent (#850) 2023-04-17 15:01:22 +05:30
Anmol Singh BhatiaandGitHub 98cef0e1e8 fix: my issue page display property (#848) 2023-04-17 14:45:51 +05:30
Aaryan KhandelwalandGitHub 8a6036a20a fix: drop to delete box zindex (#843) 2023-04-17 14:13:32 +05:30
pablohashescobar 85b6c78e75 chore: my issues endpoint to return attachment and link count 2023-04-17 14:03:19 +05:30
Anmol Singh BhatiaandGitHub 3f401b0fc5 feat: page tab context (#845)
* feat: page list tab context added

* fix: build fix
2023-04-17 13:30:39 +05:30
Anmol Singh BhatiaandGitHub 365c758a25 fix: create update view modal fix (#842) 2023-04-17 12:24:30 +05:30
Aaryan KhandelwalandGitHub ac98381f23 chore: remove estimate option from my issues (#839) 2023-04-17 12:00:30 +05:30
Kunal VishwakarmaandGitHub 3e436179fe chore: added default state for issues (#840) 2023-04-17 11:54:22 +05:30
Aaryan KhandelwalandGitHub e23075b7b9 chore: no estimates option, estimates activity (#838) 2023-04-17 11:30:48 +05:30
pablohashescobarandGitHub 61761fedc5 chore: remove view filter validation while creating or updating view (#836) 2023-04-17 11:15:38 +05:30
Kunal VishwakarmaandGitHub 5a36a7931f fix: removed extra spaces form estimate points brackets (#837) 2023-04-17 11:15:06 +05:30
Aaryan KhandelwalandGitHub 363c5c8ec4 fix: join project mutation (#835)
* fix: join project mutation

* chore: remove imports
2023-04-17 10:27:20 +05:30
Kyle Lacy 5e5d1a4699 Update .env.example with newly-added env vars 2023-04-15 12:56:43 -07:00
Kyle Lacy e2294f9105 Add EMAIL_FROM setting to change sender email for messages 2023-04-15 12:51:23 -07:00
Kyle Lacy f757d8232b Add setting to disable extra TLS config for Celery broker connection 2023-04-15 12:32:54 -07:00
Kyle Lacy 6af54ebbe7 Fix typo in `.env.example 2023-04-15 12:32:00 -07:00
Kyle Lacy 3913cf571f Make SMTP port and TLS configurable 2023-04-15 12:00:02 -07:00
Aaryan KhandelwalandGitHub 8638170a98 fix: cycles and modules sidebar mutation (#831) 2023-04-14 19:40:00 +05:30
Vamsi KuramaandGitHub ebff5d8c54 Merge pull request #830 from makeplane/develop
promote: dev to stage-release
2023-04-14 17:06:19 +05:30
vamsi b7ce69c220 dev: migrations estimate points and themes 2023-04-14 17:04:02 +05:30
Vamsi KuramaandGitHub e4da207df5 Merge pull request #768 from makeplane/feat/workspace_themes
feat: workspace themes
2023-04-14 16:56:32 +05:30
Vamsi KuramaandGitHub 3a0c5bab76 Merge pull request #804 from makeplane/fix/magic_sign_in
fix: connection error when signing in with code
2023-04-14 16:56:20 +05:30
Vamsi KuramaandGitHub 1cd1505c7d Merge pull request #805 from makeplane/chore/celery_production_settings
chore: production settings for celery
2023-04-14 16:56:00 +05:30
Vamsi KuramaandGitHub bc7fab96c3 Merge pull request #812 from makeplane/fix/parent_issue_search
fix: parent issue search
2023-04-14 16:55:20 +05:30
Vamsi KuramaandGitHub a358260a22 Merge pull request #817 from makeplane/chore/issue_estimate_points
chore: set default value as null for estimate point
2023-04-14 16:54:43 +05:30
Dakshesh JainandGitHub 3b103da6a3 chore: added unsplash flag for self-hosted (#829)
* chore: added unsplash flag for self hosted

* fix: removed actual code and only using flag

* refactor: removed extra variable
2023-04-14 16:52:31 +05:30
Aaryan KhandelwalandGitHub 23b4145565 chore: added session recorder key to env (#827) 2023-04-14 16:51:24 +05:30
Anmol Singh BhatiaandGitHub 5848c326c7 fix: cycle and module sidebar fix (#828) 2023-04-14 16:44:06 +05:30
Aaryan KhandelwalandGitHub ce253b3cc9 refactor: drag function (#826) 2023-04-14 16:41:28 +05:30
Vamsi KuramaandGitHub 3817511024 Merge pull request #824 from makeplane/feat/session_recorder
feat: session recorder
2023-04-14 16:09:31 +05:30
Kunal VishwakarmaandGitHub f50872f2a9 fix: empty issue design (#821)
* fix: empty issue design

* chore: removed unused imports
2023-04-14 16:04:16 +05:30
Anmol Singh BhatiaandGitHub a0b8f7188f fix: cycle card (#825) 2023-04-14 16:03:11 +05:30
Aaryan Khandelwal 2950877767 feat: session recorder integrated 2023-04-14 15:27:14 +05:30
Aaryan KhandelwalandGitHub c7d930f89b chore: add env flag to enable session recorder conditionally (#822) 2023-04-14 15:17:35 +05:30
pablohashescobarandGitHub 81da8715d5 chore: update bug report template to include browser environment as well (#820)
* chore: update bug report template to include browser environment as well

* chore: update type to dropdown
2023-04-14 00:45:03 +05:30
pablohashescobar b4c8323886 chore: set default value as null for estimate point 2023-04-13 19:54:34 +05:30
Anmol Singh BhatiaandGitHub c3ffd233a6 style: workspace url (#816) 2023-04-13 19:33:22 +05:30
Aaryan KhandelwalandGitHub 3fa6185b63 fix: drag and drop function (#815)
* fix: kanban drag and drop

* fix: kanban board issue dnd mutation
2023-04-13 19:09:55 +05:30
Dakshesh JainandGitHub 6de94efc7d style: removed static 'app.plane.so' (#813) 2023-04-13 18:28:23 +05:30
pablohashescobar 0cd6d9d570 fix: parent issue search 2023-04-13 17:49:09 +05:30
Aaryan KhandelwalandGitHub 657241c9c1 feat: clarity script added (#807) 2023-04-13 16:17:21 +05:30
Anmol Singh BhatiaandGitHub dc9ce5101c fix: workspace url error message (#809) 2023-04-13 15:46:25 +05:30
Kunal VishwakarmaandGitHub 3457411c6a style: issue list (#798)
* style: issue list

* chore: changed the empty state images
2023-04-13 15:39:05 +05:30
Aaryan KhandelwalandGitHub b7a7508d5d fix: workspace dashboard duplicate keys (#803) 2023-04-13 15:38:43 +05:30
Dakshesh JainandGitHub 484a88d881 fix: unusual redirection on onboarding (#808) 2023-04-13 15:38:00 +05:30
pablohashescobarandGitHub cd69b06e5e fix: error message for jira importers (#794) 2023-04-13 00:34:53 +05:30
pablohashescobarandGitHub c4609b95cd fix: remove length check condition when updating issue property (#791) 2023-04-13 00:34:37 +05:30
pablohashescobarandGitHub 6eb7ec0697 fix: typo in url for bulk creating labels (#788) 2023-04-13 00:34:23 +05:30
pablohashescobarandGitHub e232d39f0e feat: track estimate points in issue activity (#762)
* feat: track estimate points in issue activity

* dev: update comment
2023-04-13 00:34:12 +05:30
pablohashescobarandGitHub 8a26fd0a97 fix: issue link activity (#761) 2023-04-13 00:34:02 +05:30
pablohashescobarandGitHub 537a82028e fix: attachment activity (#760) 2023-04-13 00:33:50 +05:30
pablohashescobarandGitHub 440ed08728 fix: issue attachment delete (#759) 2023-04-13 00:33:37 +05:30
pablohashescobar c199e76038 chore: production settings for celery 2023-04-13 00:31:06 +05:30
pablohashescobar b40fd4bbc2 fix: connection error when signing in with code 2023-04-12 19:55:31 +05:30
Anmol Singh BhatiaandGitHub 5190ea7280 fix: send code btn fix (#802) 2023-04-12 19:03:29 +05:30
Dakshesh JainandGitHub db488338fb fix: weird redirection in index page (#801) 2023-04-12 19:03:08 +05:30
Anmol Singh BhatiaandGitHub 9196fb4562 style: onboarding screen cards (#800) 2023-04-12 19:01:46 +05:30
Saheb GiriandGitHub 34ff8fecc5 feat: add create page option in cmdk menu (#799) 2023-04-12 19:01:19 +05:30
Anmol Singh BhatiaandGitHub d6dbfdc731 feat: page improvement (#797)
* feat: remove label icon added

* feat: block menu dropdown state added

* feat: page info icon added and  style: overflow title and label fix
2023-04-12 18:07:50 +05:30
Anmol Singh BhatiaandGitHub f2e8add29d feat: attachment and link display properties (#796)
* feat: attachment and link count added

* fix: build fix
2023-04-12 15:33:30 +05:30
Saheb GiriandGitHub 032ef831b2 feat: now user can edit view (#793)
* feat: now user can edit view

* fix: build error
2023-04-12 15:33:21 +05:30
Kunal VishwakarmaandGitHub 0f9812cf2c chore: added estimate delete modal (#792) 2023-04-12 15:03:04 +05:30
Aaryan KhandelwalandGitHub f734aad10d fix: minor pages bugs (#786)
* fix: dashboard workspace activity mutation

* fix/minor_pages_bugs
2023-04-11 23:19:47 +05:30
pablohashescobarandGitHub 6279a04267 feat: attachment and link count in issues list (#777) 2023-04-11 18:47:36 +05:30
pablohashescobarandGitHub ce26bed44a fix: jira importer info endpoint to get query params (#776) 2023-04-11 18:47:18 +05:30
Anmol Singh BhatiaandGitHub 88d2adddc7 style: create issue modal icon (#784) 2023-04-11 18:46:39 +05:30
Kunal VishwakarmaandGitHub e4e66b3ae4 style: pages UI (#769)
* style: pages ui

* chore: added toast alert and tooltip

* fix: fixed issues in pages block

* fix: ai buttons inside pages block
2023-04-11 18:18:49 +05:30
Aaryan KhandelwalandGitHub f1f716e8f6 fix: estimates bugs (#785)
* fix: dashboard workspace activity mutation

* fix: minor bugs
2023-04-11 18:17:47 +05:30
Anmol Singh BhatiaandGitHub 61b9e7a161 feat: issue activity logs (#782)
* feat: attachment, link and estimate activity log added in issue detail page

* feat: attachment, link and estimate activity log added in profile activity section
2023-04-11 18:14:36 +05:30
dfa3a7b78d feat: estimates (#783)
* chore: use estimate points hook created

* chore: user auth layer

* fix: build error

* chore: estimate crud and validation

* fix: build errors

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-04-11 17:54:01 +05:30
Anmol Singh BhatiaandGitHub d5c2965946 fix: workspace joining state fix (#780) 2023-04-11 17:13:06 +05:30
Kunal VishwakarmaandGitHub cfa283116b chore: added remove workspace image in workspace settings (#781) 2023-04-11 17:12:34 +05:30
Aaryan KhandelwalandGitHub 725c9375ea fix: onboarding loop (#775)
* fix: dashboard workspace activity mutation

* fix: onboarding loop
2023-04-11 12:25:21 +05:30
Anmol Singh BhatiaandGitHub 22c1f6f8e2 feat: workspace name validation added (#767) 2023-04-11 12:11:41 +05:30
Anmol Singh BhatiaandGitHub 748e5e7fb7 fix: project setting identifier (#766)
* fix: project setting identifier validation added

* fix: project setting identifier label fix
2023-04-11 12:11:15 +05:30
Aaryan KhandelwalandGitHub 7aa0ace555 fix: pages access (#754)
* fix: dashboard workspace activity mutation

* fix: page access operation
2023-04-11 12:10:22 +05:30
Anmol Singh BhatiaandGitHub f2c5bb5c03 fix: workspace join button fix (#774) 2023-04-11 10:12:33 +05:30
pablohashescobarandGitHub 800075b781 fix: worker script (#773) 2023-04-11 09:25:33 +05:30
vamsi 6865cf4b54 chore: ssl config for REDIS connections to celery 2023-04-11 00:04:03 +05:30
pablohashescobar bc457846fe chore: move theme setting in user level from workspace level 2023-04-10 23:19:01 +05:30
Saheb GiriandGitHub d411cd7576 style: redesign view (#770)
* style: add new design to the view item

* feat: add no of filters
2023-04-10 22:46:09 +05:30
pablohashescobarandGitHub 2dbe1dd401 fix: remove migrate command from worker script (#772) 2023-04-10 22:44:11 +05:30
pablohashescobarandGitHub ec3f891b4a chore: update doc redirection url in user welcome email (#771) 2023-04-10 21:29:54 +05:30
pablohashescobar b6c911f484 feat: workspace themes 2023-04-10 18:14:09 +05:30
Anmol Singh BhatiaandGitHub fc48fb97d1 style: attachment upload button (#765) 2023-04-10 18:07:40 +05:30
Vamsi KuramaandGitHub 3618f79f89 Merge pull request #764 from makeplane/fix/setup
dev: fix setup.sh causing tr Illegal byte sequence
2023-04-10 17:45:20 +05:30
vamsi ea563d20a3 dev: fix setup.sh causing tr Illegal byte sequence 2023-04-10 17:43:19 +05:30
Vihar KuramaandGitHub 805b8f47cc Update Readme.md - Removed status section 2023-04-10 15:15:34 +05:30
Vihar KuramaandGitHub 5f216fbbbc Merge pull request #753 from makeplane/docker-hub-hotfix
Create docker-compose-hub.yml
2023-04-10 14:26:51 +05:30
Kunal VishwakarmaandGitHub 33ea1cb9d3 chore: added ai generation inside block in pages (#732) 2023-04-10 12:33:12 +05:30
Vihar KuramaandGitHub 0caadd0f7a Merge pull request #758 from nothingneko/patch-1
Fix: Dead Discord Link on Readme
2023-04-10 11:11:45 +05:30
pablohashescobarandGitHub 62e736677a fix: cycle date check endpoint (#748) 2023-04-10 10:58:32 +05:30
pablohashescobarandGitHub bd9de0c213 feat: activity for issue attachments and links (#746) 2023-04-10 10:58:22 +05:30
pablohashescobarandGitHub c80968bb23 feat: bulk update estimate endpoint (#755) 2023-04-10 10:58:09 +05:30
Jaiden RiordanandGitHub 441203867b Fix dead Discord Link 2023-04-09 18:32:08 -05:00
vamsi eac4b21ead dev: migrations for estimate Estimate, IssueAttachment 2023-04-09 03:13:46 +05:30
Narayana VadapalliandGitHub 6fe35c9fe6 Create docker-compose-hub.yml 2023-04-09 00:21:40 +05:30
Aaryan KhandelwalandGitHub 08e77cb19e fix: home page redirection logic (#752)
* fix: dashboard workspace activity mutation

* fix: home page redirection logic

* chore: add homePageRedirect function back
2023-04-08 14:32:33 -04:00
Anmol Singh BhatiaandGitHub 03e74415f2 feat: upload button validation (#747) 2023-04-08 18:20:33 +05:30
Aaryan KhandelwalandGitHub c81bc4e5d2 style: loading screens (#750)
* fix: dashboard workspace activity mutation

* style: loading screens
2023-04-08 18:20:00 +05:30
Aaryan KhandelwalandGitHub 1026ae3eb1 chore: user auth layer (#749)
* chore: use estimate points hook created

* chore: user auth layer

* fix: build error
2023-04-08 18:05:54 +05:30
Aaryan KhandelwalandGitHub 3fe32606a9 chore: disable jira importer (#745)
* fix: dashboard workspace activity mutation

* chore: remove jira importer from settings
2023-04-08 15:46:19 +05:30
pablohashescobarandGitHub d88a95b1e9 fix: celery worker for issue activities (#744)
* dev: update celery configuration to root folder

* dev: update import for celery

* fix: worker to deserialize data
2023-04-08 15:46:05 +05:30
Aaryan KhandelwalandGitHub c21fb6e942 fix: issue attachments mutation (#743)
* fix: dashboard workspace activity mutation

* fix: attachment mutation for create and delete
2023-04-08 15:33:20 +05:30
Aaryan KhandelwalandGitHub 98e6d3de22 fix: dashboard workspace activity mutation (#742) 2023-04-08 15:10:19 +05:30
Kunal VishwakarmaandGitHub 9b1ae6bcd4 fix: inconsistency in styles (#734) 2023-04-08 13:55:30 +05:30
3947a86fa7 fix: new auth layer (#740)
* chore: made workspace authorization wrapper component

* chore: added todos

* chore: workspace pages new layout

* chore: project authorization wrapper

* chore: new project authorization wrapper

* fix: authorization for member roles

* chore: new auth screens ui

---------

Co-authored-by: Dakshesh Jain <dakshesh.jain14@gmail.com>
2023-04-08 13:46:46 +05:30
Aaryan KhandelwalandGitHub beedd57ee1 fix: fetch selected project members (#741)
* fix: fetch selected project members

* chore: remove old imports
2023-04-08 13:46:21 +05:30
Kunal VishwakarmaandGitHub 0a3d13706e fix: mutation issue in cycles (#739)
* fix: mutation issue in cycles

* fix: removed comments
2023-04-08 01:24:24 +05:30
Anmol Singh BhatiaandGitHub c093209338 fix: shortcut combination key bug fix (#733)
* fix: shortcut combination key bug fix

* refactor: code refactor
2023-04-07 17:01:52 +05:30
Kunal VishwakarmaandGitHub a68d94c33f fix: added image popover for settings cover (#737)
* fix: added popover for profile cover

* fix:removed comments
2023-04-07 15:33:11 +05:30
Aaryan KhandelwalandGitHub 35f9876981 refactor: import/export code (#735) 2023-04-07 13:27:57 +05:30
pablohashescobarandGitHub c0b732f1f1 chore: rename project name to project key (#731) 2023-04-06 22:58:13 +05:30
pablohashescobarandGitHub 6be775434d fix: project issue search endpoint (#729) 2023-04-06 22:58:03 +05:30
pablohashescobarandGitHub 687b05d221 dev: upgrade python version (#728) 2023-04-06 22:57:36 +05:30
pablohashescobarandGitHub 59a33587a0 feat: delete endpoint for importers (#725)
* feat: delete endpoint for importers

* fix: delete endpoint for importers
2023-04-06 22:57:20 +05:30
pablohashescobarandGitHub e46487c130 chore: add project details on importer service endpoint (#714)
* chore: add project details on importer service endpoint

* dev: add select related for imports
2023-04-06 22:57:06 +05:30
pablohashescobarandGitHub 5b72b1672f chore: add workspace and project details on label endpoints (#713) 2023-04-06 22:56:55 +05:30
pablohashescobarandGitHub ec818a5523 refactor: move all background task from rqworker to celery (#668)
* refactor: move all background task from rqworker to celery

* dev: update background job to take input in parameters rather than a single dict

* dev: update procfile for new worker

* dev: docker updates for new celery worker
2023-04-06 22:56:36 +05:30
pablohashescobarandGitHub 100c431ac3 fix: default assignee for issues (#712)
* fix: default assignee for issues

* fix: check for empty array as well
2023-04-06 22:56:24 +05:30
Anmol Singh BhatiaandGitHub ea06ee4529 fix: issue attachment improvement (#730)
* fix: invalid file error state bug fix

* style: icons updated
2023-04-06 19:16:50 +05:30
Aaryan KhandelwalandGitHub 35455c2bf7 chore: delete import (#727)
* chore: delete import

* chore: changed button text
2023-04-06 16:06:31 +05:30
Dakshesh JainandGitHub c49f614352 feat: update project cover image (#726) 2023-04-06 15:11:01 +05:30
Kunal VishwakarmaandGitHub 95fe4a3831 feat: added estimates (#721)
* feat: added estimates

* chore: added estimate to issue sidebar
2023-04-06 15:09:24 +05:30
Anmol Singh BhatiaandGitHub 14dd498d08 feat: issue attachments feature (#717)
* chore: issue attachment services added

* feat: attachment icons added

* chore: fetch-key and icons export

* feat: issue attachment upload section added

* feat: issue attachment list section added

* feat: date helper function added

* style: responsiveness added

* feat: attachment info added

* style: attachment overflow fix

* style: cursor pointer added

* chore: issue attachment interface

* style: uploading state added

* feat: delete issue attachment modal

* style: style improvement and refactor

* style: consistent icon added , chore: refactor the code

* fix: js icon import fix

* fix: build fix

* chore: refactor code
2023-04-06 15:07:11 +05:30
pablohashescobarandGitHub 86ec46db2c dev: add imports for back migration and add migration for views (#707) 2023-04-06 13:59:43 +05:30
pablohashescobarandGitHub 6a579f85ad feat: bulk create endpoint for estimate points (#708)
* feat: bulk create endpoint for estimate points

* dev: remove integrity logic and update url
2023-04-06 13:59:17 +05:30
pablohashescobarandGitHub 105428894f fix: add check if the users need to be imported (#716) 2023-04-06 13:58:56 +05:30
pablohashescobarandGitHub f2144c3e89 fix: issue search endpoint for parent issues (#705)
* fix: issue search endpoint for parent issues

* fix: parent search

* fix: search endpoint
2023-04-06 13:58:27 +05:30
Dakshesh JainandGitHub cf662f6e6c chore: new analytic events (#699)
* feat: tracking events for issues marked as DONE, issue property update, issue moved to cycle, issue moved to modules

* fix: changed events names

* chore: sync analytic

* chore: new analytic events
2023-04-06 12:08:52 +05:30
Aaryan KhandelwalandGitHub 65037b5031 style: disabled state for buttons (#724) 2023-04-06 01:03:42 +05:30
Aaryan KhandelwalandGitHub c9d8a8dbd1 feat: github importer (#722)
* chore: github importer first step completed

* refactor: github importer code refactored

* chore: github importer functionality completed

* fix: import data step saved data
2023-04-06 00:51:15 +05:30
pablohashescobarandGitHub 6b8b981e1d docs: update readme to include default email and password for self hosting setup (#706) 2023-04-05 15:03:40 +05:30
pablohashescobarandGitHub 1562939287 chore: add workspace details on comment serializer (#697) 2023-04-05 00:20:21 +05:30
pablohashescobarandGitHub cc07e2790d feat: issue estimations (#696)
* dev: initialize estimation

* dev: issue estimation field in issues and project settings

* dev: update issue estimation logic
2023-04-05 00:19:53 +05:30
pablohashescobarandGitHub 97386e9d07 feat: issue attachments (#677) 2023-04-05 00:17:55 +05:30
pablohashescobarandGitHub ff5cddeb95 feat: issue search endpoint (#667)
* feat: issue search endpoints

* dev: update issue search for blocker and blocked by
2023-04-05 00:17:16 +05:30
Vamsi KuramaandGitHub 3d6f2dd3dc Merge pull request #703 from makeplane/stage-release
promote: stage-release to production
2023-04-04 19:38:37 +05:30
461 changed files with 15430 additions and 6449 deletions
+20
View File
@@ -0,0 +1,20 @@
# Replace with your instance Public IP
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
NEXT_PUBLIC_GOOGLE_CLIENTID=""
NEXT_PUBLIC_GITHUB_APP_NAME=""
NEXT_PUBLIC_GITHUB_ID=""
NEXT_PUBLIC_SENTRY_DSN=""
NEXT_PUBLIC_ENABLE_OAUTH=0
NEXT_PUBLIC_ENABLE_SENTRY=0
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
NEXT_PUBLIC_TRACK_EVENTS=0
NEXT_PUBLIC_SLACK_CLIENT_ID=""
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
AWS_REGION=""
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_S3_BUCKET_NAME=""
OPENAI_API_KEY=""
GPT_ENGINE=""
+9
View File
@@ -44,6 +44,15 @@ body:
- Deploy preview
validations:
required: true
type: dropdown
id: browser
attributes:
label: Browser
options:
- Google Chrome
- Mozilla Firefox
- Safari
- Other
- type: dropdown
id: version
attributes:
+34 -11
View File
@@ -1,4 +1,4 @@
name: Build Api Server Docker Image
name: Build and Push Backend Docker Image
on:
push:
@@ -10,11 +10,8 @@ on:
jobs:
build_push_backend:
name: Build Api Server Docker Image
name: Build and Push Api Server Docker Image
runs-on: ubuntu-20.04
permissions:
contents: read
packages: write
steps:
- name: Check out the repo
@@ -28,20 +25,33 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Github Container Registry
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.1.0
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
registry: "registry.hub.docker.com"
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
id: ghmeta
uses: docker/metadata-action@v4.3.0
with:
images: makeplane/plane-backend
- name: Extract metadata (tags, labels) for Docker (Github)
id: dkrmeta
uses: docker/metadata-action@v4.3.0
with:
images: ghcr.io/${{ github.repository }}-backend
- name: Build Api Server
- name: Build and Push to GitHub Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: ./apiserver
@@ -50,5 +60,18 @@ jobs:
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.ghmeta.outputs.tags }}
labels: ${{ steps.ghmeta.outputs.labels }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: ./apiserver
file: ./apiserver/Dockerfile.api
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.dkrmeta.outputs.tags }}
labels: ${{ steps.dkrmeta.outputs.labels }}
+31 -8
View File
@@ -1,4 +1,4 @@
name: Build Frontend Docker Image
name: Build and Push Frontend Docker Image
on:
push:
@@ -12,9 +12,6 @@ jobs:
build_push_frontend:
name: Build Frontend Docker Image
runs-on: ubuntu-20.04
permissions:
contents: read
packages: write
steps:
- name: Check out the repo
@@ -35,13 +32,26 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
registry: "registry.hub.docker.com"
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
id: ghmeta
uses: docker/metadata-action@v4.3.0
with:
images: makeplane/plane-frontend
- name: Extract metadata (tags, labels) for Docker (Github)
id: meta
uses: docker/metadata-action@v4.3.0
with:
images: ghcr.io/${{ github.repository }}-frontend
- name: Build Frontend Server
- name: Build and Push to GitHub Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
@@ -50,5 +60,18 @@ jobs:
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.ghmeta.outputs.tags }}
labels: ${{ steps.ghmeta.outputs.labels }}
- name: Build and Push to Docker Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./apps/app/Dockerfile.web
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.dkrmeta.outputs.tags }}
labels: ${{ steps.dkrmeta.outputs.labels }}
+19 -1
View File
@@ -3,6 +3,7 @@ RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
COPY . .
@@ -16,7 +17,7 @@ FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
@@ -26,9 +27,16 @@ RUN yarn install
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
FROM python:3.11.1-alpine3.17 AS backend
@@ -108,6 +116,16 @@ COPY nginx/nginx-single-docker-image.conf /etc/nginx/http.d/default.conf
COPY nginx/supervisor.conf /code/supervisor.conf
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
USER root
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN chmod +x /usr/local/bin/start.sh
CMD ["supervisord","-c","/code/supervisor.conf"]
+10 -10
View File
@@ -29,7 +29,7 @@
Meet Plane. An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘‍♀️.
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/29tPNhaV) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
@@ -58,13 +58,21 @@ cd plane
> If running in a cloud env replace localhost with public facing IP address of the VM
- Export Environment Variables
```bash
set -a
source .env
set +a
```
- Run Docker compose up
```bash
docker-compose up
docker-compose -f docker-compose-hub.yml up
```
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
## 🚀 Features
@@ -123,14 +131,6 @@ For full documentation, visit [docs.plane.so](https://docs.plane.so/)
To see how to Contribute, visit [here](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md).
## 🔋 Status
- [x] Early Community Previews: We are open-sourcing and sharing the development version of Plane
- [ ] Alpha: We are testing Plane with a closed set of customers
- [ ] Public Alpha: Anyone can sign up over at [app.plane.so](https://app.plane.so). But go easy on us, there are a few hiccups
- [ ] Public Beta: Stable enough for most non-enterprise use-cases
- [ ] Public: Production-ready
## ❤️ Community
The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects.
-24
View File
@@ -1,24 +0,0 @@
DJANGO_SETTINGS_MODULE="plane.settings.production"
# Database
DATABASE_URL=postgres://plane:xyzzyspoon@db:5432/plane
# Cache
REDIS_URL=redis://redis:6379/
# SMPT
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
# AWS
AWS_REGION=""
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_S3_BUCKET_NAME=""
# FE
WEB_URL="localhost/"
# OAUTH
GITHUB_CLIENT_SECRET=""
# Flags
DISABLE_COLLECTSTATIC=1
DOCKERIZED=1
# GPT Envs
OPENAI_API_KEY=0
GPT_ENGINE=0
+1 -1
View File
@@ -1,2 +1,2 @@
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 -
worker: python manage.py rqworker
worker: celery -A plane worker -l info
+37 -3
View File
@@ -3,8 +3,15 @@ import uuid
import random
from django.contrib.auth.hashers import make_password
from plane.db.models import ProjectIdentifier
from plane.db.models import Issue, IssueComment, User, Project, ProjectMember
from plane.db.models import (
Issue,
IssueComment,
User,
Project,
ProjectMember,
Label,
Integration,
)
# Update description and description html values for old descriptions
@@ -148,7 +155,7 @@ def update_user_view_property():
"collapsed": True,
"issueView": "list",
"filterIssue": None,
"groupByProperty": True,
"groupByProperty": None,
"showEmptyGroups": True,
}
updated_project_members.append(project_member)
@@ -161,6 +168,7 @@ def update_user_view_property():
print(e)
print("Failed")
def update_label_color():
try:
labels = Label.objects.filter(color="")
@@ -174,3 +182,29 @@ def update_label_color():
except Exception as e:
print(e)
print("Failed")
def create_slack_integration():
try:
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_integration_verified():
try:
integrations = Integration.objects.all()
updated_integrations = []
for integration in integrations:
integration.verified = True
updated_integrations.append(integration)
Integration.objects.bulk_update(
updated_integrations, ["verified"], batch_size=10
)
print("Sucess")
except Exception as e:
print(e)
print("Failed")
+1 -2
View File
@@ -2,5 +2,4 @@
set -e
python manage.py wait_for_db
python manage.py migrate
python manage.py rqworker
celery -A plane worker -l info
+3
View File
@@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)
@@ -11,6 +11,7 @@ from .workspace import (
TeamSerializer,
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
)
from .project import (
ProjectSerializer,
@@ -41,6 +42,7 @@ from .issue import (
IssueStateSerializer,
IssueLinkSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
)
from .module import (
@@ -60,8 +62,11 @@ from .integration import (
GithubRepositorySerializer,
GithubRepositorySyncSerializer,
GithubCommentSyncSerializer,
SlackProjectSyncSerializer,
)
from .importer import ImporterSerializer
from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
from .estimate import EstimateSerializer, EstimatePointSerializer, EstimateReadSerializer
@@ -0,0 +1,38 @@
# Module imports
from .base import BaseSerializer
from plane.db.models import Estimate, EstimatePoint
class EstimateSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
class EstimatePointSerializer(BaseSerializer):
class Meta:
model = EstimatePoint
fields = "__all__"
read_only_fields = [
"estimate",
"workspace",
"project",
]
class EstimateReadSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True)
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
@@ -1,11 +1,13 @@
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Importer
class ImporterSerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = Importer
@@ -5,3 +5,4 @@ from .github import (
GithubIssueSyncSerializer,
GithubCommentSyncSerializer,
)
from .slack import SlackProjectSyncSerializer
@@ -0,0 +1,14 @@
# Module imports
from plane.api.serializers import BaseSerializer
from plane.db.models import SlackProjectSync
class SlackProjectSyncSerializer(BaseSerializer):
class Meta:
model = SlackProjectSync
fields = "__all__"
read_only_fields = [
"project",
"workspace",
"workspace_integration",
]
+39 -5
View File
@@ -25,6 +25,7 @@ from plane.db.models import (
Module,
ModuleIssue,
IssueLink,
IssueAttachment,
)
@@ -99,7 +100,7 @@ class IssueCreateSerializer(BaseSerializer):
project = self.context["project"]
issue = Issue.objects.create(**validated_data, project=project)
if blockers is not None:
if blockers is not None and len(blockers):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
@@ -115,7 +116,7 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10,
)
if assignees is not None:
if assignees is not None and len(assignees):
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
@@ -130,8 +131,19 @@ class IssueCreateSerializer(BaseSerializer):
],
batch_size=10,
)
else:
# Then assign it to default assignee
if project.default_assignee is not None:
IssueAssignee.objects.create(
assignee=project.default_assignee,
issue=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
)
if labels is not None:
if labels is not None and len(labels):
IssueLabel.objects.bulk_create(
[
IssueLabel(
@@ -147,7 +159,7 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10,
)
if blocks is not None:
if blocks is not None and len(blocks):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
@@ -254,7 +266,8 @@ class IssueActivitySerializer(BaseSerializer):
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectSerializer(read_only=True, source="project")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueComment
@@ -297,6 +310,9 @@ class IssuePropertySerializer(BaseSerializer):
class LabelSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = Label
fields = "__all__"
@@ -439,6 +455,21 @@ class IssueLinkSerializer(BaseSerializer):
return IssueLink.objects.create(**validated_data)
class IssueAttachmentSerializer(BaseSerializer):
class Meta:
model = IssueAttachment
fields = "__all__"
read_only_fields = [
"created_by",
"updated_by",
"created_at",
"updated_at",
"workspace",
"project",
"issue",
]
# Issue Serializer with state details
class IssueStateSerializer(BaseSerializer):
state_detail = StateSerializer(read_only=True, source="state")
@@ -466,6 +497,7 @@ class IssueSerializer(BaseSerializer):
issue_cycle = IssueCycleDetailSerializer(read_only=True)
issue_module = IssueModuleDetailSerializer(read_only=True)
issue_link = IssueLinkSerializer(read_only=True, many=True)
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
class Meta:
@@ -490,6 +522,8 @@ class IssueLiteSerializer(BaseSerializer):
sub_issues_count = serializers.IntegerField(read_only=True)
cycle_id = serializers.UUIDField(read_only=True)
module_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
class Meta:
model = Issue
+8 -12
View File
@@ -25,22 +25,18 @@ class IssueViewSerializer(BaseSerializer):
def create(self, validated_data):
query_params = validated_data.get("query_data", {})
if not bool(query_params):
raise serializers.ValidationError(
{"query_data": ["Query data field cannot be empty"]}
)
validated_data["query"] = issue_filters(query_params, "POST")
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
return IssueView.objects.create(**validated_data)
def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {})
if not bool(query_params):
raise serializers.ValidationError(
{"query_data": ["Query data field cannot be empty"]}
)
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
+19 -2
View File
@@ -5,8 +5,15 @@ from rest_framework import serializers
from .base import BaseSerializer
from .user import UserLiteSerializer
from plane.db.models import User, Workspace, WorkspaceMember, Team, TeamMember
from plane.db.models import Workspace, WorkspaceMember, Team, WorkspaceMemberInvite
from plane.db.models import (
User,
Workspace,
WorkspaceMember,
Team,
TeamMember,
WorkspaceMemberInvite,
WorkspaceTheme,
)
class WorkSpaceSerializer(BaseSerializer):
@@ -100,3 +107,13 @@ class WorkspaceLiteSerializer(BaseSerializer):
"id",
]
read_only_fields = fields
class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
fields = "__all__"
read_only_fields = [
"workspace",
"actor",
]
+107
View File
@@ -42,6 +42,7 @@ from plane.api.views import (
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,
## End Workspaces
# File Assets
FileAssetEndpoint,
@@ -74,10 +75,15 @@ from plane.api.views import (
SubIssuesEndpoint,
IssueLinkViewSet,
BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint,
## End Issues
# States
StateViewSet,
## End States
# Estimates
ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint,
## End Estimates
# Shortcuts
ShortCutViewSet,
## End Shortcuts
@@ -125,6 +131,7 @@ from plane.api.views import (
GithubIssueSyncViewSet,
GithubCommentSyncViewSet,
BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
## End Integrations
# Importer
ServiceIssueImportSummaryEndpoint,
@@ -133,10 +140,14 @@ from plane.api.views import (
## End importer
# Search
GlobalSearchEndpoint,
IssueSearchEndpoint,
## End Search
# Gpt
GPTIntegrationEndpoint,
## End Gpt
# Release Notes
ReleaseNotesEndpoint,
## End Release Notes
)
@@ -342,6 +353,27 @@ urlpatterns = [
WorkspaceMemberUserViewsEndpoint.as_view(),
name="workspace-member-details",
),
path(
"workspaces/<str:slug>/workspace-themes/",
WorkspaceThemeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-themes",
),
path(
"workspaces/<str:slug>/workspace-themes/<uuid:pk>/",
WorkspaceThemeViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="workspace-themes",
),
## End Workspaces ##
# Projects
path(
@@ -477,6 +509,34 @@ urlpatterns = [
name="project-state",
),
# End States ##
# Estimates
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-estimates/",
ProjectEstimatePointEndpoint.as_view(),
name="project-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="bulk-create-estimate-points",
),
# End Estimates ##
# Shortcuts
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/shortcuts/",
@@ -741,6 +801,16 @@ urlpatterns = [
),
name="project-issue-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
## End Issues
## Issue Activity
path(
@@ -1141,6 +1211,26 @@ urlpatterns = [
),
),
## End Github Integrations
# Slack Integration
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/",
SlackProjectSyncViewSet.as_view(
{
"post": "create",
"get": "list",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/<uuid:pk>/",
SlackProjectSyncViewSet.as_view(
{
"delete": "destroy",
"get": "retrieve",
}
),
),
## End Slack Integration
## End Integrations
# Importer
path(
@@ -1158,6 +1248,11 @@ urlpatterns = [
ImportServiceEndpoint.as_view(),
name="importer",
),
path(
"workspaces/<str:slug>/importers/<str:service>/<uuid:pk>/",
ImportServiceEndpoint.as_view(),
name="importer",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/service/<str:service>/importers/<uuid:importer_id>/",
UpdateServiceImportStatusEndpoint.as_view(),
@@ -1170,6 +1265,11 @@ urlpatterns = [
GlobalSearchEndpoint.as_view(),
name="global-search",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/search-issues/",
IssueSearchEndpoint.as_view(),
name="project-issue-search",
),
## End Search
# Gpt
path(
@@ -1178,4 +1278,11 @@ urlpatterns = [
name="importer",
),
## End Gpt
# Release Notes
path(
"release-notes/",
ReleaseNotesEndpoint.as_view(),
name="release-notes",
),
## End Release Notes
]
+12 -1
View File
@@ -40,6 +40,7 @@ from .workspace import (
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,
)
from .state import StateViewSet
from .shortcut import ShortCutViewSet
@@ -69,6 +70,7 @@ from .issue import (
SubIssuesEndpoint,
IssueLinkViewSet,
BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint,
)
from .auth_extended import (
@@ -104,6 +106,7 @@ from .integration import (
GithubCommentSyncViewSet,
GithubRepositoriesEndpoint,
BulkCreateGithubIssueSyncEndpoint,
SlackProjectSyncViewSet,
)
from .importer import (
@@ -125,7 +128,15 @@ from .page import (
CreatedbyOtherPagesEndpoint,
)
from .search import GlobalSearchEndpoint
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .gpt import GPTIntegrationEndpoint
from .estimate import (
ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint,
)
from .release import ReleaseNotesEndpoint
+2
View File
@@ -65,6 +65,8 @@ class FileAssetEndpoint(BaseAPIView):
class UserAssetsEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser)
def get(self, request, asset_key):
try:
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
+2 -2
View File
@@ -10,7 +10,7 @@ from rest_framework.views import APIView
from rest_framework.filters import SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.exceptions import NotFound
from sentry_sdk import capture_exception
from django_filters.rest_framework import DjangoFilterBackend
# Module imports
@@ -39,7 +39,7 @@ class BaseViewSet(ModelViewSet, BasePaginator):
try:
return self.model.objects.all()
except Exception as e:
print(e)
capture_exception(e)
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
def dispatch(self, request, *args, **kwargs):
+70 -17
View File
@@ -28,6 +28,8 @@ from plane.db.models import (
CycleIssue,
Issue,
CycleFavorite,
IssueLink,
IssueAttachment,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
@@ -46,6 +48,28 @@ class CycleViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
)
def perform_destroy(self, instance):
cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("pk")),
"issues": [str(issue_id) for issue_id in cycle_issues],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self):
subquery = CycleFavorite.objects.filter(
user=self.request.user,
@@ -179,6 +203,22 @@ class CycleIssueViewSet(BaseViewSet):
cycle_id=self.kwargs.get("cycle_id"),
)
def perform_destroy(self, instance):
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("cycle_id")),
"issues": [str(instance.issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self):
return self.filter_queryset(
super()
@@ -226,6 +266,20 @@ class CycleIssueViewSet(BaseViewSet):
.prefetch_related("labels")
.order_by(order_by)
.filter(**filters)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
issues_data = IssueStateSerializer(issues, many=True).data
@@ -270,9 +324,9 @@ class CycleIssueViewSet(BaseViewSet):
# Get all CycleIssues already created
cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
records_to_update = []
update_cycle_issue_activity = []
record_to_create = []
records_to_update = []
for issue in issues:
cycle_issue = [
@@ -317,21 +371,19 @@ class CycleIssueViewSet(BaseViewSet):
# Capture Issue Activity
issue_activity.delay(
{
"type": "issue.activity",
"requested_data": json.dumps({"cycles_list": issues}),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("pk", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
{
"updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": serializers.serialize(
"json", record_to_create
),
}
),
},
type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": issues}),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": serializers.serialize(
"json", record_to_create
),
}
),
)
# Return all Cycle Issues
@@ -370,7 +422,8 @@ class CycleDateCheckEndpoint(BaseAPIView):
cycles = Cycle.objects.filter(
Q(start_date__lte=start_date, end_date__gte=start_date)
| Q(start_date__gte=end_date, end_date__lte=end_date),
| Q(start_date__lte=end_date, end_date__gte=end_date)
| Q(start_date__gte=start_date, end_date__lte=end_date),
workspace__slug=slug,
project_id=project_id,
)
+253
View File
@@ -0,0 +1,253 @@
# Django imports
from django.db import IntegrityError
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Project, Estimate, EstimatePoint
from plane.api.serializers import (
EstimateSerializer,
EstimatePointSerializer,
EstimateReadSerializer,
)
class ProjectEstimatePointEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
project = Project.objects.get(workspace__slug=slug, pk=project_id)
if project.estimate_id is not None:
estimate_points = EstimatePoint.objects.filter(
estimate_id=project.estimate_id,
project_id=project_id,
workspace__slug=slug,
)
serializer = EstimatePointSerializer(estimate_points, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response([], status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class BulkEstimatePointEndpoint(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
model = Estimate
serializer_class = EstimateSerializer
def list(self, request, slug, project_id):
try:
estimates = Estimate.objects.filter(
workspace__slug=slug, project_id=project_id
).prefetch_related("points")
serializer = EstimateReadSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, slug, project_id):
try:
if not request.data.get("estimate", False):
return Response(
{"error": "Estimate is required"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points = request.data.get("estimate_points", [])
if not len(estimate_points) or len(estimate_points) > 8:
return Response(
{"error": "Estimate points are required"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
if not estimate_serializer.is_valid():
return Response(
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
try:
estimate = estimate_serializer.save(project_id=project_id)
except IntegrityError:
return Response(
{"errror": "Estimate with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points = EstimatePoint.objects.bulk_create(
[
EstimatePoint(
estimate=estimate,
key=estimate_point.get("key", 0),
value=estimate_point.get("value", ""),
description=estimate_point.get("description", ""),
project_id=project_id,
workspace_id=estimate.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for estimate_point in estimate_points
],
batch_size=10,
ignore_conflicts=True,
)
estimate_point_serializer = EstimatePointSerializer(
estimate_points, many=True
)
return Response(
{
"estimate": estimate_serializer.data,
"estimate_points": estimate_point_serializer.data,
},
status=status.HTTP_200_OK,
)
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def retrieve(self, request, slug, project_id, estimate_id):
try:
estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
serializer = EstimateReadSerializer(estimate)
return Response(
serializer.data,
status=status.HTTP_200_OK,
)
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, estimate_id):
try:
if not request.data.get("estimate", False):
return Response(
{"error": "Estimate is required"},
status=status.HTTP_400_BAD_REQUEST,
)
if not len(request.data.get("estimate_points", [])):
return Response(
{"error": "Estimate points are required"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate = Estimate.objects.get(pk=estimate_id)
estimate_serializer = EstimateSerializer(
estimate, data=request.data.get("estimate"), partial=True
)
if not estimate_serializer.is_valid():
return Response(
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
try:
estimate = estimate_serializer.save()
except IntegrityError:
return Response(
{"errror": "Estimate with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_points_data = request.data.get("estimate_points", [])
estimate_points = EstimatePoint.objects.filter(
pk__in=[
estimate_point.get("id") for estimate_point in estimate_points_data
],
workspace__slug=slug,
project_id=project_id,
estimate_id=estimate_id,
)
updated_estimate_points = []
for estimate_point in estimate_points:
# Find the data for that estimate point
estimate_point_data = [
point
for point in estimate_points_data
if point.get("id") == str(estimate_point.id)
]
if len(estimate_point_data):
estimate_point.value = estimate_point_data[0].get(
"value", estimate_point.value
)
updated_estimate_points.append(estimate_point)
try:
EstimatePoint.objects.bulk_update(
updated_estimate_points, ["value"], batch_size=10
)
except IntegrityError as e:
return Response(
{"error": "Values need to be unique for each key"},
status=status.HTTP_400_BAD_REQUEST,
)
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
return Response(
{
"estimate": estimate_serializer.data,
"estimate_points": estimate_point_serializer.data,
},
status=status.HTTP_200_OK,
)
except Estimate.DoesNotExist:
return Response(
{"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, estimate_id):
try:
estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
estimate.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+129 -63
View File
@@ -28,6 +28,7 @@ from plane.db.models import (
Module,
ModuleLink,
ModuleIssue,
Label,
)
from plane.api.serializers import (
ImporterSerializer,
@@ -65,29 +66,35 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
)
if service == "jira":
project_name = request.data.get("project_name", "")
api_token = request.data.get("api_token", "")
email = request.data.get("email", "")
cloud_hostname = request.data.get("cloud_hostname", "")
if (
not bool(project_name)
or not bool(api_token)
or not bool(email)
or not bool(cloud_hostname)
):
return Response(
{
"error": "Project name, Project key, API token, Cloud hostname and email are requied"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Check for all the keys
params = {
"project_key": "Project key is required",
"api_token": "API token is required",
"email": "Email is required",
"cloud_hostname": "Cloud hostname is required",
}
return Response(
jira_project_issue_summary(
email, api_token, project_name, cloud_hostname
),
status=status.HTTP_200_OK,
for key, error_message in params.items():
if not request.GET.get(key, False):
return Response(
{"error": error_message}, status=status.HTTP_400_BAD_REQUEST
)
project_key = request.GET.get("project_key", "")
api_token = request.GET.get("api_token", "")
email = request.GET.get("email", "")
cloud_hostname = request.GET.get("cloud_hostname", "")
response = jira_project_issue_summary(
email, api_token, project_key, cloud_hostname
)
if "error" in response:
return Response(response, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
response,
status=status.HTTP_200_OK,
)
return Response(
{"error": "Service not supported yet"},
status=status.HTTP_400_BAD_REQUEST,
@@ -98,7 +105,7 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
@@ -213,8 +220,10 @@ class ImportServiceEndpoint(BaseAPIView):
def get(self, request, slug):
try:
imports = Importer.objects.filter(workspace__slug=slug).order_by(
"-created_at"
imports = (
Importer.objects.filter(workspace__slug=slug)
.order_by("-created_at")
.select_related("initiated_by", "project", "workspace")
)
serializer = ImporterSerializer(imports, many=True)
return Response(serializer.data)
@@ -225,6 +234,52 @@ class ImportServiceEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, slug, service, pk):
try:
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
# Delete all imported Issues
imported_issues = importer.imported_data.get("issues", [])
Issue.objects.filter(id__in=imported_issues).delete()
# Delete all imported Labels
imported_labels = importer.imported_data.get("labels", [])
Label.objects.filter(id__in=imported_labels).delete()
if importer.service == "jira":
imported_modules = importer.imported_data.get("modules", [])
Module.objects.filter(id__in=imported_modules).delete()
importer.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def patch(self, request, slug, service, pk):
try:
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
serializer = ImporterSerializer(importer, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Importer.DoesNotExist:
return Response(
{"error": "Importer Does not exists"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UpdateServiceImportStatusEndpoint(BaseAPIView):
def post(self, request, slug, project_id, service, importer_id):
@@ -465,48 +520,59 @@ class BulkImportModulesEndpoint(BaseAPIView):
ignore_conflicts=True,
)
_ = ModuleLink.objects.bulk_create(
[
ModuleLink(
module=module,
url=module_data.get("link", {}).get("url", "https://plane.so"),
title=module_data.get("link", {}).get(
"title", "Original Issue"
),
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for module, module_data in zip(modules, modules_data)
],
batch_size=100,
ignore_conflicts=True,
)
modules = Module.objects.filter(id__in=[module.id for module in modules])
bulk_module_issues = []
for module, module_data in zip(modules, modules_data):
module_issues_list = module_data.get("module_issues_list", [])
bulk_module_issues = bulk_module_issues + [
ModuleIssue(
issue_id=issue,
module=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for issue in module_issues_list
]
if len(modules) == len(modules_data):
_ = ModuleLink.objects.bulk_create(
[
ModuleLink(
module=module,
url=module_data.get("link", {}).get(
"url", "https://plane.so"
),
title=module_data.get("link", {}).get(
"title", "Original Issue"
),
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for module, module_data in zip(modules, modules_data)
],
batch_size=100,
ignore_conflicts=True,
)
_ = ModuleIssue.objects.bulk_create(
bulk_module_issues, batch_size=100, ignore_conflicts=True
)
bulk_module_issues = []
for module, module_data in zip(modules, modules_data):
module_issues_list = module_data.get("module_issues_list", [])
bulk_module_issues = bulk_module_issues + [
ModuleIssue(
issue_id=issue,
module=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for issue in module_issues_list
]
serializer = ModuleSerializer(modules, many=True)
return Response(
{"modules": serializer.data}, status=status.HTTP_201_CREATED
)
_ = ModuleIssue.objects.bulk_create(
bulk_module_issues, batch_size=100, ignore_conflicts=True
)
serializer = ModuleSerializer(modules, many=True)
return Response(
{"modules": serializer.data}, status=status.HTTP_201_CREATED
)
else:
return Response(
{"message": "Modules created but issues could not be imported"},
status=status.HTTP_200_OK,
)
except Project.DoesNotExist:
return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
@@ -6,3 +6,4 @@ from .github import (
GithubCommentSyncViewSet,
GithubRepositoriesEndpoint,
)
from .slack import SlackProjectSyncViewSet
+18 -9
View File
@@ -27,6 +27,7 @@ from plane.utils.integrations.github import (
)
from plane.api.permissions import WorkSpaceAdminPermission
class IntegrationViewSet(BaseViewSet):
serializer_class = IntegrationSerializer
model = Integration
@@ -101,7 +102,6 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
WorkSpaceAdminPermission,
]
def get_queryset(self):
return (
super()
@@ -112,21 +112,30 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
def create(self, request, slug, provider):
try:
installation_id = request.data.get("installation_id", None)
if not installation_id:
return Response(
{"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
integration = Integration.objects.get(provider=provider)
config = {}
if provider == "github":
installation_id = request.data.get("installation_id", None)
if not installation_id:
return Response(
{"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
metadata = get_github_metadata(installation_id)
config = {"installation_id": installation_id}
if provider == "slack":
metadata = request.data.get("metadata", {})
access_token = metadata.get("access_token", False)
team_id = metadata.get("team", {}).get("id", False)
if not metadata or not access_token or not team_id:
return Response(
{"error": "Access token and team id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
config = {"team_id": team_id, "access_token": access_token}
# Create a bot user
bot_user = User.objects.create(
email=f"{uuid.uuid4().hex}@plane.so",
@@ -0,0 +1,59 @@
# Django import
from django.db import IntegrityError
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from plane.api.views import BaseViewSet, BaseAPIView
from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
from plane.api.serializers import SlackProjectSyncSerializer
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
class SlackProjectSyncViewSet(BaseViewSet):
permission_classes = [
ProjectBasePermission,
]
serializer_class = SlackProjectSyncSerializer
model = SlackProjectSync
def create(self, request, slug, project_id, workspace_integration_id):
try:
serializer = SlackProjectSyncSerializer(data=request.data)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
if serializer.is_valid():
serializer.save(
project_id=project_id,
workspace_integration_id=workspace_integration_id,
)
workspace_integration = WorkspaceIntegration.objects.get(
pk=workspace_integration_id, workspace__slug=slug
)
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor, role=20, project_id=project_id
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response({"error": "Slack is already enabled for the project"}, status=status.HTTP_400_BAD_REQUEST)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+227 -68
View File
@@ -1,17 +1,19 @@
# Python imports
import json
import random
from itertools import groupby, chain
from itertools import chain
# Django imports
from django.db.models import Prefetch, OuterRef, Func, F, Q
from django.db.models import Prefetch, OuterRef, Func, F, Q, Count
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.db.models.functions import Coalesce
# Third Party imports
from rest_framework.response import Response
from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
from sentry_sdk import capture_exception
# Module imports
@@ -28,6 +30,7 @@ from plane.api.serializers import (
IssueFlatSerializer,
IssueLinkSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
)
from plane.api.permissions import (
ProjectEntityPermission,
@@ -43,6 +46,8 @@ from plane.db.models import (
IssueProperty,
Label,
IssueLink,
IssueAttachment,
State,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
@@ -82,16 +87,14 @@ class IssueViewSet(BaseViewSet):
)
if current_instance is not None:
issue_activity.delay(
{
"type": "issue.activity.updated",
"requested_data": requested_data,
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("pk", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
},
type="issue.activity.updated",
requested_data=requested_data,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
)
return super().perform_update(serializer)
@@ -102,18 +105,16 @@ class IssueViewSet(BaseViewSet):
)
if current_instance is not None:
issue_activity.delay(
{
"type": "issue.activity.deleted",
"requested_data": json.dumps(
{"issue_id": str(self.kwargs.get("pk", None))}
),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("pk", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
},
type="issue.activity.deleted",
requested_data=json.dumps(
{"issue_id": str(self.kwargs.get("pk", None))}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
)
return super().perform_destroy(instance)
@@ -149,6 +150,20 @@ class IssueViewSet(BaseViewSet):
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__id"))
.annotate(module_id=F("issue_module__id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
issue_queryset = (
@@ -187,16 +202,12 @@ class IssueViewSet(BaseViewSet):
# Track the issue
issue_activity.delay(
{
"type": "issue.activity.created",
"requested_data": json.dumps(
self.request.data, cls=DjangoJSONEncoder
),
"actor_id": str(request.user.id),
"issue_id": str(serializer.data.get("id", None)),
"project_id": str(project_id),
"current_instance": None,
},
type="issue.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
current_instance=None,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -237,6 +248,20 @@ class UserWorkSpaceIssues(BaseAPIView):
.prefetch_related("assignees")
.prefetch_related("labels")
.order_by("-created_at")
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
serializer = IssueLiteSerializer(issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -328,14 +353,12 @@ class IssueCommentViewSet(BaseViewSet):
actor=self.request.user if self.request.user is not None else None,
)
issue_activity.delay(
{
"type": "comment.activity.created",
"requested_data": json.dumps(serializer.data, cls=DjangoJSONEncoder),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("issue_id")),
"project_id": str(self.kwargs.get("project_id")),
"current_instance": None,
},
type="comment.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
current_instance=None,
)
def perform_update(self, serializer):
@@ -345,17 +368,15 @@ class IssueCommentViewSet(BaseViewSet):
)
if current_instance is not None:
issue_activity.delay(
{
"type": "comment.activity.updated",
"requested_data": requested_data,
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("issue_id", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
},
type="comment.activity.updated",
requested_data=requested_data,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
return super().perform_update(serializer)
@@ -366,19 +387,17 @@ class IssueCommentViewSet(BaseViewSet):
)
if current_instance is not None:
issue_activity.delay(
{
"type": "comment.activity.deleted",
"requested_data": json.dumps(
{"comment_id": str(self.kwargs.get("pk", None))}
),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("issue_id", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
},
type="comment.activity.deleted",
requested_data=json.dumps(
{"comment_id": str(self.kwargs.get("pk", None))}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
return super().perform_destroy(instance)
@@ -573,8 +592,31 @@ class SubIssuesEndpoint(BaseAPIView):
.prefetch_related("labels")
)
serializer = IssueLiteSerializer(sub_issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
state_distribution = (
State.objects.filter(workspace__slug=slug, project_id=project_id)
.annotate(
state_count=Count(
"state_issue",
filter=Q(state_issue__parent_id=issue_id),
)
)
.order_by("group")
.values("group", "state_count")
)
result = {item["group"]: item["state_count"] for item in state_distribution}
serializer = IssueLiteSerializer(
sub_issues,
many=True,
)
return Response(
{
"sub_issues": serializer.data,
"state_distribution": result,
},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
@@ -632,6 +674,54 @@ class IssueLinkViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"),
issue_id=self.kwargs.get("issue_id"),
)
issue_activity.delay(
type="link.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
current_instance=None,
)
def perform_update(self, serializer):
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = (
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:
issue_activity.delay(
type="link.activity.updated",
requested_data=requested_data,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueLinkSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
return super().perform_update(serializer)
def perform_destroy(self, instance):
current_instance = (
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:
issue_activity.delay(
type="link.activity.deleted",
requested_data=json.dumps(
{"link_id": str(self.kwargs.get("pk", None))}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueLinkSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
return super().perform_destroy(instance)
def get_queryset(self):
return (
@@ -683,3 +773,72 @@ class BulkCreateIssueLabelsEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueAttachmentEndpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
permission_classes = [
ProjectEntityPermission,
]
model = IssueAttachment
parser_classes = (MultiPartParser, FormParser)
def post(self, request, slug, project_id, issue_id):
try:
serializer = IssueAttachmentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id, issue_id=issue_id)
issue_activity.delay(
type="attachment.activity.created",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
serializer.data,
cls=DjangoJSONEncoder,
),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, slug, project_id, issue_id, pk):
try:
issue_attachment = IssueAttachment.objects.get(pk=pk)
issue_attachment.asset.delete(save=False)
issue_attachment.delete()
issue_activity.delay(
type="attachment.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return Response(status=status.HTTP_204_NO_CONTENT)
except IssueAttachment.DoesNotExist:
return Response(
{"error": "Issue Attachment does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
def get(self, request, slug, project_id, issue_id):
try:
issue_attachments = IssueAttachment.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id
)
serilaizer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serilaizer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+67 -15
View File
@@ -31,6 +31,8 @@ from plane.db.models import (
Issue,
ModuleLink,
ModuleFavorite,
IssueLink,
IssueAttachment,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
@@ -107,6 +109,28 @@ class ModuleViewSet(BaseViewSet):
.order_by("-is_favorite", "name")
)
def perform_destroy(self, instance):
module_issues = list(
ModuleIssue.objects.filter(module_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(self.kwargs.get("pk")),
"issues": [str(issue_id) for issue_id in module_issues],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def create(self, request, slug, project_id):
try:
project = Project.objects.get(workspace__slug=slug, pk=project_id)
@@ -156,6 +180,22 @@ class ModuleIssueViewSet(BaseViewSet):
module_id=self.kwargs.get("module_id"),
)
def perform_destroy(self, instance):
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(self.kwargs.get("module_id")),
"issues": [str(instance.issue_id)],
}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return super().perform_destroy(instance)
def get_queryset(self):
return self.filter_queryset(
super()
@@ -204,6 +244,20 @@ class ModuleIssueViewSet(BaseViewSet):
.prefetch_related("labels")
.order_by(order_by)
.filter(**filters)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
issues_data = IssueStateSerializer(issues, many=True).data
@@ -286,21 +340,19 @@ class ModuleIssueViewSet(BaseViewSet):
# Capture Issue Activity
issue_activity.delay(
{
"type": "issue.activity",
"requested_data": json.dumps({"modules_list": issues}),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("pk", None)),
"project_id": str(self.kwargs.get("project_id", None)),
"current_instance": json.dumps(
{
"updated_module_issues": update_module_issue_activity,
"created_module_issues": serializers.serialize(
"json", record_to_create
),
}
),
},
type="module.activity.created",
requested_data=json.dumps({"modules_list": issues}),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_module_issues": update_module_issue_activity,
"created_module_issues": serializers.serialize(
"json", record_to_create
),
}
),
)
return Response(
+3 -4
View File
@@ -14,7 +14,7 @@ from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework import status
from sentry_sdk import capture_exception
# sso authentication
from google.oauth2 import id_token
from google.auth.transport import requests as google_auth_request
@@ -48,7 +48,7 @@ def validate_google_token(token, client_id):
}
return data
except Exception as e:
print(e)
capture_exception(e)
raise exceptions.AuthenticationFailed("Error with Google connection.")
@@ -305,8 +305,7 @@ class OauthEndpoint(BaseAPIView):
)
return Response(data, status=status.HTTP_201_CREATED)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{
"error": "Something went wrong. Please try again later or contact the support team."
+31 -1
View File
@@ -96,6 +96,36 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, pk):
try:
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
# Only update access if the page owner is the requesting user
if (
page.access != request.data.get("access", page.access)
and page.owned_by_id != request.user.id
):
return Response(
{
"error": "Access cannot be updated since this page is owned by someone else"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = PageSerializer(page, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Page.DoesNotExist:
return Response(
{"error": "Page Does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class PageBlockViewSet(BaseViewSet):
serializer_class = PageBlockSerializer
@@ -344,7 +374,7 @@ class RecentPagesEndpoint(BaseAPIView):
status=status.HTTP_200_OK,
)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
+21
View File
@@ -0,0 +1,21 @@
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from .base import BaseAPIView
from plane.utils.integrations.github import get_release_notes
class ReleaseNotesEndpoint(BaseAPIView):
def get(self, request):
try:
release_notes = get_release_notes()
return Response(release_notes, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+138 -61
View File
@@ -12,6 +12,7 @@ from sentry_sdk import capture_exception
# Module imports
from .base import BaseAPIView
from plane.db.models import Workspace, Project, Issue, Cycle, Module, Page, IssueView
from plane.utils.issue_search import search_issues
class GlobalSearchEndpoint(BaseAPIView):
@@ -24,20 +25,26 @@ class GlobalSearchEndpoint(BaseAPIView):
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return Workspace.objects.filter(
q, workspace_member__member=self.request.user
).distinct().values("name", "id", "slug")
return (
Workspace.objects.filter(q, workspace_member__member=self.request.user)
.distinct()
.values("name", "id", "slug")
)
def filter_projects(self, query, slug, project_id):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return Project.objects.filter(
q,
Q(project_projectmember__member=self.request.user) | Q(network=2),
workspace__slug=slug,
).distinct().values("name", "id", "identifier", "workspace__slug")
return (
Project.objects.filter(
q,
Q(project_projectmember__member=self.request.user) | Q(network=2),
workspace__slug=slug,
)
.distinct()
.values("name", "id", "identifier", "workspace__slug")
)
def filter_issues(self, query, slug, project_id):
fields = ["name", "sequence_id"]
@@ -49,18 +56,22 @@ class GlobalSearchEndpoint(BaseAPIView):
q |= Q(**{"sequence_id": sequence_id})
else:
q |= Q(**{f"{field}__icontains": query})
return Issue.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
).distinct().values(
"name",
"id",
"sequence_id",
"project__identifier",
"project_id",
"workspace__slug",
return (
Issue.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"sequence_id",
"project__identifier",
"project_id",
"workspace__slug",
)
)
def filter_cycles(self, query, slug, project_id):
@@ -68,16 +79,20 @@ class GlobalSearchEndpoint(BaseAPIView):
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return Cycle.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
).distinct().values(
"name",
"id",
"project_id",
"workspace__slug",
return (
Cycle.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
)
def filter_modules(self, query, slug, project_id):
@@ -85,16 +100,20 @@ class GlobalSearchEndpoint(BaseAPIView):
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return Module.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
).distinct().values(
"name",
"id",
"project_id",
"workspace__slug",
return (
Module.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
)
def filter_pages(self, query, slug, project_id):
@@ -102,16 +121,20 @@ class GlobalSearchEndpoint(BaseAPIView):
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return Page.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
).distinct().values(
"name",
"id",
"project_id",
"workspace__slug",
return (
Page.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
)
def filter_views(self, query, slug, project_id):
@@ -119,16 +142,20 @@ class GlobalSearchEndpoint(BaseAPIView):
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return IssueView.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
).distinct().values(
"name",
"id",
"project_id",
"workspace__slug",
return (
IssueView.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
)
def get(self, request, slug, project_id):
@@ -168,7 +195,57 @@ class GlobalSearchEndpoint(BaseAPIView):
return Response({"results": results}, status=status.HTTP_200_OK)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueSearchEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
try:
query = request.query_params.get("search", False)
parent = request.query_params.get("parent", False)
blocker_blocked_by = request.query_params.get("blocker_blocked_by", False)
issue_id = request.query_params.get("issue_id", False)
issues = search_issues(query)
issues = issues.filter(
workspace__slug=slug,
project_id=project_id,
project__project_projectmember__member=self.request.user,
)
if parent == "true" and issue_id:
issue = Issue.objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
).exclude(
pk__in=Issue.objects.filter(parent__isnull=False).values_list(
"parent_id", flat=True
)
)
if blocker_blocked_by == "true" and issue_id:
issues = issues.filter(blocker_issues=issue_id, blocked_issues=issue_id)
return Response(
issues.values(
"name",
"id",
"sequence_id",
"project__identifier",
"project_id",
"workspace__slug",
),
status=status.HTTP_200_OK,
)
except Issue.DoesNotExist:
return Response(
{"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
+35 -2
View File
@@ -1,6 +1,9 @@
# Python imports
from itertools import groupby
# Django imports
from django.db import IntegrityError
# Third party imports
from rest_framework.response import Response
from rest_framework import status
@@ -8,10 +11,10 @@ from sentry_sdk import capture_exception
# Module imports
from . import BaseViewSet
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import StateSerializer
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import State
from plane.db.models import State, Issue
class StateViewSet(BaseViewSet):
@@ -36,6 +39,25 @@ class StateViewSet(BaseViewSet):
.distinct()
)
def create(self, request, slug, project_id):
try:
serializer = StateSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "State with the name already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def list(self, request, slug, project_id):
try:
state_dict = dict()
@@ -66,6 +88,17 @@ class StateViewSet(BaseViewSet):
{"error": "Default state cannot be deleted"}, status=False
)
# Check for any issues in the state
issue_exist = Issue.objects.filter(state=pk).exists()
if issue_exist:
return Response(
{
"error": "The state is not empty, only empty states can be deleted"
},
status=status.HTTP_400_BAD_REQUEST,
)
state.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except State.DoesNotExist:
+34 -2
View File
@@ -36,6 +36,7 @@ from plane.api.serializers import (
WorkSpaceMemberInviteSerializer,
UserLiteSerializer,
ProjectMemberSerializer,
WorkspaceThemeSerializer,
)
from plane.api.views.base import BaseAPIView
from . import BaseViewSet
@@ -48,6 +49,7 @@ from plane.db.models import (
ProjectMember,
IssueActivity,
Issue,
WorkspaceTheme,
)
from plane.api.permissions import WorkSpaceBasePermission, WorkSpaceAdminPermission
from plane.bgtasks.workspace_invitation_task import workspace_invitation
@@ -143,7 +145,6 @@ class UserWorkSpacesEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
@@ -331,7 +332,6 @@ class JoinWorkspaceEndpoint(BaseAPIView):
status=status.HTTP_404_NOT_FOUND,
)
except Exception as e:
print(e)
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
@@ -752,3 +752,35 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceThemeViewSet(BaseViewSet):
permission_classes = [
WorkSpaceAdminPermission,
]
model = WorkspaceTheme
serializer_class = WorkspaceThemeSerializer
def get_queryset(self):
return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
def create(self, request, slug):
try:
workspace = Workspace.objects.get(slug=slug)
serializer = WorkspaceThemeSerializer(data=request.data)
if serializer.is_valid():
serializer.save(workspace=workspace, actor=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Workspace.DoesNotExist:
return Response(
{"error": "Workspace does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
View File
@@ -2,23 +2,26 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import User
@job("default")
@shared_task
def email_verification(first_name, email, token, current_site):
try:
realtivelink = "/request-email-verification/" + "?token=" + str(token)
abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"Verify your Email!"
@@ -2,23 +2,24 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import User
@job("default")
@shared_task
def forgot_password(first_name, email, uidb64, token, current_site):
try:
realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/"
abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"Verify your Email!"
+56 -45
View File
@@ -11,7 +11,7 @@ from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.auth.hashers import make_password
# Third Party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
@@ -27,9 +27,10 @@ from plane.db.models import (
User,
)
from .workspace_invitation_task import workspace_invitation
from plane.bgtasks.user_welcome_task import send_welcome_email
@job("default")
@shared_task
def service_importer(service, importer_id):
try:
importer = Importer.objects.get(pk=importer_id)
@@ -38,54 +39,64 @@ def service_importer(service, importer_id):
users = importer.data.get("users", [])
# For all invited users create the uers
new_users = User.objects.bulk_create(
[
User(
email=user.get("email").strip().lower(),
username=uuid.uuid4().hex,
password=make_password(uuid.uuid4().hex),
is_password_autoset=True,
)
for user in users
if user.get("import", False) == "invite"
],
batch_size=10,
ignore_conflicts=True,
)
# Check if we need to import users as well
if len(users):
# For all invited users create the users
new_users = User.objects.bulk_create(
[
User(
email=user.get("email").strip().lower(),
username=uuid.uuid4().hex,
password=make_password(uuid.uuid4().hex),
is_password_autoset=True,
)
for user in users
if user.get("import", False) == "invite"
],
batch_size=10,
ignore_conflicts=True,
)
workspace_users = User.objects.filter(
email__in=[
user.get("email").strip().lower()
for user in users
if user.get("import", False) == "invite"
or user.get("import", False) == "map"
[
send_welcome_email.delay(
str(user.id),
True,
f"{user.email} was imported to Plane from {service}",
)
for user in new_users
]
)
workspace_users = User.objects.filter(
email__in=[
user.get("email").strip().lower()
for user in users
if user.get("import", False) == "invite"
or user.get("import", False) == "map"
]
)
# Add new users to Workspace and project automatically
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(member=user, workspace_id=importer.workspace_id)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
# Add new users to Workspace and project automatically
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(member=user, workspace_id=importer.workspace_id)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
ProjectMember.objects.bulk_create(
[
ProjectMember(
project_id=importer.project_id,
workspace_id=importer.workspace_id,
member=user,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
ProjectMember.objects.bulk_create(
[
ProjectMember(
project_id=importer.project_id,
workspace_id=importer.workspace_id,
member=user,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
# Check if sync config is on for github importers
if service == "github" and importer.config.get("sync", False):
+308 -100
View File
@@ -7,7 +7,7 @@ from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
# Third Party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
@@ -136,6 +136,7 @@ def track_priority(
comment=f"{actor.email} updated the priority to {requested_data.get('priority')}",
)
)
print(issue_activities)
# Track chnages in state of the issue
@@ -505,14 +506,181 @@ def track_blockings(
)
def track_cycles(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
def create_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} created the issue",
verb="created",
actor=actor,
)
)
def track_estimate_points(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
if requested_data.get("estimate_point") == None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=current_instance.get("estimate_point"),
new_value=requested_data.get("estimate_point"),
field="estimate_point",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated the estimate point to None",
)
)
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=current_instance.get("estimate_point"),
new_value=requested_data.get("estimate_point"),
field="estimate_point",
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated the estimate point to {requested_data.get('estimate_point')}",
)
)
def update_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
ISSUE_ACTIVITY_MAPPER = {
"name": track_name,
"parent": track_parent,
"priority": track_priority,
"state": track_state,
"description": track_description,
"target_date": track_target_date,
"start_date": track_start_date,
"labels_list": track_labels,
"assignees_list": track_assignees,
"blocks_list": track_blocks,
"blockers_list": track_blockings,
"estimate_point": track_estimate_points,
}
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
for key in requested_data:
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
if func is not None:
func(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
)
def delete_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
project=project,
workspace=project.workspace,
comment=f"{actor.email} deleted the issue",
verb="deleted",
actor=actor,
field="issue",
)
)
def create_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} created a comment",
verb="created",
actor=actor,
field="comment",
new_value=requested_data.get("comment_html", ""),
new_identifier=requested_data.get("id", None),
issue_comment_id=requested_data.get("id", None),
)
)
def update_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance.get("comment_html") != requested_data.get("comment_html"):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated a comment",
verb="updated",
actor=actor,
field="comment",
old_value=current_instance.get("comment_html", ""),
old_identifier=current_instance.get("id"),
new_value=requested_data.get("comment_html", ""),
new_identifier=current_instance.get("id", None),
issue_comment_id=current_instance.get("id", None),
)
)
def delete_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} deleted the comment",
verb="deleted",
actor=actor,
field="comment",
)
)
def create_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
# Updated Records:
updated_records = current_instance.get("updated_cycle_issues", [])
created_records = json.loads(current_instance.get("created_cycle_issues", []))
@@ -562,14 +730,43 @@ def track_cycles(
)
def track_modules(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
def delete_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
cycle_id = requested_data.get("cycle_id", "")
cycle = Cycle.objects.filter(pk=cycle_id).first()
issues = requested_data.get("issues")
for issue in issues:
issue_activities.append(
IssueActivity(
issue_id=issue,
actor=actor,
verb="deleted",
old_value=cycle.name if cycle is not None else "",
new_value="",
field="cycles",
project=project,
workspace=project.workspace,
comment=f"{actor.email} removed this issue from {cycle.name if cycle is not None else None}",
old_identifier=cycle.id if cycle is not None else None,
)
)
def create_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
# Updated Records:
updated_records = current_instance.get("updated_module_issues", [])
created_records = json.loads(current_instance.get("created_module_issues", []))
@@ -618,109 +815,85 @@ def track_modules(
)
def create_issue_activity(
def delete_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} created the issue",
verb="created",
actor=actor,
)
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
module_id = requested_data.get("module_id", "")
module = Module.objects.filter(pk=module_id).first()
issues = requested_data.get("issues")
def update_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
ISSUE_ACTIVITY_MAPPER = {
"name": track_name,
"parent": track_parent,
"priority": track_priority,
"state": track_state,
"description": track_description,
"target_date": track_target_date,
"start_date": track_start_date,
"labels_list": track_labels,
"assignees_list": track_assignees,
"blocks_list": track_blocks,
"blockers_list": track_blockings,
"cycles_list": track_cycles,
"modules_list": track_modules,
}
for key in requested_data:
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
if func is not None:
func(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
for issue in issues:
issue_activities.append(
IssueActivity(
issue_id=issue,
actor=actor,
verb="deleted",
old_value=module.name if module is not None else "",
new_value="",
field="modules",
project=project,
workspace=project.workspace,
comment=f"{actor.email} removed this issue from {module.name if module is not None else None}",
old_identifier=module.id if module is not None else None,
)
)
def create_comment_activity(
def create_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} created a comment",
comment=f"{actor.email} created a link",
verb="created",
actor=actor,
field="comment",
new_value=requested_data.get("comment_html", ""),
field="link",
new_value=requested_data.get("url", ""),
new_identifier=requested_data.get("id", None),
issue_comment_id=requested_data.get("id", None),
)
)
def update_comment_activity(
def update_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if current_instance.get("comment_html") != requested_data.get("comment_html"):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance.get("url") != requested_data.get("url"):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} updated a comment",
comment=f"{actor.email} updated a link",
verb="updated",
actor=actor,
field="comment",
old_value=current_instance.get("comment_html", ""),
field="link",
old_value=current_instance.get("url", ""),
old_identifier=current_instance.get("id"),
new_value=requested_data.get("comment_html", ""),
new_value=requested_data.get("url", ""),
new_identifier=current_instance.get("id", None),
issue_comment_id=current_instance.get("id", None),
)
)
def delete_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
project=project,
workspace=project.workspace,
comment=f"{actor.email} deleted the issue",
verb="deleted",
actor=actor,
field="issue",
)
)
def delete_comment_activity(
def delete_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
@@ -728,36 +901,62 @@ def delete_comment_activity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} deleted the comment",
comment=f"{actor.email} deleted the link",
verb="deleted",
actor=actor,
field="comment",
field="link",
)
)
def create_attachment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} created an attachment",
verb="created",
actor=actor,
field="attachment",
new_value=current_instance.get("access", ""),
new_identifier=current_instance.get("id", None),
)
)
def delete_attachment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"{actor.email} deleted the attachment",
verb="deleted",
actor=actor,
field="attachment",
)
)
# Receive message from room group
@job("default")
def issue_activity(event):
@shared_task
def issue_activity(
type, requested_data, current_instance, issue_id, actor_id, project_id
):
try:
issue_activities = []
type = event.get("type")
requested_data = (
json.loads(event.get("requested_data"))
if event.get("current_instance") is not None
else None
)
current_instance = (
json.loads(event.get("current_instance"))
if event.get("current_instance") is not None
else None
)
issue_id = event.get("issue_id", None)
actor_id = event.get("actor_id")
project_id = event.get("project_id")
actor = User.objects.get(pk=actor_id)
project = Project.objects.get(pk=project_id)
ACTIVITY_MAPPER = {
@@ -767,6 +966,15 @@ def issue_activity(event):
"comment.activity.created": create_comment_activity,
"comment.activity.updated": update_comment_activity,
"comment.activity.deleted": delete_comment_activity,
"cycle.activity.created": create_cycle_issue_activity,
"cycle.activity.deleted": delete_cycle_issue_activity,
"module.activity.created": create_module_issue_activity,
"module.activity.deleted": delete_module_issue_activity,
"link.activity.created": create_link_activity,
"link.activity.updated": update_link_activity,
"link.activity.deleted": delete_link_activity,
"attachment.activity.created": create_attachment_activity,
"attachment.activity.deleted": delete_attachment_activity,
}
func = ACTIVITY_MAPPER.get(type)
@@ -2,20 +2,20 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
@job("default")
@shared_task
def magic_link(email, key, token, current_site):
try:
realtivelink = f"/magic-sign-in/?password={token}&key={key}"
abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"Login for Plane"
@@ -30,6 +30,5 @@ def magic_link(email, key, token, current_site):
msg.send()
return
except Exception as e:
print(e)
capture_exception(e)
return
@@ -2,20 +2,19 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import Project, User, ProjectMemberInvite
@job("default")
@shared_task
def project_invitation(email, project_id, token, current_site):
try:
project = Project.objects.get(pk=project_id)
project_member_invite = ProjectMemberInvite.objects.get(
token=token, email=email
@@ -24,7 +23,7 @@ def project_invitation(email, project_id, token, current_site):
relativelink = f"/project-member-invitation/{project_member_invite.id}"
abs_url = "http://" + current_site + relativelink
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane"
@@ -35,7 +34,9 @@ def project_invitation(email, project_id, token, current_site):
"invitation_url": abs_url,
}
html_content = render_to_string("emails/invitations/project_invitation.html", context)
html_content = render_to_string(
"emails/invitations/project_invitation.html", context
)
text_content = strip_tags(html_content)
@@ -49,6 +50,5 @@ def project_invitation(email, project_id, token, current_site):
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e:
return
except Exception as e:
print(e)
capture_exception(e)
return
@@ -0,0 +1,56 @@
# Django imports
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
# Module imports
from plane.db.models import User
@shared_task
def send_welcome_email(user_id, created, message):
try:
instance = User.objects.get(pk=user_id)
if created and not instance.is_bot:
first_name = instance.first_name.capitalize()
to_email = instance.email
from_email_string = settings.EMAIL_FROM
subject = f"Welcome to Plane ✈️!"
context = {"first_name": first_name, "email": instance.email}
html_content = render_to_string(
"emails/auth/user_welcome_email.html", context
)
text_content = strip_tags(html_content)
msg = EmailMultiAlternatives(
subject, text_content, from_email_string, [to_email]
)
msg.attach_alternative(html_content, "text/html")
msg.send()
# Send message on slack as well
if settings.SLACK_BOT_TOKEN:
client = WebClient(token=settings.SLACK_BOT_TOKEN)
try:
_ = client.chat_postMessage(
channel="#trackers",
text=message,
)
except SlackApiError as e:
print(f"Got an error: {e.response['error']}")
return
except Exception as e:
capture_exception(e)
return
@@ -5,7 +5,7 @@ from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from django_rq import job
from celery import shared_task
from sentry_sdk import capture_exception
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
@@ -14,7 +14,7 @@ from slack_sdk.errors import SlackApiError
from plane.db.models import Workspace, User, WorkspaceMemberInvite
@job("default")
@shared_task
def workspace_invitation(email, workspace_id, token, current_site, invitor):
try:
workspace = Workspace.objects.get(pk=workspace_id)
@@ -27,7 +27,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
)
abs_url = "http://" + current_site + realtivelink
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"{invitor or email} invited you to join {workspace.name} on Plane"
+17
View File
@@ -0,0 +1,17 @@
import os
from celery import Celery
from plane.settings.redis import redis_instance
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
ri = redis_instance()
app = Celery("plane")
# Using a string here means the worker will not have to
# pickle the object when using Windows.
app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
@@ -0,0 +1,19 @@
# Generated by Django 3.2.18 on 2023-04-04 21:50
from django.db import migrations, models
import plane.db.models.project
class Migration(migrations.Migration):
dependencies = [
('db', '0025_auto_20230331_0203'),
]
operations = [
migrations.AlterField(
model_name='projectmember',
name='view_props',
field=models.JSONField(default=plane.db.models.project.get_default_props),
),
]
@@ -0,0 +1,97 @@
# Generated by Django 3.2.18 on 2023-04-08 21:42
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.issue
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0026_alter_projectmember_view_props'),
]
operations = [
migrations.CreateModel(
name='Estimate',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, verbose_name='Estimate Description')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimate_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_estimate', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimate_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_estimate', to='db.workspace')),
],
options={
'verbose_name': 'Estimate',
'verbose_name_plural': 'Estimates',
'db_table': 'estimates',
'ordering': ('name',),
'unique_together': {('name', 'project')},
},
),
migrations.RemoveField(
model_name='issue',
name='attachments',
),
migrations.AddField(
model_name='issue',
name='estimate_point',
field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(7)]),
),
migrations.CreateModel(
name='IssueAttachment',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('attributes', models.JSONField(default=dict)),
('asset', models.FileField(upload_to=plane.db.models.issue.get_upload_path, validators=[plane.db.models.issue.file_size])),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueattachment_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_attachment', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueattachment', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueattachment_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issueattachment', to='db.workspace')),
],
options={
'verbose_name': 'Issue Attachment',
'verbose_name_plural': 'Issue Attachments',
'db_table': 'issue_attachments',
'ordering': ('-created_at',),
},
),
migrations.AddField(
model_name='project',
name='estimate',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='db.estimate'),
),
migrations.CreateModel(
name='EstimatePoint',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('key', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(7)])),
('description', models.TextField(blank=True)),
('value', models.CharField(max_length=20)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimatepoint_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('estimate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='points', to='db.estimate')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_estimatepoint', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimatepoint_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_estimatepoint', to='db.workspace')),
],
options={
'verbose_name': 'Estimate Point',
'verbose_name_plural': 'Estimate Points',
'db_table': 'estimate_points',
'ordering': ('value',),
'unique_together': {('value', 'estimate')},
},
),
]
@@ -0,0 +1,48 @@
# Generated by Django 3.2.18 on 2023-04-14 11:33
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0027_auto_20230409_0312'),
]
operations = [
migrations.AddField(
model_name='user',
name='theme',
field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='issue',
name='estimate_point',
field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(7)]),
),
migrations.CreateModel(
name='WorkspaceTheme',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=300)),
('colors', models.JSONField(default=dict)),
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='themes', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacetheme_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacetheme_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='themes', to='db.workspace')),
],
options={
'verbose_name': 'Workspace Theme',
'verbose_name_plural': 'Workspace Themes',
'db_table': 'workspace_themes',
'ordering': ('-created_at',),
'unique_together': {('workspace', 'name')},
},
),
]
@@ -0,0 +1,58 @@
# Generated by Django 3.2.18 on 2023-05-01 19:56
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0028_auto_20230414_1703'),
]
operations = [
migrations.AddField(
model_name='cycle',
name='view_props',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='importer',
name='imported_data',
field=models.JSONField(null=True),
),
migrations.AddField(
model_name='module',
name='view_props',
field=models.JSONField(default=dict),
),
migrations.CreateModel(
name='SlackProjectSync',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('access_token', models.CharField(max_length=300)),
('scopes', models.TextField()),
('bot_user_id', models.CharField(max_length=50)),
('webhook_url', models.URLField(max_length=1000)),
('data', models.JSONField(default=dict)),
('team_id', models.CharField(max_length=30)),
('team_name', models.CharField(max_length=300)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_slackprojectsync', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_slackprojectsync', to='db.workspace')),
('workspace_integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slack_syncs', to='db.workspaceintegration')),
],
options={
'verbose_name': 'Slack Project Sync',
'verbose_name_plural': 'Slack Project Syncs',
'db_table': 'slack_project_syncs',
'ordering': ('-created_at',),
'unique_together': {('team_id', 'project')},
},
),
]
+6 -1
View File
@@ -8,6 +8,7 @@ from .workspace import (
Team,
WorkspaceMemberInvite,
TeamMember,
WorkspaceTheme,
)
from .project import (
@@ -32,6 +33,7 @@ from .issue import (
IssueBlocker,
IssueLink,
IssueSequence,
IssueAttachment,
)
from .asset import FileAsset
@@ -57,8 +59,11 @@ from .integration import (
GithubRepositorySync,
GithubIssueSync,
GithubCommentSync,
SlackProjectSync,
)
from .importer import Importer
from .page import Page, PageBlock, PageFavorite, PageLabel
from .page import Page, PageBlock, PageFavorite, PageLabel
from .estimate import Estimate, EstimatePoint
+1
View File
@@ -16,6 +16,7 @@ class Cycle(ProjectBaseModel):
on_delete=models.CASCADE,
related_name="owned_by_cycle",
)
view_props = models.JSONField(default=dict)
class Meta:
verbose_name = "Cycle"
+46
View File
@@ -0,0 +1,46 @@
# Django imports
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
# Module imports
from . import ProjectBaseModel
class Estimate(ProjectBaseModel):
name = models.CharField(max_length=255)
description = models.TextField(verbose_name="Estimate Description", blank=True)
def __str__(self):
"""Return name of the estimate"""
return f"{self.name} <{self.project.name}>"
class Meta:
unique_together = ["name", "project"]
verbose_name = "Estimate"
verbose_name_plural = "Estimates"
db_table = "estimates"
ordering = ("name",)
class EstimatePoint(ProjectBaseModel):
estimate = models.ForeignKey(
"db.Estimate",
on_delete=models.CASCADE,
related_name="points",
)
key = models.IntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(7)]
)
description = models.TextField(blank=True)
value = models.CharField(max_length=20)
def __str__(self):
"""Return name of the estimate"""
return f"{self.estimate.name} <{self.key}> <{self.value}>"
class Meta:
unique_together = ["value", "estimate"]
verbose_name = "Estimate Point"
verbose_name_plural = "Estimate Points"
db_table = "estimate_points"
ordering = ("value",)
+1
View File
@@ -33,6 +33,7 @@ class Importer(ProjectBaseModel):
token = models.ForeignKey(
"db.APIToken", on_delete=models.CASCADE, related_name="importer"
)
imported_data = models.JSONField(null=True)
class Meta:
verbose_name = "Importer"
@@ -1,2 +1,3 @@
from .base import Integration, WorkspaceIntegration
from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync
from .slack import SlackProjectSync
@@ -0,0 +1,32 @@
# Python imports
import uuid
# Django imports
from django.db import models
# Module imports
from plane.db.models import ProjectBaseModel
class SlackProjectSync(ProjectBaseModel):
access_token = models.CharField(max_length=300)
scopes = models.TextField()
bot_user_id = models.CharField(max_length=50)
webhook_url = models.URLField(max_length=1000)
data = models.JSONField(default=dict)
team_id = models.CharField(max_length=30)
team_name = models.CharField(max_length=300)
workspace_integration = models.ForeignKey(
"db.WorkspaceIntegration", related_name="slack_syncs", on_delete=models.CASCADE
)
def __str__(self):
"""Return the repo name"""
return f"{self.project.name}"
class Meta:
unique_together = ["team_id", "project"]
verbose_name = "Slack Project Sync"
verbose_name_plural = "Slack Project Syncs"
db_table = "slack_project_syncs"
ordering = ("-created_at",)
+40 -1
View File
@@ -1,3 +1,6 @@
# Python import
from uuid import uuid4
# Django imports
from django.contrib.postgres.fields import ArrayField
from django.db import models
@@ -5,6 +8,8 @@ from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError
# Module imports
from . import ProjectBaseModel
@@ -33,6 +38,9 @@ class Issue(ProjectBaseModel):
blank=True,
related_name="state_issue",
)
estimate_point = models.IntegerField(
validators=[MinValueValidator(0), MaxValueValidator(7)], null=True, blank=True
)
name = models.CharField(max_length=255, verbose_name="Issue Name")
description = models.JSONField(blank=True, default=dict)
description_html = models.TextField(blank=True, default="<p></p>")
@@ -54,7 +62,6 @@ class Issue(ProjectBaseModel):
through_fields=("issue", "assignee"),
)
sequence_id = models.IntegerField(default=1, verbose_name="Issue Sequence ID")
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
labels = models.ManyToManyField(
"db.Label", blank=True, related_name="labels", through="IssueLabel"
)
@@ -194,6 +201,38 @@ class IssueLink(ProjectBaseModel):
return f"{self.issue.name} {self.url}"
def get_upload_path(instance, filename):
return f"{instance.workspace.id}/{uuid4().hex}-{filename}"
def file_size(value):
limit = 5 * 1024 * 1024
if value.size > limit:
raise ValidationError("File too large. Size should not exceed 5 MB.")
class IssueAttachment(ProjectBaseModel):
attributes = models.JSONField(default=dict)
asset = models.FileField(
upload_to=get_upload_path,
validators=[
file_size,
],
)
issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, related_name="issue_attachment"
)
class Meta:
verbose_name = "Issue Attachment"
verbose_name_plural = "Issue Attachments"
db_table = "issue_attachments"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.asset}"
class IssueActivity(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.SET_NULL, null=True, related_name="issue_activity"
+1
View File
@@ -39,6 +39,7 @@ class Module(ProjectBaseModel):
through="ModuleMember",
through_fields=("module", "member"),
)
view_props = models.JSONField(default=dict)
class Meta:
unique_together = ["name", "project"]
+5 -2
View File
@@ -26,7 +26,7 @@ def get_default_props():
"collapsed": True,
"issueView": "list",
"filterIssue": None,
"groupByProperty": True,
"groupByProperty": None,
"showEmptyGroups": True,
}
@@ -69,6 +69,9 @@ class Project(BaseModel):
issue_views_view = models.BooleanField(default=True)
page_view = models.BooleanField(default=True)
cover_image = models.URLField(blank=True, null=True, max_length=800)
estimate = models.ForeignKey(
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
)
def __str__(self):
"""Return name of the project"""
@@ -130,7 +133,7 @@ class ProjectMember(ProjectBaseModel):
)
comment = models.TextField(blank=True, null=True)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
view_props = models.JSONField(null=True)
view_props = models.JSONField(default=get_default_props)
default_props = models.JSONField(default=get_default_props)
class Meta:
+2 -1
View File
@@ -72,6 +72,7 @@ class User(AbstractBaseUser, PermissionsMixin):
my_issues_prop = models.JSONField(null=True)
role = models.CharField(max_length=300, null=True, blank=True)
is_bot = models.BooleanField(default=False)
theme = models.JSONField(default=dict)
USERNAME_FIELD = "email"
@@ -108,7 +109,7 @@ def send_welcome_email(sender, instance, created, **kwargs):
if created and not instance.is_bot:
first_name = instance.first_name.capitalize()
to_email = instance.email
from_email_string = f"Team Plane <team@mailer.plane.so>"
from_email_string = settings.EMAIL_FROM
subject = f"Welcome to Plane ✈️!"
+21 -2
View File
@@ -36,7 +36,6 @@ class Workspace(BaseModel):
ordering = ("-created_at",)
class WorkspaceMember(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member"
@@ -111,7 +110,6 @@ class Team(BaseModel):
class TeamMember(BaseModel):
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="team_member"
)
@@ -129,3 +127,24 @@ class TeamMember(BaseModel):
verbose_name_plural = "Team Members"
db_table = "team_members"
ordering = ("-created_at",)
class WorkspaceTheme(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="themes"
)
name = models.CharField(max_length=300)
actor = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="themes"
)
colors = models.JSONField(default=dict)
def __str__(self):
return str(self.name) + str(self.actor.email)
class Meta:
unique_together = ["workspace", "name"]
verbose_name = "Workspace Theme"
verbose_name_plural = "Workspace Themes"
db_table = "workspace_themes"
ordering = ("-created_at",)
+7 -3
View File
@@ -35,7 +35,6 @@ INSTALLED_APPS = [
"rest_framework_simplejwt.token_blacklist",
"corsheaders",
"taggit",
"django_rq",
]
MIDDLEWARE = [
@@ -175,11 +174,12 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# Host for sending e-mail.
EMAIL_HOST = os.environ.get("EMAIL_HOST")
# Port for sending e-mail.
EMAIL_PORT = 587
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587))
# Optional SMTP authentication information for EMAIL_HOST.
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = True
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "1") == "1"
EMAIL_FROM = os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>")
SIMPLE_JWT = {
@@ -208,3 +208,7 @@ SIMPLE_JWT = {
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['application/json']
+7 -9
View File
@@ -59,16 +59,8 @@ if os.environ.get("SENTRY_DSN", False):
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_URL = False
REDIS_URL = os.environ.get("REDIS_URL")
RQ_QUEUES = {
"default": {
"HOST": "localhost",
"PORT": 6379,
"DB": 0,
"DEFAULT_TIMEOUT": 360,
},
}
MEDIA_URL = "/uploads/"
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads")
@@ -88,3 +80,9 @@ GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL")
CELERY_BROKER_URL = os.environ.get("REDIS_URL")
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
+17 -1
View File
@@ -1,5 +1,7 @@
"""Production settings and globals."""
from urllib.parse import urlparse
import ssl
import certifi
import dj_database_url
from urllib.parse import urlparse
@@ -103,7 +105,7 @@ if (
AWS_S3_ADDRESSING_STYLE = "auto"
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = ""
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = ""
@@ -236,3 +238,17 @@ GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
redis_url = os.environ.get("REDIS_URL")
broker_url = (
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
)
if DOCKERIZED:
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
else:
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
+19 -17
View File
@@ -1,23 +1,25 @@
import os
import redis
from django.conf import settings
from urllib.parse import urlparse
def redis_instance():
# Run in local redis url is false
if not settings.REDIS_URL:
ri = redis.StrictRedis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0)
# connect to redis
if (
settings.DOCKERIZED
or os.environ.get("DJANGO_SETTINGS_MODULE", "plane.settings.production")
== "plane.settings.local"
):
ri = redis.Redis.from_url(settings.REDIS_URL, db=0)
else:
# Run in prod redis url is true check with dockerized value
if settings.DOCKERIZED:
ri = redis.from_url(settings.REDIS_URL, db=0)
else:
url = urlparse(settings.REDIS_URL)
ri = redis.Redis(
host=url.hostname,
port=url.port,
password=url.password,
ssl=True,
ssl_cert_reqs=None,
)
return ri
url = urlparse(settings.REDIS_URL)
ri = redis.Redis(
host=url.hostname,
port=url.port,
password=url.password,
ssl=True,
ssl_cert_reqs=None,
)
return ri
+11 -2
View File
@@ -1,5 +1,7 @@
"""Production settings and globals."""
from urllib.parse import urlparse
import ssl
import certifi
import dj_database_url
from urllib.parse import urlparse
@@ -9,7 +11,6 @@ from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa
# Database
DEBUG = True
DATABASES = {
@@ -79,7 +80,7 @@ AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
AWS_S3_ADDRESSING_STYLE = "auto"
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = ""
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = ""
@@ -197,3 +198,11 @@ GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
redis_url = os.environ.get("REDIS_URL")
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
+6 -6
View File
@@ -3,33 +3,33 @@ from requests.auth import HTTPBasicAuth
from sentry_sdk import capture_exception
def jira_project_issue_summary(email, api_token, project_name, hostname):
def jira_project_issue_summary(email, api_token, project_key, hostname):
try:
auth = HTTPBasicAuth(email, api_token)
headers = {"Accept": "application/json"}
issue_url = f"https://{hostname}/rest/api/3/search?jql=project={project_name} AND issuetype=Story"
issue_url = f"https://{hostname}/rest/api/3/search?jql=project={project_key} AND issuetype=Story"
issue_response = requests.request(
"GET", issue_url, headers=headers, auth=auth
).json()["total"]
module_url = f"https://{hostname}/rest/api/3/search?jql=project={project_name} AND issuetype=Epic"
module_url = f"https://{hostname}/rest/api/3/search?jql=project={project_key} AND issuetype=Epic"
module_response = requests.request(
"GET", module_url, headers=headers, auth=auth
).json()["total"]
status_url = f"https://{hostname}/rest/api/3/status/?jql=project={project_name}"
status_url = f"https://{hostname}/rest/api/3/status/?jql=project={project_key}"
status_response = requests.request(
"GET", status_url, headers=headers, auth=auth
).json()
labels_url = f"https://{hostname}/rest/api/3/label/?jql=project={project_name}"
labels_url = f"https://{hostname}/rest/api/3/label/?jql=project={project_key}"
labels_response = requests.request(
"GET", labels_url, headers=headers, auth=auth
).json()["total"]
users_url = (
f"https://{hostname}/rest/api/3/users/search?jql=project={project_name}"
f"https://{hostname}/rest/api/3/users/search?jql=project={project_key}"
)
users_response = requests.request(
"GET", users_url, headers=headers, auth=auth
@@ -5,6 +5,7 @@ from urllib.parse import urlparse, parse_qs
from datetime import datetime, timedelta
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.backends import default_backend
from django.conf import settings
def get_jwt_token():
@@ -128,3 +129,24 @@ def get_github_repo_details(access_tokens_url, owner, repo):
).json()
return open_issues, total_labels, collaborators
def get_release_notes():
token = settings.GITHUB_ACCESS_TOKEN
if token:
headers = {
"Authorization": "Bearer " + str(token),
"Accept": "application/vnd.github.v3+json",
}
else:
headers = {
"Accept": "application/vnd.github.v3+json",
}
url = "https://api.github.com/repos/makeplane/plane/releases?per_page=5&page=1"
response = requests.get(url, headers=headers)
if response.status_code != 200:
return {"error": "Unable to render information from Github Repository"}
return response.json()
+12
View File
@@ -13,6 +13,17 @@ def filter_state(params, filter, method):
return filter
def filter_estimate_point(params, filter, method):
if method == "GET":
estimate_points = params.get("estimate_point").split(",")
if len(estimate_points) and "" not in estimate_points:
filter["estimate_point__in"] = estimate_points
else:
if params.get("estimate_point", None) and len(params.get("estimate_point")):
filter["estimate_point__in"] = params.get("estimate_point")
return filter
def filter_priority(params, filter, method):
if method == "GET":
priorties = params.get("priority").split(",")
@@ -192,6 +203,7 @@ def issue_filters(query_params, method):
ISSUE_FILTER = {
"state": filter_state,
"estimate_point": filter_estimate_point,
"priority": filter_priority,
"parent": filter_parent,
"labels": filter_labels,
+23
View File
@@ -0,0 +1,23 @@
# Python imports
import re
# Django imports
from django.db.models import Q
# Module imports
from plane.db.models import Issue
def search_issues(query):
fields = ["name", "sequence_id"]
q = Q()
for field in fields:
if field == "sequence_id":
sequences = re.findall(r"\d+\.\d+|\d+", query)
for sequence_id in sequences:
q |= Q(**{"sequence_id": sequence_id})
else:
q |= Q(**{f"{field}__icontains": query})
return Issue.objects.filter(
q,
).distinct()
+2 -2
View File
@@ -23,9 +23,9 @@ django-guardian==2.4.0
dj_rest_auth==2.2.5
google-auth==2.16.0
google-api-python-client==2.75.0
django-rq==2.6.0
django-redis==5.2.0
uvicorn==0.20.0
channels==4.0.0
openai==0.27.2
slack-sdk==3.20.2
slack-sdk==3.20.2
celery==5.2.7
+1 -1
View File
@@ -1 +1 @@
python-3.11.2
python-3.11.3
@@ -144,7 +144,7 @@
<p style="margin: 0;">We have put together some resources to help you get started. Please find them below:</p>
<p style="margin: 0;"> </p>
<ul style="margin: 0; margin-top:20px;">
<li><a href="https://docs.plane.so/get-started" target="_blank" style="color: #0092ff; text-decoration: underline;">Getting started with Plane</a></li>
<li><a href="https://docs.plane.so/quick-start" target="_blank" style="color: #0092ff; text-decoration: underline;">Getting started with Plane</a></li>
<li><a href="https://plane.so/changelog" target="_blank" style="color: #0092ff; text-decoration: underline;">Plane Changelog</a></li>
</ul>
</div>
-8
View File
@@ -1,8 +0,0 @@
# Replace with your instance Public IP
# NEXT_PUBLIC_API_BASE_URL = "http://localhost"
NEXT_PUBLIC_GOOGLE_CLIENTID=""
NEXT_PUBLIC_GITHUB_APP_NAME=""
NEXT_PUBLIC_GITHUB_ID=""
NEXT_PUBLIC_SENTRY_DSN=""
NEXT_PUBLIC_ENABLE_OAUTH=0
NEXT_PUBLIC_ENABLE_SENTRY=0
+24 -3
View File
@@ -3,6 +3,7 @@ RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
COPY . .
@@ -12,10 +13,10 @@ RUN turbo prune --scope=app --docker
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
@@ -26,9 +27,17 @@ RUN yarn install
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
FROM node:18-alpine AS runner
WORKDIR /app
@@ -43,8 +52,20 @@ COPY --from=installer /app/apps/app/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
# COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone/node_modules ./apps/app/node_modules
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
COPY --from=installer --chown=captain:plane /app/apps/app/.next ./apps/app/.next
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
USER root
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN chmod +x /usr/local/bin/start.sh
USER captain
ENV NEXT_TELEMETRY_DISABLED 1
@@ -92,13 +92,13 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
<>
<form className="space-y-5 py-5 px-5">
{(codeSent || codeResent) && (
<div className="rounded-md bg-green-50 p-4">
<div className="rounded-md bg-green-500/20 p-4">
<div className="flex">
<div className="flex-shrink-0">
<CheckCircleIcon className="h-5 w-5 text-green-400" aria-hidden="true" />
<CheckCircleIcon className="h-5 w-5 text-green-500" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-800">
<p className="text-sm font-medium text-green-500">
{codeResent
? "Please check your mail for new code."
: "Please check your mail for code."}
@@ -141,7 +141,9 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
<button
type="button"
className={`mt-5 flex w-full justify-end text-xs outline-none ${
isResendDisabled ? "cursor-default text-gray-400" : "cursor-pointer text-theme"
isResendDisabled
? "cursor-default text-brand-secondary"
: "cursor-pointer text-brand-accent"
} `}
onClick={() => {
setIsCodeResending(true);
@@ -174,13 +176,13 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
className="w-full text-center"
size="md"
onClick={handleSubmit(handleSignin)}
loading={isSubmitting || (!isValid && isDirty)}
disabled={!isValid && isDirty}
loading={isSubmitting}
>
{isSubmitting ? "Signing in..." : "Sign in"}
</PrimaryButton>
) : (
<PrimaryButton
type="submit"
className="w-full text-center"
size="md"
onClick={() => {
@@ -50,7 +50,7 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
if (!error?.response?.data) return;
Object.keys(error.response.data).forEach((key) => {
const err = error.response.data[key];
console.log("err", err);
console.log(err);
setError(key as keyof EmailPasswordFormValues, {
type: "manual",
message: Array.isArray(err) ? err.join(", ") : err,
@@ -94,7 +94,9 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
<div className="mt-2 flex items-center justify-between">
<div className="ml-auto text-sm">
<Link href={"/forgot-password"}>
<a className="font-medium text-theme hover:text-indigo-500">Forgot your password?</a>
<a className="font-medium text-brand-accent hover:text-brand-accent">
Forgot your password?
</a>
</Link>
</div>
</div>
@@ -102,7 +104,8 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
<SecondaryButton
type="submit"
className="w-full text-center"
loading={isSubmitting || (!isValid && isDirty)}
disabled={!isValid && isDirty}
loading={isSubmitting}
>
{isSubmitting ? "Signing in..." : "Sign In"}
</SecondaryButton>
@@ -33,11 +33,11 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
}, []);
return (
<div className="px-1 w-full">
<div className="w-full px-1">
<Link
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
>
<button className="flex w-full items-center justify-center gap-3 rounded-md border border-gray-200 p-2 text-sm font-medium text-gray-600 duration-300 hover:bg-gray-50">
<button className="flex w-full items-center justify-center gap-3 rounded-md border border-brand-base p-2 text-sm font-medium text-brand-secondary duration-300 hover:bg-brand-surface-2">
<Image src={githubImage} height={22} width={22} color="#000" alt="GitHub Logo" />
<span>Sign In with Github</span>
</button>
+1 -1
View File
@@ -47,7 +47,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
return (
<>
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
<div className="h-12" id="googleSignInButton" ref={googleSignInButton} />
<div className="overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
</>
);
};
@@ -0,0 +1,3 @@
export * from "./project";
export * from "./workspace";
export * from "./not-authorized-view";
@@ -7,16 +7,16 @@ import { useRouter } from "next/router";
import DefaultLayout from "layouts/default-layout";
// hooks
import useUser from "hooks/use-user";
// img
import ProjectSettingImg from "public/project-setting.svg";
// images
import ProjectNotAuthorizedImg from "public/auth/project-not-authorized.svg";
import WorkspaceNotAuthorizedImg from "public/auth/workspace-not-authorized.svg";
type TNotAuthorizedViewProps = {
type Props = {
actionButton?: React.ReactNode;
type: "project" | "workspace";
};
export const NotAuthorizedView: React.FC<TNotAuthorizedViewProps> = (props) => {
const { actionButton } = props;
export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
const { user } = useUser();
const { asPath: currentPath } = useRouter();
@@ -29,26 +29,29 @@ export const NotAuthorizedView: React.FC<TNotAuthorizedViewProps> = (props) => {
>
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 text-center">
<div className="h-44 w-72">
<Image src={ProjectSettingImg} height="176" width="288" alt="ProjectSettingImg" />
<Image
src={type === "project" ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg}
height="176"
width="288"
alt="ProjectSettingImg"
/>
</div>
<h1 className="text-xl font-medium text-gray-900">
Oops! You are not authorized to view this page
</h1>
<h1 className="text-xl font-medium">Oops! You are not authorized to view this page</h1>
<div className="w-full text-base text-gray-500 max-w-md ">
<div className="w-full max-w-md text-base text-brand-secondary">
{user ? (
<p className="">
You have signed in as {user.email}.{" "}
<p>
You have signed in as {user.email}. <br />
<Link href={`/signin?next=${currentPath}`}>
<a className="text-gray-900 font-medium">Sign in</a>
<a className="font-medium text-brand-base">Sign in</a>
</Link>{" "}
with different account that has access to this page.
</p>
) : (
<p className="">
<p>
You need to{" "}
<Link href={`/signin?next=${currentPath}`}>
<a className="text-gray-900 font-medium">Sign in</a>
<a className="font-medium text-brand-base">Sign in</a>
</Link>{" "}
with an account that has access to this page.
</p>
@@ -0,0 +1 @@
export * from "./join-project";
@@ -0,0 +1,68 @@
import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { mutate } from "swr";
// services
import projectService from "services/project.service";
// ui
import { PrimaryButton } from "components/ui";
// icons
import { AssignmentClipboardIcon } from "components/icons";
// images
import JoinProjectImg from "public/auth/project-not-authorized.svg";
// fetch-keys
import { USER_PROJECT_VIEW } from "constants/fetch-keys";
export const JoinProject: React.FC = () => {
const [isJoiningProject, setIsJoiningProject] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const handleJoin = () => {
if (!workspaceSlug || !projectId) return;
setIsJoiningProject(true);
projectService
.joinProject(workspaceSlug as string, {
project_ids: [projectId as string],
})
.then(async () => {
await mutate(USER_PROJECT_VIEW(projectId.toString()));
setIsJoiningProject(false);
})
.catch((err) => {
console.error(err);
setIsJoiningProject(false);
});
};
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 text-center">
<div className="h-44 w-72">
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
</div>
<h1 className="text-xl font-medium">You are not a member of this project</h1>
<div className="w-full max-w-md text-base text-brand-secondary">
<p className="mx-auto w-full text-sm md:w-3/4">
You are not a member of this project, but you can join this project by clicking the button
below.
</p>
</div>
<div>
<PrimaryButton
className="flex items-center gap-1"
loading={isJoiningProject}
onClick={handleJoin}
>
<AssignmentClipboardIcon height={16} width={16} color="white" />
{isJoiningProject ? "Joining..." : "Click to join"}
</PrimaryButton>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./not-a-member";
@@ -0,0 +1,44 @@
import Link from "next/link";
import { useRouter } from "next/router";
// layouts
import DefaultLayout from "layouts/default-layout";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
export const NotAWorkspaceMember = () => {
const router = useRouter();
return (
<DefaultLayout
meta={{
title: "Plane - Unauthorized User",
description: "Unauthorized user",
}}
>
<div className="grid h-full place-items-center p-4">
<div className="space-y-8 text-center">
<div className="space-y-2">
<h3 className="text-lg font-semibold">Not Authorized!</h3>
<p className="mx-auto w-1/2 text-sm text-brand-secondary">
You{"'"}re not a member of this workspace. Please contact the workspace admin to get
an invitation or check your pending invitations.
</p>
</div>
<div className="flex items-center justify-center gap-2">
<Link href="/invitations">
<a>
<SecondaryButton>Check pending invites</SecondaryButton>
</a>
</Link>
<Link href="/create-workspace">
<a>
<PrimaryButton>Create new workspace</PrimaryButton>
</a>
</Link>
</div>
</div>
</div>
</DefaultLayout>
);
};
+13 -3
View File
@@ -16,8 +16,18 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
<div className="flex items-center">
<button
type="button"
className="grid h-8 w-8 flex-shrink-0 cursor-pointer place-items-center rounded border border-gray-300 text-center text-sm hover:bg-gray-100"
onClick={() => router.back()}
className="grid h-8 w-8 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1"
onClick={() => {
const lastTenUrls = JSON.parse(localStorage.getItem("lastTenUrls") || "[]");
if (lastTenUrls.length > 0) {
const url = lastTenUrls[1];
lastTenUrls.splice(0, 2);
localStorage.setItem("lastTenUrls", JSON.stringify(lastTenUrls));
router.push(url);
} else {
router.push("/");
}
}}
>
<ArrowLeftIcon className="h-3 w-3" />
</button>
@@ -37,7 +47,7 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
<>
{link ? (
<Link href={link}>
<a className="border-r-2 border-gray-300 px-3 text-sm">
<a className="border-r-2 border-brand-base px-3 text-sm">
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
{icon ?? null}
{title}
@@ -0,0 +1,45 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
// cmdk
import { Command } from "cmdk";
import { THEMES_OBJ } from "constants/themes";
import { useTheme } from "next-themes";
import { SettingIcon } from "components/icons";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
};
export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
const [mounted, setMounted] = useState(false);
const { setTheme } = useTheme();
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
{THEMES_OBJ.map((theme) => (
<Command.Item
key={theme.value}
onSelect={() => {
setTheme(theme.value);
setIsPaletteOpen(false);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
{theme.label}
</div>
</Command.Item>
))}
</>
);
};
@@ -14,7 +14,7 @@ import stateService from "services/state.service";
// types
import { IIssue } from "types";
// fetch keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATE_LIST } from "constants/fetch-keys";
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
// icons
import { CheckIcon, getStateGroupIcon } from "components/icons";
@@ -28,7 +28,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) =
const { workspaceSlug, projectId, issueId } = router.query;
const { data: stateGroups, mutate: mutateIssueDetails } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
@@ -44,6 +44,7 @@ import {
ChangeIssueState,
ChangeIssuePriority,
ChangeIssueAssignee,
ChangeInterfaceTheme,
} from "components/command-palette";
import { BulkDeleteIssuesModal } from "components/core";
import { CreateUpdateCycleModal } from "components/cycles";
@@ -177,46 +178,49 @@ export const CommandPalette: React.FC = () => {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const singleShortcutKeys = ["p", "v", "d", "h", "q", "m"];
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
if (!key) return;
const keyPressed = key.toLowerCase();
if (
!(e.target instanceof HTMLTextAreaElement) &&
!(e.target instanceof HTMLInputElement) &&
!(e.target as Element).classList?.contains("remirror-editor")
) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
if ((ctrlKey || metaKey) && keyPressed === "k") {
e.preventDefault();
setIsPaletteOpen(true);
} else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "c") {
if (e.altKey) {
} else if ((ctrlKey || metaKey) && keyPressed === "c") {
if (altKey) {
e.preventDefault();
copyIssueUrlToClipboard();
}
} else if (e.key.toLowerCase() === "c") {
} else if (keyPressed === "c") {
e.preventDefault();
setIsIssueModalOpen(true);
} else if (e.key.toLowerCase() === "p") {
e.preventDefault();
setIsProjectModalOpen(true);
} else if (e.key.toLowerCase() === "v") {
e.preventDefault();
setIsCreateViewModalOpen(true);
} else if (e.key.toLowerCase() === "d") {
e.preventDefault();
setIsCreateUpdatePageModalOpen(true);
} else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
} else if ((ctrlKey || metaKey) && keyPressed === "b") {
e.preventDefault();
toggleCollapsed();
} else if (e.key.toLowerCase() === "h") {
e.preventDefault();
setIsShortcutsModalOpen(true);
} else if (e.key.toLowerCase() === "q") {
e.preventDefault();
setIsCreateCycleModalOpen(true);
} else if (e.key.toLowerCase() === "m") {
e.preventDefault();
setIsCreateModuleModalOpen(true);
} else if (e.key === "Delete") {
} else if (key === "Delete") {
e.preventDefault();
setIsBulkDeleteIssuesModalOpen(true);
} else if (
singleShortcutKeys.includes(keyPressed) &&
(ctrlKey || metaKey || altKey || shiftKey)
) {
e.preventDefault();
} else if (keyPressed === "p") {
setIsProjectModalOpen(true);
} else if (keyPressed === "v") {
setIsCreateViewModalOpen(true);
} else if (keyPressed === "d") {
setIsCreateUpdatePageModalOpen(true);
} else if (keyPressed === "h") {
setIsShortcutsModalOpen(true);
} else if (keyPressed === "q") {
setIsCreateCycleModalOpen(true);
} else if (keyPressed === "m") {
setIsCreateModuleModalOpen(true);
}
}
},
@@ -297,6 +301,11 @@ export const CommandPalette: React.FC = () => {
setIsCreateViewModalOpen(true);
};
const createNewPage = () => {
setIsPaletteOpen(false);
setIsCreateUpdatePageModalOpen(true);
};
const createNewModule = () => {
setIsPaletteOpen(false);
setIsCreateModuleModalOpen(true);
@@ -371,7 +380,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
@@ -384,7 +393,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-surface-2 shadow-2xl transition-all">
<Command
filter={(value, search) => {
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
@@ -407,7 +416,7 @@ export const CommandPalette: React.FC = () => {
>
{issueId && issueDetails && (
<div className="flex p-3">
<p className="overflow-hidden truncate rounded-md bg-gray-100 p-1 px-2 text-xs font-medium text-gray-500">
<p className="overflow-hidden truncate rounded-md bg-brand-surface-1 p-1 px-2 text-xs font-medium text-brand-secondary">
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
{issueDetails?.name}
</p>
@@ -415,11 +424,11 @@ export const CommandPalette: React.FC = () => {
)}
<div className="relative">
<MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-secondary"
aria-hidden="true"
/>
<Command.Input
className="w-full border-0 border-b bg-transparent p-4 pl-11 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
className="w-full border-0 border-b border-brand-base bg-transparent p-4 pl-11 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder={placeholder}
value={searchTerm}
onValueChange={(e) => {
@@ -433,7 +442,9 @@ export const CommandPalette: React.FC = () => {
resultsCount === 0 &&
searchTerm !== "" &&
debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-gray-500">No results found.</div>
<div className="my-4 text-center text-brand-secondary">
No results found.
</div>
)}
{(isLoading || isSearching) && (
@@ -494,8 +505,11 @@ export const CommandPalette: React.FC = () => {
value={value}
className="focus:outline-none"
>
<div className="flex items-center gap-2 overflow-hidden text-gray-700">
<Icon className="h-4 w-4 text-gray-500" color="#6b7280" />
<div className="flex items-center gap-2 overflow-hidden text-brand-secondary">
<Icon
className="h-4 w-4 text-brand-secondary"
color="#6b7280"
/>
<p className="block flex-1 truncate">{item.name}</p>
</div>
</Command.Item>
@@ -520,8 +534,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<Squares2X2Icon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
Change state...
</div>
</Command.Item>
@@ -533,8 +547,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<ChartBarIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<ChartBarIcon className="h-4 w-4 text-brand-secondary" />
Change priority...
</div>
</Command.Item>
@@ -546,8 +560,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<UsersIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<UsersIcon className="h-4 w-4 text-brand-secondary" />
Assign to...
</div>
</Command.Item>
@@ -558,15 +572,15 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<div className="flex items-center gap-2 text-brand-secondary">
{issueDetails?.assignees.includes(user.id) ? (
<>
<UserMinusIcon className="h-4 w-4 text-gray-500" />
<UserMinusIcon className="h-4 w-4 text-brand-secondary" />
Un-assign from me
</>
) : (
<>
<UserPlusIcon className="h-4 w-4 text-gray-500" />
<UserPlusIcon className="h-4 w-4 text-brand-secondary" />
Assign to me
</>
)}
@@ -574,8 +588,8 @@ export const CommandPalette: React.FC = () => {
</Command.Item>
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
<div className="flex items-center gap-2 text-gray-700">
<TrashIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<TrashIcon className="h-4 w-4 text-brand-secondary" />
Delete issue
</div>
</Command.Item>
@@ -586,16 +600,19 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<LinkIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<LinkIcon className="h-4 w-4 text-brand-secondary" />
Copy issue URL to clipboard
</div>
</Command.Item>
</>
)}
<Command.Group heading="Issue">
<Command.Item onSelect={createNewIssue} className="focus:bg-gray-200">
<div className="flex items-center gap-2 text-gray-700">
<Command.Item
onSelect={createNewIssue}
className="focus:bg-brand-surface-2"
>
<div className="flex items-center gap-2 text-brand-secondary">
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
Create new issue
</div>
@@ -609,7 +626,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewProject}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<div className="flex items-center gap-2 text-brand-secondary">
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
Create new project
</div>
@@ -625,7 +642,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewCycle}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<div className="flex items-center gap-2 text-brand-secondary">
<ContrastIcon className="h-4 w-4" color="#6b7280" />
Create new cycle
</div>
@@ -638,7 +655,7 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewModule}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<div className="flex items-center gap-2 text-brand-secondary">
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
Create new module
</div>
@@ -648,11 +665,21 @@ export const CommandPalette: React.FC = () => {
<Command.Group heading="View">
<Command.Item onSelect={createNewView} className="focus:outline-none">
<div className="flex items-center gap-2 text-gray-700">
<div className="flex items-center gap-2 text-brand-secondary">
<ViewListIcon className="h-4 w-4" color="#6b7280" />
Create new view
</div>
<kbd>Q</kbd>
<kbd>V</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="Page">
<Command.Item onSelect={createNewPage} className="focus:outline-none">
<div className="flex items-center gap-2 text-brand-secondary">
<DocumentTextIcon className="h-4 w-4" color="#6b7280" />
Create new page
</div>
<kbd>D</kbd>
</Command.Item>
</Command.Group>
</>
@@ -667,7 +694,7 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4" color="#6b7280" />
Search settings...
</div>
@@ -678,11 +705,24 @@ export const CommandPalette: React.FC = () => {
onSelect={createNewWorkspace}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<FolderPlusIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<FolderPlusIcon className="h-4 w-4 text-brand-secondary" />
Create new workspace
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Change interface theme...");
setSearchTerm("");
setPages([...pages, "change-interface-theme"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Change interface theme...
</div>
</Command.Item>
</Command.Group>
<Command.Group heading="Help">
<Command.Item
@@ -695,8 +735,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<RocketLaunchIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<RocketLaunchIcon className="h-4 w-4 text-brand-secondary" />
Open keyboard shortcuts
</div>
</Command.Item>
@@ -707,8 +747,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<DocumentIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<DocumentIcon className="h-4 w-4 text-brand-secondary" />
Open Plane documentation
</div>
</Command.Item>
@@ -719,7 +759,7 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<div className="flex items-center gap-2 text-brand-secondary">
<DiscordIcon className="h-4 w-4" color="#6b7280" />
Join our Discord
</div>
@@ -734,7 +774,7 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<div className="flex items-center gap-2 text-brand-secondary">
<GithubIcon className="h-4 w-4" color="#6b7280" />
Report a bug
</div>
@@ -746,8 +786,8 @@ export const CommandPalette: React.FC = () => {
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-brand-secondary" />
Chat with us
</div>
</Command.Item>
@@ -761,8 +801,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings()}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<SettingIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
General
</div>
</Command.Item>
@@ -770,8 +810,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("members")}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<SettingIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Members
</div>
</Command.Item>
@@ -779,8 +819,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("billing")}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<SettingIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Billings and Plans
</div>
</Command.Item>
@@ -788,8 +828,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("integrations")}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<SettingIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Integrations
</div>
</Command.Item>
@@ -797,8 +837,8 @@ export const CommandPalette: React.FC = () => {
onSelect={() => goToSettings("import-export")}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-gray-700">
<SettingIcon className="h-4 w-4 text-gray-500" />
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Import/Export
</div>
</Command.Item>
@@ -824,6 +864,9 @@ export const CommandPalette: React.FC = () => {
setIsPaletteOpen={setIsPaletteOpen}
/>
)}
{page === "change-interface-theme" && (
<ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />
)}
</Command.List>
</Command>
</Dialog.Panel>
@@ -3,3 +3,4 @@ export * from "./shortcuts-modal";
export * from "./change-issue-state";
export * from "./change-issue-priority";
export * from "./change-issue-assignee";
export * from "./change-interface-theme";
@@ -4,7 +4,7 @@ import { Dialog, Transition } from "@headlessui/react";
// icons
import { XMarkIcon } from "@heroicons/react/20/solid";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { MacCommandIcon } from "components/icons";
import { CommandIcon } from "components/icons";
// ui
import { Input } from "components/ui";
@@ -71,7 +71,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
@@ -85,29 +85,29 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="bg-white p-5">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="bg-brand-surface-2 p-5">
<div className="sm:flex sm:items-start">
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
<Dialog.Title
as="h3"
className="flex justify-between text-lg font-medium leading-6 text-gray-900"
className="flex justify-between text-lg font-medium leading-6 text-brand-base"
>
<span>Keyboard Shortcuts</span>
<span>
<button type="button" onClick={() => setIsOpen(false)}>
<XMarkIcon
className="h-6 w-6 text-gray-400 hover:text-gray-500"
className="h-6 w-6 text-gray-400 hover:text-brand-secondary"
aria-hidden="true"
/>
</button>
</span>
</Dialog.Title>
<div>
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-gray-200 bg-gray-100 px-3 py-2">
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-gray-500" />
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-brand-base bg-brand-surface-1 px-3 py-2">
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-brand-secondary" />
<Input
className="w-full border-none bg-transparent py-1 px-2 text-xs text-gray-500 focus:outline-none"
className="w-full border-none bg-transparent py-1 px-2 text-xs text-brand-secondary focus:outline-none"
id="search"
name="search"
type="text"
@@ -123,17 +123,23 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<div key={shortcut.keys} className="flex w-full flex-col">
<div className="flex flex-col gap-y-3">
<div className="flex items-center justify-between">
<p className="text-sm text-gray-500">{shortcut.description}</p>
<p className="text-sm text-brand-secondary">
{shortcut.description}
</p>
<div className="flex items-center gap-x-2.5">
{shortcut.keys.split(",").map((key, index) => (
<span key={index} className="flex items-center gap-1">
{key === "Ctrl" ? (
<span className="flex h-full items-center rounded-sm border border-gray-200 bg-gray-100 p-2">
<MacCommandIcon />
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</span>
) : key === "Ctrl" ? (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</kbd>
) : (
<kbd className="rounded-sm border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800">
{key === "Ctrl" ? <MacCommandIcon /> : key}
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
{key}
</kbd>
)}
</span>
@@ -145,7 +151,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
))
) : (
<div className="flex flex-col gap-y-3">
<p className="text-sm text-gray-500">
<p className="text-sm text-brand-secondary">
No shortcuts found for{" "}
<span className="font-semibold italic">
{`"`}
@@ -162,17 +168,21 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<div className="flex flex-col gap-y-3">
{shortcuts.map(({ keys, description }, index) => (
<div key={index} className="flex items-center justify-between">
<p className="text-sm text-gray-500">{description}</p>
<p className="text-sm text-brand-secondary">{description}</p>
<div className="flex items-center gap-x-2.5">
{keys.split(",").map((key, index) => (
<span key={index} className="flex items-center gap-1">
{key === "Ctrl" ? (
<span className="flex h-full items-center rounded-sm border border-gray-200 bg-gray-100 p-2">
<MacCommandIcon />
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-brand-secondary">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</span>
) : key === "Ctrl" ? (
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
</kbd>
) : (
<kbd className="rounded-sm border border-gray-200 bg-gray-100 px-2 py-1 text-sm font-medium text-gray-800">
{key === "Ctrl" ? <MacCommandIcon /> : key}
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
{key}
</kbd>
)}
</span>
@@ -17,7 +17,7 @@ type Props = {
openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string) => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean;
userAuth: UserAuth;
};
@@ -81,7 +81,7 @@ export const AllBoards: React.FC<Props> = ({
return (
<div
key={index}
className="flex items-center justify-between gap-2 rounded bg-white p-2 shadow"
className="flex items-center justify-between gap-2 rounded bg-brand-surface-1 p-2 shadow"
>
<div className="flex items-center gap-2">
{currentState &&
@@ -92,7 +92,7 @@ export const AllBoards: React.FC<Props> = ({
: addSpaceIfCamelCase(singleGroup)}
</h4>
</div>
<span className="text-xs text-gray-500">0</span>
<span className="text-xs text-brand-secondary">0</span>
</div>
);
})}
@@ -96,17 +96,17 @@ export const BoardHeader: React.FC<Props> = ({
switch (selectedGroup) {
case "state":
icon = currentState && getStateGroupIcon(currentState.group, "18", "18", bgColor);
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
break;
case "priority":
icon = getPriorityIcon(groupTitle, "h-[18px] w-[18px] flex items-center");
icon = getPriorityIcon(groupTitle, "text-lg");
break;
case "labels":
const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = (
<span
className="h-[18px] w-[18px] flex-shrink-0 rounded-full"
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
style={{ backgroundColor: labelColor }}
/>
);
@@ -123,8 +123,8 @@ export const BoardHeader: React.FC<Props> = ({
return (
<div
className={`flex justify-between items-center px-1 ${
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
className={`flex items-center justify-between px-1 ${
!isCollapsed ? "flex-col rounded-md bg-brand-surface-1" : ""
}`}
>
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
@@ -142,7 +142,11 @@ export const BoardHeader: React.FC<Props> = ({
>
{getGroupTitle()}
</h2>
<span className="ml-0.5 rounded-full bg-gray-100 py-1 px-3 text-sm">
<span
className={`${
isCollapsed ? "ml-0.5" : ""
} min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs`}
>
{groupedByIssues?.[groupTitle].length ?? 0}
</span>
</div>
@@ -151,7 +155,7 @@ export const BoardHeader: React.FC<Props> = ({
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-100"
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
onClick={() => {
setIsCollapsed((prevData) => !prevData);
}}
@@ -165,7 +169,7 @@ export const BoardHeader: React.FC<Props> = ({
{!isCompleted && (
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-100"
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
onClick={addIssueToState}
>
<PlusIcon className="h-4 w-4" />
@@ -29,7 +29,7 @@ type Props = {
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string) => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean;
userAuth: UserAuth;
};
@@ -66,7 +66,7 @@ export const SingleBoard: React.FC<Props> = ({
}, [currentState]);
return (
<div className={`h-full flex-shrink-0 ${!isCollapsed ? "" : "w-96 bg-gray-50"}`}>
<div className={`h-full flex-shrink-0 ${!isCollapsed ? "" : "w-96"}`}>
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
<BoardHeader
addIssueToState={addIssueToState}
@@ -81,7 +81,7 @@ export const SingleBoard: React.FC<Props> = ({
{(provided, snapshot) => (
<div
className={`relative h-full overflow-y-auto p-1 ${
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
snapshot.isDraggingOver ? "bg-brand-base/20" : ""
} ${!isCollapsed ? "hidden" : "block"}`}
ref={provided.innerRef}
{...provided.droppableProps}
@@ -91,15 +91,17 @@ export const SingleBoard: React.FC<Props> = ({
<div
className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden"
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-gray-100 opacity-50`}
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-brand-surface-1 opacity-50`}
/>
<div
className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden"
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-white p-2 text-xs`}
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-brand-base p-2 text-xs`}
>
This board is ordered by{" "}
{replaceUnderscoreIfSnakeCase(orderBy ?? "created_at")}
{replaceUnderscoreIfSnakeCase(
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
)}
</div>
</>
)}
@@ -108,7 +110,9 @@ export const SingleBoard: React.FC<Props> = ({
key={issue.id}
draggableId={issue.id}
index={index}
isDragDisabled={isNotAllowed || selectedGroup === "created_by"}
isDragDisabled={
isNotAllowed || selectedGroup === "created_by" || selectedGroup === "labels"
}
>
{(provided, snapshot) => (
<SingleBoardIssue
@@ -126,7 +130,8 @@ export const SingleBoard: React.FC<Props> = ({
handleDeleteIssue={handleDeleteIssue}
handleTrashBox={handleTrashBox}
removeIssue={() => {
if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id);
if (removeIssue && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
}}
isCompleted={isCompleted}
userAuth={userAuth}
@@ -144,7 +149,7 @@ export const SingleBoard: React.FC<Props> = ({
{type === "issue" ? (
<button
type="button"
className="flex items-center gap-2 font-medium text-theme outline-none"
className="flex items-center gap-2 font-medium text-brand-accent outline-none"
onClick={addIssueToState}
>
<PlusIcon className="h-4 w-4" />
@@ -156,7 +161,7 @@ export const SingleBoard: React.FC<Props> = ({
customButton={
<button
type="button"
className="flex items-center gap-2 font-medium text-theme outline-none"
className="flex items-center gap-2 font-medium text-brand-accent outline-none"
>
<PlusIcon className="h-4 w-4" />
Add Issue
@@ -21,11 +21,12 @@ import useToast from "hooks/use-toast";
import {
ViewAssigneeSelect,
ViewDueDateSelect,
ViewEstimateSelect,
ViewPrioritySelect,
ViewStateSelect,
} from "components/issues/view-select";
} from "components/issues";
// ui
import { ContextMenu, CustomMenu } from "components/ui";
import { ContextMenu, CustomMenu, Tooltip } from "components/ui";
// icons
import {
ClipboardDocumentCheckIcon,
@@ -34,7 +35,7 @@ import {
TrashIcon,
XMarkIcon,
ArrowTopRightOnSquareIcon,
PaperClipIcon,
} from "@heroicons/react/24/outline";
// helpers
import { handleIssuesMutation } from "constants/issue";
@@ -109,7 +110,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
>(
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
(prevData) =>
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
handleIssuesMutation(
formData,
groupTitle ?? "",
selectedGroup,
index,
orderBy,
prevData
),
false
);
else if (moduleId)
@@ -121,10 +129,17 @@ export const SingleBoardIssue: React.FC<Props> = ({
>(
MODULE_ISSUES_WITH_PARAMS(moduleId as string),
(prevData) =>
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
handleIssuesMutation(
formData,
groupTitle ?? "",
selectedGroup,
index,
orderBy,
prevData
),
false
);
else
else {
mutate<
| {
[key: string]: IIssue[];
@@ -132,10 +147,21 @@ export const SingleBoardIssue: React.FC<Props> = ({
| IIssue[]
>(
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params),
(prevData) =>
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, prevData),
(prevData) => {
if (!prevData) return prevData;
return handleIssuesMutation(
formData,
groupTitle ?? "",
selectedGroup,
index,
orderBy,
prevData
);
},
false
);
}
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
@@ -152,7 +178,18 @@ export const SingleBoardIssue: React.FC<Props> = ({
console.log(error);
});
},
[workspaceSlug, projectId, cycleId, moduleId, issue, groupTitle, index, selectedGroup, params]
[
workspaceSlug,
projectId,
cycleId,
moduleId,
issue,
groupTitle,
index,
selectedGroup,
orderBy,
params,
]
);
const getStyle = (
@@ -224,8 +261,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
</a>
</ContextMenu>
<div
className={`mb-3 rounded bg-white shadow ${
snapshot.isDragging ? "border-2 border-theme shadow-lg" : ""
className={`mb-3 rounded bg-brand-base shadow ${
snapshot.isDragging ? "border-2 border-brand-accent shadow-lg" : ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
@@ -275,12 +312,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a>
{properties.key && (
<div className="mb-2.5 text-xs font-medium text-gray-700">
<div className="mb-2.5 text-xs font-medium text-brand-secondary">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
)}
<h5
className="break-all text-sm group-hover:text-theme"
className="break-all text-sm group-hover:text-brand-accent"
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
>
{truncateText(issue.name, 100)}
@@ -312,7 +349,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
/>
)}
{properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm">
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border border-brand-base px-2 py-1 text-xs text-brand-secondary shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
@@ -321,7 +358,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
{issue.label_details.map((label) => (
<div
key={label.id}
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
@@ -343,6 +380,34 @@ export const SingleBoardIssue: React.FC<Props> = ({
selfPositioned
/>
)}
{properties.estimate && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
selfPositioned
/>
)}
{properties.link && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
{issue.link_count}
</div>
</Tooltip>
</div>
)}
{properties.attachment_count && (
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
{issue.attachment_count}
</div>
</Tooltip>
</div>
)}
</div>
</div>
</div>
@@ -121,7 +121,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-brand-surface-2 shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
<form>
<Combobox
onChange={(val: string) => {
@@ -136,12 +136,12 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
>
<div className="relative m-1">
<MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-base text-opacity-40"
aria-hidden="true"
/>
<input
type="text"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..."
onChange={(event) => setQuery(event.target.value)}
/>
@@ -154,7 +154,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
{filteredIssues.length > 0 ? (
<li className="p-2">
{query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-brand-base">
Select issues to delete
</h2>
)}
@@ -166,7 +166,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
value={issue.id}
className={({ active }) =>
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
active ? "bg-gray-900 bg-opacity-5 text-brand-base" : ""
}`
}
>
@@ -182,7 +182,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
<span className="flex-shrink-0 text-xs text-brand-secondary">
{issue.project_detail.identifier}-{issue.sequence_id}
</span>
<span>{issue.name}</span>
@@ -194,9 +194,9 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) =>
) : (
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
<LayerDiagonalIcon height="56" width="56" />
<h3 className="text-gray-500">
<h3 className="text-brand-secondary">
No issues found. Create a new issue with{" "}
<pre className="inline rounded bg-gray-200 px-2 py-1">C</pre>.
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>.
</h3>
</div>
)}
@@ -26,14 +26,17 @@ import {
import { Popover, Transition } from "@headlessui/react";
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { CustomMenu } from "components/ui";
import { CustomMenu, Spinner, ToggleSwitch } from "components/ui";
// icon
import {
CheckIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
// hooks
import useIssuesView from "hooks/use-issues-view";
// services
import issuesService from "services/issues.service";
import cyclesService from "services/cycles.service";
@@ -48,20 +51,28 @@ import { IIssue } from "types";
// constant
import { monthOptions, yearOptions } from "constants/calendar";
import modulesService from "services/modules.service";
import { getStateGroupIcon } from "components/icons";
type Props = {
addIssueToDate: (date: string) => void;
};
interface ICalendarRange {
startDate: Date;
endDate: Date;
}
export const CalendarView = () => {
const [showWeekEnds, setShowWeekEnds] = useState<boolean>(false);
const [currentDate, setCurrentDate] = useState<Date>(new Date());
const [isMonthlyView, setIsMonthlyView] = useState<boolean>(true);
export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
const [showWeekEnds, setShowWeekEnds] = useState(false);
const [currentDate, setCurrentDate] = useState(new Date());
const [isMonthlyView, setIsMonthlyView] = useState(true);
const [showAllIssues, setShowAllIssues] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { params } = useIssuesView();
const [calendarDateRange, setCalendarDateRange] = useState<ICalendarRange>({
startDate: startOfWeek(currentDate),
endDate: lastDayOfWeek(currentDate),
@@ -77,11 +88,13 @@ export const CalendarView = () => {
workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null,
workspaceSlug && projectId
? () =>
issuesService.getIssuesWithParams(
workspaceSlug as string,
projectId as string,
targetDateFilter
)
issuesService.getIssuesWithParams(workspaceSlug as string, projectId as string, {
...params,
target_date: `${renderDateFormat(calendarDateRange.startDate)};after,${renderDateFormat(
calendarDateRange.endDate
)};before`,
group_by: null,
})
: null
);
@@ -95,7 +108,13 @@ export const CalendarView = () => {
workspaceSlug as string,
projectId as string,
cycleId as string,
targetDateFilter
{
...params,
target_date: `${renderDateFormat(
calendarDateRange.startDate
)};after,${renderDateFormat(calendarDateRange.endDate)};before`,
group_by: null,
}
)
: null
);
@@ -110,7 +129,13 @@ export const CalendarView = () => {
workspaceSlug as string,
projectId as string,
moduleId as string,
targetDateFilter
{
...params,
target_date: `${renderDateFormat(
calendarDateRange.startDate
)};after,${renderDateFormat(calendarDateRange.endDate)};before`,
group_by: null,
}
)
: null
);
@@ -127,12 +152,16 @@ export const CalendarView = () => {
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
const calendarIssues = cycleCalendarIssues ?? moduleCalendarIssues ?? projectCalendarIssues;
const calendarIssues = cycleId
? (cycleCalendarIssues as IIssue[])
: moduleId
? (moduleCalendarIssues as IIssue[])
: (projectCalendarIssues as IIssue[]);
const currentViewDaysData = currentViewDays.map((date: Date) => {
const filterIssue =
calendarIssues && calendarIssues.length > 0
? (calendarIssues as IIssue[]).filter(
? calendarIssues.filter(
(issue) =>
issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
)
@@ -198,17 +227,17 @@ export const CalendarView = () => {
});
};
return (
return calendarIssues ? (
<DragDropContext onDragEnd={onDragEnd}>
<div className="h-full overflow-y-auto rounded-lg text-gray-600 -m-2">
<div className="-m-2 h-full overflow-y-auto rounded-lg text-brand-secondary">
<div className="mb-4 flex items-center justify-between">
<div className="relative flex h-full w-full gap-2 items-center justify-start text-sm ">
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
<Popover className="flex h-full items-center justify-start rounded-lg">
{({ open }) => (
<>
<Popover.Button className={`group flex h-full items-start gap-1 text-gray-800`}>
<Popover.Button className={`group flex h-full items-start gap-1 text-brand-base`}>
<div className="flex items-center justify-center gap-2 text-2xl font-semibold">
<span className="text-black">{formatDate(currentDate, "Month")}</span>{" "}
<span>{formatDate(currentDate, "Month")}</span>{" "}
<span>{formatDate(currentDate, "yyyy")}</span>
</div>
</Popover.Button>
@@ -222,30 +251,30 @@ export const CalendarView = () => {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 left-0 z-20 w-full max-w-xs flex flex-col transform overflow-hidden bg-white shadow-lg rounded-[10px]">
<div className="flex justify-center items-center text-sm gap-5 px-2 py-2">
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-brand-surface-2 shadow-lg">
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
{yearOptions.map((year) => (
<button
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
className={` ${
isSameYear(year.value, currentDate)
? "text-sm font-medium text-gray-800"
: "text-xs text-gray-400 "
} hover:text-sm hover:text-gray-800 hover:font-medium `}
? "text-sm font-medium text-brand-base"
: "text-xs text-brand-secondary "
} hover:text-sm hover:font-medium hover:text-brand-base`}
>
{year.label}
</button>
))}
</div>
<div className="grid grid-cols-4 px-2 border-t border-gray-200">
<div className="grid grid-cols-4 border-t border-brand-base px-2">
{monthOptions.map((month) => (
<button
onClick={() =>
updateDate(updateDateWithMonth(month.value, currentDate))
}
className={`text-gray-400 text-xs px-2 py-2 hover:font-medium hover:text-gray-800 ${
className={`px-2 py-2 text-xs text-brand-secondary hover:font-medium hover:text-brand-base ${
isSameMonth(month.value, currentDate)
? "font-medium text-gray-800"
? "font-medium text-brand-base"
: ""
}`}
>
@@ -295,9 +324,9 @@ export const CalendarView = () => {
</div>
</div>
<div className="flex w-full gap-2 items-center justify-end">
<div className="flex w-full items-center justify-end gap-2">
<button
className="group flex cursor-pointer items-center gap-2 rounded-md border bg-white px-4 py-1.5 text-sm hover:bg-gray-100 hover:text-gray-900 focus:outline-none"
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none"
onClick={() => {
if (isMonthlyView) {
updateDate(new Date());
@@ -310,13 +339,12 @@ export const CalendarView = () => {
}
}}
>
Today{" "}
Today
</button>
<CustomMenu
customButton={
<div
className={`group flex cursor-pointer items-center gap-2 rounded-md border bg-white px-3 py-1.5 text-sm hover:bg-gray-100 hover:text-gray-900 focus:outline-none `}
>
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none ">
{isMonthlyView ? "Monthly" : "Weekly"}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
@@ -330,7 +358,7 @@ export const CalendarView = () => {
endDate: lastDayOfWeek(currentDate),
});
}}
className="w-52 text-sm text-gray-600"
className="w-52 text-sm text-brand-secondary"
>
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
<span className="flex items-center gap-2">Monthly View</span>
@@ -349,7 +377,7 @@ export const CalendarView = () => {
endDate: getCurrentWeekEndDate(currentDate),
});
}}
className="w-52 text-sm text-gray-600"
className="w-52 text-sm text-brand-secondary"
>
<div className="flex w-full items-center justify-between gap-2">
<span className="flex items-center gap-2">Weekly View</span>
@@ -360,25 +388,12 @@ export const CalendarView = () => {
/>
</div>
</CustomMenu.MenuItem>
<div className="mt-1 flex w-52 items-center justify-between border-t border-gray-200 py-2 px-1 text-sm text-gray-600">
<div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary">
<h4>Show weekends</h4>
<button
type="button"
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
showWeekEnds ? "bg-green-500" : "bg-gray-200"
}`}
role="switch"
aria-checked={showWeekEnds}
onClick={() => setShowWeekEnds(!showWeekEnds)}
>
<span className="sr-only">Show weekends</span>
<span
aria-hidden="true"
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
showWeekEnds ? "translate-x-2.5" : "translate-x-0"
}`}
/>
</button>
<ToggleSwitch
value={showWeekEnds}
onChange={() => setShowWeekEnds(!showWeekEnds)}
/>
</div>
</CustomMenu>
</div>
@@ -392,7 +407,7 @@ export const CalendarView = () => {
{weeks.map((date, index) => (
<div
key={index}
className={`flex items-center justify-start p-1.5 gap-2 border-gray-300 bg-gray-100 text-base font-medium text-gray-600 ${
className={`flex items-center justify-start gap-2 border-brand-base bg-brand-surface-1 p-1.5 text-base font-medium text-brand-secondary ${
!isMonthlyView
? showWeekEnds
? (index + 1) % 7 === 0
@@ -417,54 +432,93 @@ export const CalendarView = () => {
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
} `}
>
{currentViewDaysData.map((date, index) => (
<StrictModeDroppable droppableId={date.date}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.droppableProps}
className={`flex flex-col gap-1.5 border-t border-gray-300 p-2.5 text-left text-sm font-medium hover:bg-gray-100 ${
showWeekEnds
? (index + 1) % 7 === 0
{currentViewDaysData.map((date, index) => {
const totalIssues = date.issues.length;
return (
<StrictModeDroppable droppableId={date.date}>
{(provided) => (
<div
key={index}
ref={provided.innerRef}
{...provided.droppableProps}
className={`group relative flex flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${
showWeekEnds
? (index + 1) % 7 === 0
? ""
: "border-r"
: (index + 1) % 5 === 0
? ""
: "border-r"
: (index + 1) % 5 === 0
? ""
: "border-r"
}`}
>
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
{date.issues.length > 0 &&
date.issues.map((issue: IIssue, index) => (
<Draggable draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={`w-full cursor-pointer truncate rounded bg-white p-1.5 hover:scale-105 ${
snapshot.isDragging ? "shadow-lg" : ""
}`}
>
<Link
href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}
className="w-full"
>
{issue.name}
</Link>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
))}
}`}
>
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
{totalIssues > 0 &&
date.issues
.slice(0, showAllIssues ? totalIssues : 4)
.map((issue: IIssue, index) => (
<Draggable draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={`w-full cursor-pointer truncate rounded border border-brand-base px-1.5 py-1 text-xs duration-300 hover:cursor-move hover:bg-brand-surface-2 ${
snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : ""
}`}
>
<Link
href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}
>
<a className="flex w-full items-center gap-2">
{getStateGroupIcon(
issue.state_detail.group,
"12",
"12",
issue.state_detail.color
)}
{issue.name}
</a>
</Link>
</div>
)}
</Draggable>
))}
{totalIssues > 4 && (
<button
type="button"
className="w-min whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 px-1.5 py-1 text-xs"
onClick={() => setShowAllIssues((prevData) => !prevData)}
>
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
</button>
)}
<div
className={`absolute ${
isMonthlyView ? "bottom-2" : "top-2"
} right-2 flex items-center justify-center rounded-md bg-brand-surface-2 p-1 text-xs text-brand-secondary opacity-0 group-hover:opacity-100`}
>
<button
className="flex items-center justify-center gap-1 text-center"
onClick={() => addIssueToDate(date.date)}
>
<PlusIcon className="h-3 w-3 text-brand-secondary" />
Add issue
</button>
</div>
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
);
})}
</div>
</div>
</DragDropContext>
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
);
};
@@ -0,0 +1,267 @@
import { useEffect, useState } from "react";
// react-hook-form
import { useForm } from "react-hook-form";
// ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
const defaultValues = {
palette: "",
};
export const ThemeForm: React.FC<any> = ({ handleFormSubmit, handleClose, status, data }) => {
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
} = useForm<any>({
defaultValues,
});
const [darkPalette, setDarkPalette] = useState(false);
const handleUpdateTheme = async (formData: any) => {
await handleFormSubmit({ ...formData, darkPalette });
reset({
...defaultValues,
});
};
useEffect(() => {
reset({
...defaultValues,
...data,
});
}, [data, reset]);
// --color-bg-base: 25, 27, 27;
// --color-bg-surface-1: 31, 32, 35;
// --color-bg-surface-2: 39, 42, 45;
// --color-border: 46, 50, 52;
// --color-bg-sidebar: 19, 20, 22;
// --color-accent: 60, 133, 217;
// --color-text-base: 255, 255, 255;
// --color-text-secondary: 142, 148, 146;
return (
<form onSubmit={handleSubmit(handleUpdateTheme)}>
<div className="space-y-5">
<h3 className="text-lg font-medium leading-6 text-brand-base">Customize your theme</h3>
<div className="space-y-4">
<div className="mt-6 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-6">
<div className="sm:col-span-2">
<Input
id="bgBase"
label="Background"
name="bgBase"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.bgBase}
register={register}
validations={{
required: "Background color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Background color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-2">
<Input
id="bgSurface1"
label="Background surface 1"
name="bgSurface1"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.bgSurface1}
register={register}
validations={{
required: "Background surface 1 color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Background surface 1 color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-2">
<Input
id="bgSurface2"
label="Background surface 2"
name="bgSurface1"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.bgSurface1}
register={register}
validations={{
required: "Background surface 2 color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Background surface 2 color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-2">
<Input
id="border"
label="Border"
name="border"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.border}
register={register}
validations={{
required: "Border color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Border color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-2">
<Input
id="sidebar"
label="Sidebar"
name="sidebar"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.sidebar}
register={register}
validations={{
required: "Sidebar color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Sidebar color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-2">
<Input
id="accent"
label="Accent"
name="accent"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.accent}
register={register}
validations={{
required: "Accent color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Accent color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-3">
<Input
id="textBase"
label="Text primary"
name="textBase"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.textBase}
register={register}
validations={{
required: "Text primary color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Text primary color should be hex format",
},
}}
/>
</div>
<div className="sm:col-span-3">
<Input
id="textSecondary"
label="Text secondary"
name="textSecondary"
type="name"
placeholder="#FFFFFF"
autoComplete="off"
error={errors.textSecondary}
register={register}
validations={{
required: "Text secondary color is required",
pattern: {
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Text secondary color should be hex format",
},
}}
/>
</div>
</div>
<div>
<Input
id="palette"
label="All colors"
name="palette"
type="name"
placeholder="Enter comma separated hex colors"
autoComplete="off"
error={errors.palette}
register={register}
validations={{
required: "Color values is required",
pattern: {
value: /^(#(?:[0-9a-fA-F]{3}){1,2},){7}#(?:[0-9a-fA-F]{3}){1,2}$/g,
message: "Color values should be hex format, separated by commas",
},
}}
/>
</div>
<div
className="flex cursor-pointer items-center gap-1"
onClick={() => setDarkPalette((prevData) => !prevData)}
>
<span className="text-xs">Dark palette</span>
<button
type="button"
className={`pointer-events-none relative inline-flex h-4 w-7 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent ${
darkPalette ? "bg-brand-accent" : "bg-gray-300"
} transition-colors duration-300 ease-in-out focus:outline-none`}
role="switch"
aria-checked="false"
>
<span className="sr-only">Dark palette</span>
<span
aria-hidden="true"
className={`pointer-events-none inline-block h-3 w-3 ${
darkPalette ? "translate-x-3" : "translate-x-0"
} transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-300 ease-in-out`}
/>
</button>
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{status
? isSubmitting
? "Updating Theme..."
: "Update Theme"
: isSubmitting
? "Creating Theme..."
: "Set Theme"}
</PrimaryButton>
</div>
</form>
);
};
@@ -0,0 +1,65 @@
import React from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// components
import { ThemeForm } from "./custom-theme-form";
// helpers
import { applyTheme } from "helpers/theme.helper";
// fetch-keys
type Props = {
isOpen: boolean;
handleClose: () => void;
};
export const CustomThemeModal: React.FC<Props> = ({ isOpen, handleClose }) => {
const onClose = () => {
handleClose();
};
const handleFormSubmit = async (formData: any) => {
applyTheme(formData.palette, formData.darkPalette);
onClose();
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-brand-surface-1 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<ThemeForm
handleClose={handleClose}
handleFormSubmit={handleFormSubmit}
status={false}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

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