Compare commits

...

257 Commits

Author SHA1 Message Date
Vamsi krishna
750364833b feat: added user timezone dates for cycle 2025-03-26 12:39:42 +05:30
Akshita Goyal
41447e566a [WEB-3600] fix: private project join issue (#6799)
* fix: private project join issue

* chore: return network value

* fix: refactor

* fix: refactor

* fix: type

* chore: added restricition for private projects

* chore: removed extra validations

* chore: added value to access enum

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-03-25 20:17:16 +05:30
Prateek Shourya
cebd0b3599 [RANTS-68] fix: z-index for image picker popover button (change project cover) (#6812) 2025-03-25 20:16:06 +05:30
Prateek Shourya
caa522118d [RANTS-29] fix: enter key does not work in the workspace member invite modal (#6816) 2025-03-25 20:15:35 +05:30
Prateek Shourya
aea5f39059 [RANTS-31] improvement: optimistic update for home widget reordering (#6817) 2025-03-25 20:15:00 +05:30
sriram veeraghanta
f29867968a chore: removed sentry instrumentation dependencies 2025-03-25 15:54:44 +05:30
sriram veeraghanta
c91972cc0a chore: removing sentry instrumentation 2025-03-25 15:45:31 +05:30
Anmol Singh Bhatia
84c7375d2a [WEB-3601] chore: content updated (#6811) 2025-03-24 19:57:13 +05:30
Anmol Singh Bhatia
5cb37a0b9c [WEB-3560] fix: table layout issue block and code refactor (#6805) 2025-03-24 19:06:36 +05:30
Anmol Singh Bhatia
c347dd7dcd [WEB-3614] chore: list layout display filters (#6801) 2025-03-24 19:05:53 +05:30
sriram veeraghanta
0ec206b75d fix: transpile packages update on space and admin apps 2025-03-24 18:55:59 +05:30
Vamsi Krishna
e8718a84fe chore: issue detail refactor (#6803) 2025-03-24 18:33:22 +05:30
Anmol Singh Bhatia
983e0fa081 [WEB-3438] fix: transfer completed cycle issue modal (#6802) 2025-03-24 18:30:31 +05:30
Aaryan Khandelwal
ef108839c4 [RANTS-57] chore: replace target date with due date in work item filters dropdown (#6806) 2025-03-24 18:24:10 +05:30
Bavisetti Narayan
fe04e5a292 [WEB-3658] fix: remove cycles and modules when issues are bulk deleted (#6807) 2025-03-24 18:23:09 +05:30
Aaryan Khandelwal
50e0cb7ffd [RANTS-75] chore: update profile sidebar icons and copy for consistency (#6808) 2025-03-24 18:21:12 +05:30
Akshita Goyal
d37d210921 [WEB-3677] fix: settings dynamic pages permissions (#6804)
* fix: settings dynamic pages permissions

* fix: refactor
2025-03-24 18:15:43 +05:30
Anmol Singh Bhatia
ab3eadf767 [WEB-3614] fix: cmd-k item focus state (#6800) 2025-03-24 18:13:49 +05:30
sriram veeraghanta
dbdf2f001a fix: transpile packages for web application 2025-03-24 13:47:00 +05:30
Prateek Shourya
0d069bf46e [RANTS-65] fix: undefined work item sequence in bulk delete work item modal (#6797) 2025-03-24 13:41:02 +05:30
Prateek Shourya
962923ff4f fix: admin build (#6798) 2025-03-24 13:40:07 +05:30
Samuel Torres
f720a9afb2 feat: validate github organization during OAuth login (#6700)
* feat: add GITHUB_ORGANIZATION_ID support for GitHub OAuth integration

* fix: remove debug print statements from InstanceConfigurationEndpoint
2025-03-24 12:55:20 +05:30
Akshita Goyal
4032aa62c5 [WEB-3551] fix: role improvements (#6763)
* Return Cycle start and end dates in project's timezone

* fix: role improvements

* chore: role updates

* chore: update role endpoint to update workspace admin permissions

* fix: conditions

* chore: update member role for workspace members

* chore: update workspace permission role

* fix: currentAdmin permissions

---------

Co-authored-by: Dheeraj Kumar Ketireddy <dheeru0198@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-03-24 12:52:57 +05:30
dependabot[bot]
cbe248591e chore(deps): bump next in the npm_and_yarn group across 1 directory (#6796)
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).


Updates `next` from 14.2.24 to 14.2.25
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.24...v14.2.25)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 12:52:16 +05:30
Dheeraj Kumar Ketireddy
75a9b71edb [WEB-3513] fix: return cycle start and end dates in project's timezone 2025-03-24 12:51:44 +05:30
dependabot[bot]
ef42ce04a4 chore(deps): bump gunicorn (#6793)
Bumps the pip group with 1 update in the /apiserver/requirements directory: [gunicorn](https://github.com/benoitc/gunicorn).


Updates `gunicorn` from 22.0.0 to 23.0.0
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/22.0.0...23.0.0)

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 12:48:05 +05:30
Vipin Chaudhary
6bafdb6dd8 [PE-298] Fix: Copy markdown to clipboard (#6675)
* fix: markdown for mentions fixed

* fix: copying text in mentions

* fix: refactored the component to use the same function

* chore: renamed funcion name

* add the new copy extension

* init working fix

* remove useless code

* improve readibility

* update node import

* better smaller logic

* remove log

* add open close end handler

* update readabliity

* handle tables

* handle triple click in cell

* triple tap select current line

* handle block and list

* lists fixed

* handle all possible cases of copy in table

* update the min elements

* handle multi types in table

* handle table seletion cases

* handle whole table handler

* feat: all case converd

* update markdown handling code

* update return statement

* handle using group block

* handle param

* handle multple cell in table

* handle using recursion

* add types

* fix code rabbit  suggestions

* fix root node bug

* update recursion with loop

* update transform copied to false

* refactor clipboard extension: remove options and integrate MarkdownClipboard into core extensions

* fix: header and code handler

* fix: store hooks fixed

* fix: mention id

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
2025-03-24 12:32:11 +05:30
sriram veeraghanta
72307ec100 chore: update package version 2025-03-21 17:00:14 +05:30
sriram veeraghanta
2eb1d03c20 fix: transpile and optimize package imports 2025-03-21 01:51:50 +05:30
Prateek Shourya
94bf90dac5 [WEB-3597] fix: guest work item view access when hyper mode is enabled (#6785)
* [WEB-3597] fix: guest work item view access when hyper mode is enabled

* fix: only show work item created by the guest user if the guest_view_all_features is disabled
2025-03-20 19:43:40 +05:30
Anmol Singh Bhatia
b0e941e4e2 [WEB-3590] chore: sidebar enhancements (#6780)
* chore: implement optimistic update for extended sidebar item

* chore: replace eye icon with pin icon for show/hide functionality

* chore: code refactor

* chore: code refactor
2025-03-20 16:43:45 +05:30
Akshita Goyal
e22265dc93 fix: intake refactor (#6698)
* fix: refactor

* fix: refactor

* fix: type

* chore: added source data in intake

* fix: css

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-03-20 14:06:36 +05:30
Sangeetha
075c234385 [WEB-3286] fix: allow admins to delete other admins views (#6769)
* fix: allow admins to delete other admins views

* fix: allow admins to delete other admins views
2025-03-20 14:04:28 +05:30
Vamsi Krishna
bc539e0d01 [WEB-3175]fix: favorites menu (#6773)
* fix: favrotites menu open

* fix: open fav menu on starring projec

* chore: added constant for hardcoded text
2025-03-20 14:03:24 +05:30
Nikhil
04fb13cbca chore: update celery cron configurations (#6776) 2025-03-20 14:02:55 +05:30
Vamsi Krishna
ca5cf27957 [WEB-3262]fix: incomplete activity render for activity in notifications (#6777)
* fix: incomplete activity render for activity in notifications

* fix: handled content overflow for long notification messages
2025-03-20 14:02:06 +05:30
Vamsi Krishna
433682e913 fix: space app icons display (#6784) 2025-03-20 13:58:48 +05:30
Aaryan Khandelwal
f181b671f3 chore: update year range in the calendar component (#6781) 2025-03-19 19:06:58 +05:30
Prateek Shourya
f82d4a9ead [WEB-3589] improvement: reset language to default on sign out (#6775) 2025-03-19 14:42:19 +05:30
Anmol Singh Bhatia
3f22642732 [WEB-3580] fix: automations settings translation (#6767) 2025-03-18 16:34:58 +05:30
Anmol Singh Bhatia
e339b7ad8f [WEB-3545] feat: language translations (#6762)
* feat(translations): add Korean translation (#6755)

* feat(translations): init Korean translation

Co-authored-by: NavyStack <navystack@askfront.com>
Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>

* feat(translations): add rough Korean translation

Co-authored-by: NavyStack <navystack@askfront.com>
Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>

---------

Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>

* feat(translations): add Slovak, Deutsch, Ukrainian and Polish translation (#6743)

* feat(translation): add Slovak translation

* feat(translation): add Slovak translation for workspace

* feat(translation): improved Slovak translation for views

* feat(translation): add Deutsch translation

* feat(translation): add Ukrainian translation

* feat(translation): add Polish translation

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>

* fix: project cycle translations

* fix: build error

* feat: Add zh-TW Traditional Chinese locale (#6764)

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>

* chore: zh-TW core translation updated

---------

Co-authored-by: NavyStack <navystack@askfront.com>
Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>
Co-authored-by: Ján Regeš <jan.reges@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Peter Dave Hello <hsu@peterdavehello.org>
2025-03-18 13:35:46 +05:30
Vamsi Krishna
d4991b9a48 chore: issue filters refactor (#6742)
* chore: issue filters refactor

* chore: update helper funciton implementation

* chore: removed redundant components
2025-03-17 15:45:34 +05:30
Prateek Shourya
1bf683e044 improvement: command palette search results (#6761) 2025-03-17 15:45:03 +05:30
Akshita Goyal
807148671f fix: build (#6760) 2025-03-17 14:38:46 +05:30
Akshita Goyal
d2b81ad2da fix: favorite mutation on quick actions (#6741)
* fix: fav mutation on quick actions

* fix: user favorite fetch

* fix: exist validation

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-03-17 14:14:45 +05:30
sriram veeraghanta
3d14c9d9fe fix: build test pull request changes 2025-03-17 00:38:51 +05:30
sriram veeraghanta
d7f40cf578 fix: remove files changed step from branch build workflow 2025-03-17 00:26:10 +05:30
Aaryan Khandelwal
b370ef72ee [RANTS-46] fix: modules list sidebar position (#6754) 2025-03-16 23:50:04 +05:30
sriram veeraghanta
0341205666 fix: live server dev port to 3100 2025-03-13 15:42:43 +05:30
sriram veeraghanta
41fe7a59eb chore: axios package update 2025-03-13 14:28:40 +05:30
sriram veeraghanta
dcbee45d82 chore: updated package resolutions 2025-03-13 14:05:15 +05:30
Akshita Goyal
c3560c6586 fix: translation key (#6745) 2025-03-13 13:39:14 +05:30
Anmol Singh Bhatia
a477f55b23 [WEB-3509] chore: disable search indexing for space app (#6735) 2025-03-11 16:52:25 +05:30
dependabot[bot]
9ee1d8cb03 chore(deps): bump the npm_and_yarn group across 6 directories with 2 updates (#6737)
Bumps the npm_and_yarn group with 2 updates in the / directory: [axios](https://github.com/axios/axios) and [tsup](https://github.com/egoist/tsup).
Bumps the npm_and_yarn group with 1 update in the /live directory: [tsup](https://github.com/egoist/tsup).
Bumps the npm_and_yarn group with 1 update in the /packages/editor directory: [tsup](https://github.com/egoist/tsup).
Bumps the npm_and_yarn group with 1 update in the /packages/hooks directory: [tsup](https://github.com/egoist/tsup).
Bumps the npm_and_yarn group with 1 update in the /packages/ui directory: [tsup](https://github.com/egoist/tsup).
Bumps the npm_and_yarn group with 1 update in the /packages/utils directory: [tsup](https://github.com/egoist/tsup).


Updates `axios` from 1.7.9 to 1.8.2
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.9...v1.8.2)

Updates `tsup` from 7.3.0 to 8.3.5
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

Updates `tsup` from 7.3.0 to 8.4.0
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

Updates `tsup` from 7.3.0 to 8.4.0
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

Updates `tsup` from 7.3.0 to 8.4.0
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

Updates `tsup` from 7.3.0 to 8.4.0
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

Updates `tsup` from 7.3.0 to 8.4.0
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-11 01:14:37 +05:30
sriram veeraghanta
b478e36b59 fix: package version upgrade 2025-03-10 18:47:38 +05:30
Vipin Chaudhary
2a1cef0360 [PE-262] fix: add break-all to break words (#6734) 2025-03-10 18:37:37 +05:30
Anmol Singh Bhatia
52b9b12f74 fix: ambiguous class warning (#6733) 2025-03-10 18:12:51 +05:30
Vamsi Krishna
45ba8cbc83 fix: build errors (#6732) 2025-03-10 17:54:19 +05:30
Akshat Jain
4ce40fb3db chore: add env MINIO_ENDPOINT_SSL in docker compose file (#6731) 2025-03-10 17:39:07 +05:30
Nikhil
9ba2ed7d89 chore: make api rate limit configurable through environment variable (#6730) 2025-03-10 17:27:35 +05:30
Vipin Chaudhary
6157900b23 [PE-294] fix : route back on page delete (#6729) 2025-03-10 17:19:37 +05:30
Vamsi Krishna
a9045adf17 [WEB-3504]chore: issue properties refactor (#6724)
* chore: issue properties refactor

* chore: added export to props

* chore: updated component name
2025-03-10 17:14:46 +05:30
Akshat Jain
d1e462bb37 chore: Add env for handling uploads SSL Termination (#6722) 2025-03-10 15:45:11 +05:30
Anmol Singh Bhatia
7df63151b5 [WEB-3134] fix: plane logo rendering issue in safari (#6728) 2025-03-10 15:35:07 +05:30
Anmol Singh Bhatia
05b0716822 chore: cs translations updated (#6726) 2025-03-10 14:50:48 +05:30
Anmol Singh Bhatia
b75c9a8d8d [WEB-3401] fix: platform translations (#6727)
* fix: platform translations

* chore: common translation updated
2025-03-10 14:28:38 +05:30
Ján Regeš
099c5d50ee feat(translation): add Czech translation (#6725) 2025-03-10 12:41:55 +05:30
Prateek Shourya
a953013f70 [WEB-3489] improvement: add support to disable extensions in rich and lite text editor (#6721)
* [WEB-3489] improvement: add support to disable extensions in rich text editor

* improvements: disabled extensions prop for all editor components
2025-03-07 16:34:07 +05:30
sriram veeraghanta
a77fe7aa90 fix: django version upgrade to fix vulnerability 2025-03-07 13:35:54 +05:30
Aaryan Khandelwal
cb344ea1f5 refactor: favorites sidebar implementation (#6716)
* chore: code separation for favorites

* chore: error handling
2025-03-07 13:17:13 +05:30
Nikhil
40c0bbcfb4 fix: assignee validation when updating issues (#6720)
* fix: assignee validation

* chore: remove prints

* fix: remove all assignees
2025-03-07 13:08:34 +05:30
Aaryan Khandelwal
7005ae2b53 chore: add more translation keys (#6715) 2025-03-07 11:31:41 +05:30
Vamsi Krishna
21d7a1865c fix: sidebar project list expand (#6714) 2025-03-06 16:54:28 +05:30
Aaryan Khandelwal
f65b9a4dcb improvement: add disable image upload using props (#6706) 2025-03-06 16:03:35 +05:30
Prateek Shourya
6d216f2607 [WEB-3482] refactor: platform components and mobx stores (#6713)
* improvement: platform componenents and mobx stores

* minor improvements
2025-03-06 15:47:46 +05:30
Vipin Chaudhary
4958be7898 fix: added padding bottom to editor container (#6712) 2025-03-06 15:38:53 +05:30
Vamsi Krishna
a40e44c6d5 refactor: issue list modal refactor (#6702) 2025-03-06 13:45:07 +05:30
Akshita Goyal
44af90dc6c fix: issue stats refactor (#6705)
* fix: issue stats refactor

* fix: refactor

* fix: ui color

* fix: translation key
2025-03-06 13:44:37 +05:30
Prateek Shourya
f01d82ad1e fix: work item assignee update validation (#6704) 2025-03-05 17:42:09 +05:30
Prateek Shourya
ac6fef3073 [WEB-3488] improvement: assignee validation for work item creation (#6701) 2025-03-05 16:21:09 +05:30
sriram veeraghanta
c64c15948b fix: package license repliation 2025-03-04 20:20:38 +05:30
sriram veeraghanta
e58b68b6fc fix: esbuild version fix 2025-03-04 20:13:15 +05:30
sriram veeraghanta
68325866ef fix: package version update 2025-03-04 19:32:12 +05:30
Akshita Goyal
80198f5fda [WEB-3477] fix: mutation issue on moving work items for a manually ended cycle (#6696) 2025-03-04 18:32:02 +05:30
Akshita Goyal
6ac28ad614 fix: module flicker issue on property updation (#6699) 2025-03-04 18:30:53 +05:30
Anmol Singh Bhatia
c021ffddf2 fix: attachment item created by (#6695) 2025-03-04 13:58:32 +05:30
Anmol Singh Bhatia
f8997446e2 feat: italian translations (#6692)
* Create translations.json - ITALIAN translation (#6667)

* chore: italian translation updated

* feat: italian translation added

* fix: module end date translation

---------

Co-authored-by: Nicolas Bossi <nicolasbossi@gmail.com>
Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
2025-03-03 19:56:39 +05:30
Anmol Singh Bhatia
9487c02954 chore: extended sidebar improvement (#6693) 2025-03-03 18:12:03 +05:30
Akshita Goyal
0188cabbde fix: module date picker (#6691)
* fix: Handled workspace switcher closing on click

* fix: reverted module date picker changes
2025-03-03 17:56:39 +05:30
Akshita Goyal
392a6e0137 [WEB-3475] fix: cycle dates dropdown (#6690)
* fix: Handled workspace switcher closing on click

* fix: Cycle date picker

* fix: Made onSelect optional in range range component
2025-03-03 17:39:07 +05:30
Lakhan Baheti
7e62c60748 [PE-275] chore: editor line spacing variables (#6678)
* chore: variable editor line spacing

* chore: variable list spacing

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-03-03 17:01:47 +05:30
Nikita Mitasov
9bff707fb5 chore: update russian translation (#6682)
* chore: update russian translation

* chore: rename issues to work items in russian translation
2025-03-03 16:15:06 +05:30
Akshat Jain
0d599ef2dc changed github workflow action ubuntu version to ubuntu-22.04 (#6683) 2025-03-03 16:10:23 +05:30
Nikhil
e183a0cc63 fix: issue activity task (#6689) 2025-03-03 16:06:02 +05:30
Aaryan Khandelwal
958a3676af chore: add common translation keys (#6688)
* chore: add missing translation keys

* chore: add russian translation keys
2025-03-03 13:31:09 +05:30
Akshita Goyal
59a0925d34 fix: date range picker on cycles and modules list (#6676)
* fix: Handled workspace switcher closing on click

* fix: replaced date range picker with date picker at some places
2025-02-25 21:21:02 +05:30
sriram veeraghanta
fbbf58481d fix: cleanup for deprecated functions 2025-02-25 21:20:00 +05:30
sriram veeraghanta
6356bb1dbb fix: intake work item creation refactor 2025-02-25 17:56:11 +05:30
sriram veeraghanta
d08bce35a3 fix: state drop down refactor 2025-02-25 15:39:29 +05:30
Anmol Singh Bhatia
9297448ec8 chore: ru translation updated (#6672) 2025-02-25 15:10:43 +05:30
Nikita Mitasov
5329326602 feat: russian translation (#6666) 2025-02-25 13:02:05 +05:30
Manish Gupta
062fc9dbc0 updated the action to modify the release build assets (#6669) 2025-02-25 12:59:27 +05:30
Anmol Singh Bhatia
fde8630c5b fix: work item attachment count mutation (#6670) 2025-02-25 12:54:51 +05:30
Nikhil
aa0b2c0be4 fix: issue activity for project id validation (#6668) 2025-02-25 12:32:36 +05:30
sriram veeraghanta
f70eae2f3b fix: package version update 2025-02-24 20:37:55 +05:30
sriram veeraghanta
cf8823fa96 Merge pull request #6664 from makeplane/canary
release: v0.25.0
2025-02-24 20:34:31 +05:30
Bavisetti Narayan
5f3d02606c [WEB-3449] chore: changed the logic from utc to project date conversion (#6663)
* chore: changed the logic from utc to project date conversion

* chore: changed the cycle to project timezone
2025-02-24 19:51:13 +05:30
sriram veeraghanta
aeed6590b7 chore: selfhost docker compose updated by adding comments 2025-02-24 19:44:01 +05:30
Akshat Jain
1f18b08655 feat: docker swarm support for plane community using swarm.sh file (#6406)
* added swarm stack in selfhost

* synced docker-compose and swarm-compose

* updated the BRANCH variable

* fixes

* fix: swarm script upgrade function and `APP_RELEASE` variable

* fix: remove network from compose file and fix swarm script

* removed property restart from docker compose file

* added restart_policy condition in docker compose

* fix: changed restart policy to `any`

* changed `restart_policy` from `any` to `on-failure`

* updated selfhost readme

* chore: added migrator, redis, minio, db and mq service logs and also added redeployStack in script file

* add sleep in redeployStack service

* fixed typo in swarm and install script

* updated coderabbit suggestions

* added Replica Envs for services

* removed additional replica envs from docker compose file

---------

Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
2025-02-24 17:09:06 +05:30
Akshita Goyal
e4dd2a6c07 [WEB-3452] Fix: date picker auto close (#6662)
* fix: Handled workspace switcher closing on click

* fix: removed auto close for date range picker
2025-02-24 15:10:22 +05:30
Vipin Chaudhary
bcb9c73634 [WEB-3411] fix: archive permission (#6661)
* fix restore typo

* fix archive permissions

* fix restore typo
2025-02-24 14:33:55 +05:30
sriram veeraghanta
952eee8d55 fix: minor refactoring changes for state dropdowns 2025-02-24 14:14:24 +05:30
Nikhil
da469dac18 chore: error handling (#6657)
* fix: ai completions

* fix: reset password endpoints

* fix: intake issue list

* fix: identifier validation, uuid validation
2025-02-24 13:36:21 +05:30
Aaryan Khandelwal
6fac320a05 chore: add month and year picker (#6656) 2025-02-21 17:14:28 +05:30
Anmol Singh Bhatia
cc7b34e399 [WEB-3439] fix: work item attachment mutation (#6655)
* chore: created by field added to attachment response

* fix: work item attachment mutation
2025-02-21 00:10:16 +05:30
Anmol Singh Bhatia
2d6c26a5d6 [WEB-3436] fix: work item delete permission and header translation (#6654)
* fix: work item header translation

* fix: work item delete permission
2025-02-20 18:32:22 +05:30
Vamsi Krishna
f1acd46e15 fix: add favorites (#6652) 2025-02-20 18:24:43 +05:30
Prateek Shourya
c023f7d89b chore: update breadcrumb translation of work item detail page (#6653) 2025-02-20 18:24:13 +05:30
Prateek Shourya
8fa45ef9a6 fix: command palette search (#6651) 2025-02-20 17:59:32 +05:30
sriram veeraghanta
8bcc295061 fix: turbo repo upgrade 2025-02-19 22:06:11 +05:30
sriram veeraghanta
1b080012ab fix: react date picker update 2025-02-19 22:04:10 +05:30
Akshita Goyal
f6dfca4fdc Fix: project settings pages permissions (#6649)
* fix: Handled workspace switcher closing on click

* fix: permissions for labels dnd + issue state creation
2025-02-19 18:05:43 +05:30
Anmol Singh Bhatia
3de655cbd4 fix: home recent n progress (#6648) 2025-02-19 18:04:26 +05:30
Anmol Singh Bhatia
376f781052 fix: attachment item avatar (#6650) 2025-02-19 18:02:39 +05:30
Aaryan Khandelwal
827f47809b [PE-238] refactor: page store hooks (#6409)
* refactor: page store hooks

* fix: page details instances

* fix: build errors

* refactor: page store hooks

* fix: minor bug
2025-02-19 18:02:14 +05:30
Aaryan Khandelwal
dd11ebf335 fix: sticky collapse icon (#6647) 2025-02-19 17:29:45 +05:30
Aaryan Khandelwal
0c35e196be [regression]: space app editor helpers (#6646)
* fix: editor helpers

* fix: animation ref type

* fix: animation ref type
2025-02-19 17:28:55 +05:30
Aaryan Khandelwal
6303847026 fix: editor image block condition (#6645) 2025-02-19 15:49:59 +05:30
Aaryan Khandelwal
214692f5b2 [PE-242, 243] refactor: editor file handling, image upload status (#6442)
* refactor: editor file handling

* refactor: asset store

* refactor: space app file handlers

* fix: separate webhook connection params

* chore: handle undefined status

* chore: add type to upload status

* chore: added transition for upload status update
2025-02-19 15:18:01 +05:30
Prateek Shourya
b7198234de chore: minor trasnslation update related to work items (#6643) 2025-02-19 15:14:03 +05:30
Aaryan Khandelwal
7e0ac10fe8 [PE-239] chore: add strictNullCheck flag to the editor package (#6439)
* chore: add strictNullCheck flag

* fix: types and errors

* chore: update error handling
2025-02-19 15:13:37 +05:30
Akshita Goyal
f9d154dd82 Fix: date range selector (#6625)
* fix: Handled workspace switcher closing on click

* fix: removed action btns from date range selector

* fix: updated calendar component
2025-02-19 15:01:51 +05:30
Dancia
1c6a2fb7dd Add language translation guidelines (#6639)
* Add language translation guidelines

* fix: minor formatting fix

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-02-19 14:54:35 +05:30
Aaryan Khandelwal
5c272db83b fix: app header layer (#6640) 2025-02-19 14:54:15 +05:30
Prateek Shourya
602ae01b0b fix: command modal mutation (#6641)
* fix: command modal mutation

* chore: minor update
2025-02-19 14:43:58 +05:30
Prateek Shourya
cd3fa94b9c fix: build error (#6638) 2025-02-19 11:16:57 +05:30
Anmol Singh Bhatia
51c2ea6fcb fix: cmd-k project level action in work item detail page (#6637) 2025-02-19 03:00:39 +05:30
Vipin Chaudhary
64752de3a8 fix: removing self from private project error 404 (#6631) 2025-02-19 02:21:47 +05:30
Anmol Singh Bhatia
84578a2764 fix: undefined workspaceslug (#6636) 2025-02-19 02:20:23 +05:30
M. Palanikannan
126575d22a fix: bubble menu weird flickering fixed (#6591) 2025-02-19 02:09:27 +05:30
Nikhil
d3af913ec7 fix: error handling for db based integrity errors (#6632)
* fix: error handling for db based integrity errors

* fix: meta endpoint to return correct error message

* fix: module activity
2025-02-19 02:04:28 +05:30
Anmol Singh Bhatia
db4ecee475 fix: inbox count (#6635) 2025-02-19 01:55:46 +05:30
Anmol Singh Bhatia
527c4ece57 [WEB-3422] fix: app sidebar improvements (#6634)
* chore: sidebar project list improvements

* chore: code refactor
2025-02-18 23:40:13 +05:30
Anmol Singh Bhatia
23b0d4339d [WEB-3422] fix: app sidebar fixes and improvements (#6633)
* chore: app sidebar improvements

* chore: overview icon updated
2025-02-18 20:49:17 +05:30
Anmol Singh Bhatia
1478e66dc4 fix: app sidebar fixes and improvements (#6630) 2025-02-18 18:14:31 +05:30
Akshita Goyal
a49d899ea1 Chore: search code splitting (#6628)
* fix: Handled workspace switcher closing on click

* chore: code splitting for search

* fix: refactor

* fix: quick link error validation

* fix: refactor

* fix: refactor
2025-02-18 15:11:44 +05:30
Aaryan Khandelwal
3f6ef56a0f chore: add hslToHex and hexToHsl color helpers (#6629)
* chore: add more color helpers

* chore: added error handling
2025-02-18 13:18:45 +05:30
Akshita Goyal
cba27c348d fix: home quick start widget validation (#6626)
* fix: Handled workspace switcher closing on click

* fix: home quickstart widget
2025-02-18 12:37:00 +05:30
Anmol Singh Bhatia
ffe87cc3b4 chore: work item url redirection improvement (#6627) 2025-02-18 12:35:57 +05:30
Anmol Singh Bhatia
473932af0a [WEB-3291] dev: app sidebar revamp (#6578)
* chore: workspace constant and types updated

* chore: workspace service, store and app theme store updated

* dev: extended sidebar implementation and code refactor

* chore: ux improvements

* chore: sidebar preference endpoint updated

* chore: sidebar preference endpoint updated

* chore: sidebar preference endpoint updated

* chore: code refactor

* chore: code refactor

* chore: radix-ui react-scroll-area added to plane ui package

* chore: scrollbar color token added to tailwind config

* dev: scroll area component

* chore-scroll-area-component-improvement

* fix: build error

* chore: code refactor

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
2025-02-17 23:46:55 +05:30
Anmol Singh Bhatia
a9aeeb6707 [WEB-3410] fix: work item permission and validation (#6621)
* fix: work item permission and validation

* fix: command palette

* chore: code refactor
2025-02-17 18:09:05 +05:30
Anmol Singh Bhatia
075eefe1a5 [WEB-2278] dev: scroll area enhancement (#6612)
* chore: radix-ui react-scroll-area added to plane ui package

* chore: scrollbar color token added to tailwind config

* dev: scroll area component

* chore-scroll-area-component-improvement

* fix: build error

* chore: code refactor
2025-02-17 15:15:45 +05:30
Aaryan Khandelwal
54bdd62d0c chore: add missing translation keys (#6619) 2025-02-17 15:14:25 +05:30
Anmol Singh Bhatia
d4ee32cb41 fix: telemetry url (#6620) 2025-02-17 15:13:46 +05:30
Paul Ivanov
31bba2926d fix: provide working telemetry documentation url (#6614)
Closes #6613
2025-02-17 13:41:12 +05:30
Anmol Singh Bhatia
d6c25a76f6 [WEB-3370] fix: cmd+k work item actions (#6617)
* fix: cmd+k work item actions

* chore: code refactor
2025-02-17 13:39:58 +05:30
Anmol Singh Bhatia
8a792d381b [WEB-3396] chore: work items parent select improvement (#6608)
* chore: work items parent select improvements

* chore: code refactor
2025-02-15 05:05:37 +05:30
Anmol Singh Bhatia
4353cc0c4a [WEB-3268] feat: url pattern (#6546)
* feat: meta endpoint for issue

* chore: add detail endpoint

* chore: getIssueMetaFromURL and retrieveWithIdentifier endpoint added

* chore: issue store updated

* chore: move issue detail to new route and add redirection for old route

* fix: issue details permission

* fix: work item detail header

* chore: generateWorkItemLink helper function added

* chore: copyTextToClipboard helper function updated

* chore: workItemLink updated

* chore: workItemLink updated

* chore: workItemLink updated

* fix: issues navigation tab active status

* fix: invalid workitem error state

* chore: peek view parent issue redirection improvement

* fix: issue detail endpoint to not return epics and intake issue

* fix: workitem empty state redirection and header

* fix: workitem empty state redirection and header

* chore: code refactor

* chore: project auth wrapper improvement

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-02-15 05:05:00 +05:30
Anmol Singh Bhatia
82eea3e802 [WEB-3357 | WEB-3363 | WEB-3370] chore: command-k enhancement and fixes (#6600)
* fix: command-k work item actions

* chore: command k work item context indicator improvement and default vale for workspace toggle updated

* chore: code refactor
2025-02-14 19:04:08 +05:30
Prateek Shourya
bf1f12378e improvement: minor improvements for workspace switcher (#6609) 2025-02-14 19:03:32 +05:30
Anmol Singh Bhatia
c4a3e1e8ac chore: whats new modal width updated (#6607) 2025-02-14 17:02:40 +05:30
Prateek Shourya
b62b2710f5 fix: ensure empty state group header is visible (#6606) 2025-02-14 13:54:25 +05:30
Anmol Singh Bhatia
71b41fa22b chore: whats new modal width updated (#6605) 2025-02-14 13:51:26 +05:30
Prateek Shourya
3528d2c934 [WEB-3368] feat: enhance workspace invitations with copyable invite links (#6601)
* feat: invitation link url

* feat: copy invite link from workspace invitations list

* invitation reponse cleanup and logo url fix

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-02-13 23:35:25 +05:30
Anmol Singh Bhatia
39ecfbe7e1 [WEB-3375] fix: project cover image (#6602)
* fix: project cover image

* chore: code refactor
2025-02-13 23:34:08 +05:30
Anmol Singh Bhatia
a95864ba11 fix: create sub work item operation (#6603) 2025-02-13 23:33:41 +05:30
Vamsi Krishna
b88ae112f9 fix: updated lang key for defaults (#6599) 2025-02-12 19:59:05 +05:30
sriram veeraghanta
2d20278c9b fix: esbuild deps resolution 2025-02-12 18:44:31 +05:30
Prateek Shourya
8cff059868 improvement: add timeout before resetting data in workspace delete form (#6598) 2025-02-12 17:39:12 +05:30
Akshita Goyal
6a3ccafe35 fix: code splitting for workspace delete modal (#6581)
* fix: code splitting for delete modal

* fix: redirected to profile post deletion

* fix: translations
2025-02-12 17:15:40 +05:30
Prateek Shourya
cc9b448a9b improvement: enhance workspace invitation modularity (#6594) 2025-02-12 17:05:09 +05:30
Prateek Shourya
e071bf4861 fix: minor ux copy update for recents empty state (#6597) 2025-02-12 16:57:33 +05:30
Prateek Shourya
b9da7df6b7 fix: minor grammar fix (#6595) 2025-02-12 16:10:01 +05:30
sriram veeraghanta
03cc819601 fix: esbuild vulnerbility fix 2025-02-12 14:17:16 +05:30
sriram veeraghanta
e1943ee11e fix: lock file regerated 2025-02-12 14:13:18 +05:30
dependabot[bot]
b47d2b8825 chore(deps): bump @sentry/node (#6593)
Bumps the npm_and_yarn group with 1 update in the / directory: [@sentry/node](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/node` from 9.0.0 to 9.0.1
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/9.0.0...9.0.1)

---
updated-dependencies:
- dependency-name: "@sentry/node"
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-12 13:58:25 +05:30
dependabot[bot]
300b47f9a1 chore(deps): bump cryptography (#6592)
Bumps the pip group with 1 update in the /apiserver/requirements directory: [cryptography](https://github.com/pyca/cryptography).


Updates `cryptography` from 43.0.1 to 44.0.1
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/43.0.1...44.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-12 13:52:44 +05:30
sriram veeraghanta
03a4a97375 fix: workflow changes 2025-02-12 13:46:01 +05:30
Sangeetha
6157d5771d fix: delete_at not updating for epics (#6577) 2025-02-12 12:18:08 +05:30
Bavisetti Narayan
eee43be99a chore: removed the project filter (#6590) 2025-02-11 20:43:23 +05:30
Bavisetti Narayan
4db95cc941 chore: duplicate the uploaded assets for duplicated page (#6311)
* chore: duplicate the uploaded assets in the entity

* chore: changed the filtering logic

* chore: captured exception
2025-02-11 20:42:06 +05:30
Vihar Kurama
6aa139a851 fix: workspace level toggle position, paddings, and tab navigation (#6580)
* fix: workspace level toggle position, paddings, and tab navigation

* chore: platform-specific command icons

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2025-02-11 17:40:31 +05:30
Anmol Singh Bhatia
ac74cd9e92 [WEB-3352] fix: home recent item redirection (#6586)
* fix: dashboard recent item redirection

* chore: code refactor
2025-02-11 17:37:47 +05:30
Anmol Singh Bhatia
7ae841d525 fix: work item form tab index (#6588) 2025-02-11 17:36:54 +05:30
Anmol Singh Bhatia
7aa5b6aa91 fix: board layout redirection (#6584) 2025-02-11 16:20:22 +05:30
Akshita Goyal
28c3f9d0cc fix: Handled workspace switcher closing on click (#6585) 2025-02-11 16:17:48 +05:30
Nikhil
9d01a6d5d7 fix: workspace label cache (#6587) 2025-02-11 16:17:04 +05:30
Anmol Singh Bhatia
4fd8b4a3a9 [WEB-3347] fix: list layout stats indicator (#6582)
* fix: list layout stats indicator

* chore: code refactor
2025-02-11 14:51:02 +05:30
dependabot[bot]
49cc73b6ed chore(deps): bump @sentry/node (#6579)
Bumps the npm_and_yarn group with 1 update in the / directory: [@sentry/node](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/node` from 8.54.0 to 9.0.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/8.54.0...9.0.0)

---
updated-dependencies:
- dependency-name: "@sentry/node"
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-11 14:09:43 +05:30
sriram veeraghanta
363507f987 chore: turbo upgrade 2025-02-10 23:52:06 +05:30
sriram veeraghanta
30453d1c79 fix: build errors 2025-02-10 20:08:54 +05:30
Vamsi Krishna
dff12729c0 [WEB-3287]fix: label creation in global views (#6541)
* fix: label creation on enter

* fix: update label creation permissions

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-02-10 19:35:04 +05:30
Vamsi Krishna
8efe692c80 [WEB-3235]fix: transfer issues button mutation (#6549)
* fix: pending issues mutation

* fix: transfer issues count
2025-02-10 19:33:43 +05:30
Aaryan Khandelwal
ce57c1423c [WEB-3329] dev: new chart components (#6565)
* dev: new chart components

* chore: separate out pie chart tooltip

* chore: remove unused any types

* chore: move chart components to propel package
2025-02-10 16:01:06 +05:30
Akshita Goyal
1eb1e82fe4 fix: issue icons in published project link (#6576) 2025-02-10 15:07:30 +05:30
Akshita Goyal
a2328d0cbe fix: workspace roles for settings and members button (#6574)
* fix: workspace roles for settings and members button

* fix: user role and member count for new workspace

* chore: set role to 20 while workspace creation

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-02-10 15:03:50 +05:30
Dheeraj Kumar Ketireddy
5096a15051 Merge pull request #6575 from makeplane/modify-workspace-serializer
modify: workspace user link serializer
2025-02-10 14:51:32 +05:30
sangeethailango
55c2511ab5 modify: workspace serializer 2025-02-10 14:27:21 +05:30
Vipin Chaudhary
16bc64e2fa fix: Remove AI Assistant Button from Tab Order to Prevent Focus (#6569)
* make ai button unfocused

* fix the format
2025-02-10 12:55:21 +05:30
Vipin Chaudhary
14083ea7da fix: handle shift tab tab-index focus (#6554)
* handle shift tab tab-index focus

* add comment

* fix double tap

* make label focus

* fix title focus

* focus discard and save

* remove comment
2025-02-08 20:54:23 +05:30
Anmol Singh Bhatia
feb88e64a4 [WEB-3292] fix: workspace switcher validation and ui improvements (#6570)
* fix: workspace action item validation and ui improvements

* chore: code refactor
2025-02-07 20:58:56 +05:30
Sangeetha
a00bb35e54 [WEB-3285] fix: creating and updating duplicate quick links (#6557)
* fix: creating and updating duplicate quick links

* fix: improve code readibiltiy
2025-02-07 20:06:47 +05:30
Akshita Goyal
20ba91b98c [WEB-3292] feat: workspace switcher redesign (#6543)
* feat: ui changes for workspace switcher

* fix: hover

* fix: added current plan

* feat: Return user role

* chore: remove unused imports

* fix: css

* fix: added user role in workspace switcher

* fix: return role as integer

* fix: role casing

* fix: refactor

* fix: plan pill fix

* fix: design updates

* fix: refactor

* fix: member translation

* fix: css improvements

* fix: truncate issue

* fix: workspace switcher dropdown email truncate

* fix: workspace switcher dropdown email truncate

* fix: role

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2025-02-07 20:05:57 +05:30
Akshita Goyal
456c7f55a9 [WEB-2917] Fix home widget (#6560)
* fix: home loading state

* fix: quickstart guide

* fix: link handling

* fix: home completed state

* fix: translations
2025-02-07 20:00:59 +05:30
Prateek Shourya
c2da3ea4c8 fix: intake filter label (#6567) 2025-02-07 17:46:08 +05:30
Prateek Shourya
2b595cfe62 chore: remove unnecessary useEffect for setting default image (#6566) 2025-02-07 14:08:17 +05:30
Anmol Singh Bhatia
7a6b50a6e1 chore: app sidebar section header improvement (#6564) 2025-02-07 02:36:33 +05:30
Aaryan Khandelwal
a5c2acb5f1 chore: update lucide-react versions (#6551) 2025-02-07 01:00:40 +05:30
dependabot[bot]
4cf0c702ce chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#6561)
Bumps the npm_and_yarn group with 1 update in the / directory: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/nextjs` from 8.48.0 to 8.54.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/8.54.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/8.48.0...8.54.0)

Updates `@sentry/node` from 8.48.0 to 8.54.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/8.54.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/8.48.0...8.54.0)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: "@sentry/node"
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-07 00:57:16 +05:30
Prateek Shourya
d36c3acbf7 feat: language support (#6472)
* chore: ln support modules constants

* fix: translation key

* chore: empty state refactor (#6404)

* chore: asset path helper hook added

* chore: detailed and simple empty state component added

* chore: section empty state component added

* chore: language translation for all empty states

* chore: new empty state implementation

* improvement: add more translations

* improvement: user permissions and workspace draft empty state

* chore: update translation structure

* chore: inbox empty states

* chore: disabled project features empty state

* chore: active cycle progress empty state

* chore: notification empty state

* chore: connections translation

* chore: issue comment, relation, bulk delete, and command k empty state translation

* chore: project pages empty state and translations

* chore: project module and view related empty state

* chore: remove project draft related empty state

* chore: project cycle, views and archived issues empty state

* chore: project cycles related empty state

* chore: project settings empty state

* chore: profile issue and acitivity empty state

* chore: workspace settings realted constants

* chore: stickies and home widgets empty state

* chore: remove all reference to deprecated empty state component and constnats

* chore: add support to ignore theme in resolved asset path hook

* chore: minor updates

* fix: build errors

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>

* fix: language support fo profile (#6461)

* fix: ln support fo profile

* fix: merge changes

* fix: merge changes

* [WEB-3165]feat: language support for issues (#6452)

* * chore: moved issue constants to packages
* chore: restructured issue constants
* improvement: added translations to issue constants

* chore: updated translation structure

* * chore: updated chinese, spanish and french translation
* chore: updated translation for issues mobile header

* chore: updated spanish translation

* chore: removed translation for issue priorities

* fix: build errors

* chore: minor updates

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* chore: migrated filters.ts to packages (#6459)

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* chore: workspace drafts constant moved to plane constant package

* feat: home language support without stickies (#6443)

* feat: home language support without stickies

* fix: home sidebar

* fix: added missing keys

* fix: show all btn

* fix: recents empty state

* chore: translation update

* feat: workspace constant language support and refactor (#6462)

* chore: workspace constant language support and refactor

* chore: workspace constant language support and refactor

* chore: code refactor

* chore: code refactor

* merge conflict

* chore: code refactor

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* chore: tab indices constant moved to plane package (#6464)

* chore: notification language support and refactor

* chore: ln support for inbox constants (#6432)

* chore: ln support for inbox constants

* fix: snooze duration

* fix: enum

* fix: translation keys

* fix: inbox status icon

* fix: status icon

* fix: naming

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* fix: ln support for views constants (#6431)

* fix: ln support for views constants

* fix: added translation

* fix: translation keys

* fix: access

* chore: code refactor

* chore: ln support workspace projects constants (#6429)

* chore: ln support workspace projects constants

* fix: translation key

* fix: removed state translation

* fix: removed state translation

* fi: added translations

* Chore: theme language support and refactor (#6465)

* chore: themes language support and refactor

* chore: theme language support and refactor

* fix

* [WEB-3173] chore: language support for cycles constant file (#6415)

* chore: ln support for cycles constant file

* fix: added chinese

* fix: lint

* fix: translation key

* fix: build errors

* minor updates

* chore: minor translation update

* chore: minor translation update

* refactor: move labels contants to packages

* refactor: move swr, file and error related constants to packages

* chore: timezones constant moved to plane package

* chore: metadata constant code refactor

* chore: code refactor

* fix: dashboard constants moved

* chore: code refactor (#6478)

* refactor: spreadsheet constants

* chore: drafts language support (#6485)

* chore: workspace drafts language support

* chore: code refactor

* feat: ln support for notifications (#6486)

* feat: ln support for notifications

* fix: translations

* * refactor: moved page constants to packages (#6480)

* fix: removed use-client

* chore: removed unnecessary commnets

* chore: workspace draft language support (#6490)

* chore: workspace drafts language support

* chore: code refactor

* chore: draft language support

* Feat constant event tracker (#6479)

* fix: event tracjer constants

* fix: constants event tracker

* feat: language translation  - projects list (#6493)

* feat: added translation to projects list page

* chore: restructured translation file

* chore: module language support (#6499)

* chore: module language support added

* chore: code refactor

* chore: workspace views language support (#6492)

* chore: workspace views language support

* chore: code refactor

* feat: custom analytics language support (#6494)

* feat: custom analytics language support

* fix: key

* fix: refactoring

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* chore: minor improvements

* feat: language support for intake (#6498)

* feat: language support for intake

* fix: key name

* refactor: authentications related translations

* feat: language support issues  (#6501)

* enhancement: added translations for issue list view

* chore: added translations for issue detail widgets

* chore: added missing translations

* chore: modified issue to work items

* chore: updated translations

* Feat: workspace settings language support (#6508)

* feat: language support for workspace settings

* fix: lint

* fix: export title

* chore project settings language support (#6502)

* chore: project settings language support

* chore: code refactor

* refactor: workspace creation related translations

* chore: renamed issues to work items

* fix: build errors

* fix: lint

* chore: modified translations

* chore: remove duplicate

* improvement: french translation

* chore: chinese translation improvement

* fix: japanese translations

* chore: added spanish translation

* minor improvements

* fix: miscelleous language translations

* fix: clear_all key

* fix: moved user permission constants (#6516)

* feat: language support for  issues (#6513)

* chore: added language support to issue detail widgets

* improvement: added translation for issue detail

* enhancement: added language trasnlation to issue layouts

* chore: translation improvement (#6518)

* feat: language support description (#6519)

* enhancement: added language support for description

* fix: updated keys

* chore: renamed issue to work item (#6522)

* chore: replace missing issue occurances to work items

* fix: build errors

* minor improvements

* fix: profile links

* Feat ln cycles (#6528)

* feat: added language support for cycles

* feat: added language support for cycles

* chore: added core.json

* fix: translation keys

* fix: translation keys (#6530)

* fix: changed sidebar keys

* fix: removed extras

* fix: updated keys

* chore: optimize translation imports

* fix: updated keys (#6534)

* fix: updated keys

* fix-sub work items toasts

* chore: add missing translation and minor fixes

* chore: code refactor

* fix: language support keys (#6553)

* minor improvements

* minor fixes

* fix: remove lucide import from constants package

* chore: regenerate all translations

* chore: addded chinese and japanese translation files

* chore: remove all  from translations

* fix: added member

* fix: language support keys (#6558)

* fix: renamed keys

* fix: space app

* chore: renamed issues to work items

* chore: update site manifest

* chore: updated translations

* fix: lang keys

* chore: update translations

---------

Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
Co-authored-by: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
Co-authored-by: Vamsi krishna <matalav55@gmail.com>
Co-authored-by: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com>
2025-02-06 20:41:31 +05:30
Anmol Singh Bhatia
e244f48776 chore: platform ux improvement (#6555)
* chore: IssueStats placement updated

* chore: app sidebar section header content updated
2025-02-06 13:48:26 +05:30
Prateek Shourya
89d1926727 [WEB-3251] fix: add to projects list API (#6550) 2025-02-05 15:18:02 +05:30
Sangeetha
9bd70cdb4e fix: add Your Work sidebar preference (#6548) 2025-02-05 14:56:50 +05:30
Prateek Shourya
99f3d5810d [WEB-3309] fix: project stats endpoint (#6544) 2025-02-04 23:46:32 +05:30
Prateek Shourya
10b5c625ef [WEB-3251] improvement: optimize projects API (#6542) 2025-02-04 16:02:07 +05:30
Vamsi Krishna
c14fb814c4 [WEB-3195] fix: view delete toast message (#6537) 2025-02-03 14:57:00 +05:30
Vamsi Krishna
c82dd6901e [WEB-3184] fix: link messages (#6535) 2025-02-03 14:56:10 +05:30
shuaixr
a03a41ea5f fix: delete webhook for issues, issue_comments, projects (#6539)
* fix: prevent error when triggering deletion webhook

The deletion webhook was not firing because it attempted to retrieve
data after deletion, causing a failure.

According to the webhook documentation https://developers.plane.so/webhooks/intro-webhooks, the delete event should only contain
id, so the fix aligns with this expected behavior.

* fix: make delete_comment_activity include comment_id

The delete issues comment webhook requires comment_id

* fix: trigger webhook on project delete
2025-02-03 14:53:40 +05:30
Bavisetti Narayan
9f4dd771fc chore: webhook, comments migration (#6523)
* chore: migration changes

* chore: renamed the display value

* chore: reverted the accounts code
2025-01-31 18:04:40 +05:30
Vamsi Krishna
0deec92d91 fix: cycle labels overflow issue (#6526) 2025-01-31 16:00:20 +05:30
Anmol Singh Bhatia
d2a6307bb0 fix: page version history application error (#6529) 2025-01-31 15:59:40 +05:30
Anmol Singh Bhatia
66be0b1862 fix: version history z index (#6531) 2025-01-31 15:59:15 +05:30
Aaryan Khandelwal
ddad1767a2 fix: table flixker on resize (#6524) 2025-01-31 02:31:17 +05:30
Aaryan Khandelwal
6a37a2ce21 fix: link without protocol (#6517) 2025-01-30 20:25:00 +05:30
Aaryan Khandelwal
01bd1bde64 fix: table resize overflow issues (#6520) 2025-01-30 19:27:12 +05:30
Abenezer Belachew
9268180aec path already defined on line 51 (#6427) 2025-01-30 13:36:16 +05:30
Aaryan Khandelwal
ff778b98f5 [WEB-3095] fix: recents widget title truncate (#6512)
* fix: recents widget title truncate

* chore: revert prop changes

* chore: revert list item changes
2025-01-30 13:34:42 +05:30
Sangeetha
8f5ce6b232 feat: user preference url and sort order change (#6505)
* fix: change url

* Change order of user preference keys
2025-01-30 13:29:39 +05:30
Anmol Singh Bhatia
58a4ca9f36 chore: board layout padding improvement (#6507) 2025-01-29 16:56:27 +05:30
guru_sainath
312b077657 [WEB-3177] fix: resolve cycle creation issue for equal start_date and completed_date (#6504)
* fix: fixed cycle startdate if the the start_date and completed cyles dates are today

* chore: updated validation for date match
2025-01-29 16:35:25 +05:30
Bavisetti Narayan
c65e42f807 chore: add attachment in intake issue (#6503) 2025-01-29 15:53:34 +05:30
Prateek Shourya
f4af78c0fc [WEB-3218] fix: redirection for cross projects issue relations (#6457) 2025-01-29 13:00:24 +05:30
Prateek Shourya
c0b6abc3d5 refactor: minor store level changes (#6500) 2025-01-29 01:04:54 +05:30
sriram veeraghanta
2f2e6626c6 fix: typo fixes 2025-01-28 21:07:59 +05:30
Sangeetha
6a8d3202b7 feat: workspace user preference api (#6497)
* feat: workspace user preference api

* feat: remove sort order calculation

* Return 404 error
2025-01-28 20:50:24 +05:30
Bavisetti Narayan
51b52a7fc3 [WEB-3249] chore: delete the user recent visits (#6496)
* chore: delete the user recent visits

* chore: hard deleted the recent visits
2025-01-28 20:50:15 +05:30
Bavisetti Narayan
23ede81737 chore: update project state (#6467) 2025-01-28 20:49:36 +05:30
Aaryan Khandelwal
b698f44500 [PE-155] chore: floating toolbar for pages (#6482)
* chore: add floating toolbar to pages

* fix: locked page toolbar
2025-01-28 20:21:09 +05:30
M. Palanikannan
421839ec51 [PE-255] fix: remove drag handles from content within table cells (#6487)
* fix: remove drag handles from content within table cells

* style: table cell padding

* style: table cell padding

* fix: insert resizable tables

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-01-28 20:20:40 +05:30
M. Palanikannan
940b5e4e44 fix: custom color extension markdown rule added now (#6471) 2025-01-28 20:20:23 +05:30
Aaryan Khandelwal
6003c88d62 fix: disable comment submit while uploading an image (#6445) 2025-01-28 20:19:01 +05:30
Aaryan Khandelwal
74913a6659 fix: page name and recents empty state (#6491) 2025-01-28 17:13:20 +05:30
sriram veeraghanta
97578684c6 chore: lock file updates 2025-01-28 16:18:30 +05:30
Aaryan Khandelwal
88b4d32220 [WEB-3237, 3238] dev: date picker enhancements (#6470)
* [WEB-3238] dev: datepicker with month and year selection dropdowns (#6391)

* feat: react-day-picker upgrade and caption dropdowns

* style fixes

* style: css and autofocus improved

* fix: fixed weeks for datepicker to ensure static height

---------

Co-authored-by: Vineet K <55555696+vineetk13@users.noreply.github.com>
2025-01-28 16:15:18 +05:30
Aaryan Khandelwal
f32635a6a8 [WEB-3203] fix: stickies height, overflow (#6484)
* fix: stickies height

* chore: remove unused drop indicators
2025-01-28 15:33:25 +05:30
Bavisetti Narayan
7fe58e0ea9 chore: added estimate point value in expand issues (#6483) 2025-01-28 15:11:33 +05:30
Prateek Shourya
7f22cd1ac1 [WEB-3229] fix: issue creation from modal (#6460) 2025-01-27 13:13:32 +05:30
Prateek Shourya
e2550e0b2d [WEB-3201] improvement: minor enhancements for tree map text size and color (#6458) 2025-01-24 19:20:44 +05:30
Aaryan Khandelwal
b016ed78cf [PE-248] regression: recent widgets refactor for scalability (#6456)
* fix: recent widgets types and filters

* fix: recent widgets types
2025-01-24 17:23:15 +05:30
Aaryan Khandelwal
c429ca7b36 refactor: recents widget for scalability (#6453)
Co-authored-by: pushya22 <130810100+pushya22@users.noreply.github.com>
2025-01-24 15:34:28 +05:30
Bavisetti Narayan
ee22dbba1b chore: added a condition to restrict duplicate user creation (#6447) 2025-01-24 15:33:08 +05:30
Vamsi Krishna
f4a208bd44 fix: handled label overflow in modules (#6451) 2025-01-24 15:32:44 +05:30
Aaryan Khandelwal
8edff26ccd fix: space app editor colors (#6446) 2025-01-24 15:27:57 +05:30
Aaryan Khandelwal
d08c03f557 [WEB-3203] fix: dashboard widgets' empty state content and assets (#6450)
* fix: empty state content

* chore: replace margin with padding
2025-01-24 15:23:41 +05:30
Prateek Shourya
0b53912295 [WEB-3207] chore: add state_id, priority and assignee_ids to create issue relation response (#6448) 2025-01-23 14:16:06 +05:30
Prateek Shourya
586a320d86 fix: minor fixes for issue relations list and retrival (#6444) 2025-01-23 12:42:35 +05:30
sriram veeraghanta
9ed4591edc Merge pull request #6183 from makeplane/canary
release: v0.24.1
2024-12-10 21:43:09 +05:30
1183 changed files with 54128 additions and 21037 deletions

View File

@@ -38,3 +38,9 @@ USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Force HTTPS for handling SSL Termination
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT="60/minute"

View File

@@ -88,7 +88,7 @@ jobs:
full_build_push:
if: ${{ needs.branch_build_setup.outputs.do_full_build == 'true' }}
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
env:
BUILD_TYPE: full
@@ -148,7 +148,7 @@ jobs:
slim_build_push:
if: ${{ needs.branch_build_setup.outputs.do_slim_build == 'true' }}
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
env:
BUILD_TYPE: slim

View File

@@ -25,6 +25,10 @@ on:
required: false
default: false
type: boolean
push:
branches:
- preview
- canary
env:
TARGET_BRANCH: ${{ github.ref_name }}
@@ -43,12 +47,6 @@ jobs:
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
build_apiserver: ${{ steps.changed_files.outputs.apiserver_any_changed }}
build_admin: ${{ steps.changed_files.outputs.admin_any_changed }}
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
build_web: ${{ steps.changed_files.outputs.web_any_changed }}
build_live: ${{ steps.changed_files.outputs.live_any_changed }}
dh_img_web: ${{ steps.set_env_variables.outputs.DH_IMG_WEB }}
dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }}
@@ -119,46 +117,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Get changed files
id: changed_files
uses: tj-actions/changed-files@v42
with:
files_yaml: |
apiserver:
- apiserver/**
proxy:
- nginx/**
admin:
- admin/**
- packages/**
- "package.json"
- "yarn.lock"
- "tsconfig.json"
- "turbo.json"
space:
- space/**
- packages/**
- "package.json"
- "yarn.lock"
- "tsconfig.json"
- "turbo.json"
web:
- web/**
- packages/**
- "package.json"
- "yarn.lock"
- "tsconfig.json"
- "turbo.json"
live:
- live/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
branch_build_push_admin:
if: ${{ needs.branch_build_setup.outputs.build_admin == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Admin Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -181,7 +140,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_web:
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Web Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -204,7 +162,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_space:
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Space Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -227,7 +184,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_live:
if: ${{ needs.branch_build_setup.outputs.build_live == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Live Collaboration Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -250,7 +206,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_apiserver:
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push API Server Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -273,7 +228,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_proxy:
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Proxy Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -295,31 +249,6 @@ jobs:
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
attach_assets_to_build:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Attach Assets to Release
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Update Assets
run: |
cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh
- name: Attach Assets
id: attach_assets
uses: actions/upload-artifact@v4
with:
name: selfhost-assets
retention-days: 2
path: |
${{ github.workspace }}/deploy/selfhost/setup.sh
${{ github.workspace }}/deploy/selfhost/restore.sh
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
${{ github.workspace }}/deploy/selfhost/variables.env
publish_release:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Build Release
@@ -333,7 +262,6 @@ jobs:
branch_build_push_live,
branch_build_push_apiserver,
branch_build_push_proxy,
attach_assets_to_build,
]
env:
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
@@ -344,6 +272,8 @@ jobs:
- name: Update Assets
run: |
cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh
sed -i 's/${APP_RELEASE:-stable}/${APP_RELEASE:-'${REL_VERSION}'}/g' deploy/selfhost/docker-compose.yml
sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deploy/selfhost/variables.env
- name: Create Release
id: create_release
@@ -358,6 +288,7 @@ jobs:
generate_release_notes: true
files: |
${{ github.workspace }}/deploy/selfhost/setup.sh
${{ github.workspace }}/deploy/selfhost/swarm.sh
${{ github.workspace }}/deploy/selfhost/restore.sh
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
${{ github.workspace }}/deploy/selfhost/variables.env

View File

@@ -6,49 +6,9 @@ on:
types: ["opened", "synchronize", "ready_for_review"]
jobs:
get-changed-files:
lint-apiserver:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
outputs:
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
admin_changed: ${{ steps.changed-files.outputs.admin_any_changed }}
space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
files_yaml: |
apiserver:
- apiserver/**
admin:
- admin/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
space:
- space/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
web:
- web/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
lint-apiserver:
needs: get-changed-files
runs-on: ubuntu-latest
if: needs.get-changed-files.outputs.apiserver_changed == 'true'
steps:
- uses: actions/checkout@v4
- name: Set up Python
@@ -63,41 +23,38 @@ jobs:
run: ruff check --fix apiserver
lint-admin:
needs: get-changed-files
if: needs.get-changed-files.outputs.admin_changed == 'true'
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn lint --filter=admin
lint-space:
needs: get-changed-files
if: needs.get-changed-files.outputs.space_changed == 'true'
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn lint --filter=space
lint-web:
needs: get-changed-files
if: needs.get-changed-files.outputs.web_changed == 'true'
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn lint --filter=web
@@ -109,7 +66,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn build --filter=admin
@@ -121,7 +78,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn build --filter=space
@@ -133,6 +90,6 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn build --filter=web

View File

@@ -51,7 +51,7 @@ jobs:
uses: actions/checkout@v4
full_build_push:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
env:
BUILD_TYPE: full

View File

@@ -11,7 +11,7 @@ env:
jobs:
sync_changes:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
permissions:
pull-requests: write
contents: read

View File

@@ -62,17 +62,143 @@ To ensure consistency throughout the source code, please keep these rules in min
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
## Need help? Questions and suggestions
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Discord Server](https://discord.com/invite/A92xrEGCge).
## Ways to contribute
- Try Plane Cloud and the self hosting platform and give feedback
- Add new integrations
- Add or update translations
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
- Share your thoughts and suggestions with us
- Help create tutorials and blog posts
- Request a feature by submitting a proposal
- Report a bug
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
## Contributing to language support
This guide is designed to help contributors understand how to add or update translations in the application.
### Understanding translation structure
#### File organization
Translations are organized by language in the locales directory. Each language has its own folder containing JSON files for translations. Here's how it looks:
```
packages/i18n/src/locales/
├── en/
│ ├── core.json # Critical translations
│ └── translations.json
├── fr/
│ └── translations.json
└── [language]/
└── translations.json
```
#### Nested structure
To keep translations organized, we use a nested structure for keys. This makes it easier to manage and locate specific translations. For example:
```json
{
"issue": {
"label": "Work item",
"title": {
"label": "Work item title"
}
}
}
```
### Translation formatting guide
We use [IntlMessageFormat](https://formatjs.github.io/docs/intl-messageformat/) to handle dynamic content, such as variables and pluralization. Here's how to format your translations:
#### Examples
- **Simple variables**
```json
{
"greeting": "Hello, {name}!"
}
```
- **Pluralization**
```json
{
"items": "{count, plural, one {Work item} other {Work items}}"
}
```
### Contributing guidelines
#### Updating existing translations
1. Locate the key in `locales/<language>/translations.json`.
2. Update the value while ensuring the key structure remains intact.
3. Preserve any existing ICU formats (e.g., variables, pluralization).
#### Adding new translation keys
1. When introducing a new key, ensure it is added to **all** language files, even if translations are not immediately available. Use English as a placeholder if needed.
2. Keep the nesting structure consistent across all languages.
3. If the new key requires dynamic content (e.g., variables or pluralization), ensure the ICU format is applied uniformly across all languages.
### Adding new languages
Adding a new language involves several steps to ensure it integrates seamlessly with the project. Follow these instructions carefully:
1. **Update type definitions**
Add the new language to the TLanguage type in the language definitions file:
```typescript
// types/language.ts
export type TLanguage = "en" | "fr" | "your-lang";
```
2. **Add language configuration**
Include the new language in the list of supported languages:
```typescript
// constants/language.ts
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "English", value: "en" },
{ label: "Your Language", value: "your-lang" }
];
```
3. **Create translation files**
1. Create a new folder for your language under locales (e.g., `locales/your-lang/`).
2. Add a `translations.json` file inside the folder.
3. Copy the structure from an existing translation file and translate all keys.
4. **Update import logic**
Modify the language import logic to include your new language:
```typescript
private importLanguageFile(language: TLanguage): Promise<any> {
switch (language) {
case "your-lang":
return import("../locales/your-lang/translations.json");
// ...
}
}
```
### Quality checklist
Before submitting your contribution, please ensure the following:
- All translation keys exist in every language file.
- Nested structures match across all language files.
- ICU message formats are correctly implemented.
- All languages load without errors in the application.
- Dynamic values and pluralization work as expected.
- There are no missing or untranslated keys.
#### Pro tips
- When in doubt, refer to the English translations for context.
- Verify pluralization works with different numbers.
- Ensure dynamic values (e.g., `{name}`) are correctly interpolated.
- Double-check that nested key access paths are accurate.
Happy translating! 🌍✨
## Need help? Questions and suggestions
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Discord Server](https://discord.com/invite/A92xrEGCge).

View File

@@ -43,9 +43,6 @@ NGINX_PORT=80
# Debug value for api server use it as 0 for production use
DEBUG=0
CORS_ALLOWED_ORIGINS="http://localhost"
# Error logs
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"
# Database Settings
POSTGRES_USER="plane"
POSTGRES_PASSWORD="plane"

View File

@@ -43,6 +43,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
defaultValues: {
GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"],
GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"],
GITHUB_ORGANIZATION_ID: config["GITHUB_ORGANIZATION_ID"],
},
});
@@ -93,6 +94,19 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
error: Boolean(errors.GITHUB_CLIENT_SECRET),
required: true,
},
{
key: "GITHUB_ORGANIZATION_ID",
type: "text",
label: "Organization ID",
description: (
<>
The organization github ID.
</>
),
placeholder: "123456789",
error: Boolean(errors.GITHUB_ORGANIZATION_ID),
required: false,
},
];
const GITHUB_SERVICE_FIELD: TCopyField[] = [
@@ -150,6 +164,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
reset({
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value,
GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value,
});
})
.catch((err) => console.error(err));

View File

@@ -123,7 +123,7 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
in line with{" "}
<a
href="https://docs.plane.so/self-hosting/telemetry"
href="https://developers.plane.so/self-hosting/telemetry"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"

View File

@@ -7,15 +7,15 @@ import { DefaultLayout } from "@/layouts/default-layout";
export const metadata: Metadata = {
title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.",
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
openGraph: {
title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.",
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
url: "https://plane.so/",
},
keywords:
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration",
"software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration",
twitter: {
site: "@planepowers",
},

View File

@@ -336,7 +336,7 @@ export const InstanceSetupForm: FC = (props) => {
</label>
<a
tabIndex={-1}
href="https://docs.plane.so/telemetry"
href="https://developers.plane.so/self-hosting/telemetry"
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-blue-500 hover:text-blue-600"

View File

@@ -9,6 +9,19 @@ const nextConfig = {
unoptimized: true,
},
basePath: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "",
transpilePackages: [
"@plane/constants",
"@plane/editor",
"@plane/hooks",
"@plane/i18n",
"@plane/logger",
"@plane/propel",
"@plane/services",
"@plane/shared-state",
"@plane/types",
"@plane/ui",
"@plane/utils",
],
};
module.exports = nextConfig;

View File

@@ -1,6 +1,8 @@
{
"name": "admin",
"version": "0.24.1",
"description": "Admin UI for Plane",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -19,16 +21,15 @@
"@plane/ui": "*",
"@plane/utils": "*",
"@plane/services": "*",
"@sentry/nextjs": "^8.32.0",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14",
"axios": "^1.7.9",
"axios": "^1.8.3",
"lodash": "^4.17.21",
"lucide-react": "^0.356.0",
"lucide-react": "^0.469.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"next": "^14.2.20",
"next": "^14.2.25",
"next-themes": "^0.2.1",
"postcss": "^8.4.38",
"react": "^18.3.1",

View File

@@ -145,11 +145,8 @@ RUN chmod +x /app/pg-setup.sh
# APPLICATION ENVIRONMENT SETTINGS
# *****************************************************************************
ENV APP_DOMAIN=localhost
ENV WEB_URL=http://${APP_DOMAIN}
ENV DEBUG=0
ENV SENTRY_DSN=
ENV SENTRY_ENVIRONMENT=production
ENV CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN},https://${APP_DOMAIN}
# Secret Key
ENV SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5

View File

@@ -3,10 +3,6 @@
DEBUG=0
CORS_ALLOWED_ORIGINS="http://localhost"
# Error logs
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"
# Database Settings
POSTGRES_USER="plane"
POSTGRES_PASSWORD="plane"
@@ -59,4 +55,10 @@ APP_BASE_URL=
# Hard delete files after days
HARD_DELETE_AFTER_DAYS=60
HARD_DELETE_AFTER_DAYS=60
# Force HTTPS for handling SSL Termination
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT="60/minute"

View File

@@ -1,4 +1,7 @@
{
"name": "plane-api",
"version": "0.24.1"
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"description": "API server powering Plane's backend"
}

View File

@@ -1,9 +1,13 @@
# python imports
import os
# Third party imports
from rest_framework.throttling import SimpleRateThrottle
class ApiKeyRateThrottle(SimpleRateThrottle):
scope = "api_key"
rate = "60/minute"
rate = os.environ.get("API_KEY_RATE_LIMIT", "60/minute")
def get_cache_key(self, request, view):
# Retrieve the API key from the request header

View File

@@ -15,3 +15,4 @@ from .state import StateLiteSerializer, StateSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
from .intake import IntakeIssueSerializer
from .estimate import EstimatePointSerializer

View File

@@ -72,6 +72,7 @@ class BaseSerializer(serializers.ModelSerializer):
StateLiteSerializer,
UserLiteSerializer,
WorkspaceLiteSerializer,
EstimatePointSerializer,
)
# Expansion mapper
@@ -88,6 +89,7 @@ class BaseSerializer(serializers.ModelSerializer):
"owned_by": UserLiteSerializer,
"members": UserLiteSerializer,
"parent": IssueLiteSerializer,
"estimate_point": EstimatePointSerializer,
}
# Check if field in expansion then expand the field
if expand in expansion:

View File

@@ -1,4 +1,5 @@
# Third party imports
import pytz
from rest_framework import serializers
# Module imports
@@ -18,6 +19,14 @@ class CycleSerializer(BaseSerializer):
completed_estimates = serializers.FloatField(read_only=True)
started_estimates = serializers.FloatField(read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
project = self.context.get("project")
if project and project.timezone:
project_timezone = pytz.timezone(project.timezone)
self.fields["start_date"].timezone = project_timezone
self.fields["end_date"].timezone = project_timezone
def validate(self, data):
if (
data.get("start_date", None) is not None

View File

@@ -0,0 +1,10 @@
# Module imports
from plane.db.models import EstimatePoint
from .base import BaseSerializer
class EstimatePointSerializer(BaseSerializer):
class Meta:
model = EstimatePoint
fields = ["id", "value"]
read_only_fields = fields

View File

@@ -1,6 +1,7 @@
# Django imports
from django.utils import timezone
from lxml import html
from django.db import IntegrityError
# Third party imports
from rest_framework import serializers
@@ -79,6 +80,7 @@ class IssueSerializer(BaseSerializer):
data["assignees"] = ProjectMember.objects.filter(
project_id=self.context.get("project_id"),
is_active=True,
role__gte=15,
member_id__in=data["assignees"],
).values_list("member_id", flat=True)
@@ -138,47 +140,61 @@ class IssueSerializer(BaseSerializer):
updated_by_id = issue.updated_by_id
if assignees is not None and len(assignees):
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=assignee_id,
try:
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for assignee_id in assignees
],
batch_size=10,
)
except IntegrityError:
pass
else:
try:
# Then assign it to default assignee, if it is a valid assignee
if default_assignee_id is not None and ProjectMember.objects.filter(
member_id=default_assignee_id,
project_id=project_id,
role__gte=15,
is_active=True
).exists():
IssueAssignee.objects.create(
assignee_id=default_assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for assignee_id in assignees
],
batch_size=10,
)
else:
# Then assign it to default assignee
if default_assignee_id is not None:
IssueAssignee.objects.create(
assignee_id=default_assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
except IntegrityError:
pass
if labels is not None and len(labels):
IssueLabel.objects.bulk_create(
[
IssueLabel(
label_id=label_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label_id in labels
],
batch_size=10,
)
try:
IssueLabel.objects.bulk_create(
[
IssueLabel(
label_id=label_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label_id in labels
],
batch_size=10,
)
except IntegrityError:
pass
return issue
@@ -194,37 +210,45 @@ class IssueSerializer(BaseSerializer):
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete()
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=assignee_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for assignee_id in assignees
],
batch_size=10,
)
try:
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=assignee_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for assignee_id in assignees
],
batch_size=10,
ignore_conflicts=True,
)
except IntegrityError:
pass
if labels is not None:
IssueLabel.objects.filter(issue=instance).delete()
IssueLabel.objects.bulk_create(
[
IssueLabel(
label_id=label_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label_id in labels
],
batch_size=10,
)
try:
IssueLabel.objects.bulk_create(
[
IssueLabel(
label_id=label_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label_id in labels
],
batch_size=10,
ignore_conflicts=True,
)
except IntegrityError:
pass
# Time updation occues even when other related models are updated
instance.updated_at = timezone.now()

View File

@@ -71,4 +71,9 @@ urlpatterns = [
IssueAttachmentEndpoint.as_view(),
name="attachment",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentEndpoint.as_view(),
name="issue-attachment",
),
]

View File

@@ -137,10 +137,12 @@ class CycleAPIEndpoint(BaseAPIView):
)
def get(self, request, slug, project_id, pk=None):
project = Project.objects.get(workspace__slug=slug, pk=project_id)
if pk:
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
data = CycleSerializer(
queryset, fields=self.fields, expand=self.expand
queryset, fields=self.fields,
expand=self.expand, context={"project": project}
).data
return Response(data, status=status.HTTP_200_OK)
queryset = self.get_queryset().filter(archived_at__isnull=True)
@@ -152,7 +154,8 @@ class CycleAPIEndpoint(BaseAPIView):
start_date__lte=timezone.now(), end_date__gte=timezone.now()
)
data = CycleSerializer(
queryset, many=True, fields=self.fields, expand=self.expand
queryset, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data
return Response(data, status=status.HTTP_200_OK)
@@ -163,7 +166,8 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data,
)
@@ -174,7 +178,8 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data,
)
@@ -185,7 +190,8 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data,
)
@@ -198,14 +204,16 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data,
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data,
)

View File

@@ -28,7 +28,7 @@ from plane.db.models import (
Workspace,
UserFavorite,
)
from plane.bgtasks.webhook_task import model_activity
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from .base import BaseAPIView
@@ -326,6 +326,19 @@ class ProjectAPIEndpoint(BaseAPIView):
entity_type="project", entity_identifier=pk, project_id=pk
).delete()
project.delete()
webhook_activity.delay(
event="project",
verb="deleted",
field=None,
old_value=None,
new_value=None,
actor_id=request.user.id,
slug=slug,
current_site=request.META.get("HTTP_ORIGIN"),
event_id=project.id,
old_identifier=None,
new_identifier=None,
)
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@@ -121,8 +121,6 @@ from .exporter import ExporterHistorySerializer
from .webhook import WebhookSerializer, WebhookLogSerializer
from .dashboard import DashboardSerializer, WidgetSerializer
from .favorite import UserFavoriteSerializer
from .draft import (

View File

@@ -1,21 +0,0 @@
# Module imports
from .base import BaseSerializer
from plane.db.models import DeprecatedDashboard, DeprecatedWidget
# Third party frameworks
from rest_framework import serializers
class DashboardSerializer(BaseSerializer):
class Meta:
model = DeprecatedDashboard
fields = "__all__"
class WidgetSerializer(BaseSerializer):
is_visible = serializers.BooleanField(read_only=True)
widget_filters = serializers.JSONField(read_only=True)
class Meta:
model = DeprecatedWidget
fields = ["id", "key", "is_visible", "widget_filters"]

View File

@@ -2,6 +2,7 @@
from django.utils import timezone
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from django.db import IntegrityError
# Third Party imports
from rest_framework import serializers
@@ -35,6 +36,7 @@ from plane.db.models import (
State,
IssueVersion,
IssueDescriptionVersion,
ProjectMember,
)
@@ -109,14 +111,23 @@ class IssueCreateSerializer(BaseSerializer):
data["label_ids"] = label_ids if label_ids else []
return data
def validate(self, data):
def validate(self, attrs):
if (
data.get("start_date", None) is not None
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
attrs.get("start_date", None) is not None
and attrs.get("target_date", None) is not None
and attrs.get("start_date", None) > attrs.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
return data
if attrs.get("assignee_ids", []):
attrs["assignee_ids"] = ProjectMember.objects.filter(
project_id=self.context["project_id"],
role__gte=15,
is_active=True,
member_id__in=attrs["assignee_ids"],
).values_list("member_id", flat=True)
return attrs
def create(self, validated_data):
assignees = validated_data.pop("assignee_ids", None)
@@ -134,47 +145,64 @@ class IssueCreateSerializer(BaseSerializer):
updated_by_id = issue.updated_by_id
if assignees is not None and len(assignees):
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee=user,
try:
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for assignee_id in assignees
],
batch_size=10,
)
except IntegrityError:
pass
else:
# Then assign it to default assignee, if it is a valid assignee
if (
default_assignee_id is not None
and ProjectMember.objects.filter(
member_id=default_assignee_id,
project_id=project_id,
role__gte=15,
is_active=True,
).exists()
):
try:
IssueAssignee.objects.create(
assignee_id=default_assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user in assignees
],
batch_size=10,
)
else:
# Then assign it to default assignee
if default_assignee_id is not None:
IssueAssignee.objects.create(
assignee_id=default_assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
except IntegrityError:
pass
if labels is not None and len(labels):
IssueLabel.objects.bulk_create(
[
IssueLabel(
label=label,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
)
try:
IssueLabel.objects.bulk_create(
[
IssueLabel(
label=label,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
)
except IntegrityError:
pass
return issue
@@ -190,37 +218,45 @@ class IssueCreateSerializer(BaseSerializer):
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete()
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee=user,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user in assignees
],
batch_size=10,
)
try:
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=assignee_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for assignee_id in assignees
],
batch_size=10,
ignore_conflicts=True,
)
except IntegrityError:
pass
if labels is not None:
IssueLabel.objects.filter(issue=instance).delete()
IssueLabel.objects.bulk_create(
[
IssueLabel(
label=label,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
)
try:
IssueLabel.objects.bulk_create(
[
IssueLabel(
label=label,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
ignore_conflicts=True,
)
except IntegrityError:
pass
# Time updation occues even when other related models are updated
instance.updated_at = timezone.now()
@@ -232,6 +268,20 @@ class IssueActivitySerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
source_data = serializers.SerializerMethodField()
def get_source_data(self, obj):
if (
hasattr(obj, "issue")
and hasattr(obj.issue, "source_data")
and obj.issue.source_data
):
return {
"source": obj.issue.source_data[0].source,
"source_email": obj.issue.source_data[0].source_email,
"extra": obj.issue.source_data[0].extra,
}
return None
class Meta:
model = IssueActivity
@@ -283,10 +333,26 @@ class IssueRelationSerializer(BaseSerializer):
)
name = serializers.CharField(source="related_issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
priority = serializers.CharField(source="related_issue.priority", read_only=True)
assignee_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = IssueRelation
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
"state_id",
"priority",
"assignee_ids",
]
read_only_fields = ["workspace", "project"]
@@ -298,10 +364,26 @@ class RelatedIssueSerializer(BaseSerializer):
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
name = serializers.CharField(source="issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
state_id = serializers.UUIDField(source="issue.state.id", read_only=True)
priority = serializers.CharField(source="issue.priority", read_only=True)
assignee_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = IssueRelation
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
"state_id",
"priority",
"assignee_ids",
]
read_only_fields = ["workspace", "project"]
@@ -472,6 +554,7 @@ class IssueAttachmentLiteSerializer(DynamicBaseSerializer):
"asset",
"attributes",
# "issue_id",
"created_by",
"updated_at",
"updated_by",
"asset_url",

View File

@@ -90,17 +90,7 @@ class ProjectLiteSerializer(BaseSerializer):
class ProjectListSerializer(DynamicBaseSerializer):
total_issues = serializers.IntegerField(read_only=True)
archived_issues = serializers.IntegerField(read_only=True)
archived_sub_issues = serializers.IntegerField(read_only=True)
draft_issues = serializers.IntegerField(read_only=True)
draft_sub_issues = serializers.IntegerField(read_only=True)
sub_issues = serializers.IntegerField(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
member_role = serializers.IntegerField(read_only=True)
anchor = serializers.CharField(read_only=True)
@@ -113,14 +103,9 @@ class ProjectListSerializer(DynamicBaseSerializer):
if project_members is not None:
# Filter members by the project ID
return [
{
"id": member.id,
"member_id": member.member_id,
"member__display_name": member.member.display_name,
"member__avatar": member.member.avatar,
"member__avatar_url": member.member.avatar_url,
}
member.member_id
for member in project_members
if member.is_active and not member.member.is_bot
]
return []
@@ -134,10 +119,6 @@ class ProjectDetailSerializer(BaseSerializer):
default_assignee = UserLiteSerializer(read_only=True)
project_lead = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
member_role = serializers.IntegerField(read_only=True)
anchor = serializers.CharField(read_only=True)

View File

@@ -22,6 +22,7 @@ from plane.db.models import (
ProjectMember,
WorkspaceHomePreference,
Sticky,
WorkspaceUserPreference,
)
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
@@ -31,10 +32,9 @@ from django.core.exceptions import ValidationError
class WorkSpaceSerializer(DynamicBaseSerializer):
owner = UserLiteSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
logo_url = serializers.CharField(read_only=True)
role = serializers.IntegerField(read_only=True)
def validate_slug(self, value):
# Check if the slug is restricted
@@ -59,7 +59,7 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = ["name", "slug", "id"]
fields = ["name", "slug", "id", "logo_url"]
read_only_fields = fields
@@ -90,9 +90,11 @@ class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
class WorkSpaceMemberInviteSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True)
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
workspace = WorkspaceLiteSerializer(read_only=True)
invite_link = serializers.SerializerMethodField()
def get_invite_link(self, obj):
return f"/workspace-invitations/?invitation_id={obj.id}&email={obj.email}&slug={obj.workspace.slug}"
class Meta:
model = WorkspaceMemberInvite
@@ -106,6 +108,7 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
"responded_at",
"created_at",
"updated_at",
"invite_link",
]
@@ -146,6 +149,42 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
return value
def create(self, validated_data):
# Filtering the WorkspaceUserLink with the given url to check if the link already exists.
url = validated_data.get("url")
workspace_user_link = WorkspaceUserLink.objects.filter(
url=url,
workspace_id=validated_data.get("workspace_id"),
owner_id=validated_data.get("owner_id")
)
if workspace_user_link.exists():
raise serializers.ValidationError(
{"error": "URL already exists for this workspace and owner"}
)
return super().create(validated_data)
def update(self, instance, validated_data):
# Filtering the WorkspaceUserLink with the given url to check if the link already exists.
url = validated_data.get("url")
workspace_user_link = WorkspaceUserLink.objects.filter(
url=url,
workspace_id=instance.workspace_id,
owner=instance.owner
)
if workspace_user_link.exclude(pk=instance.id).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this workspace and owner"}
)
return super().update(instance, validated_data)
class IssueRecentVisitSerializer(serializers.ModelSerializer):
project_identifier = serializers.SerializerMethodField()
@@ -258,3 +297,10 @@ class StickySerializer(BaseSerializer):
fields = "__all__"
read_only_fields = ["workspace", "owner"]
extra_kwargs = {"name": {"required": False}}
class WorkspaceUserPreferenceSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserPreference
fields = ["key", "is_pinned", "sort_order"]
read_only_fields = ["workspace", "created_by", "updated_by"]

View File

@@ -2,7 +2,6 @@ from .analytic import urlpatterns as analytic_urls
from .api import urlpatterns as api_urls
from .asset import urlpatterns as asset_urls
from .cycle import urlpatterns as cycle_urls
from .dashboard import urlpatterns as dashboard_urls
from .estimate import urlpatterns as estimate_urls
from .external import urlpatterns as external_urls
from .intake import urlpatterns as intake_urls
@@ -23,7 +22,6 @@ urlpatterns = [
*analytic_urls,
*asset_urls,
*cycle_urls,
*dashboard_urls,
*estimate_urls,
*external_urls,
*intake_urls,

View File

@@ -7,6 +7,7 @@ from plane.app.views import (
SavedAnalyticEndpoint,
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
ProjectStatsEndpoint,
)
@@ -43,4 +44,9 @@ urlpatterns = [
DefaultAnalyticsEndpoint.as_view(),
name="default-analytics",
),
path(
"workspaces/<str:slug>/project-stats/",
ProjectStatsEndpoint.as_view(),
name="project-analytics",
),
]

View File

@@ -1,23 +0,0 @@
from django.urls import path
from plane.app.views import DashboardEndpoint, WidgetsEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/dashboard/",
DashboardEndpoint.as_view(),
name="dashboard",
),
path(
"workspaces/<str:slug>/dashboard/<uuid:dashboard_id>/",
DashboardEndpoint.as_view(),
name="dashboard",
),
path(
"dashboard/<uuid:dashboard_id>/widgets/<uuid:widget_id>/",
WidgetsEndpoint.as_view(),
name="widgets",
),
]

View File

@@ -26,6 +26,8 @@ from plane.app.views import (
IssueBulkUpdateDateEndpoint,
IssueVersionEndpoint,
IssueDescriptionVersionEndpoint,
IssueMetaEndpoint,
IssueDetailIdentifierEndpoint,
)
urlpatterns = [
@@ -278,4 +280,14 @@ urlpatterns = [
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/meta/",
IssueMetaEndpoint.as_view(),
name="issue-meta",
),
path(
"workspaces/<str:slug>/work-items/<str:project_identifier>-<str:issue_identifier>/",
IssueDetailIdentifierEndpoint.as_view(),
name="issue-detail-identifier",
),
]

View File

@@ -23,6 +23,11 @@ urlpatterns = [
ProjectViewSet.as_view({"get": "list", "post": "create"}),
name="project",
),
path(
"workspaces/<str:slug>/projects/details/",
ProjectViewSet.as_view({"get": "list_detail"}),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:pk>/",
ProjectViewSet.as_view(

View File

@@ -31,6 +31,7 @@ from plane.app.views import (
UserRecentVisitViewSet,
WorkspaceHomePreferenceViewSet,
WorkspaceStickyViewSet,
WorkspaceUserPreferenceViewSet,
)
@@ -258,4 +259,15 @@ urlpatterns = [
),
name="workspace-sticky",
),
# User Preference
path(
"workspaces/<str:slug>/sidebar-preferences/",
WorkspaceUserPreferenceViewSet.as_view(),
name="workspace-user-preference",
),
path(
"workspaces/<str:slug>/sidebar-preferences/<str:key>/",
WorkspaceUserPreferenceViewSet.as_view(),
name="workspace-user-preference",
),
]

View File

@@ -48,6 +48,7 @@ from .workspace.favorite import (
WorkspaceFavoriteGroupEndpoint,
)
from .workspace.recent_visit import UserRecentVisitViewSet
from .workspace.user_preference import WorkspaceUserPreferenceViewSet
from .workspace.member import (
WorkSpaceMemberViewSet,
@@ -115,6 +116,8 @@ from .issue.base import (
IssuePaginatedViewSet,
IssueDetailEndpoint,
IssueBulkUpdateDateEndpoint,
IssueMetaEndpoint,
IssueDetailIdentifierEndpoint,
)
from .issue.activity import IssueActivityEndpoint
@@ -189,6 +192,7 @@ from .analytic.base import (
SavedAnalyticEndpoint,
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
ProjectStatsEndpoint,
)
from .notification.base import (
@@ -206,8 +210,6 @@ from .webhook.base import (
WebhookSecretRegenerateEndpoint,
)
from .dashboard.base import DashboardEndpoint, WidgetsEndpoint
from .error_404 import custom_404_view
from .notification.base import MarkAllReadNotificationViewSet

View File

@@ -3,7 +3,7 @@ from django.db.models import Count, F, Sum, Q
from django.db.models.functions import ExtractMonth
from django.utils import timezone
from django.db.models.functions import Concat
from django.db.models import Case, When, Value
from django.db.models import Case, When, Value, OuterRef, Func
from django.db import models
# Third party imports
@@ -15,7 +15,16 @@ from plane.app.permissions import WorkSpaceAdminPermission
from plane.app.serializers import AnalyticViewSerializer
from plane.app.views.base import BaseAPIView, BaseViewSet
from plane.bgtasks.analytic_plot_export import analytic_export_task
from plane.db.models import AnalyticView, Issue, Workspace
from plane.db.models import (
AnalyticView,
Issue,
Workspace,
Project,
ProjectMember,
Cycle,
Module,
)
from plane.utils.analytics_plot import build_graph_plot
from plane.utils.issue_filters import issue_filters
from plane.app.permissions import allow_permission, ROLE
@@ -441,3 +450,74 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
},
status=status.HTTP_200_OK,
)
class ProjectStatsEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
fields = request.GET.get("fields", "").split(",")
project_ids = request.GET.get("project_ids", "")
valid_fields = {
"total_issues",
"completed_issues",
"total_members",
"total_cycles",
"total_modules",
}
requested_fields = set(filter(None, fields)) & valid_fields
if not requested_fields:
requested_fields = valid_fields
projects = Project.objects.filter(workspace__slug=slug)
if project_ids:
projects = projects.filter(id__in=project_ids.split(","))
annotations = {}
if "total_issues" in requested_fields:
annotations["total_issues"] = (
Issue.issue_objects.filter(project_id=OuterRef("pk"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "completed_issues" in requested_fields:
annotations["completed_issues"] = (
Issue.issue_objects.filter(
project_id=OuterRef("pk"), state__group="completed"
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "total_cycles" in requested_fields:
annotations["total_cycles"] = (
Cycle.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "total_modules" in requested_fields:
annotations["total_modules"] = (
Module.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "total_members" in requested_fields:
annotations["total_members"] = (
ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
projects = projects.annotate(**annotations).values("id", *requested_fields)
return Response(projects, status=status.HTTP_200_OK)

View File

@@ -5,6 +5,7 @@ import uuid
from django.conf import settings
from django.http import HttpResponseRedirect
from django.utils import timezone
from django.db import IntegrityError
# Third party imports
from rest_framework import status
@@ -679,15 +680,30 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
[self.save_project_cover(asset, project_id) for asset in assets]
if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION:
assets.update(issue_id=entity_id)
# For some cases, the bulk api is called after the issue is deleted creating
# an integrity error
try:
assets.update(issue_id=entity_id)
except IntegrityError:
pass
if asset.entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
assets.update(comment_id=entity_id)
# For some cases, the bulk api is called after the comment is deleted
# creating an integrity error
try:
assets.update(comment_id=entity_id)
except IntegrityError:
pass
if asset.entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
assets.update(page_id=entity_id)
if asset.entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION:
assets.update(draft_issue_id=entity_id)
# For some cases, the bulk api is called after the draft issue is deleted
# creating an integrity error
try:
assets.update(draft_issue_id=entity_id)
except IntegrityError:
pass
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@@ -47,6 +47,7 @@ from plane.db.models import (
User,
Project,
ProjectMember,
UserRecentVisit,
)
from plane.utils.analytics_plot import burndown_plot
from plane.bgtasks.recent_visited_task import recent_visited_task
@@ -133,11 +134,11 @@ class CycleViewSet(BaseViewSet):
)
)
.annotate(
pending_issues=Count(
cancelled_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group__in=["backlog", "unstarted", "started"],
issue_cycle__issue__state__group__in=["cancelled"],
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
@@ -226,7 +227,7 @@ class CycleViewSet(BaseViewSet):
"is_favorite",
"total_issues",
"completed_issues",
"pending_issues",
"cancelled_issues",
"assignee_ids",
"status",
"version",
@@ -258,7 +259,7 @@ class CycleViewSet(BaseViewSet):
# meta fields
"is_favorite",
"total_issues",
"pending_issues",
"cancelled_issues",
"completed_issues",
"assignee_ids",
"status",
@@ -267,7 +268,7 @@ class CycleViewSet(BaseViewSet):
)
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(
data, datetime_fields, request.user.user_timezone
data, datetime_fields, project_timezone
)
return Response(data, status=status.HTTP_200_OK)
@@ -317,9 +318,13 @@ class CycleViewSet(BaseViewSet):
.first()
)
# Fetch the project timezone
project = Project.objects.get(id=self.kwargs.get("project_id"))
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter(
cycle, datetime_fields, request.user.user_timezone
cycle, datetime_fields, project_timezone
)
# Send the model activity
@@ -406,9 +411,13 @@ class CycleViewSet(BaseViewSet):
"created_by",
).first()
# Fetch the project timezone
project = Project.objects.get(id=self.kwargs.get("project_id"))
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter(
cycle, datetime_fields, request.user.user_timezone
cycle, datetime_fields, project_timezone
)
# Send the model activity
@@ -479,10 +488,11 @@ class CycleViewSet(BaseViewSet):
)
queryset = queryset.first()
# Fetch the project timezone
project = Project.objects.get(id=self.kwargs.get("project_id"))
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(
data, datetime_fields, request.user.user_timezone
)
data = user_timezone_converter(data, datetime_fields, project_timezone)
recent_visited_task.delay(
slug=slug,
@@ -543,6 +553,13 @@ class CycleViewSet(BaseViewSet):
entity_identifier=pk,
project_id=project_id,
).delete()
# Delete the cycle from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="cycle",
).delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@@ -1,812 +0,0 @@
# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import (
Case,
CharField,
Count,
Exists,
F,
Func,
IntegerField,
JSONField,
OuterRef,
Prefetch,
Q,
Subquery,
UUIDField,
Value,
When,
)
from django.db.models.functions import Coalesce
from django.utils import timezone
from rest_framework import status
# Third Party imports
from rest_framework.response import Response
from plane.app.serializers import (
DashboardSerializer,
IssueActivitySerializer,
IssueSerializer,
WidgetSerializer,
)
from plane.db.models import (
DeprecatedDashboard,
DeprecatedDashboardWidget,
Issue,
IssueActivity,
FileAsset,
IssueLink,
IssueRelation,
Project,
DeprecatedWidget,
WorkspaceMember,
CycleIssue,
)
from plane.utils.issue_filters import issue_filters
# Module imports
from .. import BaseAPIView
def dashboard_overview_stats(self, request, slug):
assigned_issues = (
Issue.issue_objects.filter(
(Q(assignees__in=[request.user]) & Q(issue_assignee__deleted_at__isnull=True)),
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
workspace__slug=slug,
)
.filter(
Q(
project__project_projectmember__role=5,
project__guest_view_all_features=True,
)
| Q(
project__project_projectmember__role=5,
project__guest_view_all_features=False,
created_by=self.request.user,
)
|
# For other roles (role < 5), show all issues
Q(project__project_projectmember__role__gt=5),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.count()
)
pending_issues_count = (
Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
target_date__lt=timezone.now().date(),
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
workspace__slug=slug,
assignees__in=[request.user],
)
.filter(
Q(
project__project_projectmember__role=5,
project__guest_view_all_features=True,
)
| Q(
project__project_projectmember__role=5,
project__guest_view_all_features=False,
created_by=self.request.user,
)
|
# For other roles (role < 5), show all issues
Q(project__project_projectmember__role__gt=5),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.count()
)
created_issues_count = (
Issue.issue_objects.filter(
workspace__slug=slug,
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
created_by_id=request.user.id,
)
.filter(
Q(
project__project_projectmember__role=5,
project__guest_view_all_features=True,
)
| Q(
project__project_projectmember__role=5,
project__guest_view_all_features=False,
created_by=self.request.user,
)
|
# For other roles (role < 5), show all issues
Q(project__project_projectmember__role__gt=5),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.count()
)
completed_issues_count = (
Issue.issue_objects.filter(
(
Q(assignees__in=[request.user])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
state__group="completed",
)
.filter(
Q(
project__project_projectmember__role=5,
project__guest_view_all_features=True,
)
| Q(
project__project_projectmember__role=5,
project__guest_view_all_features=False,
created_by=self.request.user,
)
|
# For other roles (role < 5), show all issues
Q(project__project_projectmember__role__gt=5),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.count()
)
return Response(
{
"assigned_issues_count": assigned_issues,
"pending_issues_count": pending_issues_count,
"completed_issues_count": completed_issues_count,
"created_issues_count": created_issues_count,
},
status=status.HTTP_200_OK,
)
def dashboard_assigned_issues(self, request, slug):
filters = issue_filters(request.query_params, "GET")
issue_type = request.GET.get("issue_type", None)
# get all the assigned issues
assigned_issues = (
Issue.issue_objects.filter(
(
Q(assignees__in=[request.user])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_relation",
queryset=IssueRelation.objects.select_related(
"related_issue"
).select_related("issue"),
)
)
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
)
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role=5, is_active=True
).exists():
assigned_issues = assigned_issues.filter(created_by=request.user)
# Priority Ordering
priority_order = ["urgent", "high", "medium", "low", "none"]
assigned_issues = assigned_issues.annotate(
priority_order=Case(
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
output_field=CharField(),
)
).order_by("priority_order")
if issue_type == "pending":
pending_issues_count = assigned_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
).count()
pending_issues = assigned_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
)[:5]
return Response(
{
"issues": IssueSerializer(
pending_issues, many=True, expand=self.expand
).data,
"count": pending_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "completed":
completed_issues_count = assigned_issues.filter(
state__group__in=["completed"]
).count()
completed_issues = assigned_issues.filter(state__group__in=["completed"])[:5]
return Response(
{
"issues": IssueSerializer(
completed_issues, many=True, expand=self.expand
).data,
"count": completed_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "overdue":
overdue_issues_count = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now(),
).count()
overdue_issues = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now(),
)[:5]
return Response(
{
"issues": IssueSerializer(
overdue_issues, many=True, expand=self.expand
).data,
"count": overdue_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "upcoming":
upcoming_issues_count = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now(),
).count()
upcoming_issues = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now(),
)[:5]
return Response(
{
"issues": IssueSerializer(
upcoming_issues, many=True, expand=self.expand
).data,
"count": upcoming_issues_count,
},
status=status.HTTP_200_OK,
)
return Response(
{"error": "Please specify a valid issue type"},
status=status.HTTP_400_BAD_REQUEST,
)
def dashboard_created_issues(self, request, slug):
filters = issue_filters(request.query_params, "GET")
issue_type = request.GET.get("issue_type", None)
# get all the assigned issues
created_issues = (
Issue.issue_objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
created_by=request.user,
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.order_by("created_at")
)
# Priority Ordering
priority_order = ["urgent", "high", "medium", "low", "none"]
created_issues = created_issues.annotate(
priority_order=Case(
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
output_field=CharField(),
)
).order_by("priority_order")
if issue_type == "pending":
pending_issues_count = created_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
).count()
pending_issues = created_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
)[:5]
return Response(
{
"issues": IssueSerializer(
pending_issues, many=True, expand=self.expand
).data,
"count": pending_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "completed":
completed_issues_count = created_issues.filter(
state__group__in=["completed"]
).count()
completed_issues = created_issues.filter(state__group__in=["completed"])[:5]
return Response(
{
"issues": IssueSerializer(completed_issues, many=True).data,
"count": completed_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "overdue":
overdue_issues_count = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now(),
).count()
overdue_issues = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now(),
)[:5]
return Response(
{
"issues": IssueSerializer(overdue_issues, many=True).data,
"count": overdue_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "upcoming":
upcoming_issues_count = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now(),
).count()
upcoming_issues = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now(),
)[:5]
return Response(
{
"issues": IssueSerializer(upcoming_issues, many=True).data,
"count": upcoming_issues_count,
},
status=status.HTTP_200_OK,
)
return Response(
{"error": "Please specify a valid issue type"},
status=status.HTTP_400_BAD_REQUEST,
)
def dashboard_issues_by_state_groups(self, request, slug):
filters = issue_filters(request.query_params, "GET")
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
extra_filters = {}
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role=5, is_active=True
).exists():
extra_filters = {"created_by": request.user}
issues_by_state_groups = (
Issue.issue_objects.filter(
workspace__slug=slug,
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
assignees__in=[request.user],
)
.filter(**filters, **extra_filters)
.values("state__group")
.annotate(count=Count("id"))
)
# default state
all_groups = {state: 0 for state in state_order}
# Update counts for existing groups
for entry in issues_by_state_groups:
all_groups[entry["state__group"]] = entry["count"]
# Prepare output including all groups with their counts
output_data = [
{"state": group, "count": count} for group, count in all_groups.items()
]
return Response(output_data, status=status.HTTP_200_OK)
def dashboard_issues_by_priority(self, request, slug):
filters = issue_filters(request.query_params, "GET")
priority_order = ["urgent", "high", "medium", "low", "none"]
extra_filters = {}
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role=5, is_active=True
).exists():
extra_filters = {"created_by": request.user}
issues_by_priority = (
Issue.issue_objects.filter(
workspace__slug=slug,
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
assignees__in=[request.user],
)
.filter(**filters, **extra_filters)
.values("priority")
.annotate(count=Count("id"))
)
# default priority
all_groups = {priority: 0 for priority in priority_order}
# Update counts for existing groups
for entry in issues_by_priority:
all_groups[entry["priority"]] = entry["count"]
# Prepare output including all groups with their counts
output_data = [
{"priority": group, "count": count} for group, count in all_groups.items()
]
return Response(output_data, status=status.HTTP_200_OK)
def dashboard_recent_activity(self, request, slug):
queryset = IssueActivity.objects.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
actor=request.user,
).select_related("actor", "workspace", "issue", "project")[:8]
return Response(
IssueActivitySerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
def dashboard_recent_projects(self, request, slug):
project_ids = (
IssueActivity.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
actor=request.user,
)
.values_list("project_id", flat=True)
.distinct()
)
# Extract project IDs from the recent projects
unique_project_ids = set(project_id for project_id in project_ids)
# Fetch additional projects only if needed
if len(unique_project_ids) < 4:
additional_projects = Project.objects.filter(
project_projectmember__member=request.user,
project_projectmember__is_active=True,
archived_at__isnull=True,
workspace__slug=slug,
).exclude(id__in=unique_project_ids)
# Append additional project IDs to the existing list
unique_project_ids.update(additional_projects.values_list("id", flat=True))
return Response(list(unique_project_ids)[:4], status=status.HTTP_200_OK)
def dashboard_recent_collaborators(self, request, slug):
project_members_with_activities = (
WorkspaceMember.objects.filter(workspace__slug=slug, is_active=True)
.annotate(
active_issue_count=Count(
Case(
When(
member__issue_assignee__issue__state__group__in=[
"unstarted",
"started",
],
member__issue_assignee__issue__workspace__slug=slug,
member__issue_assignee__issue__project__project_projectmember__member=request.user,
member__issue_assignee__issue__project__project_projectmember__is_active=True,
then=F("member__issue_assignee__issue__id"),
),
distinct=True,
output_field=IntegerField(),
),
distinct=True,
),
user_id=F("member_id"),
)
.values("user_id", "active_issue_count")
.order_by("-active_issue_count")
.distinct()
)
return Response((project_members_with_activities), status=status.HTTP_200_OK)
class DashboardEndpoint(BaseAPIView):
def create(self, request, slug):
serializer = DashboardSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, slug, pk):
serializer = DashboardSerializer(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)
def delete(self, request, slug, pk):
serializer = DashboardSerializer(data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get(self, request, slug, dashboard_id=None):
if not dashboard_id:
dashboard_type = request.GET.get("dashboard_type", None)
if dashboard_type == "home":
dashboard, created = DeprecatedDashboard.objects.get_or_create(
type_identifier=dashboard_type,
owned_by=request.user,
is_default=True,
)
if created:
widgets_to_fetch = [
"overview_stats",
"assigned_issues",
"created_issues",
"issues_by_state_groups",
"issues_by_priority",
"recent_activity",
"recent_projects",
"recent_collaborators",
]
updated_dashboard_widgets = []
for widget_key in widgets_to_fetch:
widget = DeprecatedWidget.objects.filter(
key=widget_key
).values_list("id", flat=True)
if widget:
updated_dashboard_widgets.append(
DeprecatedDashboardWidget(
widget_id=widget, dashboard_id=dashboard.id
)
)
DeprecatedDashboardWidget.objects.bulk_create(
updated_dashboard_widgets, batch_size=100
)
widgets = (
DeprecatedWidget.objects.annotate(
is_visible=Exists(
DeprecatedDashboardWidget.objects.filter(
widget_id=OuterRef("pk"),
dashboard_id=dashboard.id,
is_visible=True,
)
)
)
.annotate(
dashboard_filters=Subquery(
DeprecatedDashboardWidget.objects.filter(
widget_id=OuterRef("pk"),
dashboard_id=dashboard.id,
filters__isnull=False,
)
.exclude(filters={})
.values("filters")[:1]
)
)
.annotate(
widget_filters=Case(
When(
dashboard_filters__isnull=False,
then=F("dashboard_filters"),
),
default=F("filters"),
output_field=JSONField(),
)
)
)
return Response(
{
"dashboard": DashboardSerializer(dashboard).data,
"widgets": WidgetSerializer(widgets, many=True).data,
},
status=status.HTTP_200_OK,
)
return Response(
{"error": "Please specify a valid dashboard type"},
status=status.HTTP_400_BAD_REQUEST,
)
widget_key = request.GET.get("widget_key", "overview_stats")
WIDGETS_MAPPER = {
"overview_stats": dashboard_overview_stats,
"assigned_issues": dashboard_assigned_issues,
"created_issues": dashboard_created_issues,
"issues_by_state_groups": dashboard_issues_by_state_groups,
"issues_by_priority": dashboard_issues_by_priority,
"recent_activity": dashboard_recent_activity,
"recent_projects": dashboard_recent_projects,
"recent_collaborators": dashboard_recent_collaborators,
}
func = WIDGETS_MAPPER.get(widget_key)
if func is not None:
response = func(self, request=request, slug=slug)
if isinstance(response, Response):
return response
return Response(
{"error": "Please specify a valid widget key"},
status=status.HTTP_400_BAD_REQUEST,
)
class WidgetsEndpoint(BaseAPIView):
def patch(self, request, dashboard_id, widget_id):
dashboard_widget = DeprecatedDashboardWidget.objects.filter(
widget_id=widget_id, dashboard_id=dashboard_id
).first()
dashboard_widget.is_visible = request.data.get(
"is_visible", dashboard_widget.is_visible
)
dashboard_widget.sort_order = request.data.get(
"sort_order", dashboard_widget.sort_order
)
dashboard_widget.filters = request.data.get("filters", dashboard_widget.filters)
dashboard_widget.save()
return Response({"message": "successfully updated"}, status=status.HTTP_200_OK)

View File

@@ -3,7 +3,7 @@ import os
from typing import List, Dict, Tuple
# Third party import
import litellm
from openai import OpenAI
import requests
from rest_framework import status
@@ -116,12 +116,14 @@ def get_llm_response(task, prompt, api_key: str, model: str, provider: str) -> T
if provider.lower() == "gemini":
model = f"gemini/{model}"
response = litellm.completion(
client = OpenAI(api_key=api_key)
chat_completion = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": final_text}],
api_key=api_key,
messages=[
{"role": "user", "content": final_text}
]
)
text = response.choices[0].message.content.strip()
text = chat_completion.choices[0].message.content
return text, None
except Exception as e:
log_exception(e)
@@ -175,7 +177,7 @@ class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug):
api_key, model, provider = get_llm_config()
if not api_key or not model or not provider:
return Response(
{"error": "LLM provider API key and model are required"},

View File

@@ -174,14 +174,19 @@ class IntakeIssueViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
intake_id = Intake.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
if not intake:
return Response(
{"error": "Intake not found"}, status=status.HTTP_404_NOT_FOUND
)
project = Project.objects.get(pk=project_id)
filters = issue_filters(request.GET, "GET", "issue__")
intake_issue = (
IntakeIssue.objects.filter(
intake_id=intake_id.id, project_id=project_id, **filters
intake_id=intake.id, project_id=project_id, **filters
)
.select_related("issue")
.prefetch_related("issue__labels")
@@ -382,7 +387,7 @@ class IntakeIssueViewSet(BaseViewSet):
}
issue_serializer = IssueCreateSerializer(
issue, data=issue_data, partial=True
issue, data=issue_data, partial=True, context={"project_id": project_id}
)
if issue_serializer.is_valid():

View File

@@ -14,7 +14,7 @@ from rest_framework import status
from .. import BaseAPIView
from plane.app.serializers import IssueActivitySerializer, IssueCommentSerializer
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
from plane.db.models import IssueActivity, IssueComment, CommentReaction
from plane.db.models import IssueActivity, IssueComment, CommentReaction, IntakeIssue
class IssueActivityEndpoint(BaseAPIView):
@@ -57,13 +57,22 @@ class IssueActivityEndpoint(BaseAPIView):
)
)
)
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
if request.GET.get("activity_type", None) == "issue-property":
issue_activities = issue_activities.prefetch_related(
Prefetch(
"issue__issue_intake",
queryset=IntakeIssue.objects.only(
"source_email", "source", "extra"
),
to_attr="source_data",
)
)
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
return Response(issue_activities, status=status.HTTP_200_OK)
if request.GET.get("activity_type", None) == "issue-comment":
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
return Response(issue_comments, status=status.HTTP_200_OK)
result_list = sorted(

View File

@@ -44,6 +44,8 @@ from plane.db.models import (
Project,
ProjectMember,
CycleIssue,
UserRecentVisit,
ModuleIssue,
)
from plane.utils.grouper import (
issue_group_values,
@@ -546,7 +548,7 @@ class IssueViewSet(BaseViewSet):
)
"""
if the role is guest and guest_view_all_features is false and owned by is not
if the role is guest and guest_view_all_features is false and owned by is not
the requesting user then dont show the issue
"""
@@ -634,7 +636,9 @@ class IssueViewSet(BaseViewSet):
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
serializer = IssueCreateSerializer(issue, data=request.data, partial=True)
serializer = IssueCreateSerializer(
issue, data=request.data, partial=True, context={"project_id": project_id}
)
if serializer.is_valid():
serializer.save()
issue_activity.delay(
@@ -671,6 +675,13 @@ class IssueViewSet(BaseViewSet):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue.delete()
# delete the issue from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="issue",
).delete(soft=False)
issue_activity.delay(
type="issue.activity.deleted",
requested_data=json.dumps({"issue_id": str(pk)}),
@@ -728,6 +739,13 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
total_issues = len(issues)
# First, delete all related cycle issues
CycleIssue.objects.filter(issue_id__in=issue_ids).delete()
# Then, delete all related module issues
ModuleIssue.objects.filter(issue_id__in=issue_ids).delete()
# Finally, delete the issues themselves
issues.delete()
return Response(
@@ -1088,3 +1106,188 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
return Response(
{"message": "Issues updated successfully"}, status=status.HTTP_200_OK
)
class IssueMetaEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="PROJECT")
def get(self, request, slug, project_id, issue_id):
issue = Issue.issue_objects.only("sequence_id", "project__identifier").get(
id=issue_id, project_id=project_id, workspace__slug=slug
)
return Response(
{
"sequence_id": issue.sequence_id,
"project_identifier": issue.project.identifier,
},
status=status.HTTP_200_OK,
)
class IssueDetailIdentifierEndpoint(BaseAPIView):
def strict_str_to_int(self, s):
if not s.isdigit() and not (s.startswith("-") and s[1:].isdigit()):
raise ValueError("Invalid integer string")
return int(s)
def get(self, request, slug, project_identifier, issue_identifier):
# Check if the issue identifier is a valid integer
try:
issue_identifier = self.strict_str_to_int(issue_identifier)
except ValueError:
return Response(
{"error": "Invalid issue identifier"},
status=status.HTTP_400_BAD_REQUEST,
)
# Fetch the project
project = Project.objects.get(
identifier__iexact=project_identifier, workspace__slug=slug
)
# Check if the user is a member of the project
if not ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project.id,
member=request.user,
is_active=True,
).exists():
return Response(
{"error": "You are not allowed to view this issue"},
status=status.HTTP_403_FORBIDDEN,
)
# Fetch the issue
issue = (
Issue.issue_objects.filter(project_id=project.id)
.filter(workspace__slug=slug)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[
:1
]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(sequence_id=issue_identifier)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("issue", "actor"),
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project.id,
issue__sequence_id=issue_identifier,
subscriber=request.user,
)
)
)
).first()
# Check if the issue exists
if not issue:
return Response(
{"error": "The required object does not exist."},
status=status.HTTP_404_NOT_FOUND,
)
"""
if the role is guest and guest_view_all_features is false and owned by is not
the requesting user then dont show the issue
"""
if (
ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project.id,
member=request.user,
role=5,
is_active=True,
).exists()
and not project.guest_view_all_features
and not issue.created_by == request.user
):
return Response(
{"error": "You are not allowed to view this issue"},
status=status.HTTP_400_BAD_REQUEST,
)
recent_visited_task.delay(
slug=slug,
entity_name="issue",
entity_identifier=str(issue.id),
user_id=str(request.user.id),
project_id=str(project.id),
)
# Serialize the issue
serializer = IssueDetailSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)

View File

@@ -5,6 +5,7 @@ import json
from django.utils import timezone
from django.db.models import Exists
from django.core.serializers.json import DjangoJSONEncoder
from django.db import IntegrityError
# Third Party imports
from rest_framework.response import Response
@@ -164,24 +165,32 @@ class CommentReactionViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def create(self, request, slug, project_id, comment_id):
serializer = CommentReactionSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id, actor_id=request.user.id, comment_id=comment_id
try:
serializer = CommentReactionSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
actor_id=request.user.id,
comment_id=comment_id,
)
issue_activity.delay(
type="comment_reaction.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "Reaction already exists for the user"},
status=status.HTTP_400_BAD_REQUEST,
)
issue_activity.delay(
type="comment_reaction.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def destroy(self, request, slug, project_id, comment_id, reaction_code):

View File

@@ -35,7 +35,9 @@ class LabelViewSet(BaseViewSet):
.order_by("sort_order")
)
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
@invalidate_cache(
path="/api/workspaces/:slug/labels/", url_params=True, user=False, multiple=True
)
@allow_permission([ROLE.ADMIN])
def create(self, request, slug, project_id):
try:
@@ -53,6 +55,20 @@ class LabelViewSet(BaseViewSet):
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
@allow_permission([ROLE.ADMIN])
def partial_update(self, request, *args, **kwargs):
# Check if the label name is unique within the project
if (
"name" in request.data
and Label.objects.filter(
project_id=kwargs["project_id"], name=request.data["name"]
)
.exclude(pk=kwargs["pk"])
.exists()
):
return Response(
{"error": "Label with the same name already exists in the project"},
status=status.HTTP_400_BAD_REQUEST,
)
# call the parent method to perform the update
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
@@ -72,7 +88,7 @@ class BulkCreateIssueLabelsEndpoint(BaseAPIView):
Label(
name=label.get("name", "Migrated"),
description=label.get("description", "Migrated Issue"),
color=f"#{random.randint(0, 0xFFFFFF+1):06X}",
color=f"#{random.randint(0, 0xFFFFFF + 1):06X}",
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,

View File

@@ -272,10 +272,9 @@ class IssueRelationViewSet(BaseViewSet):
issue_relations = IssueRelation.objects.filter(
workspace__slug=slug,
project_id=project_id,
).filter(
Q(issue_id=related_issue, related_issue_id=issue_id) |
Q(issue_id=issue_id, related_issue_id=related_issue)
Q(issue_id=related_issue, related_issue_id=issue_id)
| Q(issue_id=issue_id, related_issue_id=related_issue)
)
issue_relations = issue_relations.first()
current_instance = json.dumps(

View File

@@ -54,6 +54,7 @@ from plane.db.models import (
ModuleLink,
ModuleUserProperties,
Project,
UserRecentVisit,
)
from plane.utils.analytics_plot import burndown_plot
from plane.utils.timezone_converter import user_timezone_converter
@@ -808,6 +809,13 @@ class ModuleViewSet(BaseViewSet):
entity_identifier=pk,
project_id=project_id,
).delete()
# delete the module from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="module",
).delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@@ -280,7 +280,7 @@ class ModuleIssueViewSet(BaseViewSet):
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
{"module_name": module_issue.first().module.name}
{"module_name": module_issue.first().module.name if (module_issue.first() and module_issue.first().module) else None}
),
epoch=int(timezone.now().timestamp()),
notification=True,

View File

@@ -33,13 +33,14 @@ from plane.db.models import (
ProjectMember,
ProjectPage,
Project,
UserRecentVisit,
)
from plane.utils.error_codes import ERROR_CODES
from ..base import BaseAPIView, BaseViewSet
from plane.bgtasks.page_transaction_task import page_transaction
from plane.bgtasks.page_version_task import page_version
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.bgtasks.copy_s3_object import copy_s3_objects
def unarchive_archive_page_and_descendants(page_id, archived_at):
# Your SQL query
@@ -387,6 +388,13 @@ class PageViewSet(BaseViewSet):
entity_identifier=pk,
entity_type="page",
).delete()
# Delete the page from recent visit
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="page",
).delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -589,6 +597,16 @@ class PageDuplicateEndpoint(BaseAPIView):
page_transaction.delay(
{"description_html": page.description_html}, None, page.id
)
# Copy the s3 objects uploaded in the page
copy_s3_objects.delay(
entity_name="PAGE",
entity_identifier=page.id,
project_id=project_id,
slug=slug,
user_id=request.user.id,
)
page = (
Page.objects.filter(pk=page.id)
.annotate(

View File

@@ -6,7 +6,7 @@ import json
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.core.serializers.json import DjangoJSONEncoder
# Third Party imports
@@ -25,12 +25,9 @@ from plane.app.serializers import (
from plane.app.permissions import ProjectMemberPermission, allow_permission, ROLE
from plane.db.models import (
UserFavorite,
Cycle,
Intake,
DeployBoard,
IssueUserProperty,
Issue,
Module,
Project,
ProjectIdentifier,
ProjectMember,
@@ -39,7 +36,7 @@ from plane.db.models import (
WorkspaceMember,
)
from plane.utils.cache import cache_response
from plane.bgtasks.webhook_task import model_activity
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.utils.exception_logger import log_exception
@@ -73,36 +70,6 @@ class ProjectViewSet(BaseViewSet):
)
)
)
.annotate(
is_member=Exists(
ProjectMember.objects.filter(
member=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
is_active=True,
)
)
)
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_cycles=Cycle.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_modules=Module.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
member_role=ProjectMember.objects.filter(
project_id=OuterRef("pk"),
@@ -133,7 +100,7 @@ class ProjectViewSet(BaseViewSet):
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug):
def list_detail(self, request, slug):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
projects = self.get_queryset().order_by("sort_order", "name")
if WorkspaceMember.objects.filter(
@@ -170,6 +137,75 @@ class ProjectViewSet(BaseViewSet):
).data
return Response(projects, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug):
sort_order = ProjectMember.objects.filter(
member=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
is_active=True,
).values("sort_order")
projects = (
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
.select_related(
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate(
member_role=ProjectMember.objects.filter(
project_id=OuterRef("pk"),
member_id=self.request.user.id,
is_active=True,
).values("role")
)
.annotate(inbox_view=F("intake_view"))
.annotate(sort_order=Subquery(sort_order))
.distinct()
).values(
"id",
"name",
"identifier",
"sort_order",
"logo_props",
"member_role",
"archived_at",
"workspace",
"cycle_view",
"issue_views_view",
"module_view",
"page_view",
"inbox_view",
"guest_view_all_features",
"project_lead",
"network",
"created_at",
"updated_at",
"created_by",
"updated_by",
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=5
).exists():
projects = projects.filter(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=15
).exists():
projects = projects.filter(
Q(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
| Q(network=2)
)
return Response(projects, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
@@ -182,58 +218,6 @@ class ProjectViewSet(BaseViewSet):
)
.filter(archived_at__isnull=True)
.filter(pk=pk)
.annotate(
total_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("pk")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("pk"), parent__isnull=False
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
archived_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"), archived_at__isnull=False
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
archived_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
archived_at__isnull=False,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
draft_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"), is_draft=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
draft_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
is_draft=True,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
).first()
if project is None:
@@ -462,7 +446,19 @@ class ProjectViewSet(BaseViewSet):
):
project = Project.objects.get(pk=pk)
project.delete()
webhook_activity.delay(
event="project",
verb="deleted",
field=None,
old_value=None,
new_value=None,
actor_id=request.user.id,
slug=slug,
current_site=request.META.get("HTTP_ORIGIN"),
event_id=project.id,
old_identifier=None,
new_identifier=None,
)
# Delete the project members
DeployBoard.objects.filter(project_id=pk, workspace__slug=slug).delete()

View File

@@ -16,17 +16,17 @@ from rest_framework.permissions import AllowAny
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.app.serializers import ProjectMemberInviteSerializer
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import (
ProjectMember,
Workspace,
ProjectMemberInvite,
User,
WorkspaceMember,
Project,
IssueUserProperty,
)
from plane.db.models.project import ProjectNetwork
class ProjectInvitationsViewset(BaseViewSet):
@@ -128,6 +128,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
.select_related("workspace", "workspace__owner", "project")
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def create(self, request, slug):
project_ids = request.data.get("project_ids", [])
@@ -136,11 +137,20 @@ class UserProjectInvitationsViewset(BaseViewSet):
member=request.user, workspace__slug=slug, is_active=True
)
if workspace_member.role not in [ROLE.ADMIN.value, ROLE.MEMBER.value]:
return Response(
{"error": "You do not have permission to join the project"},
status=status.HTTP_403_FORBIDDEN,
)
# Get all the projects
projects = Project.objects.filter(
id__in=project_ids, workspace__slug=slug
).only("id", "network")
# Check if user has permission to join each project
for project in projects:
if (
project.network == ProjectNetwork.SECRET.value
and workspace_member.role != ROLE.ADMIN.value
):
return Response(
{"error": "Only workspace admins can join private project"},
status=status.HTTP_403_FORBIDDEN,
)
workspace_role = workspace_member.role
workspace = workspace_member.workspace

View File

@@ -10,11 +10,7 @@ from plane.app.serializers import (
ProjectMemberRoleSerializer,
)
from plane.app.permissions import (
ProjectMemberPermission,
ProjectLitePermission,
WorkspaceUserPermission,
)
from plane.app.permissions import WorkspaceUserPermission
from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember
from plane.bgtasks.project_add_user_email_task import project_add_user_email
@@ -26,14 +22,6 @@ class ProjectMemberViewSet(BaseViewSet):
serializer_class = ProjectMemberAdminSerializer
model = ProjectMember
def get_permissions(self):
if self.action == "leave":
self.permission_classes = [ProjectLitePermission]
else:
self.permission_classes = [ProjectMemberPermission]
return super(ProjectMemberViewSet, self).get_permissions()
search_fields = ["member__display_name", "member__first_name"]
def get_queryset(self):
@@ -187,12 +175,20 @@ class ProjectMemberViewSet(BaseViewSet):
)
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN])
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def partial_update(self, request, slug, project_id, pk):
project_member = ProjectMember.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, is_active=True
)
if request.user.id == project_member.member_id:
# Fetch the workspace role of the project member
workspace_role = WorkspaceMember.objects.get(
workspace__slug=slug, member=project_member.member, is_active=True
).role
is_workspace_admin = workspace_role == ROLE.ADMIN.value
# Check if the user is not editing their own role if they are not an admin
if request.user.id == project_member.member_id and not is_workspace_admin:
return Response(
{"error": "You cannot update your own role"},
status=status.HTTP_400_BAD_REQUEST,
@@ -205,9 +201,6 @@ class ProjectMemberViewSet(BaseViewSet):
is_active=True,
)
workspace_role = WorkspaceMember.objects.get(
workspace__slug=slug, member=project_member.member, is_active=True
).role
if workspace_role in [5] and int(
request.data.get("role", project_member.role)
) in [15, 20]:
@@ -222,6 +215,7 @@ class ProjectMemberViewSet(BaseViewSet):
"role" in request.data
and int(request.data.get("role", project_member.role))
> requested_project_member.role
and not is_workspace_admin
):
return Response(
{"error": "You cannot update a role that is higher than your own role"},

View File

@@ -53,6 +53,23 @@ class StateViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def partial_update(self, request, slug, project_id, pk):
try:
state = State.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
serializer = StateSerializer(state, 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 IntegrityError as e:
if "already exists" in str(e):
return Response(
{"name": "The state name is already taken"},
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):

View File

@@ -24,6 +24,7 @@ from plane.db.models import (
ProjectMember,
Project,
CycleIssue,
UserRecentVisit,
)
from plane.utils.grouper import (
issue_group_values,
@@ -116,7 +117,7 @@ class WorkspaceViewViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView
allowed_roles=[ROLE.ADMIN], level="WORKSPACE", creator=True, model=IssueView
)
def destroy(self, request, slug, pk):
workspace_view = IssueView.objects.get(pk=pk, workspace__slug=slug)
@@ -495,6 +496,13 @@ class IssueViewViewSet(BaseViewSet):
entity_identifier=pk,
entity_type="view",
).delete()
# Delete the page from recent visit
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="view",
).delete(soft=False)
else:
return Response(
{"error": "Only admin or owner can delete the view"},

View File

@@ -120,7 +120,7 @@ class WebhookLogsEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def get(self, request, slug, webhook_id):
webhook_logs = WebhookLog.objects.filter(
workspace__slug=slug, webhook_id=webhook_id
workspace__slug=slug, webhook=webhook_id
)
serializer = WebhookLogSerializer(webhook_logs, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

View File

@@ -7,9 +7,11 @@ from datetime import date
from dateutil.relativedelta import relativedelta
from django.db import IntegrityError
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
from django.db.models.fields import DateField
from django.db.models.functions import Cast, ExtractDay, ExtractWeek
# Django imports
from django.http import HttpResponse
from django.utils import timezone
@@ -62,12 +64,6 @@ class WorkSpaceViewSet(BaseViewSet):
.values("count")
)
issue_count = (
Issue.issue_objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
return (
self.filter_queryset(super().get_queryset().select_related("owner"))
.order_by("name")
@@ -76,8 +72,6 @@ class WorkSpaceViewSet(BaseViewSet):
workspace_member__is_active=True,
)
.annotate(total_members=member_count)
.annotate(total_issues=issue_count)
.select_related("owner")
)
def create(self, request):
@@ -123,7 +117,14 @@ class WorkSpaceViewSet(BaseViewSet):
role=20,
company_role=request.data.get("company_role", ""),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
# Get total members and role
total_members=WorkspaceMember.objects.filter(workspace_id=serializer.data["id"]).count()
data = serializer.data
data["total_members"] = total_members
data["role"] = 20
return Response(data, status=status.HTTP_201_CREATED)
return Response(
[serializer.errors[error][0] for error in serializer.errors],
status=status.HTTP_400_BAD_REQUEST,
@@ -166,11 +167,9 @@ class UserWorkSpacesEndpoint(BaseAPIView):
.values("count")
)
issue_count = (
Issue.issue_objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
role = (
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member=request.user, is_active=True)
.values("role")
)
workspace = (
@@ -182,19 +181,19 @@ class UserWorkSpacesEndpoint(BaseAPIView):
),
)
)
.select_related("owner")
.annotate(total_members=member_count)
.annotate(total_issues=issue_count)
.annotate(role=role, total_members=member_count)
.filter(
workspace_member__member=request.user, workspace_member__is_active=True
)
.distinct()
)
workspaces = WorkSpaceSerializer(
self.filter_queryset(workspace),
fields=fields if fields else None,
many=True,
).data
return Response(workspaces, status=status.HTTP_200_OK)

View File

@@ -4,6 +4,7 @@ from rest_framework.response import Response
# Django modules
from django.db.models import Q
from django.db import IntegrityError
# Module imports
from plane.app.views.base import BaseAPIView
@@ -31,16 +32,37 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
serializer = UserFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
user_id=request.user.id,
workspace=workspace,
project_id=request.data.get("project_id", None),
try:
workspace = Workspace.objects.get(slug=slug)
# If the favorite exists return
if request.data.get("entity_identifier"):
user_favorites = UserFavorite.objects.filter(
workspace=workspace,
user_id=request.user.id,
entity_type=request.data.get("entity_type"),
entity_identifier=request.data.get("entity_identifier"),
).first()
# If the favorite exists return
if user_favorites:
serializer = UserFavoriteSerializer(user_favorites)
return Response(serializer.data, status=status.HTTP_200_OK)
# else create a new favorite
serializer = UserFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
user_id=request.user.id,
workspace=workspace,
project_id=request.data.get("project_id", None),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "Favorite already exists"}, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def patch(self, request, slug, favorite_id):

View File

@@ -251,8 +251,7 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
super()
.get_queryset()
.filter(email=self.request.user.email)
.select_related("workspace", "workspace__owner", "created_by")
.annotate(total_members=Count("workspace__workspace_member"))
.select_related("workspace")
)
@invalidate_cache(path="/api/workspaces/", user=False)

View File

@@ -68,10 +68,11 @@ class WorkSpaceMemberViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
if workspace_member.role > int(request.data.get("role")):
_ = ProjectMember.objects.filter(
# If a user is moved to a guest role he can't have any other role in projects
if "role" in request.data and int(request.data.get("role")) == 5:
ProjectMember.objects.filter(
workspace__slug=slug, member_id=workspace_member.member_id
).update(role=int(request.data.get("role")))
).update(role=5)
serializer = WorkSpaceMemberSerializer(
workspace_member, data=request.data, partial=True

View File

@@ -21,7 +21,7 @@ class QuickLinkViewSet(BaseViewSet):
serializer = WorkspaceUserLinkSerializer(data=request.data)
if serializer.is_valid():
serializer.save(workspace_id=workspace.id, owner=request.user)
serializer.save(workspace_id=workspace.id, owner_id=request.user.id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -0,0 +1,85 @@
# Module imports
from ..base import BaseAPIView
from plane.db.models.workspace import WorkspaceUserPreference
from plane.app.serializers.workspace import WorkspaceUserPreferenceSerializer
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import Workspace
# Third party imports
from rest_framework.response import Response
from rest_framework import status
class WorkspaceUserPreferenceViewSet(BaseAPIView):
model = WorkspaceUserPreference
def get_serializer_class(self):
return WorkspaceUserPreferenceSerializer
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
get_preference = WorkspaceUserPreference.objects.filter(
user=request.user, workspace_id=workspace.id
)
create_preference_keys = []
keys = [
key
for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices
]
for preference in keys:
if preference not in get_preference.values_list("key", flat=True):
create_preference_keys.append(preference)
preference = WorkspaceUserPreference.objects.bulk_create(
[
WorkspaceUserPreference(
key=key, user=request.user, workspace=workspace, sort_order=(65535 + (i*10000))
)
for i, key in enumerate(create_preference_keys)
],
batch_size=10,
ignore_conflicts=True,
)
preferences = WorkspaceUserPreference.objects.filter(
user=request.user, workspace_id=workspace.id
).order_by("sort_order").values("key", "is_pinned", "sort_order")
user_preferences = {}
for preference in preferences:
user_preferences[(str(preference["key"]))] = {
"is_pinned": preference["is_pinned"],
"sort_order": preference["sort_order"],
}
return Response(
user_preferences,
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def patch(self, request, slug, key):
preference = WorkspaceUserPreference.objects.filter(
key=key, workspace__slug=slug, user=request.user
).first()
if preference:
serializer = WorkspaceUserPreferenceSerializer(
preference, 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)
return Response(
{"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND
)

View File

@@ -36,10 +36,12 @@ AUTHENTICATION_ERROR_CODES = {
"OAUTH_NOT_CONFIGURED": 5104,
"GOOGLE_NOT_CONFIGURED": 5105,
"GITHUB_NOT_CONFIGURED": 5110,
"GITHUB_USER_NOT_IN_ORG": 5122,
"GITLAB_NOT_CONFIGURED": 5111,
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
"GITLAB_OAUTH_PROVIDER_ERROR": 5121,
# Reset Password
"INVALID_PASSWORD_TOKEN": 5125,
"EXPIRED_PASSWORD_TOKEN": 5130,

View File

@@ -18,11 +18,16 @@ from plane.authentication.adapter.error import (
class GitHubOAuthProvider(OauthAdapter):
token_url = "https://github.com/login/oauth/access_token"
userinfo_url = "https://api.github.com/user"
org_membership_url = f"https://api.github.com/orgs"
provider = "github"
scope = "read:user user:email"
organization_scope = "read:org"
def __init__(self, request, code=None, state=None, callback=None):
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET = get_configuration_value(
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_ORGANIZATION_ID = get_configuration_value(
[
{
"key": "GITHUB_CLIENT_ID",
@@ -32,6 +37,10 @@ class GitHubOAuthProvider(OauthAdapter):
"key": "GITHUB_CLIENT_SECRET",
"default": os.environ.get("GITHUB_CLIENT_SECRET"),
},
{
"key": "GITHUB_ORGANIZATION_ID",
"default": os.environ.get("GITHUB_ORGANIZATION_ID"),
},
]
)
@@ -43,6 +52,10 @@ class GitHubOAuthProvider(OauthAdapter):
client_id = GITHUB_CLIENT_ID
client_secret = GITHUB_CLIENT_SECRET
self.organization_id = GITHUB_ORGANIZATION_ID
if self.organization_id:
self.scope += f" {self.organization_scope}"
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/github/callback/"""
url_params = {
@@ -113,12 +126,26 @@ class GitHubOAuthProvider(OauthAdapter):
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
)
def is_user_in_organization(self, github_username):
headers = {"Authorization": f"Bearer {self.token_data.get('access_token')}"}
response = requests.get(f"{self.org_membership_url}/{self.organization_id}/memberships/{github_username}", headers=headers)
return response.status_code == 200 # 200 means the user is a member
def set_user_data(self):
user_info_response = self.get_user_response()
headers = {
"Authorization": f"Bearer {self.token_data.get('access_token')}",
"Accept": "application/json",
}
if self.organization_id:
if not self.is_user_in_organization(user_info_response.get("login")):
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_USER_NOT_IN_ORG"],
error_message="GITHUB_USER_NOT_IN_ORG",
)
email = self.__get_email(headers=headers)
super().set_user_data(
{

View File

@@ -53,7 +53,6 @@ urlpatterns = [
path("magic-generate/", MagicGenerateEndpoint.as_view(), name="magic-generate"),
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
path("magic-sign-up/", MagicSignUpEndpoint.as_view(), name="magic-sign-up"),
path("get-csrf-token/", CSRFTokenEndpoint.as_view(), name="get_csrf_token"),
path(
"spaces/magic-generate/",
MagicGenerateSpaceEndpoint.as_view(),

View File

@@ -100,8 +100,20 @@ class ResetPasswordEndpoint(View):
def post(self, request, uidb64, token):
try:
# Decode the id from the uidb64
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
try:
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
except (ValueError, User.DoesNotExist):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD_TOKEN"],
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = urljoin(
base_host(request=request, is_app=True),
"accounts/reset-password?" + urlencode(params),
)
return HttpResponseRedirect(url)
# check if the token is valid for the user
if not PasswordResetTokenGenerator().check_token(user, token):

View File

@@ -0,0 +1,150 @@
# Python imports
import uuid
import base64
import requests
from bs4 import BeautifulSoup
# Django imports
from django.conf import settings
# Module imports
from plane.db.models import FileAsset, Page, Issue
from plane.utils.exception_logger import log_exception
from plane.settings.storage import S3Storage
from celery import shared_task
def get_entity_id_field(entity_type, entity_id):
entity_mapping = {
FileAsset.EntityTypeContext.WORKSPACE_LOGO: {"workspace_id": entity_id},
FileAsset.EntityTypeContext.PROJECT_COVER: {"project_id": entity_id},
FileAsset.EntityTypeContext.USER_AVATAR: {"user_id": entity_id},
FileAsset.EntityTypeContext.USER_COVER: {"user_id": entity_id},
FileAsset.EntityTypeContext.ISSUE_ATTACHMENT: {"issue_id": entity_id},
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION: {"issue_id": entity_id},
FileAsset.EntityTypeContext.PAGE_DESCRIPTION: {"page_id": entity_id},
FileAsset.EntityTypeContext.COMMENT_DESCRIPTION: {"comment_id": entity_id},
FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION: {
"draft_issue_id": entity_id
},
}
return entity_mapping.get(entity_type, {})
def extract_asset_ids(html, tag):
try:
soup = BeautifulSoup(html, "html.parser")
return [tag.get("src") for tag in soup.find_all(tag) if tag.get("src")]
except Exception as e:
log_exception(e)
return []
def replace_asset_ids(html, tag, duplicated_assets):
try:
soup = BeautifulSoup(html, "html.parser")
for mention_tag in soup.find_all(tag):
for asset in duplicated_assets:
if mention_tag.get("src") == asset["old_asset_id"]:
mention_tag["src"] = asset["new_asset_id"]
return str(soup)
except Exception as e:
log_exception(e)
return html
def update_description(entity, duplicated_assets, tag):
updated_html = replace_asset_ids(entity.description_html, tag, duplicated_assets)
entity.description_html = updated_html
entity.save()
return updated_html
# Get the description binary and description from the live server
def sync_with_external_service(entity_name, description_html):
try:
data = {
"description_html": description_html,
"variant": "rich" if entity_name == "PAGE" else "document",
}
response = requests.post(
f"{settings.LIVE_BASE_URL}/convert-document/",
json=data,
headers=None,
)
if response.status_code == 200:
return response.json()
except requests.RequestException as e:
log_exception(e)
return {}
@shared_task
def copy_s3_objects(entity_name, entity_identifier, project_id, slug, user_id):
"""
Step 1: Extract asset ids from the description_html of the entity
Step 2: Duplicate the assets
Step 3: Update the description_html of the entity with the new asset ids (change the src of img tag)
Step 4: Request the live server to generate the description_binary and description for the entity
"""
try:
model_class = {"PAGE": Page, "ISSUE": Issue}.get(entity_name)
if not model_class:
raise ValueError(f"Unsupported entity_name: {entity_name}")
entity = model_class.objects.get(id=entity_identifier)
asset_ids = extract_asset_ids(entity.description_html, "image-component")
duplicated_assets = []
workspace = entity.workspace
storage = S3Storage()
original_assets = FileAsset.objects.filter(
workspace=workspace, project_id=project_id, id__in=asset_ids
)
for original_asset in original_assets:
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
duplicated_asset = FileAsset.objects.create(
attributes={
"name": original_asset.attributes.get("name"),
"type": original_asset.attributes.get("type"),
"size": original_asset.attributes.get("size"),
},
asset=destination_key,
size=original_asset.size,
workspace=workspace,
created_by_id=user_id,
entity_type=original_asset.entity_type,
project_id=project_id,
storage_metadata=original_asset.storage_metadata,
**get_entity_id_field(original_asset.entity_type, entity_identifier),
)
storage.copy_object(original_asset.asset, destination_key)
duplicated_assets.append(
{
"new_asset_id": str(duplicated_asset.id),
"old_asset_id": str(original_asset.id),
}
)
if duplicated_assets:
FileAsset.objects.filter(
pk__in=[item["new_asset_id"] for item in duplicated_assets]
).update(is_uploaded=True)
updated_html = update_description(
entity, duplicated_assets, "image-component"
)
external_data = sync_with_external_service(entity_name, updated_html)
if external_data:
entity.description = external_data.get("description")
entity.description_binary = base64.b64decode(
external_data.get("description_binary")
)
entity.save()
return
except Exception as e:
log_exception(e)
return []

View File

@@ -82,7 +82,10 @@ def soft_delete_related_objects(app_label, model_name, instance_pk, using=None):
)
else:
# Handle other relationships
related_queryset = getattr(instance, related_name).all()
related_queryset = getattr(instance, related_name)(
manager="objects"
).all()
for related_obj in related_queryset:
if hasattr(related_obj, "deleted_at"):
if not related_obj.deleted_at:

View File

@@ -9,10 +9,10 @@ from celery import shared_task
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
# Module imports
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
from plane.db.models import (
CommentReaction,
Cycle,
@@ -32,6 +32,7 @@ from plane.settings.redis import redis_instance
from plane.utils.exception_logger import log_exception
from plane.bgtasks.webhook_task import webhook_activity
from plane.utils.issue_relation_mapper import get_inverse_relation
from plane.utils.valid_uuid import is_valid_uuid
# Track Changes in name
@@ -738,8 +739,10 @@ def delete_comment_activity(
issue_activities,
epoch,
):
requested_data = json.loads(requested_data) if requested_data is not None else None
issue_activities.append(
IssueActivity(
issue_comment_id=requested_data.get("comment_id", None),
issue_id=issue_id,
project_id=project_id,
workspace_id=workspace_id,
@@ -788,14 +791,15 @@ def create_cycle_issue_activity(
issue_id=updated_record.get("issue_id"),
actor_id=actor_id,
verb="updated",
old_value=old_cycle.name,
new_value=new_cycle.name,
old_value=old_cycle.name if old_cycle else "",
new_value=new_cycle.name if new_cycle else "",
field="cycles",
project_id=project_id,
workspace_id=workspace_id,
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
old_identifier=old_cycle.id,
new_identifier=new_cycle.id,
comment=f"""updated cycle from {old_cycle.name if old_cycle else ""}
to {new_cycle.name if new_cycle else ""}""",
old_identifier=old_cycle.id if old_cycle else None,
new_identifier=new_cycle.id if new_cycle else None,
epoch=epoch,
)
)
@@ -849,7 +853,7 @@ def delete_cycle_issue_activity(
issues = requested_data.get("issues")
for issue in issues:
current_issue = Issue.objects.filter(pk=issue).first()
if issue:
if current_issue:
current_issue.updated_at = timezone.now()
current_issue.save(update_fields=["updated_at"])
issue_activities.append(
@@ -891,11 +895,11 @@ def create_module_issue_activity(
actor_id=actor_id,
verb="created",
old_value="",
new_value=module.name,
new_value=module.name if module else "",
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"added module {module.name}",
comment=f"added module {module.name if module else ''}",
new_identifier=requested_data.get("module_id"),
epoch=epoch,
)
@@ -1411,7 +1415,7 @@ def delete_issue_relation_activity(
),
project_id=project_id,
workspace_id=workspace_id,
comment=f'deleted {requested_data.get("relation_type")} relation',
comment=f"deleted {requested_data.get('relation_type')} relation",
old_identifier=requested_data.get("related_issue"),
epoch=epoch,
)
@@ -1565,6 +1569,10 @@ def issue_activity(
try:
issue_activities = []
# check if project_id is valid
if not is_valid_uuid(str(project_id)):
return
project = Project.objects.get(pk=project_id)
workspace_id = project.workspace_id

View File

@@ -1,5 +1,6 @@
# Python imports
from django.utils import timezone
from django.db import DatabaseError
# Third party imports
from celery import shared_task
@@ -22,8 +23,12 @@ def recent_visited_task(entity_name, entity_identifier, user_id, project_id, slu
).first()
if recent_visited:
recent_visited.visited_at = timezone.now()
recent_visited.save(update_fields=["visited_at"])
# Check if the database is available
try:
recent_visited.visited_at = timezone.now()
recent_visited.save(update_fields=["visited_at"])
except DatabaseError:
pass
else:
recent_visited_count = UserRecentVisit.objects.filter(
user_id=user_id, workspace_id=workspace.id

View File

@@ -136,7 +136,7 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site):
# Log the webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -153,7 +153,7 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site):
# Log the failed webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -304,7 +304,7 @@ def webhook_send_task(
# Log the webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -319,7 +319,7 @@ def webhook_send_task(
# Log the failed webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -387,7 +387,11 @@ def webhook_activity(
webhook=webhook.id,
slug=slug,
event=event,
event_data=get_model_data(event=event, event_id=event_id),
event_data=(
{"id": event_id}
if verb == "deleted"
else get_model_data(event=event, event_id=event_id)
),
action=verb,
current_site=current_site,
activity={

View File

@@ -15,34 +15,35 @@ app = Celery("plane")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.conf.beat_schedule = {
# Executes every day at 12 AM
"check-every-day-to-archive-and-close": {
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete_exporter_history": {
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete-file-asset": {
"task": "plane.bgtasks.file_asset_task.delete_unuploaded_file_asset",
"schedule": crontab(hour=0, minute=0),
},
# Intra day recurring jobs
"check-every-five-minutes-to-send-email-notifications": {
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
"schedule": crontab(minute="*/5"),
},
"check-every-day-to-delete-hard-delete": {
"task": "plane.bgtasks.deletion_task.hard_delete",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete-api-logs": {
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
"schedule": crontab(hour=0, minute=0),
"schedule": crontab(minute="*/5"), # Every 5 minutes
},
"run-every-6-hours-for-instance-trace": {
"task": "plane.license.bgtasks.tracer.instance_traces",
"schedule": crontab(hour="*/6", minute=0),
"schedule": crontab(hour="*/6", minute=0), # Every 6 hours
},
# Occurs once every day
"check-every-day-to-delete-hard-delete": {
"task": "plane.bgtasks.deletion_task.hard_delete",
"schedule": crontab(hour=0, minute=0), # UTC 00:00
},
"check-every-day-to-archive-and-close": {
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
"schedule": crontab(hour=1, minute=0), # UTC 01:00
},
"check-every-day-to-delete_exporter_history": {
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
"schedule": crontab(hour=1, minute=30), # UTC 01:30
},
"check-every-day-to-delete-file-asset": {
"task": "plane.bgtasks.file_asset_task.delete_unuploaded_file_asset",
"schedule": crontab(hour=2, minute=0), # UTC 02:00
},
"check-every-day-to-delete-api-logs": {
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
"schedule": crontab(hour=2, minute=30), # UTC 02:30
},
}

View File

@@ -3,7 +3,6 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from sentry_sdk import capture_exception
import uuid
@@ -29,7 +28,6 @@ def create_issue_relation(apps, schema_editor):
)
except Exception as e:
print(e)
capture_exception(e)
def update_issue_priority_choice(apps, schema_editor):

View File

@@ -0,0 +1,33 @@
# Generated by Django 4.2.17 on 2025-01-30 16:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0090_rename_dashboard_deprecateddashboard_and_more'),
]
operations = [
migrations.AddField(
model_name='issuecomment',
name='edited_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='profile',
name='is_smooth_cursor_enabled',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='userrecentvisit',
name='entity_name',
field=models.CharField(max_length=30),
),
migrations.AlterField(
model_name='webhooklog',
name='webhook',
field=models.UUIDField(),
)
]

View File

@@ -0,0 +1,41 @@
# Generated by Django 4.2.18 on 2025-02-25 15:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("db", "0091_issuecomment_edited_at_and_more"),
]
operations = [
migrations.AlterUniqueTogether(
name="deprecateddashboardwidget",
unique_together=None,
),
migrations.RemoveField(
model_name="deprecateddashboardwidget",
name="created_by",
),
migrations.RemoveField(
model_name="deprecateddashboardwidget",
name="dashboard",
),
migrations.RemoveField(
model_name="deprecateddashboardwidget",
name="updated_by",
),
migrations.RemoveField(
model_name="deprecateddashboardwidget",
name="widget",
),
migrations.DeleteModel(
name="DeprecatedDashboard",
),
migrations.DeleteModel(
name="DeprecatedDashboardWidget",
),
migrations.DeleteModel(
name="DeprecatedWidget",
),
]

View File

@@ -3,7 +3,6 @@ from .api import APIActivityLog, APIToken
from .asset import FileAsset
from .base import BaseModel
from .cycle import Cycle, CycleIssue, CycleUserProperties
from .dashboard import DeprecatedDashboard, DeprecatedDashboardWidget, DeprecatedWidget
from .deploy_board import DeployBoard
from .draft import (
DraftIssue,
@@ -69,7 +68,8 @@ from .workspace import (
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceUserLink,
WorkspaceHomePreference
WorkspaceHomePreference,
WorkspaceUserPreference,
)
from .favorite import UserFavorite

View File

@@ -1,92 +0,0 @@
import uuid
# Django imports
from django.db import models
# Module imports
from ..mixins import TimeAuditModel
from .base import BaseModel
class DeprecatedDashboard(BaseModel):
DASHBOARD_CHOICES = (
("workspace", "Workspace"),
("project", "Project"),
("home", "Home"),
("team", "Team"),
("user", "User"),
)
name = models.CharField(max_length=255)
description_html = models.TextField(blank=True, default="<p></p>")
identifier = models.UUIDField(null=True)
owned_by = models.ForeignKey(
"db.User", on_delete=models.CASCADE, related_name="dashboards"
)
is_default = models.BooleanField(default=False)
type_identifier = models.CharField(
max_length=30,
choices=DASHBOARD_CHOICES,
verbose_name="Dashboard Type",
default="home",
)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the dashboard"""
return f"{self.name}"
class Meta:
verbose_name = "DeprecatedDashboard"
verbose_name_plural = "DeprecatedDashboards"
db_table = "deprecated_dashboards"
ordering = ("-created_at",)
class DeprecatedWidget(TimeAuditModel):
id = models.UUIDField(
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
)
key = models.CharField(max_length=255)
filters = models.JSONField(default=dict)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the widget"""
return f"{self.key}"
class Meta:
verbose_name = "DeprecatedWidget"
verbose_name_plural = "DeprecatedWidgets"
db_table = "deprecated_widgets"
ordering = ("-created_at",)
class DeprecatedDashboardWidget(BaseModel):
widget = models.ForeignKey(
DeprecatedWidget, on_delete=models.CASCADE, related_name="dashboard_widgets"
)
dashboard = models.ForeignKey(
DeprecatedDashboard, on_delete=models.CASCADE, related_name="dashboard_widgets"
)
is_visible = models.BooleanField(default=True)
sort_order = models.FloatField(default=65535)
filters = models.JSONField(default=dict)
properties = models.JSONField(default=dict)
def __str__(self):
"""Return name of the dashboard"""
return f"{self.dashboard.name} {self.widget.key}"
class Meta:
unique_together = ("widget", "dashboard", "deleted_at")
constraints = [
models.UniqueConstraint(
fields=["widget", "dashboard"],
condition=models.Q(deleted_at__isnull=True),
name="dashboard_widget_unique_widget_dashboard_when_deleted_at_null",
)
]
verbose_name = "Deprecated Dashboard Widget"
verbose_name_plural = "Deprecated Dashboard Widgets"
db_table = "deprecated_dashboard_widgets"
ordering = ("-created_at",)

View File

@@ -467,6 +467,7 @@ class IssueComment(ProjectBaseModel):
)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
edited_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
self.comment_stripped = (

View File

@@ -1,6 +1,7 @@
# Python imports
import pytz
from uuid import uuid4
from enum import Enum
# Django imports
from django.conf import settings
@@ -17,6 +18,15 @@ from .base import BaseModel
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
class ProjectNetwork(Enum):
SECRET = 0
PUBLIC = 2
@classmethod
def choices(cls):
return [(0, "Secret"), (2, "Public")]
def get_default_props():
return {
"filters": {

View File

@@ -17,7 +17,7 @@ class EntityNameEnum(models.TextChoices):
class UserRecentVisit(WorkspaceBaseModel):
entity_identifier = models.UUIDField(null=True)
entity_name = models.CharField(max_length=30, choices=EntityNameEnum.choices)
entity_name = models.CharField(max_length=30)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,

View File

@@ -186,6 +186,8 @@ class Profile(TimeAuditModel):
billing_address = models.JSONField(null=True)
has_billing_address = models.BooleanField(default=False)
company_name = models.CharField(max_length=255, blank=True)
is_smooth_cursor_enabled = models.BooleanField(default=False)
# mobile
is_mobile_onboarded = models.BooleanField(default=False)
mobile_onboarding_step = models.JSONField(default=get_mobile_default_onboarding)

View File

@@ -66,7 +66,7 @@ class WebhookLog(BaseModel):
"db.Workspace", on_delete=models.CASCADE, related_name="webhook_logs"
)
# Associated webhook
webhook = models.ForeignKey(Webhook, on_delete=models.CASCADE, related_name="logs")
webhook = models.UUIDField()
# Basic request details
event_type = models.CharField(max_length=255, blank=True, null=True)
@@ -89,4 +89,4 @@ class WebhookLog(BaseModel):
ordering = ("-created_at",)
def __str__(self):
return f"{self.event_type} {str(self.webhook.url)}"
return f"{self.event_type} {str(self.webhook)}"

View File

@@ -388,15 +388,16 @@ class WorkspaceHomePreference(BaseModel):
return f"{self.workspace.name} {self.user.email} {self.key}"
class WorkspaceUserPreference(BaseModel):
"""Preference for the workspace for a user"""
class UserPreferenceKeys(models.TextChoices):
CYCLES = "cycles", "Cycles"
class UserPreferenceKeys(models.TextChoices):
VIEWS = "views", "Views"
ACTIVE_CYCLES = "active_cycles", "Active Cycles"
ANALYTICS = "analytics", "Analytics"
PROJECTS = "projects", "Projects"
DRAFTS = "drafts", "Drafts"
YOUR_WORK = "your_work", "Your Work"
ARCHIVES = "archives", "Archives"
workspace = models.ForeignKey(
"db.Workspace",

View File

@@ -71,6 +71,12 @@ class Command(BaseCommand):
"category": "GITHUB",
"is_encrypted": True,
},
{
"key": "GITHUB_ORGANIZATION_ID",
"value": os.environ.get("GITHUB_ORGANIZATION_ID"),
"category": "GITHUB",
"is_encrypted": False,
},
{
"key": "GITLAB_HOST",
"value": os.environ.get("GITLAB_HOST"),

View File

@@ -7,13 +7,9 @@ from urllib.parse import urlparse
# Third party imports
import dj_database_url
import sentry_sdk
# Django imports
from django.core.management.utils import get_random_secret_key
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from corsheaders.defaults import default_headers
@@ -267,25 +263,6 @@ CELERY_IMPORTS = (
"plane.bgtasks.issue_description_version_sync",
)
# Sentry Settings
# Enable Sentry Settings
if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get(
"SENTRY_DSN"
).startswith("https://"):
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN", ""),
integrations=[
DjangoIntegration(),
RedisIntegration(),
CeleryIntegration(monitor_beat_tasks=True),
],
traces_sample_rate=1,
send_default_pii=True,
environment=os.environ.get("SENTRY_ENVIRONMENT", "development"),
profiles_sample_rate=float(os.environ.get("SENTRY_PROFILE_SAMPLE_RATE", 0)),
)
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
# Unsplash Access key
@@ -336,6 +313,8 @@ CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure"
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
APP_BASE_URL = os.environ.get("APP_BASE_URL")
LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL")
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))

View File

@@ -32,6 +32,12 @@ class S3Storage(S3Boto3Storage):
) or os.environ.get("MINIO_ENDPOINT_URL")
if os.environ.get("USE_MINIO") == "1":
# Determine protocol based on environment variable
if os.environ.get("MINIO_ENDPOINT_SSL") == "1":
endpoint_protocol = "https"
else:
endpoint_protocol = request.scheme if request else "http"
# Create an S3 client for MinIO
self.s3_client = boto3.client(
"s3",
@@ -39,7 +45,7 @@ class S3Storage(S3Boto3Storage):
aws_secret_access_key=self.aws_secret_access_key,
region_name=self.aws_region,
endpoint_url=(
f"{request.scheme}://{request.get_host()}"
f"{endpoint_protocol}://{request.get_host()}"
if request
else self.aws_s3_endpoint_url
),
@@ -151,3 +157,17 @@ class S3Storage(S3Boto3Storage):
"ETag": response.get("ETag"),
"Metadata": response.get("Metadata", {}),
}
def copy_object(self, object_name, new_object_name):
"""Copy an S3 object to a new location"""
try:
response = self.s3_client.copy_object(
Bucket=self.aws_storage_bucket_name,
CopySource={"Bucket": self.aws_storage_bucket_name, "Key": object_name},
Key=new_object_name,
)
except ClientError as e:
log_exception(e)
return None
return response

View File

@@ -12,7 +12,7 @@ from rest_framework.response import Response
# Module imports
from .base import BaseViewSet
from plane.db.models import IntakeIssue, Issue, State, IssueLink, FileAsset, DeployBoard
from plane.db.models import IntakeIssue, Issue, IssueLink, FileAsset, DeployBoard
from plane.app.serializers import (
IssueSerializer,
IntakeIssueSerializer,
@@ -202,7 +202,12 @@ class IntakeIssuePublicViewSet(BaseViewSet):
"description": issue_data.get("description", issue.description),
}
issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True)
issue_serializer = IssueCreateSerializer(
issue,
data=issue_data,
partial=True,
context={"project_id": project_deploy_board.project_id},
)
if issue_serializer.is_valid():
current_instance = issue

View File

@@ -14,9 +14,9 @@ class ProjectMetaDataEndpoint(BaseAPIView):
def get(self, request, anchor):
try:
deploy_board = DeployBoard.objects.filter(
deploy_board = DeployBoard.objects.get(
anchor=anchor, entity_name="project"
).first()
)
except DeployBoard.DoesNotExist:
return Response(
{"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND

View File

@@ -5,9 +5,6 @@ import traceback
# Django imports
from django.conf import settings
# Third party imports
from sentry_sdk import capture_exception
def log_exception(e):
# Log the error
@@ -18,6 +15,4 @@ def log_exception(e):
# Print the traceback if in debug mode
print(traceback.format_exc())
# Capture in sentry if configured
capture_exception(e)
return

View File

@@ -1,8 +1,14 @@
# Python imports
import pytz
from plane.db.models import Project
from datetime import datetime, time
from datetime import timedelta
# Django imports
from django.utils import timezone
# Module imports
from plane.db.models import Project
def user_timezone_converter(queryset, datetime_fields, user_timezone):
# Create a timezone object for the user's timezone
@@ -65,16 +71,27 @@ def convert_to_utc(
if is_start_date:
localized_datetime += timedelta(minutes=0, seconds=1)
# If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds
# to make it the end of the day
if is_start_date_end_date_equal:
localized_datetime += timedelta(hours=23, minutes=59, seconds=59)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
current_datetime_in_project_tz = timezone.now().astimezone(local_tz)
current_datetime_in_utc = current_datetime_in_project_tz.astimezone(pytz.utc)
# Return the UTC datetime for storage
return utc_datetime
if localized_datetime.date() == current_datetime_in_project_tz.date():
return current_datetime_in_utc
return utc_datetime
else:
# If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds
# to make it the end of the day
if is_start_date_end_date_equal:
localized_datetime += timedelta(hours=23, minutes=59, seconds=59)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
# Return the UTC datetime for storage
return utc_datetime
def convert_utc_to_project_timezone(utc_datetime, project_id):

View File

@@ -0,0 +1,8 @@
import uuid
def is_valid_uuid(uuid_str):
try:
uuid.UUID(uuid_str, version=4)
return True
except ValueError:
return False

View File

@@ -1,10 +1,10 @@
# base requirements
# django
Django==4.2.18
Django==4.2.20
# rest framework
djangorestframework==3.15.2
# postgres
# postgres
psycopg==3.1.18
psycopg-binary==3.1.18
psycopg-c==3.1.18
@@ -26,8 +26,6 @@ faker==25.0.0
django-filter==24.2
# json model
jsonmodels==2.7.0
# sentry
sentry-sdk==2.8.0
# storage
django-storages==1.14.2
# user management
@@ -37,7 +35,7 @@ uvicorn==0.29.0
# sockets
channels==4.1.0
# ai
litellm==1.51.0
openai==1.63.2
# slack
slack-sdk==3.27.1
# apm
@@ -51,7 +49,7 @@ beautifulsoup4==4.12.3
# analytics
posthog==3.5.0
# crypto
cryptography==43.0.1
cryptography==44.0.1
# html validator
lxml==5.2.1
# s3
@@ -66,4 +64,4 @@ PyJWT==2.8.0
opentelemetry-api==1.28.1
opentelemetry-sdk==1.28.1
opentelemetry-instrumentation-django==0.49b1
opentelemetry-exporter-otlp==1.28.1
opentelemetry-exporter-otlp==1.28.1

View File

@@ -2,4 +2,4 @@
# debug toolbar
django-debug-toolbar==4.3.0
# formatter
ruff==0.4.2
ruff==0.9.7

View File

@@ -1,3 +1,3 @@
-r base.txt
# server
gunicorn==22.0.0
gunicorn==23.0.0

View File

@@ -6,16 +6,8 @@
"website": "https://plane.so/",
"success_url": "/",
"stack": "heroku-22",
"keywords": [
"plane",
"project management",
"django",
"next"
],
"addons": [
"heroku-postgresql:mini",
"heroku-redis:mini"
],
"keywords": ["plane", "project management", "django", "next"],
"addons": ["heroku-postgresql:mini", "heroku-redis:mini"],
"buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-python.git"
@@ -61,10 +53,6 @@
"description": "AWS Bucket Name to use for S3",
"value": ""
},
"SENTRY_DSN": {
"description": "",
"value": ""
},
"WEB_URL": {
"description": "Web URL for Plane this will be used for redirections in the emails",
"value": ""
@@ -82,4 +70,4 @@
"value": ""
}
}
}
}

View File

@@ -55,18 +55,30 @@ Installing plane is a very easy and minimal step process.
- User context used must have access to docker services. In most cases, use sudo su to switch as root user
- Use the terminal (or gitbash) window to run all the future steps
### Downloading Latest Stable Release
### Downloading Latest Release
```
mkdir plane-selfhost
cd plane-selfhost
```
#### For *Docker Compose* based setup
```
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh
chmod +x setup.sh
```
#### For *Docker Swarm* based setup
```
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/swarm.sh
chmod +x setup.sh
```
---
### Proceed with setup
@@ -77,8 +89,9 @@ Lets get started by running the `./setup.sh` command.
This will prompt you with the below options.
#### Docker Compose
```bash
Select a Action you want to perform:
Select an Action you want to perform:
1) Install (x86_64)
2) Start
3) Stop
@@ -87,17 +100,42 @@ Select a Action you want to perform:
6) View Logs
7) Backup Data
8) Exit
Action [2]: 1
```
For the 1st time setup, type "1" as action input.
This will create a create a folder `plane-app` or `plane-app-preview` (in case of preview deployment) and will download 2 files inside that
This will create a folder `plane-app` and will download 2 files inside that
- `docker-compose.yaml`
- `plane.env`
Again the `options [1-8]` will be popped up and this time hit `8` to exit.
Again the `options [1-8]` will be popped up, and this time hit `8` to exit.
#### Docker Swarm
```bash
Select an Action you want to perform:
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 1
```
For the 1st time setup, type "1" as action input.
This will create a create a folder `plane-app` and will download 2 files inside that
- `docker-compose.yaml`
- `plane.env`
Again the `options [1-7]` will be popped up, and this time hit `7` to exit.
---
@@ -116,7 +154,7 @@ There are many other settings you can play with, but we suggest you configure `E
---
### Continue with setup - Start Server
### Continue with setup - Start Server (Docker Compose)
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `2` to start the sevices
@@ -147,9 +185,11 @@ You have successfully self hosted `Plane` instance. Access the application by go
---
### Stopping the Server
### Stopping the Server / Remove Stack
In case you want to make changes to `.env` variables, we suggest you to stop the services before doing that.
In case you want to make changes to `plane.env` variables, we suggest you to stop the services before doing that.
#### Docker Compose
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `3` to stop the sevices
@@ -171,14 +211,34 @@ If all goes well, you must see something like this
![Stop Services](images/stopped.png)
#### Docker Swarm
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `2` to stop the sevices
```bash
Select an Action you want to perform:
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 2
```
If all goes well, you will see the confirmation from docker cli
---
### Restarting the Server
### Restarting the Server / Redeploy Stack
In case you want to make changes to `.env` variables, without stopping the server or you noticed some abnormalies in services, you can restart the services with RESTART option.
In case you want to make changes to `plane.env` variables, without stopping the server or you noticed some abnormalies in services, you can restart the services with `RESTART` / `REDEPLOY` option.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `4` to restart the sevices
#### Docker Compose
```bash
Select a Action you want to perform:
1) Install (x86_64)
@@ -197,14 +257,32 @@ If all goes well, you must see something like this
![Restart Services](images/restart.png)
#### Docker Swarm
```bash
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 4
```
If all goes well, you will see the confirmation from docker cli
---
### Upgrading Plane Version
### Upgrading Plane Version
It is always advised to keep Plane up to date with the latest release.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release.
#### Docker Compose
```bash
Select a Action you want to perform:
1) Install (x86_64)
@@ -231,13 +309,41 @@ Once done, choose `8` to exit from prompt.
Once done with making changes in `plane.env` file, jump on to `Start Server`
#### Docker Swarm
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release.
```bash
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 5
```
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`.
Once done, choose `7` to exit from prompt.
> It is very important for you to validate the `plane.env` for the new changes.
Once done with making changes in `plane.env` file, jump on to `Redeploy Stack`
---
### View Logs
There would a time when you might want to check what is happening inside the API, Worker or any other container.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `6` to view logs.
Lets again run the `./setup.sh` command. You will again be prompted with the below options.
This time select `6` to view logs.
#### Docker Compose
```bash
Select a Action you want to perform:
@@ -253,7 +359,22 @@ Select a Action you want to perform:
Action [2]: 6
```
#### Docker Swarm
```bash
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 6
```
#### Service Menu Options for Logs
This will further open sub-menu with list of services
```bash
Select a Service you want to view the logs for:
@@ -267,9 +388,10 @@ Select a Service you want to view the logs for:
8) Redis
9) Postgres
10) Minio
11) RabbitMQ
0) Back to Main Menu
Service:
Service: 3
```
Select any of the service to view the logs e.g. `3`. Expect something similar to this
@@ -323,7 +445,7 @@ Similarly, you can view the logs of other services.
---
### Backup Data
### Backup Data (Docker Compose)
There would a time when you might want to backup your data from docker volumes to external storage like S3 or drives.
@@ -355,7 +477,7 @@ Backup completed successfully. Backup files are stored in /....../plane-app/back
---
### Restore Data
### Restore Data (Docker Compose)
When you want to restore the previously backed-up data, follow the instructions below.

View File

@@ -15,7 +15,7 @@ x-redis-env: &redis-env
x-minio-env: &minio-env
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key}
x-aws-s3-env: &aws-s3-env
AWS_REGION: ${AWS_REGION:-}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-access-key}
@@ -28,8 +28,7 @@ x-proxy-env: &proxy-env
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
x-mq-env: &mq-env
# RabbitMQ Settings
x-mq-env: &mq-env # RabbitMQ Settings
RABBITMQ_HOST: ${RABBITMQ_HOST:-plane-mq}
RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
@@ -43,40 +42,34 @@ x-live-env: &live-env
x-app-env: &app-env
WEB_URL: ${WEB_URL:-http://localhost}
DEBUG: ${DEBUG:-0}
SENTRY_DSN: ${SENTRY_DSN}
SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT:-production}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
GUNICORN_WORKERS: 1
USE_MINIO: ${USE_MINIO:-1}
DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
ADMIN_BASE_URL: ${ADMIN_BASE_URL}
SPACE_BASE_URL: ${SPACE_BASE_URL}
APP_BASE_URL: ${APP_BASE_URL}
AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
services:
web:
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: node web/server.js web
deploy:
replicas: ${WEB_REPLICAS:-1}
restart_policy:
condition: on-failure
depends_on:
- api
- worker
space:
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: node space/server.js space
deploy:
replicas: ${SPACE_REPLICAS:-1}
restart_policy:
condition: on-failure
depends_on:
- api
- worker
@@ -84,42 +77,39 @@ services:
admin:
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: node admin/server.js admin
deploy:
replicas: ${ADMIN_REPLICAS:-1}
restart_policy:
condition: on-failure
depends_on:
- api
- web
live:
image: ${DOCKERHUB_USER:-makeplane}/plane-live:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: node live/dist/server.js live
environment:
<<: [ *live-env ]
<<: [*live-env]
deploy:
replicas: ${LIVE_REPLICAS:-1}
restart_policy:
condition: on-failure
depends_on:
- api
- web
api:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-api.sh
deploy:
replicas: ${API_REPLICAS:-1}
restart_policy:
condition: on-failure
volumes:
- logs_api:/code/plane/logs
environment:
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- plane-db
- plane-redis
@@ -127,14 +117,15 @@ services:
worker:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-worker.sh
deploy:
replicas: ${WORKER_REPLICAS:-1}
restart_policy:
condition: on-failure
volumes:
- logs_worker:/code/plane/logs
environment:
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
@@ -143,14 +134,15 @@ services:
beat-worker:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-beat.sh
deploy:
replicas: ${BEAT_WORKER_REPLICAS:-1}
restart_policy:
condition: on-failure
volumes:
- logs_beat-worker:/code/plane/logs
environment:
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
@@ -159,23 +151,27 @@ services:
migrator:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: "no"
command: ./bin/docker-entrypoint-migrator.sh
deploy:
replicas: 1
restart_policy:
condition: on-failure
volumes:
- logs_migrator:/code/plane/logs
environment:
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- plane-db
- plane-redis
# Comment this if you already have a database running
plane-db:
image: postgres:15.7-alpine
pull_policy: if_not_present
restart: unless-stopped
command: postgres -c 'max_connections=1000'
deploy:
replicas: 1
restart_policy:
condition: on-failure
environment:
<<: *db-env
volumes:
@@ -183,24 +179,32 @@ services:
plane-redis:
image: valkey/valkey:7.2.5-alpine
pull_policy: if_not_present
restart: unless-stopped
deploy:
replicas: 1
restart_policy:
condition: on-failure
volumes:
- redisdata:/data
plane-mq:
image: rabbitmq:3.13.6-management-alpine
restart: always
deploy:
replicas: 1
restart_policy:
condition: on-failure
environment:
<<: *mq-env
volumes:
- rabbitmq_data:/var/lib/rabbitmq
# Comment this if you using any external s3 compatible storage
plane-minio:
image: minio/minio:latest
pull_policy: if_not_present
restart: unless-stopped
command: server /export --console-address ":9090"
deploy:
replicas: 1
restart_policy:
condition: on-failure
environment:
<<: *minio-env
volumes:
@@ -209,13 +213,17 @@ services:
# Comment this if you already have a reverse proxy running
proxy:
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
ports:
- ${NGINX_PORT}:80
- target: 80
published: ${NGINX_PORT:-80}
protocol: tcp
mode: host
environment:
<<: *proxy-env
deploy:
replicas: 1
restart_policy:
condition: on-failure
depends_on:
- web
- api
@@ -224,7 +232,6 @@ services:
volumes:
pgdata:
redisdata:
uploads:
logs_api:
logs_worker:

View File

@@ -457,12 +457,13 @@ function viewLogs(){
echo " 8) Redis"
echo " 9) Postgres"
echo " 10) Minio"
echo " 11) RabbitMQ"
echo " 0) Back to Main Menu"
echo
read -p "Service: " DOCKER_SERVICE_NAME
until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 10 )); do
echo "Invalid selection. Please enter a number between 1 and 11."
until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 11 )); do
echo "Invalid selection. Please enter a number between 0 and 11."
read -p "Service: " DOCKER_SERVICE_NAME
done
@@ -481,6 +482,7 @@ function viewLogs(){
8) viewSpecificLogs "plane-redis";;
9) viewSpecificLogs "plane-db";;
10) viewSpecificLogs "plane-minio";;
11) viewSpecificLogs "plane-mq";;
0) askForAction;;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
@@ -499,6 +501,7 @@ function viewLogs(){
redis) viewSpecificLogs "plane-redis";;
postgres) viewSpecificLogs "plane-db";;
minio) viewSpecificLogs "plane-minio";;
rabbitmq) viewSpecificLogs "plane-mq";;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
else

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