Compare commits

..
Author SHA1 Message Date
sriram veeraghantaandGitHub 1e27e37b51 Merge pull request #3666 from makeplane/preview
release: v0.15.2-dev
2024-02-14 19:41:55 +05:30
Bavisetti NarayanandGitHub d901277a20 chore: parent can be added to issue (#3665) 2024-02-14 18:16:27 +05:30
rahulrameshaandGitHub 489555f788 fix: horizontal scroll in gantt chart while dragging (#3664)
* fix horizontal scroll in gantt chart while dragging

* add aline indicator for quick add

* add border color for line above quick add in gantt to make it look better in dark mode
2024-02-14 18:15:13 +05:30
Anmol Singh BhatiaandGitHub b827a1af27 fix: issue details page parent issue redirection bug (#3662) 2024-02-14 18:13:29 +05:30
Ramesh Kumar ChandraandGitHub 1d2e331cec fix: mobile responsive sidebar close on item tap (#3661) 2024-02-14 14:55:33 +05:30
Anmol Singh BhatiaandGitHub e51e4761b9 fix: cycle favorite mutation (#3659)
* fix: cycle favorite mutation

* fix: issue details sidebar overflow fix
2024-02-14 14:17:46 +05:30
Anmol Singh BhatiaandGitHub 9299478539 chore: module & cycle empty state improvement (#3658) 2024-02-14 13:03:37 +05:30
guru_sainathandGitHub fb4cffdd1c fix: infinity loader in the issue description in issue peek overview and issue detail page (#3656) 2024-02-14 12:46:52 +05:30
Anmol Singh BhatiaandGitHub 83bf28bb83 chore: loading state improvement (#3657) 2024-02-14 12:40:03 +05:30
guru_sainathandGitHub 25a2816a76 chore: empty state loaders and filter cancel state update for modules and cycles (#3653) 2024-02-13 20:58:44 +05:30
rahulrameshaandGitHub 571b89632c fix: issue spillover on switching workspace (#3652)
* fix global issues spillover to other workspace

* fix the same for profile issues as well
2024-02-13 20:50:42 +05:30
sriram veeraghanta 5b5698ad97 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-13 19:13:07 +05:30
Anmol Singh BhatiaandGitHub b1989bae1b feat: loading states update (#3639)
* dev: implement layout skeleton loader and helper function

* chore: implemented layout loader

* chore: settings loader added

* chore: cycle, module, view, pages, notification and projects loader added

* chore: kanban loader improvement

* chore: loader utils updated
2024-02-13 19:12:10 +05:30
rahulrameshaandGitHub 83139989c2 fix: sort order in the same list for Kanban (#3651)
* fixing kanban dnd by stooping the modification of the original array by spreading to change the array reference

* fix sort order in the same list

* minor change in condition
2024-02-13 19:11:20 +05:30
Anmol Singh BhatiaandGitHub 1bf06821bb fix: add existing issue modal fix (#3649) 2024-02-13 18:57:56 +05:30
Anmol Singh BhatiaandGitHub eea3b4fa54 chore: new empty state (#3640)
* chore: empty state asset updated

* chore: empty state config updated

* chore: cycle and module issues layout empty state added

* chore: workspace and project settings empty state added
2024-02-13 16:35:20 +05:30
rahulrameshaandGitHub f64284f6a0 fixing kanban dnd by stooping the modification of the original array by spreading to change the array reference (#3646) 2024-02-13 16:33:19 +05:30
Anmol Singh BhatiaandGitHub 06496ff0f0 chore: app sidebar state (#3644)
* chore: app sidebar state fix

* chore: app sidebar state fix
2024-02-13 16:33:01 +05:30
Aaryan KhandelwalandGitHub 247937d93a fix: scroll sync (#3645) 2024-02-13 14:33:24 +05:30
AbId KhAnandGitHub e0a18578f5 fix local development setup issue due to docker compose local file makeplane/plane#3641 (#3642) 2024-02-13 12:39:58 +05:30
sriram veeraghantaandGitHub e93bbec4cd fix: pages tabs responsive fixes (#3635) 2024-02-12 22:14:11 +05:30
sriram veeraghantaandGitHub 7df2e9cf11 Merge pull request #3632 from makeplane/preview
release: v0.15.1-dev
2024-02-12 20:59:56 +05:30
rahulrameshaandGitHub d90aaba842 chore: change border of placeholder for spreadsheet (#3631)
* change border to place holder for spreadsheet

* add completed cylces exception fix

* remove window idle callback for making issues visible
2024-02-12 20:25:37 +05:30
sriram veeraghanta a303e52039 fix: package version upgrade 2024-02-12 20:23:39 +05:30
sriram veeraghanta 73d12cf1bb fix: package version upgrade 2024-02-12 20:23:12 +05:30
sriram veeraghanta 7651640e29 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-12 19:12:26 +05:30
Ramesh Kumar ChandraandGitHub 41a9dc3603 fix: pages table of content drop down close on click and app sidebar expansion by default on big screens (#3629) 2024-02-12 19:11:56 +05:30
Prateek ShouryaandGitHub afda3ee6ca fix: priority sort order in display filters. (#3627) 2024-02-12 19:10:41 +05:30
Aaryan KhandelwalandGitHub fc8edab59b fix: sidebar height, authorization (#3626) 2024-02-12 19:10:07 +05:30
sriram veeraghanta 9cf5348019 fix: merge conflicts resolved 2024-02-12 17:09:22 +05:30
Lakhan BahetiandGitHub 042ed04a03 fix: project-pages responsiveness (#3624)
* fix: pages responsiveness

* fix: build errors
2024-02-12 17:01:58 +05:30
Ramesh Kumar ChandraandGitHub e29edfc02b style: issue detail responsiveness (#3625) 2024-02-12 17:01:24 +05:30
eb0af8de59 fix: inbox issues icon in project header render in big screen (#3622)
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-02-12 15:58:22 +05:30
0fb43c6fc5 chore: Removing 'description_html' from Issue List (#3623)
* chore: removed issue description from issue list

* fix: issue description handling on peekoverview

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-02-12 15:26:02 +05:30
963d26ccda refactor: Gantt chart layout (#3585)
* chore: gantt sidebar and main content scroll sync

* chore: add arrow navigation position logic

* refactor: scroll position update logic

* refactor: gantt chart components

* refactor: gantt sidebar

* fix: vertical scroll issue

* fix: move to the hidden block button flickering

* refactor: gantt sidebar components

* chore: move timeline header outside

* fix gantt scroll issue

* fix: sticky position issues

* fix: infinite timeline scroll logic

* chore: removed unnecessary import statements

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
2024-02-12 15:08:17 +05:30
sriram veeraghanta 3eb819c4ae fix: merge conflicts resolved 2024-02-12 13:14:00 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4b67a41b41 chore(deps): bump django from 4.2.7 to 4.2.10 in /apiserver/requirements (#3606)
Bumps [django](https://github.com/django/django) from 4.2.7 to 4.2.10.
- [Commits](https://github.com/django/django/compare/4.2.7...4.2.10)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 12:44:16 +05:30
Aaryan KhandelwalandGitHub 0a36850d03 chore: show project tour modal before project creation (#3611) 2024-02-12 12:43:45 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5bce2014c5 chore(deps): bump cryptography in /apiserver/requirements (#3619)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.6 to 42.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.6...42.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 12:42:52 +05:30
Ramesh Kumar ChandraandGitHub 3f7f91e120 fix: breadcrumbs responsiveness fix, modules list mobile responsive layout component added, inbox button fix for responsiveness, board view card responsiveness in cycles and modules (#3620) 2024-02-12 12:34:15 +05:30
Prateek ShouryaandGitHub 1927fdd437 feat: checkbox component (#3603)
* feat: custom checkbox component.

* improvement: checkbox component implementation in email notification settings.

* improvement: add loader in email notification settings page.
2024-02-09 16:37:39 +05:30
sriram veeraghanta b86c6c906a fix: merge conflicts resolved 2024-02-09 16:30:05 +05:30
sriram veeraghanta 99975d0ba0 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-09 16:22:41 +05:30
Lakhan BahetiandGitHub 4f72ebded9 chore: added sign-up/in, onboarding, dashboard, all-issues related events (#3595)
* chore: added event constants

* chore: added workspace events

* chore: added workspace group for events

* chore: member invitation event added

* chore: added project pages related events.

* fix: member integer role to string

* chore: added sign-up & sign-in events

* chore: added global-view related events

* chore: added notification related events

* chore: project, cycle property change added

* chore: cycle favourite, and change-properties added

* chore: module davorite, and sidebar property changes added

* fix: build errors

* chore: all events defined in constants
2024-02-09 16:22:08 +05:30
Lakhan BahetiandGitHub be5d1eb9f9 fix: notification popover responsiveness (#3602)
* fix: notification popover responsiveness

* fix: build errors

* fix: typo
2024-02-09 16:17:39 +05:30
rahulrameshaandGitHub 8d730e6680 fix: spreadsheet date validation and sorting (#3607)
* fix validation for start and end date in spreadsheet layout

* revamp logic for sorting in all fields
2024-02-09 16:14:08 +05:30
sriram veeraghanta 41a3cb708c Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-09 15:59:44 +05:30
27037a2177 feat: completed cycle snapshot (#3600)
* fix: transfer cycle old distribtion captured

* chore: active cycle snapshot

* chore: migration file changed

* chore: distribution payload changed

* chore: labels and assignee structure change

* chore: migration changes

* chore: cycle snapshot progress payload updated

* chore: cycle snapshot progress type added

* chore: snapshot progress stats updated in cycle sidebar

* chore: empty string validation

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-02-09 15:53:54 +05:30
rahulrameshaandGitHub e2affc3fa6 chore: virtualization ish behaviour for issue layouts (#3538)
* Virtualization like core changes with intersection observer

* Virtualization like changes for spreadsheet

* Virtualization like changes for list

* Virtualization like changes for kanban

* add logic to render all the issues at once

* revert back the changes for list to follow the old pattern of grouping

* fix column shadow in spreadsheet for rendering rows

* fix constant draggable height while dragging and rendering blocks in kanban

* fix height glitch while rendered rows adjust to default height

* remove loading animation for issue layouts

* reduce requestIdleCallback timer to 300ms

* remove logic for index tarcking to force render as the same effect seems to be achieved by removing requestIdleCallback

* Fix Kanban droppable height

* fix spreadsheet sub issue loading

* force change in reference to re render the render if visible component when the order of list changes

* add comments and minor changes
2024-02-09 15:53:15 +05:30
Ramesh Kumar ChandraandGitHub 3a14f19c99 style: responsive analytics (#3604) 2024-02-09 15:52:25 +05:30
sriram veeraghanta eb4c3a4db5 fix: merge conflicts resolved 2024-02-08 18:50:04 +05:30
2e129682b7 fix: incorrect dashboard tab (#3597)
* fix: incorrect dashboard tab

* chore: added comments for the helper functions

* style: updated tabs list UI

* chore: default widget filters changed

* fix: build errors

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-02-08 17:57:22 +05:30
Anmol Singh BhatiaandGitHub 4a145f7a06 fix: notification card snooze (#3598)
* fix: resolved event propagation issue with notification card snooze button

* fix: resolved event propagation issue with notification card snooze button
2024-02-08 17:55:57 +05:30
Anmol Singh BhatiaandGitHub e69fcd410c chore: custom menu dropdown improvement (#3599)
* conflict: merge conflict resolved

* chore: breadcrumbs component improvement
2024-02-08 17:53:36 +05:30
Ramesh Kumar ChandraandGitHub 55afef204d style: responsive profile (#3596)
* style: responsive profile

* style: profile header drop down, sidebar auto show hide depending on the screen width

* fix: item tap on white space in the drop down menu in profile header

* fix: profile layout breaking on big screens in page visit
2024-02-08 17:49:26 +05:30
sriram veeraghanta f9e187d8b9 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-08 13:30:39 +05:30
Aaryan KhandelwalandGitHub 9545dc77d6 fix: cycle and module reordering in the gantt chart (#3570)
* fix: cycle and module reordering in the gantt chart

* chore: hide duration from sidebar if no dates are assigned

* chore: updated date helper functions to accept undefined params

* chore: update cycle sidebar condition
2024-02-08 13:30:16 +05:30
sriram veeraghanta 1fc987c6c9 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-08 13:14:25 +05:30
guru_sainathandGitHub c1c0297b6d fix: handled issue create modal submission on clicking enter key (#3593) 2024-02-08 13:08:19 +05:30
Manish GuptaandGitHub 1a7b5d7222 build fix (#3594) 2024-02-08 12:26:55 +05:30
rahulrameshaandGitHub fb3dd77b66 feat: Keyboard navigation spreadsheet layout for issues (#3564)
* enable keyboard navigation for spreadsheet layout

* move the logic to table level instead of cell level

* fix perf issue that made it unusable

* fix scroll issue with navigation

* fix build errors
2024-02-08 11:49:00 +05:30
Manish GuptaandGitHub a43dfc097d feat: muti-arch build (#3569)
* arm build, supporting centos & alpine
2024-02-08 11:47:52 +05:30
346c6f5414 fix: module header hide on bigger screens (#3590)
* fix: module header hide on bigger screens

* fix: Add Inbox back on mobile

---------

Co-authored-by: Maximilian Engel <maximilian.engel@swg.de>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-02-08 00:16:00 +05:30
729b6ac79e fix: Handled the draft issue from issue create modal and optimised the draft issue store (#3588)
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-02-07 20:45:05 +05:30
0a35fcfbc0 chore: email template logo changed (#3575)
* chore: email template logo changed

* fix: icons image extensions

* fix: state icons

---------

Co-authored-by: LAKHAN BAHETI <lakhanbaheti9@gmail.com>
2024-02-07 20:20:54 +05:30
Anmol Singh BhatiaandGitHub 75b4c6e7d6 chore: posthog code refactor (#3586) 2024-02-07 17:29:39 +05:30
a1d6c40627 fix: show window closing alert only when page is not saved (#3577)
* fix: show window closing alert only when page is not saved

* chore: Refactor useReloadConfirmations hook

- Removed the `message` parameter, as it was not being used and not
  supported in modern browsers
- Changed the `isActive` flag to a temporary flag and added a TODO comment to remove it later.
- Implemented the `handleRouteChangeStart` function to handle route change events and prompt the user with a confirmation dialog before leaving the page.
- Updated the dependencies of the `handleBeforeUnload` and `handleRouteChangeStart` callbacks.
- Added event listeners for `beforeunload` and `routeChangeStart` events in the `useEffect` hook.
- Cleaned up the event listeners in the cleanup function of the `useEffect` hook.

fix: Fix reload confirmations in PageDetailsPage

- Removed the TODO comment regarding fixing reload confirmations with MobX, as it has been resolved.
- Passed the `pageStore?.isSubmitting === "submitting"` flag to the `useReloadConfirmations` hook instead of an undefined message.

This commit refactors the `useReloadConfirmations` hook to improve its functionality and fixes the usage in the `PageDetailsPage` component.

---------

Co-authored-by: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
2024-02-07 17:10:44 +05:30
Anmol Singh BhatiaandGitHub 4a2e648f6d chore: workspace dashboard refactor (#3584) 2024-02-07 16:21:20 +05:30
Bavisetti NarayanandGitHub 76db394ab1 chore: mention notification and webhook faliure (#3573)
* fix: mention rstrip error

* chore: webhook deactivation email

* chore: changed template

* chore: current site for external api's

* chore: mention in template displayed

* chore: mention tag fix

* chore: comment user name displayed
2024-02-07 15:14:30 +05:30
Bavisetti NarayanandGitHub 4563b50fad chore: module issue count (#3566) 2024-02-07 15:13:35 +05:30
Anmol Singh BhatiaandGitHub 065226f8b2 fix: draft issue peek overview (#3582)
* chore: project, view and shortcut modal alignment consistency

* chore: issue highlight list layout improvement

* fix: draft issue peek overview fix

* fix: draft issue layout inline editing
2024-02-07 15:06:07 +05:30
Anmol Singh BhatiaandGitHub 0a99a1a091 fix: dashboard header z index and workspace active cycles fix (#3581)
* fix: dashboard header z index fix

* chore: workspace active cycles upgrade page improvement
2024-02-07 13:44:45 +05:30
Ramesh Kumar ChandraandGitHub 4b2a9c8335 style: responsive breadcrumbs and headers for dashboard, projects, project issues, cycles, cycle issues, module issues (#3580) 2024-02-07 13:44:03 +05:30
NikhilandGitHub 751b15a7a7 dev: update the response for conflicting errors (#3568) 2024-02-06 17:00:42 +05:30
Bavisetti NarayanandGitHub ac22769220 chore: email trigger for new assignee (#3572) 2024-02-06 16:47:14 +05:30
Manish GuptaandGitHub 46ae0f98dc app_release value handled (#3571) 2024-02-06 15:35:05 +05:30
Anmol Singh BhatiaandGitHub 30aaec9097 chore: module date validation (#3565) 2024-02-05 20:46:01 +05:30
Anmol Singh BhatiaandGitHub 4041c5bc5b chore: cycle and module sidebar improvement (#3562) 2024-02-05 19:13:21 +05:30
Bavisetti NarayanandGitHub 403595a897 chore: removed email notification for new users (#3561) 2024-02-05 19:13:02 +05:30
Aaryan KhandelwalandGitHub 0ee93dfd8c chore: added None filter option to the dashboard widgets (#3556)
* chore: added tab change animation

* chore: widgets filtering logic updated

* refactor: issues list widget

* fix: tab navigation transition

* fix: extra top spacing on opening the peek overview
2024-02-05 19:12:33 +05:30
Anmol Singh BhatiaandGitHub ee0e3e2e25 chore: active cycle issue transfer validation (#3560)
* fix: completed cycle list layout validation

* fix: completed cycle kanban layout validation

* fix: completed cycle spreadsheet layout validation

* fix: date dropdown disabled fix

* chore: quick action validation added for list, kanban and spreadsheet layout

* fix: calendar layout validation added
2024-02-05 14:47:40 +05:30
0165abab3e chore: posthog events improved (#3554)
* chore: events naming convention changed

* chore: track element added for project related events

* chore: track element added for cycle related events

* chore: track element added for module related events

* chore: issue related events updated

* refactor: event tracker store

* refactor: event-tracker store

* fix: posthog changes

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-02-05 13:19:07 +05:30
Anmol Singh BhatiaandGitHub 7d07afd59c chore: cycle and module sidebar analytics improvement (#3559)
* chore: cycle and module store update action updated

* chore: cycle and module issue store actions updated

* chore: cycle and module retrieve endpoints updated

* fix: app sidebar z index and priority icon fix

* chore: cycle and module sidebar and stats updated
2024-02-05 12:34:53 +05:30
sriram veeraghanta efaba43494 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-03 14:55:38 +05:30
sriram veeraghanta a8ec2b6914 fix: error handling in pages list page 2024-02-03 00:35:29 +05:30
NikhilandGitHub 39eb8c98d1 dev: validation for external id and external source (#3552)
* dev: error response for duplicate items created through external apis

* dev: return identifier and also add the validation for state

* fix: validation for external id and external source
2024-02-02 16:43:05 +05:30
rahulrameshaandGitHub 138d06868b fix: all issues spreadsheet sorting and kanban dnd for long lists (#3550)
* fix all issues filter for spreadsheet view

* fix kanban dnd with long lists
2024-02-02 14:50:23 +05:30
Ramesh Kumar ChandraandGitHub 2eab3b41a2 chore: responsive and styling fixes (#3541) 2024-02-02 14:49:42 +05:30
Anmol Singh BhatiaandGitHub 662b497082 fix: create issue modal project select (#3549) 2024-02-02 14:29:59 +05:30
Anmol Singh BhatiaandGitHub 67cf1785b8 chore: breadcrumb component improvement (#3537)
* chore: breadcrumb component improvement

* chore: code refactor
2024-02-01 18:13:30 +05:30
sriram veeraghantaandGitHub c6e3f1b932 Merge pull request #3535 from makeplane/preview
release: 0.15-dev
2024-02-01 15:01:49 +05:30
7d08a57be6 chore: remove build warnings (#3534)
* chore: remove build warnings

* fix: posthog wrapper fixes

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-02-01 14:53:08 +05:30
Anmol Singh BhatiaandGitHub b0ad48e35a chore: enhance loading state for setting page updates (#3533) 2024-02-01 13:35:01 +05:30
Anmol Singh BhatiaandGitHub d68669df51 fix: resolved issue with resetting the draft issue form (#3532) 2024-02-01 13:33:08 +05:30
Anmol Singh BhatiaandGitHub 4e600e4e9b fix: update cycle error (#3530)
* fix: update cycle response and implement required changes

* chore: update cycle response
2024-02-01 13:32:37 +05:30
Lakhan BahetiandGitHub 4fc4da7982 fix: email-template heading overview (#3525)
* fix: email-template heading

* fix: typo
2024-01-31 20:16:47 +05:30
M. PalanikannanandGitHub 6f210e1f4b chore: removing unnecessary code (#3524)
* fix: adding back enter key extension with mentions

* removed unncessary code
2024-01-31 19:38:37 +05:30
Aaryan KhandelwalandGitHub f7803dab56 chore: added validation to cloud hostname field (#3523) 2024-01-31 18:06:57 +05:30
M. PalanikannanandGitHub 70172f8e3d fix: adding back enter key extension with mentions (#3499) 2024-01-31 18:06:12 +05:30
M. PalanikannanandGitHub 21bc668a56 fix: horizontal rule no more causes issues on last node (#3507) 2024-01-31 18:05:06 +05:30
Anmol Singh BhatiaandGitHub dc5a5f4a91 fix/draft issue modal focus issue fix (#3522) 2024-01-31 17:47:47 +05:30
Anmol Singh BhatiaandGitHub 2c67aced15 fix: cycle and module sidebar mutation fix (#3521)
* fix: cycle and module sidebar mutation

* fix: cycle and module calendar drag n drop fix
2024-01-31 17:09:24 +05:30
Ramesh Kumar ChandraandGitHub 3a4c893368 Style/workspace project settings layout (#3520)
* style: project settings layout for mobile screens

* style: workspace settings layout for mobile screens
2024-01-31 15:42:20 +05:30
0d036e6bf5 refactor: dropdown button components (#3508)
* refactor: dropdown button components

* chore: dropdowns accessibility improvement

* chore: update module dropdown

* chore: update option content

* chore: hide icon from the peek overview

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-01-31 15:36:55 +05:30
rahulrameshaandGitHub 3ef0570f6a Fix all issues custom views by defining list layout for my-issue (#3517) 2024-01-31 13:59:46 +05:30
Prateek ShouryaandGitHub c9d2ea36b8 chore: allow guests/ viewers to comment. (#3515) 2024-01-30 20:13:28 +05:30
f0836ceb10 chore: email-template changes overview (#3494)
* style: template design

* fix: project url and actors

* fix: heading jinja

* fix: typo in workspace

* fix: issue title change

* fix: top project, issue redirection

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-01-30 20:12:59 +05:30
Anmol Singh BhatiaandGitHub 888665783e feat: issue highlighting (#3514)
* chore: kanban issue highlight

* chore: issue highlighting added
2024-01-30 20:12:39 +05:30
Prateek ShouryaandGitHub 817737b2c0 improvement: add hide/ unhide icon for all password field in the platform. (#3513) 2024-01-30 20:11:16 +05:30
rahulrameshaandGitHub 638c1e21c9 fix quick add issue creation in cycles (#3511) 2024-01-30 18:03:49 +05:30
Prateek ShouryaandGitHub c67e097fc2 chore: fix assignee tooltip logic in list and kanban layout for consistency. (#3510) 2024-01-30 16:25:13 +05:30
804dd8300d chore: implemented multiple modules select in the issues (#3484)
* fix: add multiple module in an issue

* feat: implemented multiple modules select in the issue detail and issue peekoverview and resolved build errors.

* feat: handled module parameters type error in the issue create and draft modal

* feat: handled UI for modules select dropdown

* fix: delete module activity updated

* ui: module issue activity

* fix: module search endpoint and issue fetch in the modules

* fix: module ids optimized

* fix: replaced module_id from boolean to array of module Id's in module search modal params

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-01-30 15:23:20 +05:30
Ramesh Kumar ChandraandGitHub c6d6b9a0e9 style/responsive sidebar (#3505)
* style: added sidebar toggle in all the screens for mobile responsive

* chore: close sidebar on click of empty space when opened

* chore: setting the sidebar collapsed in smaller screens
2024-01-30 15:22:24 +05:30
Aaryan KhandelwalandGitHub 9debd81a50 dev: show all issues on the gantt chart (#3487) 2024-01-30 14:25:15 +05:30
Anmol Singh BhatiaandGitHub d53a086206 chore: project active cycle and linear progress indicator improvement (#3504)
* chore: linear progress indicator improvement

* chore: project active cycle improvement
2024-01-30 13:59:49 +05:30
Anmol Singh BhatiaandGitHub ef8472ce5e chore: unused var removed (#3503) 2024-01-30 13:59:07 +05:30
Anmol Singh BhatiaandGitHub 4aa34f3eda fix: create update cycle modal project id pre-load data updated (#3502) 2024-01-30 13:58:18 +05:30
Anmol Singh BhatiaandGitHub c7616fda11 chore: empty state theme improvement (#3501) 2024-01-29 20:38:32 +05:30
483fc57601 chore: issue sidebar and peek overview improvement (#3488)
* chore: issue peek overview and sidebar properties focused state improvement

* fix: added name of the issue in issue relation

* chore: issue sidebar and peek overview properties improvement

* chore: issue assignee improvement for sidebar and peek overview

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-01-29 20:36:14 +05:30
Aaryan KhandelwalandGitHub 09a1a55da8 fix: unique key errors (#3500) 2024-01-29 20:35:23 +05:30
Anmol Singh BhatiaandGitHub 61f92563a9 fix: profile issue application error (#3496)
* fix: profile issue application error

* chore: app sidebar projects updated to your projects

* fix: profile issues update
2024-01-29 18:04:25 +05:30
rahulrameshaandGitHub 00e07443b0 fix: Add missing project and subscriber filters to the list of filter params (#3497)
* add missing project and subscriber filters to the list of filter params

* add toast message on successfully marking all notifications as read
2024-01-29 17:26:48 +05:30
sriram veeraghanta c4efdcd704 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-01-29 16:12:57 +05:30
Aaryan KhandelwalandGitHub 3c9679dff9 chore: update time in real-time in dashboard and profile sidebar (#3489)
* chore: update dashboard and profile time in realtime

* chore: remove seconds

* fix: cycle and module sidebar datepicker
2024-01-29 15:42:57 +05:30
rahulrameshaandGitHub b3393f5c48 fix: Filters in all issues and enable dnd in kanban based on roles (#3493)
* fix filters mutation issue

* fix all issue filters for state group

* disable drag in Kanban for non members

* remove unused imports
2024-01-29 15:27:14 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f995736642 chore(deps): bump cryptography in /apiserver/requirements (#3492)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.5 to 41.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.5...41.0.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 15:26:08 +05:30
Anmol Singh BhatiaandGitHub f7f1f2bea4 fix: issue preload data in issue create update modal (#3491) 2024-01-29 14:08:45 +05:30
guru_sainathandGitHub 532da80375 fix: handled attachment upload filename size error in the issue detail (#3485)
* fix: handled attachment upload filename size error in the issue detail

* ui: profile detail sidebar border
2024-01-28 18:30:53 +05:30
212f2b54f8 chore: issue relation modal and issue peek overview mutation fix (#3482)
* fix: resolve addtocycle and addtomodule mutation in peek overview and issue sidebar

* fix: issue relation modal fix for all issues peek overview

* fix: cycle and module mutation in issue detail and issue peek overview

* fix: updated the issue actions for cycle and module mutation in peek overview

* chore: module issue store updated

* chore: existing isssue modal improvement and build error fix

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-01-27 15:20:36 +05:30
Anmol Singh BhatiaandGitHub 9ecdcc6fde chore: spreadsheet layout improvement (#3483)
* chore: spreadsheet layout improvement

* chore: spreadsheet layout improvement

* chore: spreadsheet layout improvement
2024-01-27 15:18:42 +05:30
rahulrameshaandGitHub ddae745669 fix exception in member list (#3479) 2024-01-25 19:50:34 +05:30
e78c1f2060 Fix/inbox issue bugs (#3477)
* fix: inbox pending_issue_count updation from the store

* fix: inbox list item overflow issue on issue title

* fix: inbox issue mutation

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-01-25 19:20:02 +05:30
Manish GuptaandGitHub ebc891b985 fix: one click install fixes (#3476)
* test 1

* test 2

* test 3

* test 4

* minor fix

* installer script created

* minor fix

* installer modified

* cleanup
2024-01-25 18:21:21 +05:30
60b5589c48 chore: archived issues restructure. (#3469)
* chore: archived issues restructure.

* fix issue detail functionalities

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
2024-01-25 18:09:01 +05:30
Prateek ShouryaandGitHub f8208b1b5e Fix/empty state flicker (#3475)
* fix: flicker issue between loader and empty states.

* chore: fix `All Issues` tab highlight when we switch between tabs inside all issues.
2024-01-25 18:08:21 +05:30
Anmol Singh BhatiaandGitHub 6c6b764421 chore: empty state and project active cycle improvement (#3472)
* chore: pages empty state improvement

* chore: workspace all issues empty state improvement

* chore: profile issue empty state improvement

* chore: empty state sm size updated

* chore: project view empty state image updated

* chore: dashboard widgets permission uodated

* chore: draft issues and project issue empty state image

* chore: active cycle label updated
2024-01-25 18:00:45 +05:30
Manish GuptaandGitHub 5c912b8821 feat: One Click Deployment (#3474)
* wip

* 1-click install script added
2024-01-25 17:13:30 +05:30
NikhilandGitHub ff19980502 chore: update python version (#3471) 2024-01-25 16:39:19 +05:30
a1a24e4574 fix: pre release bug fixes (#3473)
* clear store on signout

* fix: project member list response change

* fix adding member to project

* fix exceptions with invitations

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-01-25 16:38:34 +05:30
guru_sainathandGitHub c1c2a6ddce fix: removing the issue from the issue root store while we are deleting from the issue bulk delete modal (#3470) 2024-01-25 16:37:59 +05:30
guru_sainathandGitHub ec3cad1f25 chore: applying query params to the global issues filters in the global views (#3464)
* chore: applying filters from the route params to the global issue filters store and Typos

* chore: enabled posthog

* fix: labels disbaled and loader while creating the label in isse detail and relation modal loader and mutation issue
2024-01-25 14:27:35 +05:30
Anmol Singh BhatiaandGitHub 336c97d336 chore: bug fixes and ui improvements (#3468)
* chore: empty state improvement

* chore: app sidebar improvement

* chore: dashboard empty state improvement
2024-01-25 14:23:55 +05:30
Aaryan KhandelwalandGitHub e4a3d0db5c fix: addIssue function logic (#3467) 2024-01-25 14:09:52 +05:30
Anmol Singh BhatiaandGitHub 7f2e99dd2d fix: resolve module and cycle mutation issues in peek overview, issue sidebar and empty state (#3466)
* fix: resolve module and cycle mutation issues in peek overview, sidebar, and empty state

* chore: code refactor
2024-01-25 14:06:03 +05:30
Prateek ShouryaandGitHub a104cc4814 chore: remove deprecated components related to issue activity/ comments. (#3465) 2024-01-25 14:04:38 +05:30
03cbad5110 fix: inbox issue bug fixes and improvements. (#3460)
* style: fix create comment card overflow issue.

* chore: improved loader and filter selection logic.

* chore: implement inbox issue navigation functionality.

* chore: loaders in inbox issue sidebar and the content root

* chore: inbox issue detail sidebar revamp.

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-01-25 13:41:02 +05:30
Aaryan KhandelwalandGitHub 2956c43ed5 fix: issues list modal not closing on escape key (#3463) 2024-01-25 13:30:44 +05:30
Aaryan KhandelwalandGitHub eae32593cb chore: added optional tooltip to dropdowns (#3462) 2024-01-25 13:29:56 +05:30
Anmol Singh BhatiaandGitHub 7fd625e0e3 fix: project pages loader (#3461) 2024-01-25 12:38:55 +05:30
Bavisetti NarayanandGitHub f007dcff26 fix: page archive and unarchive (#3459) 2024-01-25 00:48:18 +05:30
adf091fa07 fix: email notifications (#3457)
* fix: email-template design

* fix: priority and state new value

* dev: update template with comments, cta text and view issue button.

* dev: fix priority and state

* dev: update data condition

* fix: added avatar url

* fix: comment avatar url

* fix: priority, labels, state design

* style: assignee property

* dev: fix template for comments and profile changes

* fix: spacing between properties

* fix: todo image for state change

* fix: blocking and blocked by value change

* dev: update template summsary

* fix: blocking, duplicate

* fix: comments spacing

* chore: improve `state change` checkbox logic.

* fix: email notification message change

* fix: updated date format

* fix: updates text color

* fix: labels sequence rendering

* fix: schedular time change

---------

Co-authored-by: LAKHAN BAHETI <lakhanbaheti9@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2024-01-24 20:34:32 +05:30
b66f07845a chore: inbox issue restructure the components and store (#3456)
* chore: inbox-issues store and type updates

* chore: issue inbox payload change for GET and POST

* chore: issue inbox payload change for PATCH

* chore: inbox-issue new hooks and store updates

* chore: update inbox issue template.

* chore: UI root

* chore: sidebar issues render

* chore: inbox issue details page layout.

* chore: inbox issue filters

* chore: inbox issue status card.

* chore: add loader.

* chore: active inbox issue styles.

* chore: inbox filters

* chore: inbox applied filters UI

* chore: inbox issue approval header

* chore: inbox issue approval header operations

* chore: issue reaction and activity fetch in issue_inbox store

* chore: posthog enabled

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2024-01-24 20:33:54 +05:30
rahulrameshaandGitHub 911211cf3d Chore: change kanban layout and issue activity fixes (#3458)
* change back the to kanban to enable full board scrolling

* fix adding context to activity
2024-01-24 20:32:13 +05:30
Aaryan KhandelwalandGitHub 53b41481a2 chore: dashboard empty states, fetching logic (#3455)
* chore: remove one-time fetching

* chore: update empty states

* chore: updated icons

* chore: empty state content
2024-01-24 19:41:02 +05:30
9d9d703c62 chore: workspace active cycles upgrade page implementation (#3454)
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-01-24 19:37:49 +05:30
Aaryan KhandelwalandGitHub a2f34e9573 style: peek overview and issue details properties (#3447)
* style: peek overview and issue details properties

* fix: cycle and module remove function

* style: update placeholder text color

* fix: relation constant

* chore: added todos to fix later
2024-01-24 19:21:59 +05:30
Aaryan KhandelwalandGitHub 81f84f24f7 fix: outsideClickDetector not working on the date dropdown (#3450) 2024-01-24 19:13:38 +05:30
Anmol Singh BhatiaandGitHub 87f39d7372 chore: empty state revamp and loader improvement (#3448)
* chore: empty state asset added

* chore: empty state asset updated and image path helper function added

* chore: empty state asset updated

* chore: empty state asset updated and empty state details constant added

* chore: empty state component, helper function and comicbox button added

* chore: draft, archived and project issue empty state

* chore: cycle, module and issue layout empty state

* chore: analytics, dashboard, all issues, pages and project view empty state

* chore:projects empty state

* chore:projects empty state improvement

* chore: cycle, module, view and page loader improvement

* chore: code refactor
2024-01-24 19:12:54 +05:30
Henit ChobisaandGitHub 1a1594e818 chore: Removed Issue Embeds from the Document Editor (#3449)
* fix: issue embed going to next line when selected on slash commands

* fix: issue suggestions selecting next embed on arrow down

* fix: pages crashing, because of incorrect data type

* chore: removed issue embeds from document editor and page interface

* fix: upgraded issue widget card to show only Placeholder

* fix: pricing url changes for issue embed placeholder

* fix: build errors
2024-01-24 19:12:36 +05:30
M. PalanikannanandGitHub 8d3ea5bb3e fix: delete and restore for dynamic urls for minio hosted images fix… (#3452)
* feat: delete and restore for dynamic urls for minio hosted images fixed in spaces

* feat: delete and restore images calls fixed for web
2024-01-24 18:56:19 +05:30
rahulrameshaandGitHub 6a2be6afc4 fix: archived issues and minor bug fixes (#3451)
* make computedFn without optional arguments

* fix archived issues

* fix activity changes with proper context

* fix display filters that require server side filtering
2024-01-24 18:50:54 +05:30
Henit ChobisaandGitHub 338d58f79d fix: fixed checklist scrolling on click (#3453) 2024-01-24 18:49:44 +05:30
M. PalanikannanandGitHub e23e4bc392 fix: inline codes now exitable with right arrow key and inclusive (#3446)
* fix: inline codes now exitable with right arrow key and inclusive

* regression: toggle inline code on bubble menu selection

* feat: added different code block behaviour on selection and not selection

* fix: blockquote toggling and isActive state fixed
2024-01-24 14:59:48 +05:30
Prateek ShouryaandGitHub c1598c3d38 fix: input field value not changing in delete import modal. (#3445) 2024-01-24 13:16:46 +05:30
4a436eeee2 fix: project exports (#3441)
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-01-23 22:32:53 +05:30
rahulrameshaandGitHub 47681fe9f8 fix: minor bug fixes and quality of life improvements (#3444)
* add concurrency to dev command to avaoid erroring out

* add context to issue activity

* minor quality of life improvement for exporter modal

* show the option to save draft issue only when there is content in name and description

* maintain commonality while referencing the user in activity

* fix minor changes in draft save issue modal logical condition

* minor change is state component for filter selection

* change logic for create issue activity

* change use last draft issue button to state control over previous on hover as that was inconsistent
2024-01-23 20:45:44 +05:30
f27efb80e1 dev: email notifications (#3421)
* dev: create email notification preference model

* dev: intiate models

* dev: user notification preferences

* dev: create notification logs for the user.

* dev: email notification stacking and sending logic

* feat: email notification preference settings page.

* dev: delete subscribers

* dev: issue update ui implementation in email notification

* chore: integrate email notification endpoint.

* chore: remove toggle switch.

* chore: added labels part

* fix: refactored base design with tables

* dev: email notification templates

* dev: template updates

* dev: update models

* dev: update template for labels and new migrations

* fix: profile settings preference sidebar.

* dev: update preference endpoints

* dev: update the schedule to 5 minutes

* dev: update template with priority data

* dev: update templates

* chore: enable `issue subscribe` button for all users.

* chore: notification handling for external api

* dev: update origin request

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: LAKHAN BAHETI <lakhanbaheti9@gmail.com>
Co-authored-by: Ramesh Kumar Chandra <rameshkumar2299@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-01-23 17:49:22 +05:30
guru_sainathandGitHub c1e1b81b99 chore: sub issue mutation for state and project change (#3442)
* chore: sub-issues mutation

* chore: posthog un commented
2024-01-23 16:56:22 +05:30
sriram veeraghantaandGitHub d9db765ae3 chore: updating package json version (#3443)
* chore: updating package json version

* chore: update package json in apiserver
2024-01-23 16:37:51 +05:30
Aaryan KhandelwalandGitHub 0531dc3308 chore: fix authorization for new projects in the peek overview (#3439)
* chore: fix authorization for new projects in the peek overview

* fix: prjects empty state authorization

* fix: peek overview auth
2024-01-23 16:16:55 +05:30
e36b7a5ab9 fix: Kanban related issues (#3436)
* fix for drag and drop issues

* add horizontal scroll for kanban

* fix all issues quick action overlap

---------

Co-authored-by: Rahul R <rahul.ramesha@plane.so>
2024-01-23 16:09:37 +05:30
Anmol Singh BhatiaandGitHub c6b756d918 chore: module and cycle bug fixes (#3435)
* chore: project active cycle improvement

* chore: cycle and module add existing mutation fix
2024-01-23 15:57:26 +05:30
sriram veeraghanta f3ae57bc85 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-01-23 15:56:29 +05:30
Manish GuptaandGitHub 2374161030 Sync Action Modified (#3437) 2024-01-23 15:53:07 +05:30
NikhilandGitHub 512ad83c08 dev: migration check (#3440)
* dev: wait for  migrations script for beat, worker, takeoff

* dev: update docker compose and migrator commands

* dev: migrator commands for self hosted setup

* dev: add recursive flag for chown
2024-01-23 15:07:45 +05:30
Aaryan KhandelwalandGitHub 801f75f406 chore: drop-downs improvements and bug fixes (#3433)
* chore: dropdowns should close on selecting an option

* style: @plane/ui dropdown styling

* refactor: @plane/ui dropdowns

* fix: build errors

* fix: list layout dropdowns positioning

* fix: priority dropdown text in dark mode
2024-01-23 14:25:09 +05:30
f88109ef04 chore: issue activity, comments, and comment reaction store and component restructure (#3428)
* fix: issue activity and comment change

* chore: posthog enabled

* chore: comment creation in activity

* chore: comment crud in store mutation

* fix: issue activity/ comments `disable` and `showAccessSpecifier` logic.

* chore: comment reaction serializer change

* conflicts: merge conflicts resolved

* conflicts: merge conflicts resolved

* chore: add issue activity/ comments to peek-overview.
* imporve `showAccessIdentifier` logic.

* chore: remove quotes from issue activity.

* chore: use `projectLabels` instead of `workspaceLabels` in labels activity.

* fix: project publish `is_deployed` not updating bug.

* cleanup

* fix: posthog enabled

* fix: typos and the comment endpoint updates

* fix: issue activity icons update

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2024-01-23 13:28:58 +05:30
Anmol Singh BhatiaandGitHub bb50df0dff chore: bug fixes and improvement (#3434)
* fix: peek overview issue delete bug

* chore: create cycle workflow improvement

* chore: project setting improvement
2024-01-23 13:11:39 +05:30
Aaryan KhandelwalandGitHub 2986769f28 fix: bugs in dashboard widgets (#3420)
* fix: bugs in dashboard widgets

* chore: updated view all issues button

* refactor: make use of getWidgetDetails and getWidgetStats computed functions

* fix: widgets redirection

* fix: build errors
2024-01-22 20:50:30 +05:30
fd5326dec6 chore: project cycle bug fixes and improvement (#3427)
* chore: burndown chart's completed at changes

* chore: project cycle bug fixes and improvement

* chore: cycle state constant updated

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-01-22 20:42:09 +05:30
49452a68ab ♻️ fix: Added new drag handle hide behavior to Pages (#3426)
- Removed unused `switchLinkView` function in `PageRenderer`
- Added `hideDragHandle` prop to `PageRenderer` component
- Updated `EditorContainer` component in `PageRenderer` to include `hideDragHandle` prop
- Removed unused code and variables in `DocumentEditor` component
- Added `hideDragHandleOnMouseLeave` state variable in `DocumentEditor` component
- Added `setHideDragHandleFunction` to set the hideDragHandle function from the DragAndDrop extension
- Passed `hideDragHandleOnMouseLeave` as prop to `PageRenderer` component in `DocumentEditor`
- Removed unused code and variables in `PageDetailsPage` component
- Updated `customClassName` prop in `PageRenderer` component used in `PageDetailsPage`

chore(deps): Update dependencies

- Updated `@tippyjs/react` dependency to version 4.2.6
- Updated `mobx-react-lite` dependency to version 4.0.5
- Updated `tippy.js` dependency to version 6.3.7

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-01-22 17:47:44 +05:30
b3ac9def8d fix: issue property dropdown data flow (#3425)
* dev: workspace states and estimates

* refactor issue dropdown logic to help work properly with issues on global level

* fix: project labels response change

* fix label type

* change store computed actions to computed functions from mobx-utils

* fix: state response change

* chore: project and workspace state change

* fix state and label types

* chore: state and label serializer change

* modify state and label types

* fix dropdown reset on project id change

* fix label sort order

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Rahul R <rahul.ramesha@plane.so>
2024-01-22 17:07:32 +05:30
Prateek ShouryaandGitHub be62662bb1 chore: remove active cycle link from dashboard sidebar. (#3423) 2024-01-22 15:27:49 +05:30
sriram veeraghantaandGitHub 4eba5c115a fix: docker branch builds (#3424)
* fix: build branch fixes

* fix: build branch fixes typos

* fix: changing push branches
2024-01-22 15:27:41 +05:30
sriram veeraghanta 24442ccc9c fix: deleting old issue store 2024-01-22 13:58:34 +05:30
sriram veeraghanta e32a50fba6 fix: merge conflicts reoslved 2024-01-22 13:56:38 +05:30
Aaryan Khandelwalandsriram veeraghanta 4baf2a2f09 fix: workspace project ids computed (#3417) 2024-01-22 13:23:10 +05:30
Aaryan Khandelwalandsriram veeraghanta 577118ca02 chore: new sign-in, sign-up and forgot password workflows (#3415)
* chore: sign up workflow updated

* chore: sign in workflow updated

* refactor: folder structure

* chore: forgot password workflow

* refactor: form component props

* chore: forgot password popover for instances with smtp unconfigured

* chore: updated UX copy

* chore: update reset password link

* chore: update email placeholder
2024-01-22 13:23:10 +05:30
Nikhilandsriram veeraghanta 4a26f11e23 dev: update apiserver .env.example (#3412) 2024-01-22 13:23:10 +05:30
Facundo Martin Gordilloandsriram veeraghanta 9926e321f6 fix: Updated "deployment documentation" link (#3413) 2024-01-22 13:23:10 +05:30
Prateek Shouryaandsriram veeraghanta 7f5028a4f6 refactor: move all sidebar links to constant file. (#3414)
* chore: move all workspace, project and profile links constants into their own constants file.

* chore: sidebar menu links improvement.
2024-01-22 13:23:10 +05:30
06a7bdffd7 Improvement: High Performance MobX Integration for Pages ✈︎ (#3397)
* fix: removed parameters `workspace`, `project` & `id` from the patch calls

* feat: modified components to work with new pages hooks

* feat: modified stores

* feat: modified initial component

* feat: component implementation changes

* feat: store implementation

* refactor pages store

* feat: updated page store to perform async operations faster

* fix: added types for archive and restore pages

* feat: implemented archive and restore pages

* fix: page creating twice when form submit

* feat: updated create-page-modal

* feat: updated page form and delete page modal

* fix: create page modal not updating isSubmitted prop

* feat: list items and list view refactored for pages

* feat: refactored project-page-store for inserting computed pagesids

* chore: renamed project pages hook

* feat: added favourite pages implementation

* fix: implemented store for archived pages

* fix: project page store for recent pages

* fix: issue suggestions breaking pages

* fix: issue embeds and suggestions breaking

* feat: implemented page store and project page store in page editor

* chore: lock file changes

* fix: modified page details header to catch mobx updates instead of swr calls

* fix: modified usePage hook to fetch page details when reloaded directly on page

* fix: fixed deleting pages

* fix: removed render on props changed

* feat: implemented page store inside page details

* fix: role change in pages archives

* fix: rerending of pages on tab change

* fix: reimplementation of peek overview inside pages

* chore: typo fixes

* fix: issue suggestion widget selecting wrong issues on click

* feat: added labels in pages

* fix: deepsource errors fixed

* fix: build errors

* fix: review comments

* fix: removed swr hooks from the `usePage` store hook and refactored `issueEmbed` hook

* fix: resolved reviewed comments

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:22:09 +05:30
Bavisetti Narayanandsriram veeraghanta d3dedc8e51 fix: user profile issues (#3409) 2024-01-22 13:22:09 +05:30
d656f8e62a fix: update global issues filter and enable profile issues (#3410)
* fix all issues and fix profile issues

* minor comments update

* minor change nullish check logic

* update nullish check logic

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:22:09 +05:30
Anmol Singh Bhatiaandsriram veeraghanta 864519e770 chore: issue filter loader improvement (#3406) 2024-01-22 13:22:09 +05:30
Nikhilandsriram veeraghanta 034f0a06db fix: key validation when magic sign in (#3403) 2024-01-22 13:22:09 +05:30
dependabot[bot]sriram veeraghantadependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7263cb072c chore(deps): bump follow-redirects from 1.15.3 to 1.15.4 (#3349)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 13:22:09 +05:30
ea3a0362b0 fix: enable global/ all issues (#3405)
* fix global issues and views

* remove separate layouts for specific views

* add permissions to views

* fix global issues filters

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:22:09 +05:30
c9337d4a41 feat: dashboard widgets (#3362)
* fix: created dashboard, widgets and dashboard widget model

* fix: new user home dashboard

* chore: recent projects list

* chore: recent collaborators

* chore: priority order change

* chore: payload changes

* chore: collaborator's active issue count

* chore: all dashboard widgets added with services and typs

* chore: centered metric for pie chart

* chore: widget filters

* chore: created issue filter

* fix: created and assigned issues payload change

* chore: created issue payload change

* fix: date filter change

* chore: implement filters

* fix: added expansion fields

* fix: changed issue structure with relation

* chore: new issues response

* fix: project member fix

* chore: updated issue_relation structure

* chore: code cleanup

* chore: update issues response and added empty states

* fix: button text wrap

* chore: update empty state messages

* fix: filters

* chore: update dark mode empty states

* build-error: Type check in the issue relation service

* fix: issues redirection

* fix: project empty state

* chore: project member active check

* chore: project member check in state and priority

* chore: remove console logs and replace harcoded values with constants

* fix: code refactoring

* fix: key name changed

* refactor: mapping through similar components using an array

* fix: build errors

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-01-22 13:22:09 +05:30
Prateek Shouryaandsriram veeraghanta f347c1cd69 refactor: update create/update issue modal to use currently active store's create/update method. (#3395)
* refactor: update `create/update issue` modal to use currently active store's create/update method.

* chore: add condition to avoid multiple API calls if the current store is MODULE or CYCLE.

* remove: console log

* chore: update `currentStore` to `storeType`.
2024-01-22 13:21:45 +05:30
Nikhilandsriram veeraghanta 3f07c48b35 chore: update issue template to auto assign and update labels (#3404) 2024-01-22 13:21:45 +05:30
M. Palanikannanandsriram veeraghanta 04b2214bf2 🐛 fix: Hide drag handle when cursor leaves the editor container (#3401)
This fix adds support for hiding the drag handle when the cursor leaves the editor container. It improves the user experience by providing a cleaner interface and removing unnecessary visual elements especially while scrolling.

- Add `hideDragHandle` prop to `EditorContainer` component in `editor-container.tsx`.
- Implement `onMouseLeave` event handler in `EditorContainer` to invoke `hideDragHandle` function.
- Update `DragAndDrop` extension in `drag-drop.tsx` to accept a `setHideDragHandle` function as an optional parameter.
- Pass the `setHideDragHandle` function from `RichTextEditor` component to `DragAndDrop` extension in `RichTextEditorExtensions` function in `index.tsx`.
- Set `hideDragHandleOnMouseLeave` state in `RichTextEditor` component to store the `hideDragHandlerFromDragDrop` function.
- Create `setHideDragHandleFunction` callback function in `RichTextEditor` to update the `hideDragHandleOnMouseLeave` state.
- Pass `hideDragHandleOnMouseLeave` as `hideDragHandle` prop to `EditorContainer` component in `RichTextEditor`.
2024-01-22 13:21:45 +05:30
Prateek Shouryaandsriram veeraghanta 1adb38655a fix: stack integration disable button mutation issue in project settings. (#3402) 2024-01-22 13:21:45 +05:30
Manish Guptaandsriram veeraghanta 4ab64b6905 dev: self host local build for other arch cpu (#3358)
handled x86_64 along with amd64
2024-01-22 13:21:45 +05:30
sriram veeraghanta 4b0d85591e fix: updated env variables at root (#3390) 2024-01-22 13:21:45 +05:30
Erhanandsriram veeraghanta 8e2789af3e Fix self-hosting docker-compose doc link (#3389)
Fixes the link as it leads to a 404 at the moment in the documentation / readme.
2024-01-22 13:21:45 +05:30
3592feb3d7 fix error meesage on successful error message (#3387)
Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:21:45 +05:30
Nikhilandsriram veeraghanta bc6a2542f5 dev: remove slack ping (#3377) 2024-01-22 13:20:06 +05:30
Prateek Shouryaandsriram veeraghanta 5e2d93df52 fix: project views bugs related to store refactor. (#3391)
* chore: remove debounce logic to fix create/ update view modal bugs.

* fix: bug in delete views not mutating the store.

* chore: replace `Project Empty State` with `Project Views Empty State`.

* chore: add issue peek overview.

* refactor: issue update, delete actions for project views layout.
fix: issue update and delete action throwing error bug.
fix: issue quick add throwing error bug.
2024-01-22 13:19:44 +05:30
M. Palanikannanandsriram veeraghanta cce349b805 [chore]: Removed explicit dependencies and cleaned up turbo config (#3388)
* Removed explicit dependencies and cleaned up turbo config

* fix: upgrade turbo

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-01-22 13:19:44 +05:30
fadda7cf04 fix: refactor related bugs (#3384)
* fix sub issues inside issue detail

* close peek over view after opening issue detail

* fix error while opening peek overview

* fix saving project views

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:19:44 +05:30
Anmol Singh Bhatiaandsriram veeraghanta c4093d29a7 chore: filter and display properties improvement (#3382) 2024-01-22 13:19:44 +05:30
Anmol Singh Bhatiaandsriram veeraghanta 8c89e9cc01 chore: esc to close peek overview added (#3380) 2024-01-22 13:19:44 +05:30
Anmol Singh Bhatiaandsriram veeraghanta 5625a3581a fix: drag and delete issue (#3379) 2024-01-22 13:19:44 +05:30
Lakhan Bahetiandsriram veeraghanta ee387c4222 chore webhook create page removed (#3376)
* chore webhook create page removed

* fix: removed unused variables
2024-01-22 13:19:44 +05:30
6a16a98b03 chore: update in sub-issues component and property validation and issue loaders (#3375)
* fix: handled undefined issue_id in list layout

* chore: refactor peek overview and user role validation.

* chore: sub issues

* fix: sub issues state distribution changed

* chore: sub_issues implementation in issue detail page

* chore: fixes in cycle/ module layout.
* Fix progress chart
* Module issues's update/ delete.
* Peek Overview for Modules/ Cycle.
* Fix Cycle Filters not applying bug.

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-01-22 13:19:43 +05:30
sriram veeraghanta 11f84a986c chore: formatting all python files using black formatter (#3366) 2024-01-22 13:19:43 +05:30
4b0d48b290 enable peekoverview for spreadsheet and minor refactor for faster opening of the peekoverview component (#3361)
Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:19:43 +05:30
9789068880 fix: project loaders for mobx store (#3356)
* add loaders to all the dropdowns outside project wrpper

* fix build errors

* minor refactor for project states color

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:19:43 +05:30
Henit Chobisaandsriram veeraghanta 151c355177 [FIX] Pages Malfunctioning on Load and Recent Pages Computation (#3359)
* fix: fixed `usePage` hook returning context instead of IPageStore

* fix: updated recent pages with `updated_at` instead of `created_at`

* fix: thown error instead of returning empty array
2024-01-22 13:19:43 +05:30
38580c3940 Fix env substitute issue in websocket docker setup (#3296)
* fix websocket connection issue in docker makeplane/plane#3195

* fix websocket connection issue for local env with removing from the prod nginx conf template makeplane/plane#319

* fix env substitution issue  of proxy_set_header makeplane/plane#3196

* review fixes

---------

Co-authored-by: Manish Gupta <manish@mgupta.me>
2024-01-22 13:19:43 +05:30
sriram veeraghanta 7ff91fdb82 fix: create more toggle fixes in create issue modal (#3355)
* fix: create more issue bugfixes

* fix: removing all warning
2024-01-22 13:19:43 +05:30
sriram veeraghanta a679b42200 fix: create sync action (#3353)
* fix: create sync action changes

* fix: typo changes
2024-01-22 13:19:43 +05:30
M. Palanikannanandsriram veeraghanta 27762ea500 fix: inline code blocks, code blocks and links have saner behaviour (#3318)
* fix: removed backticks in inline code blocks

* added better error handling while cancelling uploads

* fix: inline code blocks, code blocks and links have saner behaviour

- Inline code blocks are now exitable, don't have backticks, have better padding vertically and better regex matching
- Code blocks on the top and bottom of the document are now exitable via Up and Down Arrow keys
- Links are now exitable while being autolinkable via a custom re-write of the tiptap-link-extension

* fix: more robust link checking
2024-01-22 13:19:43 +05:30
2cd5dbcd02 chore: Error Handling and Validation Updates (#3351)
* fix: handled undefined issue_id in list layout

* chore: updated label select dropdown in the issue detail

* fix: peekoverview issue is resolved

* chore: user role validation for issue details.

* fix: Link, Attachement, parent mutation

* build-error: build error resolved in peekoverview

* chore: user role validation for issue details.

* chore: user role validation for `issue description`, `parent`, `relation` and `subscription`.

* chore: issue subscription mutation

* chore: user role validation for `labels` in issue details.

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2024-01-22 13:19:43 +05:30
96868760a3 update swr config to not fetch everything on focus (#3350)
Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:19:43 +05:30
df97b35a99 chore: Refactor Spreadsheet view for better code maintainability and performance (#3322)
* refcator spreadsheet to use table and roow based approach rather than column based

* update spreadsheet and optimized layout

* fix issues in spread sheet

* close quick action menu on click

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:19:43 +05:30
4611ec0b83 chore: refactored and resolved build issues on the issues and issue detail page (#3340)
* fix: handled undefined issue_id in list layout

* dev: issue detail store and optimization

* dev: issue filter and list operations

* fix: typo on labels update

* dev: Handled all issues in the list layout in project issues

* dev: handled kanban and auick add issue in swimlanes

* chore: fixed peekoverview in kanban

* chore: fixed peekoverview in calendar

* chore: fixed peekoverview in gantt

* chore: updated quick add in the gantt chart

* chore: handled issue detail properties and resolved build issues

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2024-01-22 13:19:43 +05:30
e6b31e2550 fix: link preview editor (#3335)
* feat: added link preview plugin in document editor

* fix: readonly editor page renderer css

* fix: autolink issue with links

* chore: added floating UI

* feat: added link preview components

* feat: added floating UI to page renderer for link previews

* feat: added actionCompleteHandler to page renderer

* chore: Lock file changes

* fix: regex security error

* chore: updated radix with lucid icons

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2024-01-22 13:19:43 +05:30
Nikhilandsriram veeraghanta 59fb371e3d dev: fix smtp configuration (#3339) 2024-01-22 13:19:43 +05:30
Nikhilandsriram veeraghanta 23e5306f6d dev: update the instance urls (#3329) 2024-01-22 13:19:43 +05:30
0f99fb302b chore: modal and dropdown improvement (#3332)
* dev: dropdown key down custom hook added

* chore: plane ui dropdowns updated

* chore: cycle and module tab index added in modals

* chore: view and page tab index added in modals

* chore: issue modal tab indexing added

* chore: project modal tab indexing added

* fix: build fix

* build-error: build error in pages new structure and reverted back to old page structure

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-01-22 13:19:43 +05:30
Manish Guptaandsriram veeraghanta 0e49d616b7 fixes web container public assets (#3336) 2024-01-22 13:19:43 +05:30
Nikhilandsriram veeraghanta e72920d33e fix: update jira summary endpoints (#3333)
* dev: update jira summary endpoints

* dev: update jira project key validations

* dev: updated key length
2024-01-22 13:19:43 +05:30
Nikhilandsriram veeraghanta 80dc38b649 fix: jira importer validations (#3323)
* fix: jira importer validations

* dev: update validation for cloud hostname

* dev: update the function to be used externally

* dev: update codeql workflow

* dev: update repository selection api
2024-01-22 13:19:43 +05:30
Nikhilandsriram veeraghanta 43b503c756 fix: security warnings related to information exposure and regex validations (#3325) 2024-01-22 13:19:43 +05:30
dependabot[bot]sriram veeraghantadependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
68d370fd86 chore(deps): bump tj-actions/changed-files in /.github/workflows (#3327)
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 38 to 41.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v38...v41)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 13:19:43 +05:30
Bavisetti Narayanandsriram veeraghanta 5e03f3dd82 chore: mobile configs (#3328)
* chore: mobile configs

* chore: mobile configurations changed

* chore: removed the slack id

* chore: reversed google client id
2024-01-22 13:19:43 +05:30
1257a88089 fix: breaking cycle issues and replacing router.push with Links (#3330)
* fix cycle creation and active cycle map

* minor fix in cycle store

* create cycle breaking fix

* replace last possible bits of router.push with Link

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
2024-01-22 13:19:43 +05:30
Prateek Shouryaandsriram veeraghanta 12a3392722 fix: estimate order not maintained in create/ update modal. (#3326)
* fix: estimate order not maintained in create/ update modal.

* fix: estimate points mutation on update.
2024-01-22 13:19:43 +05:30
sriram veeraghanta b62a1b11b1 fix: pages store structure changes 2024-01-22 13:19:43 +05:30
Anmol Singh Bhatiaandsriram veeraghanta 0a05aef046 fix: workspace invitations response updated (#3321) 2024-01-22 13:19:43 +05:30
efd3ebf067 chore: bug fixes and improvement (#3303)
* refactor: updated preloaded function for the list view quick add

* fix: resolved bug in the assignee dropdown

* chore: issue sidebar link improvement

* fix: resolved subscription store bug

* chore: updated preloaded function for the kanban layout quick add

* chore: resolved issues in the list filters and component

* chore: filter store updated

* fix: issue serializer changed

* chore: quick add preload function updated

* fix: build error

* fix: serializer changed

* fix: minor request change

* chore: resolved build issues and updated the prepopulated data in the quick add issue.

* fix: build fix and code refactor

* fix: spreadsheet layout quick add fix

* fix: issue peek overview link section updated

* fix: cycle status bug fix

* fix: serializer changes

* fix: assignee and labels listing

* chore: issue modal parent_id default value updated

* fix: cycle and module issue serializer change

* fix: cycle list serializer changed

* chore: prepopulated validation in both list and kanban for quick add and group header add issues

* chore: group header validation added

* fix: issue response payload change

* dev: make cycle and module issue create response simillar

* chore: custom control link component added

* dev: make issue create and update response simillar to list and retrieve

* fix: build error

* chore: control link component improvement

* chore: globalise issue peek overview

* chore: control link component improvement

* chore: made changes and optimised the issue peek overview root

* build-error: resolved build erros for issueId dependancy from issue detail store

* chore: peek overview link fix

* dev: update state nullable rule

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2024-01-22 13:19:43 +05:30
Prateek Shouryaandsriram veeraghanta 266f14d550 fix: project identifier cursor behaviour in create project modal. (#3320) 2024-01-22 13:19:43 +05:30
Prateek Shouryaandsriram veeraghanta 9eb8f41008 chore: UI/UX improvements (#3319)
* chore: add proper message for cycle/ module having start & end date but isn't active yet.

* fix: infinite loader after updating workspace settings.

* fix: user profile icon dropdown doesn't closes automatically.

* style: fix inconsistent padding in cycle empty state.

* chore: remove multiple `empty state` in labels settings and improve add label logic.

* style: fix inconsistent padding in project label, integration and estimates empty state.

* style: fix integrations settings breadcrumb title.

* style: add proper `disabled` styles for email field in profile settings.

* style: fix cycle layout height.
2024-01-22 13:19:43 +05:30
M. Palanikannanandsriram veeraghanta b340232e76 chore: Updated TableView component in table extension to solve sentry (#3309)
error of table not being defined while getting getBoundingClientRect()
and solve other TS issues

- Added ResolvedPos import from @tiptap/pm/model
- Updated setCellsBackgroundColor function parameter type to string
- Declared ToolboxItem type for toolbox items
- Modified columnsToolboxItems and rowsToolboxItems to use the ToolboxItem type
- Updated createToolbox function parameters to specify Element or null for triggerButton and ToolboxItem[] for items
- Added ts-expect-error comment above the toolbox variable declaration
- Updated update method parameter type to readonly Decoration[]
- Changed destructuring assignment of hoveredTable and hoveredCell in updateControls method to use Object.values and reduce method
- Added null check for this.table in updateControls method
- Wrapped the code that updates columnsControl and rowsControl with null checks for each control
- Replaced ts-ignore comments with proper dispatch calls in selectColumn and selectRow methods

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-01-22 13:19:43 +05:30
Aaryan Khandelwalandsriram veeraghanta 64ca267db4 dev: new create issue modal (#3312) 2024-01-22 13:19:43 +05:30
sriram veeraghanta 46e79dde27 fix: Login workflow depending on smtp is configured (#3307) 2024-01-22 13:19:43 +05:30
Jigin Jayaprakashandsriram veeraghanta 7272c54439 Fixes 3299 (#3308) 2024-01-22 13:19:20 +05:30
993 changed files with 29044 additions and 18094 deletions
+122 -103
View File
@@ -1,99 +1,90 @@
name: Branch Build
on:
pull_request:
types:
- closed
workflow_dispatch:
push:
branches:
- master
- preview
- qa
- develop
- release-*
release:
types: [released, prereleased]
env:
TARGET_BRANCH: ${{ github.event.pull_request.base.ref || github.event.release.target_commitish }}
TARGET_BRANCH: ${{ github.ref_name || github.event.release.target_commitish }}
jobs:
branch_build_setup:
if: ${{ (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) || github.event_name == 'release' }}
name: Build-Push Web/Space/API/Proxy Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
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 }}
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Uploading Proxy Source
uses: actions/upload-artifact@v3
with:
name: proxy-src-code
path: ./nginx
- name: Uploading Backend Source
uses: actions/upload-artifact@v3
with:
name: backend-src-code
path: ./apiserver
- name: Uploading Web Source
uses: actions/upload-artifact@v3
with:
name: web-src-code
path: |
./
!./apiserver
!./nginx
!./deploy
!./space
- name: Uploading Space Source
uses: actions/upload-artifact@v3
with:
name: space-src-code
path: |
./
!./apiserver
!./nginx
!./deploy
!./web
outputs:
gh_branch_name: ${{ env.TARGET_BRANCH }}
- id: set_env_variables
name: Set Environment Variables
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=makeplane/plane-dev" >> $GITHUB_OUTPUT
else
echo "BUILDX_DRIVER=docker-container" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
fi
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
branch_build_push_frontend:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
steps:
- name: Set Frontend Docker Tag
- name: Set Frontend Docker Tag
run: |
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable
else
TAG=${{ env.FRONTEND_TAG }}
fi
echo "FRONTEND_TAG=${TAG}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Web Source Code
uses: actions/download-artifact@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
name: web-src-code
driver: ${{ env.BUILDX_DRIVER }}
version: ${{ env.BUILDX_VERSION }}
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Check out the repo
uses: actions/checkout@v4.1.1
- name: Build and Push Frontend to Docker Container Registry
uses: docker/build-push-action@v4.0.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./web/Dockerfile.web
platforms: linux/amd64
platforms: ${{ env.BUILDX_PLATFORMS }}
tags: ${{ env.FRONTEND_TAG }}
push: true
env:
@@ -105,37 +96,46 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
steps:
- name: Set Space Docker Tag
- name: Set Space Docker Tag
run: |
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable
else
TAG=${{ env.SPACE_TAG }}
fi
echo "SPACE_TAG=${TAG}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Space Source Code
uses: actions/download-artifact@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
name: space-src-code
driver: ${{ env.BUILDX_DRIVER }}
version: ${{ env.BUILDX_VERSION }}
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Check out the repo
uses: actions/checkout@v4.1.1
- name: Build and Push Space to Docker Hub
uses: docker/build-push-action@v4.0.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./space/Dockerfile.space
platforms: linux/amd64
platforms: ${{ env.BUILDX_PLATFORMS }}
tags: ${{ env.SPACE_TAG }}
push: true
env:
@@ -147,37 +147,46 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
steps:
- name: Set Backend Docker Tag
- name: Set Backend Docker Tag
run: |
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable
else
TAG=${{ env.BACKEND_TAG }}
fi
echo "BACKEND_TAG=${TAG}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Backend Source Code
uses: actions/download-artifact@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
name: backend-src-code
driver: ${{ env.BUILDX_DRIVER }}
version: ${{ env.BUILDX_VERSION }}
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Check out the repo
uses: actions/checkout@v4.1.1
- name: Build and Push Backend to Docker Hub
uses: docker/build-push-action@v4.0.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./Dockerfile.api
platforms: linux/amd64
context: ./apiserver
file: ./apiserver/Dockerfile.api
platforms: ${{ env.BUILDX_PLATFORMS }}
push: true
tags: ${{ env.BACKEND_TAG }}
env:
@@ -185,45 +194,55 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_proxy:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
steps:
- name: Set Proxy Docker Tag
- name: Set Proxy Docker Tag
run: |
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable
else
TAG=${{ env.PROXY_TAG }}
fi
echo "PROXY_TAG=${TAG}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Proxy Source Code
uses: actions/download-artifact@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
name: proxy-src-code
driver: ${{ env.BUILDX_DRIVER }}
version: ${{ env.BUILDX_VERSION }}
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Check out the repo
uses: actions/checkout@v4.1.1
- name: Build and Push Plane-Proxy to Docker Hub
uses: docker/build-push-action@v4.0.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
context: ./nginx
file: ./nginx/Dockerfile
platforms: ${{ env.BUILDX_PLATFORMS }}
tags: ${{ env.PROXY_TAG }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
+5 -7
View File
@@ -1,25 +1,23 @@
name: Create Sync Action
on:
pull_request:
workflow_dispatch:
push:
branches:
- preview
types:
- closed
env:
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
jobs:
sync_changes:
# Only run the job when a PR is merged
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v2
uses: actions/checkout@v4.1.1
with:
persist-credentials: false
fetch-depth: 0
+3 -8
View File
@@ -33,15 +33,10 @@ RUN pip install -r requirements/local.txt --compile --no-cache-dir
RUN addgroup -S plane && \
adduser -S captain -G plane
RUN chown captain.plane /code
COPY . .
USER captain
# Add in Django deps and generate Django's static files
USER root
# RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
RUN chown -R captain.plane /code
RUN chmod -R +x /code/bin
RUN chmod -R 777 /code
USER captain
Regular → Executable
+3
View File
@@ -2,4 +2,7 @@
set -e
python manage.py wait_for_db
# Wait for migrations
python manage.py wait_for_migrations
# Run the processes
celery -A plane beat -l info
+2 -1
View File
@@ -1,7 +1,8 @@
#!/bin/bash
set -e
python manage.py wait_for_db
python manage.py migrate
# Wait for migrations
python manage.py wait_for_migrations
# Create the default bucket
#!/bin/bash
+2 -1
View File
@@ -1,7 +1,8 @@
#!/bin/bash
set -e
python manage.py wait_for_db
python manage.py migrate
# Wait for migrations
python manage.py wait_for_migrations
# Create the default bucket
#!/bin/bash
+3
View File
@@ -2,4 +2,7 @@
set -e
python manage.py wait_for_db
# Wait for migrations
python manage.py wait_for_migrations
# Run the processes
celery -A plane worker -l info
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.14.0"
"version": "0.15.1"
}
+8
View File
@@ -1,6 +1,8 @@
# Python imports
import zoneinfo
import json
from urllib.parse import urlparse
# Django imports
from django.conf import settings
@@ -51,6 +53,11 @@ class WebhookMixin:
and self.request.method in ["POST", "PATCH", "DELETE"]
and response.status_code in [200, 201, 204]
):
url = request.build_absolute_uri()
parsed_url = urlparse(url)
# Extract the scheme and netloc
scheme = parsed_url.scheme
netloc = parsed_url.netloc
# Push the object to delay
send_webhook.delay(
event=self.webhook_event,
@@ -59,6 +66,7 @@ class WebhookMixin:
action=self.request.method,
slug=self.workspace_slug,
bulk=self.bulk,
current_site=f"{scheme}://{netloc}",
)
return response
+40
View File
@@ -243,6 +243,29 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
):
serializer = CycleSerializer(data=request.data)
if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Cycle.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
cycle = Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).first()
return Response(
{
"error": "Cycle with the same external id and external source already exists",
"id": str(cycle.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(
project_id=project_id,
owned_by=request.user,
@@ -289,6 +312,23 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
serializer = CycleSerializer(cycle, data=request.data, partial=True)
if serializer.is_valid():
if (
request.data.get("external_id")
and (cycle.external_id != request.data.get("external_id"))
and Cycle.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", cycle.external_source),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Cycle with the same external id and external source already exists",
"id": str(cycle.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+97 -2
View File
@@ -220,6 +220,30 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
)
if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
issue = Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_id=request.data.get("external_id"),
external_source=request.data.get("external_source"),
).first()
return Response(
{
"error": "Issue with the same external id and external source already exists",
"id": str(issue.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save()
# Track the issue
@@ -256,6 +280,26 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
partial=True,
)
if serializer.is_valid():
if (
str(request.data.get("external_id"))
and (issue.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get(
"external_source", issue.external_source
),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Issue with the same external id and external source already exists",
"id": str(issue.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save()
issue_activity.delay(
type="issue.activity.updated",
@@ -263,6 +307,8 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
actor_id=str(request.user.id),
issue_id=str(pk),
project_id=str(project_id),
external_id__isnull=False,
external_source__isnull=False,
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
@@ -318,6 +364,30 @@ class LabelAPIEndpoint(BaseAPIView):
try:
serializer = LabelSerializer(data=request.data)
if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Label.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
label = Label.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_id=request.data.get("external_id"),
external_source=request.data.get("external_source"),
).first()
return Response(
{
"error": "Label with the same external id and external source already exists",
"id": str(label.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(project_id=project_id)
return Response(
serializer.data, status=status.HTTP_201_CREATED
@@ -326,11 +396,17 @@ class LabelAPIEndpoint(BaseAPIView):
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError:
label = Label.objects.filter(
workspace__slug=slug,
project_id=project_id,
name=request.data.get("name"),
).first()
return Response(
{
"error": "Label with the same name already exists in the project"
"error": "Label with the same name already exists in the project",
"id": str(label.id),
},
status=status.HTTP_400_BAD_REQUEST,
status=status.HTTP_409_CONFLICT,
)
def get(self, request, slug, project_id, pk=None):
@@ -357,6 +433,25 @@ class LabelAPIEndpoint(BaseAPIView):
label = self.get_queryset().get(pk=pk)
serializer = LabelSerializer(label, data=request.data, partial=True)
if serializer.is_valid():
if (
str(request.data.get("external_id"))
and (label.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get(
"external_source", label.external_source
),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Label with the same external id and external source already exists",
"id": str(label.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+41 -1
View File
@@ -132,6 +132,29 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
},
)
if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and Module.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
module = Module.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).first()
return Response(
{
"error": "Module with the same external id and external source already exists",
"id": str(module.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save()
module = Module.objects.get(pk=serializer.data["id"])
serializer = ModuleSerializer(module)
@@ -149,8 +172,25 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
partial=True,
)
if serializer.is_valid():
if (
request.data.get("external_id")
and (module.external_id != request.data.get("external_id"))
and Module.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", module.external_source),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "Module with the same external id and external source already exists",
"id": str(module.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get(self, request, slug, project_id, pk=None):
+41
View File
@@ -38,6 +38,30 @@ class StateAPIEndpoint(BaseAPIView):
data=request.data, context={"project_id": project_id}
)
if serializer.is_valid():
if (
request.data.get("external_id")
and request.data.get("external_source")
and State.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
).exists()
):
state = State.objects.filter(
workspace__slug=slug,
project_id=project_id,
external_id=request.data.get("external_id"),
external_source=request.data.get("external_source"),
).first()
return Response(
{
"error": "State with the same external id and external source already exists",
"id": str(state.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -91,6 +115,23 @@ class StateAPIEndpoint(BaseAPIView):
)
serializer = StateSerializer(state, data=request.data, partial=True)
if serializer.is_valid():
if (
str(request.data.get("external_id"))
and (state.external_id != str(request.data.get("external_id")))
and State.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", state.external_source),
external_id=request.data.get("external_id"),
).exists()
):
return Response(
{
"error": "State with the same external id and external source already exists",
"id": str(state.id),
},
status=status.HTTP_409_CONFLICT,
)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+4 -1
View File
@@ -68,6 +68,7 @@ from .issue import (
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
IssueDetailSerializer,
)
from .module import (
@@ -104,17 +105,19 @@ from .estimate import (
EstimateSerializer,
EstimatePointSerializer,
EstimateReadSerializer,
WorkspaceEstimateSerializer,
)
from .inbox import (
InboxSerializer,
InboxIssueSerializer,
IssueStateInboxSerializer,
InboxIssueLiteSerializer,
)
from .analytic import AnalyticViewSerializer
from .notification import NotificationSerializer
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
from .exporter import ExporterHistorySerializer
+6 -2
View File
@@ -60,6 +60,7 @@ class DynamicBaseSerializer(BaseSerializer):
CycleIssueSerializer,
IssueFlatSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer
)
# Expansion mapper
@@ -80,9 +81,10 @@ class DynamicBaseSerializer(BaseSerializer):
"issue_cycle": CycleIssueSerializer,
"parent": IssueSerializer,
"issue_relation": IssueRelationSerializer,
"issue_inbox" : InboxIssueLiteSerializer,
}
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation"] else False)
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False)
return self.fields
@@ -103,6 +105,7 @@ class DynamicBaseSerializer(BaseSerializer):
LabelSerializer,
CycleIssueSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer
)
# Expansion mapper
@@ -122,7 +125,8 @@ class DynamicBaseSerializer(BaseSerializer):
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
"parent": IssueSerializer,
"issue_relation": IssueRelationSerializer
"issue_relation": IssueRelationSerializer,
"issue_inbox" : InboxIssueLiteSerializer,
}
# Check if field in expansion then expand the field
if expand in expansion:
-1
View File
@@ -33,7 +33,6 @@ class CycleWriteSerializer(BaseSerializer):
class CycleSerializer(BaseSerializer):
owned_by = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True)
@@ -61,3 +61,18 @@ class EstimateReadSerializer(BaseSerializer):
"name",
"description",
]
class WorkspaceEstimateSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True)
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
+39 -27
View File
@@ -259,14 +259,17 @@ class IssuePropertySerializer(BaseSerializer):
class LabelSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = Label
fields = "__all__"
fields = [
"parent",
"name",
"color",
"id",
"project_id",
"workspace_id",
"sort_order",
]
read_only_fields = [
"workspace",
"project",
@@ -295,8 +298,13 @@ class IssueLabelSerializer(BaseSerializer):
class IssueRelationSerializer(BaseSerializer):
id = serializers.UUIDField(source="related_issue.id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField(source="related_issue.project_id", read_only=True)
sequence_id = serializers.IntegerField(source="related_issue.sequence_id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField(
source="related_issue.project_id", read_only=True
)
sequence_id = serializers.IntegerField(
source="related_issue.sequence_id", read_only=True
)
name = serializers.CharField(source="related_issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
class Meta:
@@ -306,6 +314,7 @@ class IssueRelationSerializer(BaseSerializer):
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
@@ -315,8 +324,13 @@ class IssueRelationSerializer(BaseSerializer):
class RelatedIssueSerializer(BaseSerializer):
id = serializers.UUIDField(source="issue.id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True)
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField(
source="issue.project_id", read_only=True
)
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)
class Meta:
@@ -326,6 +340,7 @@ class RelatedIssueSerializer(BaseSerializer):
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
@@ -458,19 +473,6 @@ class IssueReactionSerializer(BaseSerializer):
]
class CommentReactionLiteSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
class Meta:
model = CommentReaction
fields = [
"id",
"reaction",
"comment",
"actor_detail",
]
class CommentReactionSerializer(BaseSerializer):
class Meta:
model = CommentReaction
@@ -501,7 +503,7 @@ class IssueCommentSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
comment_reactions = CommentReactionLiteSerializer(
comment_reactions = CommentReactionSerializer(
read_only=True, many=True
)
is_member = serializers.BooleanField(read_only=True)
@@ -560,7 +562,7 @@ class IssueSerializer(DynamicBaseSerializer):
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.SerializerMethodField()
# Many to many
label_ids = serializers.PrimaryKeyRelatedField(
@@ -584,7 +586,6 @@ class IssueSerializer(DynamicBaseSerializer):
"id",
"name",
"state_id",
"description_html",
"sort_order",
"completed_at",
"estimate_point",
@@ -595,7 +596,7 @@ class IssueSerializer(DynamicBaseSerializer):
"project_id",
"parent_id",
"cycle_id",
"module_id",
"module_ids",
"label_ids",
"assignee_ids",
"sub_issues_count",
@@ -611,6 +612,17 @@ class IssueSerializer(DynamicBaseSerializer):
]
read_only_fields = fields
def get_module_ids(self, obj):
# Access the prefetched modules and extract module IDs
return [module for module in obj.issue_module.values_list("module_id", flat=True)]
class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField()
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + ['description_html']
class IssueLiteSerializer(DynamicBaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
@@ -1,7 +1,7 @@
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from plane.db.models import Notification
from plane.db.models import Notification, UserNotificationPreference
class NotificationSerializer(BaseSerializer):
@@ -12,3 +12,10 @@ class NotificationSerializer(BaseSerializer):
class Meta:
model = Notification
fields = "__all__"
class UserNotificationPreferenceSerializer(BaseSerializer):
class Meta:
model = UserNotificationPreference
fields = "__all__"
+11 -1
View File
@@ -8,7 +8,17 @@ from plane.db.models import State
class StateSerializer(BaseSerializer):
class Meta:
model = State
fields = "__all__"
fields = [
"id",
"project_id",
"workspace_id",
"name",
"color",
"group",
"default",
"description",
"sequence",
]
read_only_fields = [
"workspace",
"project",
-6
View File
@@ -8,16 +8,10 @@ from plane.app.views import (
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
ActiveCycleEndpoint
)
urlpatterns = [
path(
"workspaces/<str:slug>/active-cycles/",
ActiveCycleEndpoint.as_view(),
name="workspace-active-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
CycleViewSet.as_view(
+12 -3
View File
@@ -35,17 +35,26 @@ urlpatterns = [
name="project-modules",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
ModuleIssueViewSet.as_view(
{
"post": "create_issue_modules",
}
),
name="issue-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
ModuleIssueViewSet.as_view(
{
"post": "create_module_issues",
"get": "list",
"post": "create",
}
),
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
ModuleIssueViewSet.as_view(
{
"get": "retrieve",
+6
View File
@@ -5,6 +5,7 @@ from plane.app.views import (
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
UserNotificationPreferenceEndpoint,
)
@@ -63,4 +64,9 @@ urlpatterns = [
),
name="mark-all-read-notifications",
),
path(
"users/me/notification-preferences/",
UserNotificationPreferenceEndpoint.as_view(),
name="user-notification-preferences",
),
]
+12
View File
@@ -20,6 +20,8 @@ from plane.app.views import (
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
)
@@ -207,4 +209,14 @@ urlpatterns = [
WorkspaceUserPropertiesEndpoint.as_view(),
name="workspace-user-filters",
),
path(
"workspaces/<str:slug>/states/",
WorkspaceStatesEndpoint.as_view(),
name="workspace-state",
),
path(
"workspaces/<str:slug>/estimates/",
WorkspaceEstimatesEndpoint.as_view(),
name="workspace-estimate",
),
]
+3 -1
View File
@@ -47,6 +47,8 @@ from .workspace import (
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
)
from .state import StateViewSet
from .view import (
@@ -62,7 +64,6 @@ from .cycle import (
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
ActiveCycleEndpoint,
)
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
from .issue import (
@@ -166,6 +167,7 @@ from .notification import (
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
UserNotificationPreferenceEndpoint,
)
from .exporter import ExportIssuesEndpoint
+6 -6
View File
@@ -401,8 +401,8 @@ class EmailCheckEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN",
medium="MAGIC_LINK",
event_name="Sign up",
medium="Magic link",
first_time=True,
)
key, token, current_attempt = generate_magic_token(email=email)
@@ -438,8 +438,8 @@ class EmailCheckEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN",
medium="MAGIC_LINK",
event_name="Sign in",
medium="Magic link",
first_time=False,
)
@@ -468,8 +468,8 @@ class EmailCheckEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN",
medium="EMAIL",
event_name="Sign in",
medium="Email",
first_time=False,
)
+4 -4
View File
@@ -274,8 +274,8 @@ class SignInEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN",
medium="EMAIL",
event_name="Sign in",
medium="Email",
first_time=False,
)
@@ -349,8 +349,8 @@ class MagicSignInEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN",
medium="MAGIC_LINK",
event_name="Sign in",
medium="Magic link",
first_time=False,
)
+1
View File
@@ -64,6 +64,7 @@ class WebhookMixin:
action=self.request.method,
slug=self.workspace_slug,
bulk=self.bulk,
current_site=request.META.get("HTTP_ORIGIN"),
)
return response
+243 -253
View File
@@ -20,6 +20,7 @@ from django.core import serializers
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework.response import Response
@@ -39,7 +40,6 @@ from plane.app.serializers import (
from plane.app.permissions import (
ProjectEntityPermission,
ProjectLitePermission,
WorkspaceUserPermission
)
from plane.db.models import (
User,
@@ -243,13 +243,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
.values("display_name", "assignee_id", "avatar")
.annotate(
total_issues=Count(
"assignee_id",
"id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
completed_issues=Count(
"assignee_id",
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -259,7 +259,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
pending_issues=Count(
"assignee_id",
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -282,13 +282,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"label_id",
"id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate(
completed_issues=Count(
"label_id",
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -298,7 +298,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
pending_issues=Count(
"label_id",
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -313,6 +313,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"labels": label_distribution,
"completion_chart": {},
}
if data[0]["start_date"] and data[0]["end_date"]:
data[0]["distribution"][
"completion_chart"
@@ -420,13 +421,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
total_issues=Count(
"assignee_id",
"id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
completed_issues=Count(
"assignee_id",
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -436,7 +437,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
pending_issues=Count(
"assignee_id",
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -460,13 +461,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"label_id",
"id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
completed_issues=Count(
"label_id",
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -476,7 +477,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
pending_issues=Count(
"label_id",
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -531,6 +532,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
# Delete the cycle
cycle.delete()
@@ -598,16 +601,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.order_by(order_by)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -722,6 +720,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
# Return all Cycle Issues
@@ -754,6 +754,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
cycle_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -840,10 +842,230 @@ class TransferCycleIssueEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
new_cycle = Cycle.objects.get(
new_cycle = Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
).first()
old_cycle = (
Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, pk=cycle_id
)
.annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
total_estimates=Sum("issue_cycle__issue__estimate_point")
)
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
)
# Pass the new_cycle queryset to burndown_plot
completion_chart = burndown_plot(
queryset=old_cycle.first(),
slug=slug,
project_id=project_id,
cycle_id=cycle_id,
)
assignee_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
)
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("display_name", "assignee_id", "avatar")
.annotate(
total_issues=Count(
"id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
completed_issues=Count(
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
)
label_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
)
.annotate(label_name=F("labels__name"))
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate(
completed_issues=Count(
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
)
assignee_distribution_data = [
{
"display_name": item["display_name"],
"assignee_id": str(item["assignee_id"]) if item["assignee_id"] else None,
"avatar": item["avatar"],
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
}
for item in assignee_distribution
]
label_distribution_data = [
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": str(item["label_id"]) if item["label_id"] else None,
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
}
for item in label_distribution
]
current_cycle = Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, pk=cycle_id
).first()
current_cycle.progress_snapshot = {
"total_issues": old_cycle.first().total_issues,
"completed_issues": old_cycle.first().completed_issues,
"cancelled_issues": old_cycle.first().cancelled_issues,
"started_issues": old_cycle.first().started_issues,
"unstarted_issues": old_cycle.first().unstarted_issues,
"backlog_issues": old_cycle.first().backlog_issues,
"total_estimates": old_cycle.first().total_estimates,
"completed_estimates": old_cycle.first().completed_estimates,
"started_estimates": old_cycle.first().started_estimates,
"distribution":{
"labels": label_distribution_data,
"assignees": assignee_distribution_data,
"completion_chart": completion_chart,
},
}
current_cycle.save(update_fields=["progress_snapshot"])
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now().date()
@@ -910,235 +1132,3 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
)
serializer = CycleUserPropertiesSerializer(cycle_properties)
return Response(serializer.data, status=status.HTTP_200_OK)
class ActiveCycleEndpoint(BaseAPIView):
permission_classes = [
WorkspaceUserPermission,
]
def get(self, request, slug):
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
active_cycles = (
Cycle.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=self.request.user,
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
.annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
status=Case(
When(
Q(start_date__lte=timezone.now())
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"),
),
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
then=Value("DRAFT"),
),
default=Value("DRAFT"),
output_field=CharField(),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__labels",
queryset=Label.objects.only("name", "color", "id").distinct(),
)
)
.order_by("-created_at")
)
cycles = CycleSerializer(active_cycles, many=True).data
for cycle in cycles:
assignee_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=cycle["id"],
project_id=cycle["project"],
workspace__slug=slug,
)
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("display_name", "assignee_id", "avatar")
.annotate(
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
completed_issues=Count(
"assignee_id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"assignee_id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
)
label_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=cycle["id"],
project_id=cycle["project"],
workspace__slug=slug,
)
.annotate(label_name=F("labels__name"))
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate(
completed_issues=Count(
"label_id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"label_id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
)
cycle["distribution"] = {
"assignees": assignee_distribution,
"labels": label_distribution,
"completion_chart": {},
}
if cycle["start_date"] and cycle["end_date"]:
cycle["distribution"][
"completion_chart"
] = burndown_plot(
queryset=active_cycles.get(pk=cycle["id"]),
slug=slug,
project_id=cycle["project"],
cycle_id=cycle["id"],
)
return Response(cycles, status=status.HTTP_200_OK)
+36 -4
View File
@@ -100,7 +100,7 @@ def dashboard_assigned_issues(self, request, slug):
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_relation",
@@ -110,7 +110,6 @@ def dashboard_assigned_issues(self, request, slug):
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -146,6 +145,23 @@ def dashboard_assigned_issues(self, request, slug):
)
).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"]
@@ -221,9 +237,8 @@ def dashboard_created_issues(self, request, slug):
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -259,6 +274,23 @@ def dashboard_created_issues(self, request, slug):
)
).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"]
+37 -46
View File
@@ -88,39 +88,23 @@ class InboxIssueViewSet(BaseViewSet):
]
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(
Q(snoozed_till__gte=timezone.now())
| Q(snoozed_till__isnull=True),
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
inbox_id=self.kwargs.get("inbox_id"),
)
.select_related("issue", "workspace", "project")
)
def list(self, request, slug, project_id, inbox_id):
filters = issue_filters(request.query_params, "GET")
issues = (
return (
Issue.objects.filter(
issue_inbox__inbox_id=inbox_id,
workspace__slug=slug,
project_id=project_id,
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
issue_inbox__inbox_id=self.kwargs.get("inbox_id")
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_inbox",
queryset=InboxIssue.objects.only(
"status", "duplicate_to", "snoozed_till", "source"
),
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -135,16 +119,20 @@ class InboxIssueViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.prefetch_related(
Prefetch(
"issue_inbox",
queryset=InboxIssue.objects.only(
"status", "duplicate_to", "snoozed_till", "source"
),
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
issues_data = IssueStateInboxSerializer(issues, many=True).data
).distinct()
def list(self, request, slug, project_id, inbox_id):
filters = issue_filters(request.query_params, "GET")
issue_queryset = self.get_queryset().filter(**filters).order_by("issue_inbox__snoozed_till", "issue_inbox__status")
issues_data = IssueSerializer(issue_queryset, expand=self.expand, many=True).data
return Response(
issues_data,
status=status.HTTP_200_OK,
@@ -200,6 +188,8 @@ class InboxIssueViewSet(BaseViewSet):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
# create an inbox issue
InboxIssue.objects.create(
@@ -209,7 +199,8 @@ class InboxIssueViewSet(BaseViewSet):
source=request.data.get("source", "in-app"),
)
serializer = IssueStateInboxSerializer(issue)
issue = (self.get_queryset().filter(pk=issue.id).first())
serializer = IssueSerializer(issue ,expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, inbox_id, issue_id):
@@ -277,6 +268,8 @@ class InboxIssueViewSet(BaseViewSet):
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
issue_serializer.save()
else:
@@ -327,22 +320,20 @@ class InboxIssueViewSet(BaseViewSet):
if state is not None:
issue.state = state
issue.save()
issue = (self.get_queryset().filter(pk=issue_id).first())
serializer = IssueSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
return Response(
InboxIssueSerializer(inbox_issue).data,
status=status.HTTP_200_OK,
)
issue = (self.get_queryset().filter(pk=issue_id).first())
serializer = IssueSerializer(issue ,expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
issue = Issue.objects.get(
pk=issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueStateInboxSerializer(issue)
issue = self.get_queryset().filter(pk=issue_id).first()
serializer = IssueSerializer(issue, expand=self.expand,)
return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, issue_id):
+135 -73
View File
@@ -48,10 +48,9 @@ from plane.app.serializers import (
ProjectMemberLiteSerializer,
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
IssueDetailSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
@@ -83,6 +82,7 @@ from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
from collections import defaultdict
class IssueViewSet(WebhookMixin, BaseViewSet):
def get_serializer_class(self):
return (
@@ -113,12 +113,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
project_id=self.kwargs.get("project_id")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_reactions",
@@ -126,7 +122,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -260,6 +255,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
issue = (
self.get_queryset().filter(pk=serializer.data["id"]).first()
@@ -271,7 +268,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
def retrieve(self, request, slug, project_id, pk=None):
issue = self.get_queryset().filter(pk=pk).first()
return Response(
IssueSerializer(
IssueDetailSerializer(
issue, fields=self.fields, expand=self.expand
).data,
status=status.HTTP_200_OK,
@@ -298,6 +295,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
issue = self.get_queryset().filter(pk=pk).first()
return Response(
@@ -321,6 +320,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -492,17 +493,27 @@ class IssueActivityEndpoint(BaseAPIView):
@method_decorator(gzip_page)
def get(self, request, slug, project_id, issue_id):
filters = {}
if request.GET.get("created_at__gt", None) is not None:
filters = {"created_at__gt": request.GET.get("created_at__gt")}
issue_activities = (
IssueActivity.objects.filter(issue_id=issue_id)
.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
)
.filter(**filters)
.select_related("actor", "workspace", "issue", "project")
).order_by("created_at")
issue_comments = (
IssueComment.objects.filter(issue_id=issue_id)
.filter(project__project_projectmember__member=self.request.user)
.filter(
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
)
.filter(**filters)
.order_by("created_at")
.select_related("actor", "issue", "project", "workspace")
.prefetch_related(
@@ -517,6 +528,12 @@ class IssueActivityEndpoint(BaseAPIView):
).data
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
if request.GET.get("activity_type", None) == "issue-property":
return Response(issue_activities, status=status.HTTP_200_OK)
if request.GET.get("activity_type", None) == "issue-comment":
return Response(issue_comments, status=status.HTTP_200_OK)
result_list = sorted(
chain(issue_activities, issue_comments),
key=lambda instance: instance["created_at"],
@@ -580,6 +597,8 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet):
project_id=str(self.kwargs.get("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)
@@ -609,6 +628,8 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -633,6 +654,8 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -821,7 +844,9 @@ class SubIssuesEndpoint(BaseAPIView):
_ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate(state_group=F("state__group"))
updated_sub_issues = Issue.issue_objects.filter(
id__in=sub_issue_ids
).annotate(state_group=F("state__group"))
# Track the issue
_ = [
@@ -833,10 +858,12 @@ class SubIssuesEndpoint(BaseAPIView):
project_id=str(project_id),
current_instance=json.dumps({"parent": str(sub_issue_id)}),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
for sub_issue_id in sub_issue_ids
]
# create's a dict with state group name with their respective issue id's
result = defaultdict(list)
for sub_issue in updated_sub_issues:
@@ -853,7 +880,6 @@ class SubIssuesEndpoint(BaseAPIView):
},
status=status.HTTP_200_OK,
)
class IssueLinkViewSet(BaseViewSet):
@@ -893,6 +919,8 @@ class IssueLinkViewSet(BaseViewSet):
project_id=str(self.kwargs.get("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)
@@ -922,6 +950,8 @@ class IssueLinkViewSet(BaseViewSet):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -945,6 +975,8 @@ class IssueLinkViewSet(BaseViewSet):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
issue_link.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1001,6 +1033,8 @@ class IssueAttachmentEndpoint(BaseAPIView):
cls=DjangoJSONEncoder,
),
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)
@@ -1017,6 +1051,8 @@ class IssueAttachmentEndpoint(BaseAPIView):
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1047,12 +1083,31 @@ class IssueArchiveViewSet(BaseViewSet):
.filter(archived_at__isnull=False)
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
@method_decorator(gzip_page)
@@ -1080,22 +1135,6 @@ class IssueArchiveViewSet(BaseViewSet):
issue_queryset = (
self.get_queryset()
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
# Priority Ordering
@@ -1195,6 +1234,8 @@ class IssueArchiveViewSet(BaseViewSet):
IssueSerializer(issue).data, cls=DjangoJSONEncoder
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
issue.archived_at = None
issue.save()
@@ -1340,6 +1381,8 @@ class IssueReactionViewSet(BaseViewSet):
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)
@@ -1365,6 +1408,8 @@ class IssueReactionViewSet(BaseViewSet):
}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
issue_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1405,6 +1450,8 @@ class CommentReactionViewSet(BaseViewSet):
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)
@@ -1431,6 +1478,8 @@ class CommentReactionViewSet(BaseViewSet):
}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
comment_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1559,6 +1608,8 @@ class IssueRelationViewSet(BaseViewSet):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
if relation_type == "blocking":
@@ -1603,6 +1654,8 @@ class IssueRelationViewSet(BaseViewSet):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -1616,7 +1669,35 @@ class IssueDraftViewSet(BaseViewSet):
def get_queryset(self):
return (
Issue.objects.annotate(
Issue.objects.filter(
project_id=self.kwargs.get("project_id")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(is_draft=True)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
@@ -1624,22 +1705,7 @@ class IssueDraftViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(is_draft=True)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
)
).distinct()
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
@@ -1665,22 +1731,6 @@ class IssueDraftViewSet(BaseViewSet):
issue_queryset = (
self.get_queryset()
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
# Priority Ordering
@@ -1774,8 +1824,13 @@ class IssueDraftViewSet(BaseViewSet):
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)
issue = (
self.get_queryset().filter(pk=serializer.data["id"]).first()
)
return Response(IssueSerializer(issue).data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def partial_update(self, request, slug, project_id, pk):
@@ -1804,15 +1859,20 @@ class IssueDraftViewSet(BaseViewSet):
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True
issue = self.get_queryset().filter(pk=pk).first()
return Response(
IssueSerializer(
issue, fields=self.fields, expand=self.expand
).data,
status=status.HTTP_200_OK,
)
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
@@ -1830,5 +1890,7 @@ class IssueDraftViewSet(BaseViewSet):
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
+126 -157
View File
@@ -7,6 +7,8 @@ from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
from django.core import serializers
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework.response import Response
@@ -195,7 +197,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
total_issues=Count(
"assignee_id",
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
@@ -204,7 +206,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
completed_issues=Count(
"assignee_id",
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -214,7 +216,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
pending_issues=Count(
"assignee_id",
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -237,7 +239,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"label_id",
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
@@ -246,7 +248,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
completed_issues=Count(
"label_id",
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -256,7 +258,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
pending_issues=Count(
"label_id",
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -296,21 +298,20 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"issue", flat=True
)
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(pk),
"module_name": str(module.name),
"issues": [str(issue_id) for issue_id in module_issues],
}
),
actor_id=str(request.user.id),
issue_id=str(pk),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
_ = [
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps({"module_id": str(pk)}),
actor_id=str(request.user.id),
issue_id=str(issue),
project_id=project_id,
current_instance=json.dumps({"module_name": str(module.name)}),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
for issue in module_issues
]
module.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -330,62 +331,18 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
ProjectEntityPermission,
]
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(module_id=self.kwargs.get("module_id"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.select_related("module")
.select_related("issue", "issue__state", "issue__project")
.prefetch_related("issue__assignees", "issue__labels")
.prefetch_related("module__members")
.distinct()
)
@method_decorator(gzip_page)
def list(self, request, slug, project_id, module_id):
fields = [
field
for field in request.GET.get("fields", "").split(",")
if field
]
order_by = request.GET.get("order_by", "created_at")
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.issue_objects.filter(issue_module__module_id=module_id)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
def get_queryset(self):
return (
Issue.issue_objects.filter(
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
issue_module__module_id=self.kwargs.get("module_id")
)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.order_by(order_by)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("labels", "assignees")
.prefetch_related('issue_module__module')
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -401,103 +358,118 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
).distinct()
@method_decorator(gzip_page)
def list(self, request, slug, project_id, module_id):
fields = [
field
for field in request.GET.get("fields", "").split(",")
if field
]
filters = issue_filters(request.query_params, "GET")
issue_queryset = self.get_queryset().filter(**filters)
serializer = IssueSerializer(
issues, many=True, fields=fields if fields else None
issue_queryset, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, module_id):
# create multiple issues inside a module
def create_module_issues(self, request, slug, project_id, module_id):
issues = request.data.get("issues", [])
if not len(issues):
return Response(
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
)
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=module_id
)
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
update_module_issue_activity = []
records_to_update = []
record_to_create = []
for issue in issues:
module_issue = [
module_issue
for module_issue in module_issues
if str(module_issue.issue_id) in issues
]
if len(module_issue):
if module_issue[0].module_id != module_id:
update_module_issue_activity.append(
{
"old_module_id": str(module_issue[0].module_id),
"new_module_id": str(module_id),
"issue_id": str(module_issue[0].issue_id),
}
)
module_issue[0].module_id = module_id
records_to_update.append(module_issue[0])
else:
record_to_create.append(
ModuleIssue(
module=module,
issue_id=issue,
project_id=project_id,
workspace=module.workspace,
created_by=request.user,
updated_by=request.user,
)
project = Project.objects.get(pk=project_id)
_ = ModuleIssue.objects.bulk_create(
[
ModuleIssue(
issue_id=str(issue),
module_id=module_id,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
ModuleIssue.objects.bulk_create(
record_to_create,
for issue in issues
],
batch_size=10,
ignore_conflicts=True,
)
# Bulk Update the activity
_ = [
issue_activity.delay(
type="module.activity.created",
requested_data=json.dumps({"module_id": str(module_id)}),
actor_id=str(request.user.id),
issue_id=str(issue),
project_id=project_id,
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
for issue in issues
]
issues = (self.get_queryset().filter(pk__in=issues))
serializer = IssueSerializer(issues , many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
ModuleIssue.objects.bulk_update(
records_to_update,
["module"],
# create multiple module inside an issue
def create_issue_modules(self, request, slug, project_id, issue_id):
modules = request.data.get("modules", [])
if not len(modules):
return Response(
{"error": "Modules are required"},
status=status.HTTP_400_BAD_REQUEST,
)
project = Project.objects.get(pk=project_id)
_ = ModuleIssue.objects.bulk_create(
[
ModuleIssue(
issue_id=issue_id,
module_id=module,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for module in modules
],
batch_size=10,
ignore_conflicts=True,
)
# Bulk Update the activity
_ = [
issue_activity.delay(
type="module.activity.created",
requested_data=json.dumps({"module_id": module}),
actor_id=str(request.user.id),
issue_id=issue_id,
project_id=project_id,
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
for module in modules
]
# Capture Issue Activity
issue_activity.delay(
type="module.activity.created",
requested_data=json.dumps({"modules_list": issues}),
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_module_issues": update_module_issue_activity,
"created_module_issues": serializers.serialize(
"json", record_to_create
),
}
),
epoch=int(timezone.now().timestamp()),
)
issue = (self.get_queryset().filter(pk=issue_id).first())
serializer = IssueSerializer(issue)
return Response(serializer.data, status=status.HTTP_201_CREATED)
issues = self.get_queryset().values_list("issue_id", flat=True)
return Response(
IssueSerializer(
Issue.objects.filter(pk__in=issues), many=True
).data,
status=status.HTTP_200_OK,
)
def destroy(self, request, slug, project_id, module_id, issue_id):
module_issue = ModuleIssue.objects.get(
@@ -508,17 +480,14 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(module_id),
"issues": [str(issue_id)],
}
),
requested_data=json.dumps({"module_id": str(module_id)}),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
current_instance=json.dumps({"module_name": module_issue.module.name}),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
module_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+53 -6
View File
@@ -1,5 +1,5 @@
# Django imports
from django.db.models import Q
from django.db.models import Q, OuterRef, Exists
from django.utils import timezone
# Third party imports
@@ -15,8 +15,9 @@ from plane.db.models import (
IssueSubscriber,
Issue,
WorkspaceMember,
UserNotificationPreference,
)
from plane.app.serializers import NotificationSerializer
from plane.app.serializers import NotificationSerializer, UserNotificationPreferenceSerializer
class NotificationViewSet(BaseViewSet, BasePaginator):
@@ -71,11 +72,29 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
# Subscribed issues
if type == "watching":
issue_ids = IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True)
issue_ids = (
IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
)
.annotate(
created=Exists(
Issue.objects.filter(
created_by=request.user, pk=OuterRef("issue_id")
)
)
)
.annotate(
assigned=Exists(
IssueAssignee.objects.filter(
pk=OuterRef("issue_id"), assignee=request.user
)
)
)
.filter(created=False, assigned=False)
.values_list("issue_id", flat=True)
)
notifications = notifications.filter(
entity_identifier__in=issue_ids
entity_identifier__in=issue_ids,
)
# Assigned Issues
@@ -295,3 +314,31 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
updated_notifications, ["read_at"], batch_size=100
)
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
class UserNotificationPreferenceEndpoint(BaseAPIView):
model = UserNotificationPreference
serializer_class = UserNotificationPreferenceSerializer
# request the object
def get(self, request):
user_notification_preference = UserNotificationPreference.objects.get(
user=request.user
)
serializer = UserNotificationPreferenceSerializer(
user_notification_preference
)
return Response(serializer.data, status=status.HTTP_200_OK)
# update the object
def patch(self, request):
user_notification_preference = UserNotificationPreference.objects.get(
user=request.user
)
serializer = UserNotificationPreferenceSerializer(
user_notification_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)
+2 -2
View File
@@ -296,7 +296,7 @@ class OauthEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN",
event_name="Sign in",
medium=medium.upper(),
first_time=False,
)
@@ -427,7 +427,7 @@ class OauthEndpoint(BaseAPIView):
email=email,
user_agent=request.META.get("HTTP_USER_AGENT"),
ip=request.META.get("REMOTE_ADDR"),
event_name="SIGN_IN",
event_name="Sign up",
medium=medium.upper(),
first_time=True,
)
+8 -8
View File
@@ -158,18 +158,18 @@ class PageViewSet(BaseViewSet):
pk=page_id, workspace__slug=slug, project_id=project_id
)
# only the owner and admin can archive the page
# only the owner or admin can archive the page
if (
ProjectMember.objects.filter(
project_id=project_id,
member=request.user,
is_active=True,
role__gte=20,
role__lte=15,
).exists()
or request.user.id != page.owned_by_id
and request.user.id != page.owned_by_id
):
return Response(
{"error": "Only the owner and admin can archive the page"},
{"error": "Only the owner or admin can archive the page"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -182,18 +182,18 @@ class PageViewSet(BaseViewSet):
pk=page_id, workspace__slug=slug, project_id=project_id
)
# only the owner and admin can un archive the page
# only the owner or admin can un archive the page
if (
ProjectMember.objects.filter(
project_id=project_id,
member=request.user,
is_active=True,
role__gt=20,
role__lte=15,
).exists()
or request.user.id != page.owned_by_id
and request.user.id != page.owned_by_id
):
return Response(
{"error": "Only the owner and admin can un archive the page"},
{"error": "Only the owner or admin can un archive the page"},
status=status.HTTP_400_BAD_REQUEST,
)
+16 -27
View File
@@ -68,7 +68,7 @@ from plane.bgtasks.project_invitation_task import project_invitation
class ProjectViewSet(WebhookMixin, BaseViewSet):
serializer_class = ProjectSerializer
serializer_class = ProjectListSerializer
model = Project
webhook_event = "project"
@@ -76,11 +76,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
ProjectBasePermission,
]
def get_serializer_class(self, *args, **kwargs):
if self.action in ["update", "partial_update"]:
return ProjectSerializer
return ProjectDetailSerializer
def get_queryset(self):
return self.filter_queryset(
super()
@@ -690,6 +685,19 @@ class ProjectMemberViewSet(BaseViewSet):
.order_by("sort_order")
)
bulk_project_members = []
member_roles = {member.get("member_id"): member.get("role") for member in members}
# Update roles in the members array based on the member_roles dictionary
for project_member in ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]):
project_member.role = member_roles[str(project_member.member_id)]
project_member.is_active = True
bulk_project_members.append(project_member)
# Update the roles of the existing members
ProjectMember.objects.bulk_update(
bulk_project_members, ["is_active", "role"], batch_size=100
)
for member in members:
sort_order = [
project_member.get("sort_order")
@@ -716,25 +724,6 @@ class ProjectMemberViewSet(BaseViewSet):
)
)
# Check if the user is already a member of the project and is inactive
if ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project_id,
member_id=member.get("member_id"),
is_active=False,
).exists():
member_detail = ProjectMember.objects.get(
workspace__slug=slug,
project_id=project_id,
member_id=member.get("member_id"),
is_active=False,
)
# Check if the user has not deactivated the account
user = User.objects.filter(pk=member.get("member_id")).first()
if user.is_active:
member_detail.is_active = True
member_detail.save(update_fields=["is_active"])
project_members = ProjectMember.objects.bulk_create(
bulk_project_members,
batch_size=10,
@@ -745,8 +734,8 @@ class ProjectMemberViewSet(BaseViewSet):
bulk_issue_props, batch_size=10, ignore_conflicts=True
)
serializer = ProjectMemberSerializer(project_members, many=True)
project_members = ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members])
serializer = ProjectMemberRoleSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, slug, project_id):
+4 -9
View File
@@ -228,7 +228,7 @@ class IssueSearchEndpoint(BaseAPIView):
parent = request.query_params.get("parent", "false")
issue_relation = request.query_params.get("issue_relation", "false")
cycle = request.query_params.get("cycle", "false")
module = request.query_params.get("module", "false")
module = request.query_params.get("module", False)
sub_issue = request.query_params.get("sub_issue", "false")
issue_id = request.query_params.get("issue_id", False)
@@ -247,12 +247,7 @@ class IssueSearchEndpoint(BaseAPIView):
if parent == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
).exclude(
pk__in=Issue.issue_objects.filter(
parent__isnull=False
).values_list("parent_id", flat=True)
)
~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id))
if issue_relation == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
@@ -269,8 +264,8 @@ class IssueSearchEndpoint(BaseAPIView):
if cycle == "true":
issues = issues.exclude(issue_cycle__isnull=False)
if module == "true":
issues = issues.exclude(issue_module__isnull=False)
if module:
issues = issues.exclude(issue_module__module=module)
return Response(
issues.values(
+5 -5
View File
@@ -9,9 +9,12 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from . import BaseViewSet
from . import BaseViewSet, BaseAPIView
from plane.app.serializers import StateSerializer
from plane.app.permissions import ProjectEntityPermission
from plane.app.permissions import (
ProjectEntityPermission,
WorkspaceEntityPermission,
)
from plane.db.models import State, Issue
@@ -22,9 +25,6 @@ class StateViewSet(BaseViewSet):
ProjectEntityPermission,
]
def perform_create(self, serializer):
serializer.save(project_id=self.kwargs.get("project_id"))
def get_queryset(self):
return self.filter_queryset(
super()
+2 -14
View File
@@ -87,12 +87,8 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.values("count")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_reactions",
@@ -127,7 +123,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.filter(**filters)
.filter(project__project_projectmember__member=self.request.user)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -150,13 +145,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
)
# Priority Ordering
+49 -6
View File
@@ -46,10 +46,14 @@ from plane.app.serializers import (
WorkspaceMemberMeSerializer,
ProjectMemberRoleSerializer,
WorkspaceUserPropertiesSerializer,
WorkspaceEstimateSerializer,
StateSerializer,
LabelSerializer,
)
from plane.app.views.base import BaseAPIView
from . import BaseViewSet
from plane.db.models import (
State,
User,
Workspace,
WorkspaceMemberInvite,
@@ -67,6 +71,8 @@ from plane.db.models import (
CycleIssue,
IssueReaction,
WorkspaceUserProperties,
Estimate,
EstimatePoint,
)
from plane.app.permissions import (
WorkSpaceBasePermission,
@@ -1340,9 +1346,8 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -1358,7 +1363,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -1441,10 +1448,46 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
labels = Label.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
).values(
"parent", "name", "color", "id", "project_id", "workspace__slug"
)
return Response(labels, status=status.HTTP_200_OK)
serializer = LabelSerializer(labels, many=True).data
return Response(serializer, status=status.HTTP_200_OK)
class WorkspaceStatesEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
states = State.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
)
serializer = StateSerializer(states, many=True).data
return Response(serializer, status=status.HTTP_200_OK)
class WorkspaceEstimatesEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
estimate_ids = Project.objects.filter(
workspace__slug=slug, estimate__isnull=False
).values_list("estimate_id", flat=True)
estimates = Estimate.objects.filter(
pk__in=estimate_ids
).prefetch_related(
Prefetch(
"points",
queryset=EstimatePoint.objects.select_related(
"estimate", "workspace", "project"
),
)
)
serializer = WorkspaceEstimateSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class WorkspaceUserPropertiesEndpoint(BaseAPIView):
@@ -0,0 +1,276 @@
from datetime import datetime
from bs4 import BeautifulSoup
# Third party imports
from celery import shared_task
# Django imports
from django.utils import timezone
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Module imports
from plane.db.models import EmailNotificationLog, User, Issue
from plane.license.utils.instance_value import get_email_configuration
from plane.settings.redis import redis_instance
@shared_task
def stack_email_notification():
# get all email notifications
email_notifications = (
EmailNotificationLog.objects.filter(processed_at__isnull=True)
.order_by("receiver")
.values()
)
# Create the below format for each of the issues
# {"issue_id" : { "actor_id1": [ { data }, { data } ], "actor_id2": [ { data }, { data } ] }}
# Convert to unique receivers list
receivers = list(
set(
[
str(notification.get("receiver_id"))
for notification in email_notifications
]
)
)
processed_notifications = []
# Loop through all the issues to create the emails
for receiver_id in receivers:
# Notification triggered for the receiver
receiver_notifications = [
notification
for notification in email_notifications
if str(notification.get("receiver_id")) == receiver_id
]
# create payload for all issues
payload = {}
email_notification_ids = []
for receiver_notification in receiver_notifications:
payload.setdefault(
receiver_notification.get("entity_identifier"), {}
).setdefault(
str(receiver_notification.get("triggered_by_id")), []
).append(
receiver_notification.get("data")
)
# append processed notifications
processed_notifications.append(receiver_notification.get("id"))
email_notification_ids.append(receiver_notification.get("id"))
# Create emails for all the issues
for issue_id, notification_data in payload.items():
send_email_notification.delay(
issue_id=issue_id,
notification_data=notification_data,
receiver_id=receiver_id,
email_notification_ids=email_notification_ids,
)
# Update the email notification log
EmailNotificationLog.objects.filter(pk__in=processed_notifications).update(
processed_at=timezone.now()
)
def create_payload(notification_data):
# return format {"actor_id": { "key": { "old_value": [], "new_value": [] } }}
data = {}
for actor_id, changes in notification_data.items():
for change in changes:
issue_activity = change.get("issue_activity")
if issue_activity: # Ensure issue_activity is not None
field = issue_activity.get("field")
old_value = str(issue_activity.get("old_value"))
new_value = str(issue_activity.get("new_value"))
# Append old_value if it's not empty and not already in the list
if old_value:
data.setdefault(actor_id, {}).setdefault(
field, {}
).setdefault("old_value", []).append(
old_value
) if old_value not in data.setdefault(
actor_id, {}
).setdefault(
field, {}
).get(
"old_value", []
) else None
# Append new_value if it's not empty and not already in the list
if new_value:
data.setdefault(actor_id, {}).setdefault(
field, {}
).setdefault("new_value", []).append(
new_value
) if new_value not in data.setdefault(
actor_id, {}
).setdefault(
field, {}
).get(
"new_value", []
) else None
if not data.get("actor_id", {}).get("activity_time", False):
data[actor_id]["activity_time"] = str(
datetime.fromisoformat(
issue_activity.get("activity_time").rstrip("Z")
).strftime("%Y-%m-%d %H:%M:%S")
)
return data
def process_mention(mention_component):
soup = BeautifulSoup(mention_component, 'html.parser')
mentions = soup.find_all('mention-component')
for mention in mentions:
user_id = mention['id']
user = User.objects.get(pk=user_id)
user_name = user.display_name
highlighted_name = f"@{user_name}"
mention.replace_with(highlighted_name)
return str(soup)
def process_html_content(content):
processed_content_list = []
for html_content in content:
processed_content = process_mention(html_content)
processed_content_list.append(processed_content)
return processed_content_list
@shared_task
def send_email_notification(
issue_id, notification_data, receiver_id, email_notification_ids
):
try:
ri = redis_instance()
base_api = (ri.get(str(issue_id)).decode())
data = create_payload(notification_data=notification_data)
# Get email configurations
(
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_FROM,
) = get_email_configuration()
receiver = User.objects.get(pk=receiver_id)
issue = Issue.objects.get(pk=issue_id)
template_data = []
total_changes = 0
comments = []
actors_involved = []
for actor_id, changes in data.items():
actor = User.objects.get(pk=actor_id)
total_changes = total_changes + len(changes)
comment = changes.pop("comment", False)
mention = changes.pop("mention", False)
actors_involved.append(actor_id)
if comment:
comments.append(
{
"actor_comments": comment,
"actor_detail": {
"avatar_url": actor.avatar,
"first_name": actor.first_name,
"last_name": actor.last_name,
},
}
)
if mention:
mention["new_value"] = process_html_content(mention.get("new_value"))
mention["old_value"] = process_html_content(mention.get("old_value"))
comments.append(
{
"actor_comments": mention,
"actor_detail": {
"avatar_url": actor.avatar,
"first_name": actor.first_name,
"last_name": actor.last_name,
},
}
)
activity_time = changes.pop("activity_time")
# Parse the input string into a datetime object
formatted_time = datetime.strptime(activity_time, "%Y-%m-%d %H:%M:%S").strftime("%H:%M %p")
if changes:
template_data.append(
{
"actor_detail": {
"avatar_url": actor.avatar,
"first_name": actor.first_name,
"last_name": actor.last_name,
},
"changes": changes,
"issue_details": {
"name": issue.name,
"identifier": f"{issue.project.identifier}-{issue.sequence_id}",
},
"activity_time": str(formatted_time),
}
)
summary = "Updates were made to the issue by"
# Send the mail
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
context = {
"data": template_data,
"summary": summary,
"actors_involved": len(set(actors_involved)),
"issue": {
"issue_identifier": f"{str(issue.project.identifier)}-{str(issue.sequence_id)}",
"name": issue.name,
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
},
"receiver": {
"email": receiver.email,
},
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
"project_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/",
"workspace":str(issue.project.workspace.slug),
"project": str(issue.project.name),
"user_preference": f"{base_api}/profile/preferences/email",
"comments": comments,
}
html_content = render_to_string(
"emails/notifications/issue-updates.html", context
)
text_content = strip_tags(html_content)
try:
connection = get_connection(
host=EMAIL_HOST,
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
)
msg = EmailMultiAlternatives(
subject=subject,
body=text_content,
from_email=EMAIL_FROM,
to=[receiver.email],
connection=connection,
)
msg.attach_alternative(html_content, "text/html")
msg.send()
EmailNotificationLog.objects.filter(
pk__in=email_notification_ids
).update(sent_at=timezone.now())
return
except Exception as e:
print(e)
return
except Issue.DoesNotExist:
return
+1 -1
View File
@@ -97,7 +97,7 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug):
)
# Create the new url with updated domain and protocol
presigned_url = presigned_url.replace(
"http://plane-minio:9000/uploads/",
f"{settings.AWS_S3_ENDPOINT_URL}/{settings.AWS_STORAGE_BUCKET_NAME}/",
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/",
)
else:
@@ -21,7 +21,7 @@ from plane.license.utils.instance_value import get_email_configuration
def forgot_password(first_name, email, uidb64, token, current_site):
try:
relative_link = (
f"/accounts/password/?uidb64={uidb64}&token={token}&email={email}"
f"/accounts/reset-password/?uidb64={uidb64}&token={token}&email={email}"
)
abs_url = str(current_site) + relative_link
+16 -1
View File
@@ -24,6 +24,7 @@ from plane.db.models import (
Label,
User,
IssueProperty,
UserNotificationPreference,
)
@@ -50,10 +51,24 @@ def service_importer(service, importer_id):
for user in users
if user.get("import", False) == "invite"
],
batch_size=10,
batch_size=100,
ignore_conflicts=True,
)
_ = UserNotificationPreference.objects.bulk_create(
[UserNotificationPreference(user=user) for user in new_users],
batch_size=100,
)
_ = [
send_welcome_slack.delay(
str(user.id),
True,
f"{user.email} was imported to Plane from {service}",
)
for user in new_users
]
workspace_users = User.objects.filter(
email__in=[
user.get("email").strip().lower()
+117 -121
View File
@@ -24,9 +24,11 @@ from plane.db.models import (
IssueReaction,
CommentReaction,
IssueComment,
IssueSubscriber,
)
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
from plane.settings.redis import redis_instance
# Track Changes in name
@@ -111,15 +113,15 @@ def track_parent(
issue_activities,
epoch,
):
if current_instance.get("parent") != requested_data.get("parent"):
if current_instance.get("parent_id") != requested_data.get("parent_id"):
old_parent = (
Issue.objects.filter(pk=current_instance.get("parent")).first()
if current_instance.get("parent") is not None
Issue.objects.filter(pk=current_instance.get("parent_id")).first()
if current_instance.get("parent_id") is not None
else None
)
new_parent = (
Issue.objects.filter(pk=requested_data.get("parent")).first()
if requested_data.get("parent") is not None
Issue.objects.filter(pk=requested_data.get("parent_id")).first()
if requested_data.get("parent_id") is not None
else None
)
@@ -188,9 +190,11 @@ def track_state(
issue_activities,
epoch,
):
if current_instance.get("state") != requested_data.get("state"):
new_state = State.objects.get(pk=requested_data.get("state", None))
old_state = State.objects.get(pk=current_instance.get("state", None))
if current_instance.get("state_id") != requested_data.get("state_id"):
new_state = State.objects.get(pk=requested_data.get("state_id", None))
old_state = State.objects.get(
pk=current_instance.get("state_id", None)
)
issue_activities.append(
IssueActivity(
@@ -288,10 +292,10 @@ def track_labels(
epoch,
):
requested_labels = set(
[str(lab) for lab in requested_data.get("labels", [])]
[str(lab) for lab in requested_data.get("label_ids", [])]
)
current_labels = set(
[str(lab) for lab in current_instance.get("labels", [])]
[str(lab) for lab in current_instance.get("label_ids", [])]
)
added_labels = requested_labels - current_labels
@@ -349,16 +353,22 @@ def track_assignees(
issue_activities,
epoch,
):
requested_assignees = set(
[str(asg) for asg in requested_data.get("assignees", [])]
requested_assignees = (
set([str(asg) for asg in requested_data.get("assignee_ids", [])])
if requested_data is not None
else set()
)
current_assignees = set(
[str(asg) for asg in current_instance.get("assignees", [])]
current_assignees = (
set([str(asg) for asg in current_instance.get("assignee_ids", [])])
if current_instance is not None
else set()
)
added_assignees = requested_assignees - current_assignees
dropped_assginees = current_assignees - requested_assignees
bulk_subscribers = []
for added_asignee in added_assignees:
assignee = User.objects.get(pk=added_asignee)
issue_activities.append(
@@ -376,6 +386,21 @@ def track_assignees(
epoch=epoch,
)
)
bulk_subscribers.append(
IssueSubscriber(
subscriber_id=assignee.id,
issue_id=issue_id,
workspace_id=workspace_id,
project_id=project_id,
created_by_id=assignee.id,
updated_by_id=assignee.id,
)
)
# Create assignees subscribers to the issue and ignore if already
IssueSubscriber.objects.bulk_create(
bulk_subscribers, batch_size=10, ignore_conflicts=True
)
for dropped_assignee in dropped_assginees:
assignee = User.objects.get(pk=dropped_assignee)
@@ -527,6 +552,20 @@ def create_issue_activity(
epoch=epoch,
)
)
requested_data = (
json.loads(requested_data) if requested_data is not None else None
)
if requested_data.get("assignee_ids") is not None:
track_assignees(
requested_data,
current_instance,
issue_id,
project_id,
workspace_id,
actor_id,
issue_activities,
epoch,
)
def update_issue_activity(
@@ -541,14 +580,14 @@ def update_issue_activity(
):
ISSUE_ACTIVITY_MAPPER = {
"name": track_name,
"parent": track_parent,
"parent_id": track_parent,
"priority": track_priority,
"state": track_state,
"state_id": track_state,
"description_html": track_description,
"target_date": track_target_date,
"start_date": track_start_date,
"labels": track_labels,
"assignees": track_assignees,
"label_ids": track_labels,
"assignee_ids": track_assignees,
"estimate_point": track_estimate_points,
"archived_at": track_archive_at,
"closed_to": track_closed_to,
@@ -833,70 +872,26 @@ def create_module_issue_activity(
requested_data = (
json.loads(requested_data) if requested_data is not None else None
)
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
# Updated Records:
updated_records = current_instance.get("updated_module_issues", [])
created_records = json.loads(
current_instance.get("created_module_issues", [])
)
for updated_record in updated_records:
old_module = Module.objects.filter(
pk=updated_record.get("old_module_id", None)
).first()
new_module = Module.objects.filter(
pk=updated_record.get("new_module_id", None)
).first()
issue = Issue.objects.filter(pk=updated_record.get("issue_id")).first()
if issue:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
issue_activities.append(
IssueActivity(
issue_id=updated_record.get("issue_id"),
actor_id=actor_id,
verb="updated",
old_value=old_module.name,
new_value=new_module.name,
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"updated module to ",
old_identifier=old_module.id,
new_identifier=new_module.id,
epoch=epoch,
)
)
for created_record in created_records:
module = Module.objects.filter(
pk=created_record.get("fields").get("module")
).first()
issue = Issue.objects.filter(
pk=created_record.get("fields").get("issue")
).first()
if issue:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
issue_activities.append(
IssueActivity(
issue_id=created_record.get("fields").get("issue"),
actor_id=actor_id,
verb="created",
old_value="",
new_value=module.name,
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"added module {module.name}",
new_identifier=module.id,
epoch=epoch,
)
module = Module.objects.filter(pk=requested_data.get("module_id")).first()
issue = Issue.objects.filter(pk=issue_id).first()
if issue:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor_id=actor_id,
verb="created",
old_value="",
new_value=module.name,
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"added module {module.name}",
new_identifier=requested_data.get("module_id"),
epoch=epoch,
)
)
def delete_module_issue_activity(
@@ -915,32 +910,26 @@ def delete_module_issue_activity(
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
module_id = requested_data.get("module_id", "")
module_name = requested_data.get("module_name", "")
module = Module.objects.filter(pk=module_id).first()
issues = requested_data.get("issues")
for issue in issues:
current_issue = Issue.objects.filter(pk=issue).first()
if issue:
current_issue.updated_at = timezone.now()
current_issue.save(update_fields=["updated_at"])
issue_activities.append(
IssueActivity(
issue_id=issue,
actor_id=actor_id,
verb="deleted",
old_value=module.name if module is not None else module_name,
new_value="",
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"removed this issue from {module.name if module is not None else module_name}",
old_identifier=module_id if module_id is not None else None,
epoch=epoch,
)
module_name = current_instance.get("module_name")
current_issue = Issue.objects.filter(pk=issue_id).first()
if current_issue:
current_issue.updated_at = timezone.now()
current_issue.save(update_fields=["updated_at"])
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor_id=actor_id,
verb="deleted",
old_value=module_name,
new_value="",
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"removed this issue from {module_name}",
old_identifier=requested_data.get("module_id") if requested_data.get("module_id") is not None else None,
epoch=epoch,
)
)
def create_link_activity(
@@ -1543,6 +1532,8 @@ def issue_activity(
project_id,
epoch,
subscriber=True,
notification=False,
origin=None,
):
try:
issue_activities = []
@@ -1551,6 +1542,10 @@ def issue_activity(
workspace_id = project.workspace_id
if issue_id is not None:
if origin:
ri = redis_instance()
# set the request origin in redis
ri.set(str(issue_id), origin, ex=600)
issue = Issue.objects.filter(pk=issue_id).first()
if issue:
try:
@@ -1624,21 +1619,22 @@ def issue_activity(
except Exception as e:
capture_exception(e)
notifications.delay(
type=type,
issue_id=issue_id,
actor_id=actor_id,
project_id=project_id,
subscriber=subscriber,
issue_activities_created=json.dumps(
IssueActivitySerializer(
issue_activities_created, many=True
).data,
cls=DjangoJSONEncoder,
),
requested_data=requested_data,
current_instance=current_instance,
)
if notification:
notifications.delay(
type=type,
issue_id=issue_id,
actor_id=actor_id,
project_id=project_id,
subscriber=subscriber,
issue_activities_created=json.dumps(
IssueActivitySerializer(
issue_activities_created, many=True
).data,
cls=DjangoJSONEncoder,
),
requested_data=requested_data,
current_instance=current_instance,
)
return
except Exception as e:
@@ -87,6 +87,7 @@ def archive_old_issues():
current_instance=json.dumps({"archived_at": None}),
subscriber=False,
epoch=int(timezone.now().timestamp()),
notification=True,
)
for issue in issues_to_update
]
@@ -169,6 +170,7 @@ def close_old_issues():
current_instance=None,
subscriber=False,
epoch=int(timezone.now().timestamp()),
notification=True,
)
for issue in issues_to_update
]
+480 -256
View File
@@ -10,9 +10,12 @@ from plane.db.models import (
User,
IssueAssignee,
Issue,
State,
EmailNotificationLog,
Notification,
IssueComment,
IssueActivity,
UserNotificationPreference,
)
# Third Party imports
@@ -20,7 +23,7 @@ from celery import shared_task
from bs4 import BeautifulSoup
# =========== Issue Description Html Parsing and Notification Functions ======================
# =========== Issue Description Html Parsing and notification Functions ======================
def update_mentions_for_issue(issue, project, new_mentions, removed_mention):
@@ -37,9 +40,7 @@ def update_mentions_for_issue(issue, project, new_mentions, removed_mention):
)
IssueMention.objects.bulk_create(aggregated_issue_mentions, batch_size=100)
IssueMention.objects.filter(
issue=issue, mention__in=removed_mention
).delete()
IssueMention.objects.filter(issue=issue, mention__in=removed_mention).delete()
def get_new_mentions(requested_instance, current_instance):
@@ -60,8 +61,6 @@ def get_new_mentions(requested_instance, current_instance):
# Get Removed Mention
def get_removed_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database
@@ -79,8 +78,6 @@ def get_removed_mentions(requested_instance, current_instance):
# Adds mentions as subscribers
def extract_mentions_as_subscribers(project_id, issue_id, mentions):
# mentions is an array of User IDs representing the FILTERED set of mentioned users
@@ -95,9 +92,7 @@ def extract_mentions_as_subscribers(project_id, issue_id, mentions):
project_id=project_id,
).exists()
and not IssueAssignee.objects.filter(
project_id=project_id,
issue_id=issue_id,
assignee_id=mention_id,
project_id=project_id, issue_id=issue_id, assignee_id=mention_id
).exists()
and not Issue.objects.filter(
project_id=project_id, pk=issue_id, created_by_id=mention_id
@@ -125,9 +120,7 @@ def extract_mentions(issue_instance):
data = json.loads(issue_instance)
html = data.get("description_html")
soup = BeautifulSoup(html, "html.parser")
mention_tags = soup.find_all(
"mention-component", attrs={"target": "users"}
)
mention_tags = soup.find_all("mention-component", attrs={"target": "users"})
mentions = [mention_tag["id"] for mention_tag in mention_tags]
@@ -136,14 +129,12 @@ def extract_mentions(issue_instance):
return []
# =========== Comment Parsing and Notification Functions ======================
# =========== Comment Parsing and notification Functions ======================
def extract_comment_mentions(comment_value):
try:
mentions = []
soup = BeautifulSoup(comment_value, "html.parser")
mentions_tags = soup.find_all(
"mention-component", attrs={"target": "users"}
)
mentions_tags = soup.find_all("mention-component", attrs={"target": "users"})
for mention_tag in mentions_tags:
mentions.append(mention_tag["id"])
return list(set(mentions))
@@ -165,14 +156,8 @@ def get_new_comment_mentions(new_value, old_value):
return new_mentions
def createMentionNotification(
project,
notification_comment,
issue,
actor_id,
mention_id,
issue_id,
activity,
def create_mention_notification(
project, notification_comment, issue, actor_id, mention_id, issue_id, activity
):
return Notification(
workspace=project.workspace,
@@ -215,244 +200,199 @@ def notifications(
requested_data,
current_instance,
):
issue_activities_created = (
json.loads(issue_activities_created)
if issue_activities_created is not None
else None
)
if type not in [
"issue.activity.deleted",
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
"issue_reaction.activity.created",
"issue_reaction.activity.deleted",
"comment_reaction.activity.created",
"comment_reaction.activity.deleted",
"issue_vote.activity.created",
"issue_vote.activity.deleted",
"issue_draft.activity.created",
"issue_draft.activity.updated",
"issue_draft.activity.deleted",
]:
# Create Notifications
bulk_notifications = []
"""
Mention Tasks
1. Perform Diffing and Extract the mentions, that mention notification needs to be sent
2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers
"""
# Get new mentions from the newer instance
new_mentions = get_new_mentions(
requested_instance=requested_data,
current_instance=current_instance,
)
removed_mention = get_removed_mentions(
requested_instance=requested_data,
current_instance=current_instance,
try:
issue_activities_created = (
json.loads(issue_activities_created)
if issue_activities_created is not None
else None
)
if type not in [
"issue.activity.deleted",
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
"issue_reaction.activity.created",
"issue_reaction.activity.deleted",
"comment_reaction.activity.created",
"comment_reaction.activity.deleted",
"issue_vote.activity.created",
"issue_vote.activity.deleted",
"issue_draft.activity.created",
"issue_draft.activity.updated",
"issue_draft.activity.deleted",
]:
# Create Notifications
bulk_notifications = []
bulk_email_logs = []
comment_mentions = []
all_comment_mentions = []
"""
Mention Tasks
1. Perform Diffing and Extract the mentions, that mention notification needs to be sent
2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers
"""
# Get New Subscribers from the mentions of the newer instance
requested_mentions = extract_mentions(issue_instance=requested_data)
mention_subscribers = extract_mentions_as_subscribers(
project_id=project_id,
issue_id=issue_id,
mentions=requested_mentions,
)
for issue_activity in issue_activities_created:
issue_comment = issue_activity.get("issue_comment")
issue_comment_new_value = issue_activity.get("new_value")
issue_comment_old_value = issue_activity.get("old_value")
if issue_comment is not None:
# TODO: Maybe save the comment mentions, so that in future, we can filter out the issues based on comment mentions as well.
all_comment_mentions = (
all_comment_mentions
+ extract_comment_mentions(issue_comment_new_value)
)
new_comment_mentions = get_new_comment_mentions(
old_value=issue_comment_old_value,
new_value=issue_comment_new_value,
)
comment_mentions = comment_mentions + new_comment_mentions
comment_mention_subscribers = extract_mentions_as_subscribers(
project_id=project_id,
issue_id=issue_id,
mentions=all_comment_mentions,
)
"""
We will not send subscription activity notification to the below mentioned user sets
- Those who have been newly mentioned in the issue description, we will send mention notification to them.
- When the activity is a comment_created and there exist a mention in the comment, then we have to send the "mention_in_comment" notification
- When the activity is a comment_updated and there exist a mention change, then also we have to send the "mention_in_comment" notification
"""
issue_assignees = list(
IssueAssignee.objects.filter(
project_id=project_id, issue_id=issue_id
# Get new mentions from the newer instance
new_mentions = get_new_mentions(
requested_instance=requested_data,
current_instance=current_instance,
)
.exclude(assignee_id__in=list(new_mentions + comment_mentions))
.values_list("assignee", flat=True)
)
issue_subscribers = list(
IssueSubscriber.objects.filter(
project_id=project_id, issue_id=issue_id
removed_mention = get_removed_mentions(
requested_instance=requested_data,
current_instance=current_instance,
)
.exclude(
subscriber_id__in=list(
new_mentions + comment_mentions + [actor_id]
)
comment_mentions = []
all_comment_mentions = []
# Get New Subscribers from the mentions of the newer instance
requested_mentions = extract_mentions(
issue_instance=requested_data
)
mention_subscribers = extract_mentions_as_subscribers(
project_id=project_id,
issue_id=issue_id,
mentions=requested_mentions,
)
.values_list("subscriber", flat=True)
)
issue = Issue.objects.filter(pk=issue_id).first()
if issue.created_by_id is not None and str(issue.created_by_id) != str(
actor_id
):
issue_subscribers = issue_subscribers + [issue.created_by_id]
if subscriber:
# add the user to issue subscriber
try:
if (
str(issue.created_by_id) != str(actor_id)
and uuid.UUID(actor_id) not in issue_assignees
):
_ = IssueSubscriber.objects.get_or_create(
project_id=project_id,
issue_id=issue_id,
subscriber_id=actor_id,
)
except Exception as e:
pass
project = Project.objects.get(pk=project_id)
issue_subscribers = list(
set(issue_subscribers + issue_assignees) - {uuid.UUID(actor_id)}
)
for subscriber in issue_subscribers:
if subscriber in issue_subscribers:
sender = "in_app:issue_activities:subscribed"
if (
issue.created_by_id is not None
and subscriber == issue.created_by_id
):
sender = "in_app:issue_activities:created"
if subscriber in issue_assignees:
sender = "in_app:issue_activities:assigned"
for issue_activity in issue_activities_created:
# Do not send notification for description update
if issue_activity.get("field") == "description":
continue
issue_comment = issue_activity.get("issue_comment")
issue_comment_new_value = issue_activity.get("new_value")
issue_comment_old_value = issue_activity.get("old_value")
if issue_comment is not None:
issue_comment = IssueComment.objects.get(
id=issue_comment,
issue_id=issue_id,
project_id=project_id,
workspace_id=project.workspace_id,
# TODO: Maybe save the comment mentions, so that in future, we can filter out the issues based on comment mentions as well.
all_comment_mentions = (
all_comment_mentions
+ extract_comment_mentions(issue_comment_new_value)
)
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender=sender,
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
project=project,
title=issue_activity.get("comment"),
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(issue.project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.get("id")),
"verb": str(issue_activity.get("verb")),
"field": str(issue_activity.get("field")),
"actor": str(issue_activity.get("actor_id")),
"new_value": str(
issue_activity.get("new_value")
),
"old_value": str(
issue_activity.get("old_value")
),
"issue_comment": str(
issue_comment.comment_stripped
if issue_activity.get("issue_comment")
is not None
else ""
),
},
},
new_comment_mentions = get_new_comment_mentions(
old_value=issue_comment_old_value,
new_value=issue_comment_new_value,
)
comment_mentions = comment_mentions + new_comment_mentions
comment_mention_subscribers = extract_mentions_as_subscribers(
project_id=project_id,
issue_id=issue_id,
mentions=all_comment_mentions,
)
"""
We will not send subscription activity notification to the below mentioned user sets
- Those who have been newly mentioned in the issue description, we will send mention notification to them.
- When the activity is a comment_created and there exist a mention in the comment, then we have to send the "mention_in_comment" notification
- When the activity is a comment_updated and there exist a mention change, then also we have to send the "mention_in_comment" notification
"""
# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
issue_subscribers = list(
IssueSubscriber.objects.filter(
project_id=project_id, issue_id=issue_id
)
.exclude(
subscriber_id__in=list(
new_mentions + comment_mentions + [actor_id]
)
)
.values_list("subscriber", flat=True)
)
# Add Mentioned as Issue Subscribers
IssueSubscriber.objects.bulk_create(
mention_subscribers + comment_mention_subscribers, batch_size=100
)
issue = Issue.objects.filter(pk=issue_id).first()
last_activity = (
IssueActivity.objects.filter(issue_id=issue_id)
.order_by("-created_at")
.first()
)
actor = User.objects.get(pk=actor_id)
for mention_id in comment_mentions:
if mention_id != actor_id:
for issue_activity in issue_activities_created:
notification = createMentionNotification(
project=project,
issue=issue,
notification_comment=f"{actor.display_name} has mentioned you in a comment in issue {issue.name}",
actor_id=actor_id,
mention_id=mention_id,
issue_id=issue_id,
activity=issue_activity,
if subscriber:
# add the user to issue subscriber
try:
_ = IssueSubscriber.objects.get_or_create(
project_id=project_id, issue_id=issue_id, subscriber_id=actor_id
)
bulk_notifications.append(notification)
except Exception as e:
pass
for mention_id in new_mentions:
if mention_id != actor_id:
if (
last_activity is not None
and last_activity.field == "description"
and actor_id == str(last_activity.actor_id)
project = Project.objects.get(pk=project_id)
issue_assignees = IssueAssignee.objects.filter(
issue_id=issue_id, project_id=project_id
).values_list("assignee", flat=True)
issue_subscribers = list(
set(issue_subscribers) - {uuid.UUID(actor_id)}
)
for subscriber in issue_subscribers:
if issue.created_by_id and issue.created_by_id == subscriber:
sender = "in_app:issue_activities:created"
elif (
subscriber in issue_assignees
and issue.created_by_id not in issue_assignees
):
sender = "in_app:issue_activities:assigned"
else:
sender = "in_app:issue_activities:subscribed"
preference = UserNotificationPreference.objects.get(
user_id=subscriber
)
for issue_activity in issue_activities_created:
# If activity done in blocking then blocked by email should not go
if issue_activity.get("issue_detail").get("id") != issue_id:
continue;
# Do not send notification for description update
if issue_activity.get("field") == "description":
continue
# Check if the value should be sent or not
send_email = False
if (
issue_activity.get("field") == "state"
and preference.state_change
):
send_email = True
elif (
issue_activity.get("field") == "state"
and preference.issue_completed
and State.objects.filter(
project_id=project_id,
pk=issue_activity.get("new_identifier"),
group="completed",
).exists()
):
send_email = True
elif (
issue_activity.get("field") == "comment"
and preference.comment
):
send_email = True
elif preference.property_change:
send_email = True
else:
send_email = False
# If activity is of issue comment fetch the comment
issue_comment = (
IssueComment.objects.filter(
id=issue_activity.get("issue_comment"),
issue_id=issue_id,
project_id=project_id,
workspace_id=project.workspace_id,
).first()
if issue_activity.get("issue_comment")
else None
)
# Create in app notification
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities:mentioned",
sender=sender,
triggered_by_id=actor_id,
receiver_id=mention_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
project=project,
message=f"You have been mentioned in the issue {issue.name}",
title=issue_activity.get("comment"),
data={
"issue": {
"id": str(issue_id),
@@ -465,36 +405,320 @@ def notifications(
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(last_activity.id),
"verb": str(last_activity.verb),
"field": str(last_activity.field),
"actor": str(last_activity.actor_id),
"new_value": str(last_activity.new_value),
"old_value": str(last_activity.old_value),
"id": str(issue_activity.get("id")),
"verb": str(issue_activity.get("verb")),
"field": str(issue_activity.get("field")),
"actor": str(
issue_activity.get("actor_id")
),
"new_value": str(
issue_activity.get("new_value")
),
"old_value": str(
issue_activity.get("old_value")
),
"issue_comment": str(
issue_comment.comment_stripped
if issue_comment is not None
else ""
),
},
},
)
)
else:
# Create email notification
if send_email:
bulk_email_logs.append(
EmailNotificationLog(
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(
issue.project.identifier
),
"project_id": str(issue.project.id),
"workspace_slug": str(
issue.project.workspace.slug
),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.get("id")),
"verb": str(
issue_activity.get("verb")
),
"field": str(
issue_activity.get("field")
),
"actor": str(
issue_activity.get("actor_id")
),
"new_value": str(
issue_activity.get("new_value")
),
"old_value": str(
issue_activity.get("old_value")
),
"issue_comment": str(
issue_comment.comment_stripped
if issue_comment is not None
else ""
),
"activity_time": issue_activity.get("created_at"),
},
},
)
)
# ----------------------------------------------------------------------------------------------------------------- #
# Add Mentioned as Issue Subscribers
IssueSubscriber.objects.bulk_create(
mention_subscribers + comment_mention_subscribers,
batch_size=100,
ignore_conflicts=True,
)
last_activity = (
IssueActivity.objects.filter(issue_id=issue_id)
.order_by("-created_at")
.first()
)
actor = User.objects.get(pk=actor_id)
for mention_id in comment_mentions:
if mention_id != actor_id:
preference = UserNotificationPreference.objects.get(
user_id=mention_id
)
for issue_activity in issue_activities_created:
notification = createMentionNotification(
notification = create_mention_notification(
project=project,
issue=issue,
notification_comment=f"You have been mentioned in the issue {issue.name}",
notification_comment=f"{actor.display_name} has mentioned you in a comment in issue {issue.name}",
actor_id=actor_id,
mention_id=mention_id,
issue_id=issue_id,
activity=issue_activity,
)
# check for email notifications
if preference.mention:
bulk_email_logs.append(
EmailNotificationLog(
triggered_by_id=actor_id,
receiver_id=mention_id,
entity_identifier=issue_id,
entity_name="issue",
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(
issue.project.identifier
),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
"project_id": str(
issue.project.id
),
"workspace_slug": str(
issue.project.workspace.slug
),
},
"issue_activity": {
"id": str(
issue_activity.get("id")
),
"verb": str(
issue_activity.get("verb")
),
"field": str("mention"),
"actor": str(
issue_activity.get("actor_id")
),
"new_value": str(
issue_activity.get("new_value")
),
"old_value": str(
issue_activity.get("old_value")
),
"activity_time": issue_activity.get("created_at"),
},
},
)
)
bulk_notifications.append(notification)
# save new mentions for the particular issue and remove the mentions that has been deleted from the description
update_mentions_for_issue(
issue=issue,
project=project,
new_mentions=new_mentions,
removed_mention=removed_mention,
)
for mention_id in new_mentions:
if mention_id != actor_id:
preference = UserNotificationPreference.objects.get(
user_id=mention_id
)
if (
last_activity is not None
and last_activity.field == "description"
and actor_id == str(last_activity.actor_id)
):
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities:mentioned",
triggered_by_id=actor_id,
receiver_id=mention_id,
entity_identifier=issue_id,
entity_name="issue",
project=project,
message=f"You have been mentioned in the issue {issue.name}",
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(
issue.project.identifier
),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
"project_id": str(issue.project.id),
"workspace_slug": str(
issue.project.workspace.slug
),
},
"issue_activity": {
"id": str(last_activity.id),
"verb": str(last_activity.verb),
"field": str(last_activity.field),
"actor": str(last_activity.actor_id),
"new_value": str(
last_activity.new_value
),
"old_value": str(
last_activity.old_value
),
},
},
)
)
if preference.mention:
bulk_email_logs.append(
EmailNotificationLog(
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(
issue.project.identifier
),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(last_activity.id),
"verb": str(last_activity.verb),
"field": "mention",
"actor": str(
last_activity.actor_id
),
"new_value": str(
last_activity.new_value
),
"old_value": str(
last_activity.old_value
),
"activity_time": issue_activity.get("created_at"),
},
},
)
)
else:
for issue_activity in issue_activities_created:
notification = create_mention_notification(
project=project,
issue=issue,
notification_comment=f"You have been mentioned in the issue {issue.name}",
actor_id=actor_id,
mention_id=mention_id,
issue_id=issue_id,
activity=issue_activity,
)
if preference.mention:
bulk_email_logs.append(
EmailNotificationLog(
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(
issue.project.identifier
),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(
issue_activity.get("id")
),
"verb": str(
issue_activity.get("verb")
),
"field": str("mention"),
"actor": str(
issue_activity.get(
"actor_id"
)
),
"new_value": str(
issue_activity.get(
"new_value"
)
),
"old_value": str(
issue_activity.get(
"old_value"
)
),
"activity_time": issue_activity.get("created_at"),
},
},
)
)
bulk_notifications.append(notification)
# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
# save new mentions for the particular issue and remove the mentions that has been deleted from the description
update_mentions_for_issue(
issue=issue,
project=project,
new_mentions=new_mentions,
removed_mention=removed_mention,
)
# Bulk create notifications
Notification.objects.bulk_create(
bulk_notifications, batch_size=100
)
EmailNotificationLog.objects.bulk_create(
bulk_email_logs, batch_size=100, ignore_conflicts=True
)
return
except Exception as e:
print(e)
return
+75 -4
View File
@@ -7,6 +7,9 @@ import hmac
# Django imports
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Third party imports
from celery import shared_task
@@ -22,10 +25,10 @@ from plane.db.models import (
ModuleIssue,
CycleIssue,
IssueComment,
User,
)
from plane.api.serializers import (
ProjectSerializer,
IssueSerializer,
CycleSerializer,
ModuleSerializer,
CycleIssueSerializer,
@@ -34,6 +37,9 @@ from plane.api.serializers import (
IssueExpandSerializer,
)
# Module imports
from plane.license.utils.instance_value import get_email_configuration
SERIALIZER_MAPPER = {
"project": ProjectSerializer,
"issue": IssueExpandSerializer,
@@ -72,7 +78,7 @@ def get_model_data(event, event_id, many=False):
max_retries=5,
retry_jitter=True,
)
def webhook_task(self, webhook, slug, event, event_data, action):
def webhook_task(self, webhook, slug, event, event_data, action, current_site):
try:
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
@@ -151,7 +157,18 @@ def webhook_task(self, webhook, slug, event, event_data, action):
response_body=str(e),
retry_count=str(self.request.retries),
)
# Retry logic
if self.request.retries >= self.max_retries:
Webhook.objects.filter(pk=webhook.id).update(is_active=False)
if webhook:
# send email for the deactivation of the webhook
send_webhook_deactivation_email(
webhook_id=webhook.id,
receiver_id=webhook.created_by_id,
reason=str(e),
current_site=current_site,
)
return
raise requests.RequestException()
except Exception as e:
@@ -162,7 +179,7 @@ def webhook_task(self, webhook, slug, event, event_data, action):
@shared_task()
def send_webhook(event, payload, kw, action, slug, bulk):
def send_webhook(event, payload, kw, action, slug, bulk, current_site):
try:
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
@@ -216,6 +233,7 @@ def send_webhook(event, payload, kw, action, slug, bulk):
event=event,
event_data=data,
action=action,
current_site=current_site,
)
except Exception as e:
@@ -223,3 +241,56 @@ def send_webhook(event, payload, kw, action, slug, bulk):
print(e)
capture_exception(e)
return
@shared_task
def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reason):
# Get email configurations
(
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_FROM,
) = get_email_configuration()
receiver = User.objects.get(pk=receiver_id)
webhook = Webhook.objects.get(pk=webhook_id)
subject="Webhook Deactivated"
message=f"Webhook {webhook.url} has been deactivated due to failed requests."
# Send the mail
context = {
"email": receiver.email,
"message": message,
"webhook_url":f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}",
}
html_content = render_to_string(
"emails/notifications/webhook-deactivate.html", context
)
text_content = strip_tags(html_content)
try:
connection = get_connection(
host=EMAIL_HOST,
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
)
msg = EmailMultiAlternatives(
subject=subject,
body=text_content,
from_email=EMAIL_FROM,
to=[receiver.email],
connection=connection,
)
msg.attach_alternative(html_content, "text/html")
msg.send()
return
except Exception as e:
print(e)
return
+5
View File
@@ -2,6 +2,7 @@ import os
from celery import Celery
from plane.settings.redis import redis_instance
from celery.schedules import crontab
from django.utils.timezone import timedelta
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
@@ -28,6 +29,10 @@ app.conf.beat_schedule = {
"task": "plane.bgtasks.file_asset_task.delete_file_asset",
"schedule": crontab(hour=0, minute=0),
},
"check-every-five-minutes-to-send-email-notifications": {
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
"schedule": crontab(minute='*/5')
},
}
# Load task modules from all registered Django app configs.
@@ -0,0 +1,21 @@
# wait_for_migrations.py
import time
from django.core.management.base import BaseCommand
from django.db.migrations.executor import MigrationExecutor
from django.db import connections, DEFAULT_DB_ALIAS
class Command(BaseCommand):
help = 'Wait for database migrations to complete before starting Celery worker/beat'
def handle(self, *args, **kwargs):
while self._pending_migrations():
self.stdout.write("Waiting for database migrations to complete...")
time.sleep(10) # wait for 10 seconds before checking again
self.stdout.write(self.style.SUCCESS("No migrations Pending. Starting processes ..."))
def _pending_migrations(self):
connection = connections[DEFAULT_DB_ALIAS]
executor = MigrationExecutor(connection)
targets = executor.loader.graph.leaf_nodes()
return bool(executor.migration_plan(targets))
@@ -0,0 +1,184 @@
# Generated by Django 4.2.7 on 2024-01-22 08:55
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
("db", "0055_auto_20240108_0648"),
]
operations = [
migrations.CreateModel(
name="UserNotificationPreference",
fields=[
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Created At"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("property_change", models.BooleanField(default=True)),
("state_change", models.BooleanField(default=True)),
("comment", models.BooleanField(default=True)),
("mention", models.BooleanField(default=True)),
("issue_completed", models.BooleanField(default=True)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"project",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="project_notification_preferences",
to="db.project",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notification_preferences",
to=settings.AUTH_USER_MODEL,
),
),
(
"workspace",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_notification_preferences",
to="db.workspace",
),
),
],
options={
"verbose_name": "UserNotificationPreference",
"verbose_name_plural": "UserNotificationPreferences",
"db_table": "user_notification_preferences",
"ordering": ("-created_at",),
},
),
migrations.CreateModel(
name="EmailNotificationLog",
fields=[
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Created At"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("entity_identifier", models.UUIDField(null=True)),
("entity_name", models.CharField(max_length=255)),
("data", models.JSONField(null=True)),
("processed_at", models.DateTimeField(null=True)),
("sent_at", models.DateTimeField(null=True)),
("entity", models.CharField(max_length=200)),
(
"old_value",
models.CharField(blank=True, max_length=300, null=True),
),
(
"new_value",
models.CharField(blank=True, max_length=300, null=True),
),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"receiver",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="email_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"triggered_by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="triggered_emails",
to=settings.AUTH_USER_MODEL,
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
],
options={
"verbose_name": "Email Notification Log",
"verbose_name_plural": "Email Notification Logs",
"db_table": "email_notification_logs",
"ordering": ("-created_at",),
},
),
]
@@ -0,0 +1,28 @@
# Generated by Django 4.2.7 on 2024-01-22 09:01
from django.db import migrations
def create_notification_preferences(apps, schema_editor):
UserNotificationPreference = apps.get_model("db", "UserNotificationPreference")
User = apps.get_model("db", "User")
bulk_notification_preferences = []
for user_id in User.objects.filter(is_bot=False).values_list("id", flat=True):
bulk_notification_preferences.append(
UserNotificationPreference(
user_id=user_id,
created_by_id=user_id,
)
)
UserNotificationPreference.objects.bulk_create(
bulk_notification_preferences, batch_size=1000, ignore_conflicts=True
)
class Migration(migrations.Migration):
dependencies = [
("db", "0056_usernotificationpreference_emailnotificationlog"),
]
operations = [
migrations.RunPython(create_notification_preferences)
]
@@ -0,0 +1,23 @@
# Generated by Django 4.2.7 on 2024-01-24 18:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('db', '0057_auto_20240122_0901'),
]
operations = [
migrations.AlterField(
model_name='moduleissue',
name='issue',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_module', to='db.issue'),
),
migrations.AlterUniqueTogether(
name='moduleissue',
unique_together={('issue', 'module')},
),
]
@@ -0,0 +1,33 @@
# Generated by Django 4.2.7 on 2024-02-08 09:57
from django.db import migrations
def widgets_filter_change(apps, schema_editor):
Widget = apps.get_model("db", "Widget")
widgets_to_update = []
# Define the filter dictionaries for each widget key
filters_mapping = {
"assigned_issues": {"duration": "none", "tab": "pending"},
"created_issues": {"duration": "none", "tab": "pending"},
"issues_by_state_groups": {"duration": "none"},
"issues_by_priority": {"duration": "none"},
}
# Iterate over widgets and update filters if applicable
for widget in Widget.objects.all():
if widget.key in filters_mapping:
widget.filters = filters_mapping[widget.key]
widgets_to_update.append(widget)
# Bulk update the widgets
Widget.objects.bulk_update(widgets_to_update, ["filters"], batch_size=10)
class Migration(migrations.Migration):
dependencies = [
('db', '0058_alter_moduleissue_issue_and_more'),
]
operations = [
migrations.RunPython(widgets_filter_change)
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.7 on 2024-02-08 09:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0059_auto_20240208_0957'),
]
operations = [
migrations.AddField(
model_name='cycle',
name='progress_snapshot',
field=models.JSONField(default=dict),
),
]
+1 -1
View File
@@ -85,7 +85,7 @@ from .inbox import Inbox, InboxIssue
from .analytic import AnalyticView
from .notification import Notification
from .notification import Notification, UserNotificationPreference, EmailNotificationLog
from .exporter import ExporterHistory
+1
View File
@@ -68,6 +68,7 @@ class Cycle(ProjectBaseModel):
sort_order = models.FloatField(default=65535)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
progress_snapshot = models.JSONField(default=dict)
class Meta:
verbose_name = "Cycle"
+12
View File
@@ -9,6 +9,7 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError
from django.utils import timezone
# Module imports
from . import ProjectBaseModel
@@ -183,6 +184,17 @@ class Issue(ProjectBaseModel):
self.state = default_state
except ImportError:
pass
else:
try:
from plane.db.models import State
# Check if the current issue state group is completed or not
if self.state.group == "completed":
self.completed_at = timezone.now()
else:
self.completed_at = None
except ImportError:
pass
if self._state.adding:
# Get the maximum display_id value from the database
+2 -1
View File
@@ -134,11 +134,12 @@ class ModuleIssue(ProjectBaseModel):
module = models.ForeignKey(
"db.Module", on_delete=models.CASCADE, related_name="issue_module"
)
issue = models.OneToOneField(
issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, related_name="issue_module"
)
class Meta:
unique_together = ["issue", "module"]
verbose_name = "Module Issue"
verbose_name_plural = "Module Issues"
db_table = "module_issues"
+82 -3
View File
@@ -1,9 +1,9 @@
# Django imports
from django.db import models
from django.conf import settings
# Third party imports
from .base import BaseModel
# Module imports
from . import BaseModel
class Notification(BaseModel):
workspace = models.ForeignKey(
@@ -47,3 +47,82 @@ class Notification(BaseModel):
def __str__(self):
"""Return name of the notifications"""
return f"{self.receiver.email} <{self.workspace.name}>"
def get_default_preference():
return {
"property_change": {
"email": True,
},
"state": {
"email": True,
},
"comment": {
"email": True,
},
"mentions": {
"email": True,
},
}
class UserNotificationPreference(BaseModel):
# user it is related to
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="notification_preferences",
)
# workspace if it is applicable
workspace = models.ForeignKey(
"db.Workspace",
on_delete=models.CASCADE,
related_name="workspace_notification_preferences",
null=True,
)
# project
project = models.ForeignKey(
"db.Project",
on_delete=models.CASCADE,
related_name="project_notification_preferences",
null=True,
)
# preference fields
property_change = models.BooleanField(default=True)
state_change = models.BooleanField(default=True)
comment = models.BooleanField(default=True)
mention = models.BooleanField(default=True)
issue_completed = models.BooleanField(default=True)
class Meta:
verbose_name = "UserNotificationPreference"
verbose_name_plural = "UserNotificationPreferences"
db_table = "user_notification_preferences"
ordering = ("-created_at",)
def __str__(self):
"""Return the user"""
return f"<{self.user}>"
class EmailNotificationLog(BaseModel):
# receiver
receiver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="email_notifications")
triggered_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="triggered_emails")
# entity - can be issues, pages, etc.
entity_identifier = models.UUIDField(null=True)
entity_name = models.CharField(max_length=255)
# data
data = models.JSONField(null=True)
# sent at
processed_at = models.DateTimeField(null=True)
sent_at = models.DateTimeField(null=True)
entity = models.CharField(max_length=200)
old_value = models.CharField(max_length=300, blank=True, null=True)
new_value = models.CharField(max_length=300, blank=True, null=True)
class Meta:
verbose_name = "Email Notification Log"
verbose_name_plural = "Email Notification Logs"
db_table = "email_notification_logs"
ordering = ("-created_at",)
+44
View File
@@ -11,8 +11,16 @@ from django.contrib.auth.models import (
UserManager,
PermissionsMixin,
)
from django.db.models.signals import post_save
from django.conf import settings
from django.dispatch import receiver
from django.utils import timezone
# Third party imports
from sentry_sdk import capture_exception
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
def get_default_onboarding():
return {
@@ -134,3 +142,39 @@ class User(AbstractBaseUser, PermissionsMixin):
self.is_staff = True
super(User, self).save(*args, **kwargs)
@receiver(post_save, sender=User)
def send_welcome_slack(sender, instance, created, **kwargs):
try:
if created and not instance.is_bot:
# Send message on slack as well
if settings.SLACK_BOT_TOKEN:
client = WebClient(token=settings.SLACK_BOT_TOKEN)
try:
_ = client.chat_postMessage(
channel="#trackers",
text=f"New user {instance.email} has signed up and begun the onboarding journey.",
)
except SlackApiError as e:
print(f"Got an error: {e.response['error']}")
return
except Exception as e:
capture_exception(e)
return
@receiver(post_save, sender=User)
def create_user_notification(sender, instance, created, **kwargs):
# create preferences
if created and not instance.is_bot:
# Module imports
from plane.db.models import UserNotificationPreference
UserNotificationPreference.objects.create(
user=instance,
property_change=False,
state_change=False,
comment=False,
mention=False,
issue_completed=False,
)
+1 -2
View File
@@ -282,15 +282,14 @@ if REDIS_SSL:
redis_url = os.environ.get("REDIS_URL")
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
CELERY_BROKER_URL = broker_url
CELERY_RESULT_BACKEND = broker_url
else:
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
CELERY_IMPORTS = (
"plane.bgtasks.issue_automation_task",
"plane.bgtasks.exporter_expired_task",
"plane.bgtasks.file_asset_task",
"plane.bgtasks.email_notification_task",
)
# Sentry Settings
+5 -1
View File
@@ -4,6 +4,7 @@ from datetime import timedelta
# Django import
from django.db import models
from django.utils import timezone
from django.db.models.functions import TruncDate
from django.db.models import Count, F, Sum, Value, Case, When, CharField
from django.db.models.functions import (
@@ -168,6 +169,9 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
if item["date"] is not None and item["date"] <= date
)
cumulative_pending_issues -= total_completed
chart_data[str(date)] = cumulative_pending_issues
if date > timezone.now().date():
chart_data[str(date)] = None
else:
chart_data[str(date)] = cumulative_pending_issues
return chart_data
+2 -2
View File
@@ -1,6 +1,6 @@
# base requirements
Django==4.2.7
Django==4.2.10
psycopg==3.1.12
djangorestframework==3.14.0
redis==4.6.0
@@ -30,7 +30,7 @@ openpyxl==3.1.2
beautifulsoup4==4.12.2
dj-database-url==2.1.0
posthog==3.0.2
cryptography==41.0.5
cryptography==42.0.0
lxml==4.9.3
boto3==1.28.40
+1 -1
View File
@@ -1 +1 @@
python-3.11.6
python-3.11.7
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# Check if the user has sudo access
if command -v curl &> /dev/null; then
sudo curl -sSL \
-o /usr/local/bin/plane-app \
https://raw.githubusercontent.com/makeplane/plane/${BRANCH:-master}/deploy/1-click/plane-app?token=$(date +%s)
else
sudo wget -q \
-O /usr/local/bin/plane-app \
https://raw.githubusercontent.com/makeplane/plane/${BRANCH:-master}/deploy/1-click/plane-app?token=$(date +%s)
fi
sudo chmod +x /usr/local/bin/plane-app
sudo sed -i 's/export DEPLOY_BRANCH=${BRANCH:-master}/export DEPLOY_BRANCH='${BRANCH:-master}'/' /usr/local/bin/plane-app
plane-app --help
+756
View File
@@ -0,0 +1,756 @@
#!/bin/bash
function print_header() {
clear
cat <<"EOF"
---------------------------------------
____ _
| _ \| | __ _ _ __ ___
| |_) | |/ _` | '_ \ / _ \
| __/| | (_| | | | | __/
|_| |_|\__,_|_| |_|\___|
---------------------------------------
Project management tool from the future
---------------------------------------
EOF
}
function update_env_file() {
config_file=$1
key=$2
value=$3
# Check if the config file exists
if [ ! -f "$config_file" ]; then
echo "Config file not found. Creating a new one..." >&2
sudo touch "$config_file"
fi
# Check if the key already exists in the config file
if sudo grep "^$key=" "$config_file"; then
sudo awk -v key="$key" -v value="$value" -F '=' '{if ($1 == key) $2 = value} 1' OFS='=' "$config_file" | sudo tee "$config_file.tmp" > /dev/null
sudo mv "$config_file.tmp" "$config_file" &> /dev/null
else
# sudo echo "$key=$value" >> "$config_file"
echo -e "$key=$value" | sudo tee -a "$config_file" > /dev/null
fi
}
function read_env_file() {
config_file=$1
key=$2
# Check if the config file exists
if [ ! -f "$config_file" ]; then
echo "Config file not found. Creating a new one..." >&2
sudo touch "$config_file"
fi
# Check if the key already exists in the config file
if sudo grep -q "^$key=" "$config_file"; then
value=$(sudo awk -v key="$key" -F '=' '{if ($1 == key) print $2}' "$config_file")
echo "$value"
else
echo ""
fi
}
function update_config() {
config_file="$PLANE_INSTALL_DIR/config.env"
update_env_file $config_file $1 $2
}
function read_config() {
config_file="$PLANE_INSTALL_DIR/config.env"
read_env_file $config_file $1
}
function update_env() {
config_file="$PLANE_INSTALL_DIR/.env"
update_env_file $config_file $1 $2
}
function read_env() {
config_file="$PLANE_INSTALL_DIR/.env"
read_env_file $config_file $1
}
function show_message() {
print_header
if [ "$2" == "replace_last_line" ]; then
PROGRESS_MSG[-1]="$1"
else
PROGRESS_MSG+=("$1")
fi
for statement in "${PROGRESS_MSG[@]}"; do
echo "$statement"
done
}
function prepare_environment() {
show_message "Prepare Environment..." >&2
show_message "- Updating OS with required tools ✋" >&2
sudo "$PACKAGE_MANAGER" update -y
sudo "$PACKAGE_MANAGER" upgrade -y
local required_tools=("curl" "awk" "wget" "nano" "dialog" "git" "uidmap")
for tool in "${required_tools[@]}"; do
if ! command -v $tool &> /dev/null; then
sudo "$PACKAGE_MANAGER" install -y $tool
fi
done
show_message "- OS Updated ✅" "replace_last_line" >&2
# Install Docker if not installed
if ! command -v docker &> /dev/null; then
show_message "- Installing Docker ✋" >&2
# curl -o- https://get.docker.com | bash -
if [ "$PACKAGE_MANAGER" == "yum" ]; then
sudo $PACKAGE_MANAGER install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo &> /dev/null
elif [ "$PACKAGE_MANAGER" == "apt-get" ]; then
# Add Docker's official GPG key:
sudo $PACKAGE_MANAGER update
sudo $PACKAGE_MANAGER install ca-certificates curl &> /dev/null
sudo install -m 0755 -d /etc/apt/keyrings &> /dev/null
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc &> /dev/null
sudo chmod a+r /etc/apt/keyrings/docker.asc &> /dev/null
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo $PACKAGE_MANAGER update
fi
sudo $PACKAGE_MANAGER install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
show_message "- Docker Installed ✅" "replace_last_line" >&2
else
show_message "- Docker is already installed ✅" >&2
fi
update_config "PLANE_ARCH" "$CPU_ARCH"
update_config "DOCKER_VERSION" "$(docker -v | awk '{print $3}' | sed 's/,//g')"
update_config "PLANE_DATA_DIR" "$DATA_DIR"
update_config "PLANE_LOG_DIR" "$LOG_DIR"
# echo "TRUE"
echo "Environment prepared successfully ✅"
show_message "Environment prepared successfully ✅" >&2
show_message "" >&2
return 0
}
function download_plane() {
# Download Docker Compose File from github url
show_message "Downloading Plane Setup Files ✋" >&2
sudo curl -H 'Cache-Control: no-cache, no-store' \
-s -o $PLANE_INSTALL_DIR/docker-compose.yaml \
https://raw.githubusercontent.com/makeplane/plane/$DEPLOY_BRANCH/deploy/selfhost/docker-compose.yml?token=$(date +%s)
sudo curl -H 'Cache-Control: no-cache, no-store' \
-s -o $PLANE_INSTALL_DIR/variables-upgrade.env \
https://raw.githubusercontent.com/makeplane/plane/$DEPLOY_BRANCH/deploy/selfhost/variables.env?token=$(date +%s)
# if .env does not exists rename variables-upgrade.env to .env
if [ ! -f "$PLANE_INSTALL_DIR/.env" ]; then
sudo mv $PLANE_INSTALL_DIR/variables-upgrade.env $PLANE_INSTALL_DIR/.env
fi
show_message "Plane Setup Files Downloaded ✅" "replace_last_line" >&2
show_message "" >&2
echo "PLANE_DOWNLOADED"
return 0
}
function printUsageInstructions() {
show_message "" >&2
show_message "----------------------------------" >&2
show_message "Usage Instructions" >&2
show_message "----------------------------------" >&2
show_message "" >&2
show_message "To use the Plane Setup utility, use below commands" >&2
show_message "" >&2
show_message "Usage: plane-app [OPTION]" >&2
show_message "" >&2
show_message " start Start Server" >&2
show_message " stop Stop Server" >&2
show_message " restart Restart Server" >&2
show_message "" >&2
show_message "other options" >&2
show_message " -i, --install Install Plane" >&2
show_message " -c, --configure Configure Plane" >&2
show_message " -up, --upgrade Upgrade Plane" >&2
show_message " -un, --uninstall Uninstall Plane" >&2
show_message " -ui, --update-installer Update Plane Installer" >&2
show_message " -h, --help Show help" >&2
show_message "" >&2
show_message "" >&2
show_message "Application Data is stored in mentioned folders" >&2
show_message " - DB Data: $DATA_DIR/postgres" >&2
show_message " - Redis Data: $DATA_DIR/redis" >&2
show_message " - Minio Data: $DATA_DIR/minio" >&2
show_message "" >&2
show_message "" >&2
show_message "----------------------------------" >&2
show_message "" >&2
}
function build_local_image() {
show_message "- Downloading Plane Source Code ✋" >&2
REPO=https://github.com/makeplane/plane.git
CURR_DIR=$PWD
PLANE_TEMP_CODE_DIR=$PLANE_INSTALL_DIR/temp
sudo rm -rf $PLANE_TEMP_CODE_DIR > /dev/null
sudo git clone $REPO $PLANE_TEMP_CODE_DIR --branch $DEPLOY_BRANCH --single-branch -q > /dev/null
sudo cp $PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml $PLANE_TEMP_CODE_DIR/build.yml
show_message "- Plane Source Code Downloaded ✅" "replace_last_line" >&2
show_message "- Building Docker Images ✋" >&2
sudo docker compose --env-file=$PLANE_INSTALL_DIR/.env -f $PLANE_TEMP_CODE_DIR/build.yml build --no-cache
}
function check_for_docker_images() {
show_message "" >&2
# show_message "Building Plane Images" >&2
CURR_DIR=$(pwd)
if [ "$DEPLOY_BRANCH" == "master" ]; then
update_env "APP_RELEASE" "latest"
export APP_RELEASE=latest
else
update_env "APP_RELEASE" "$DEPLOY_BRANCH"
export APP_RELEASE=$DEPLOY_BRANCH
fi
if [ $USE_GLOBAL_IMAGES == 1 ]; then
# show_message "Building Plane Images for $CPU_ARCH is not required. Skipping... ✅" "replace_last_line" >&2
export DOCKERHUB_USER=makeplane
update_env "DOCKERHUB_USER" "$DOCKERHUB_USER"
update_env "PULL_POLICY" "always"
echo "Building Plane Images for $CPU_ARCH is not required. Skipping..."
else
export DOCKERHUB_USER=myplane
show_message "Building Plane Images for $CPU_ARCH " >&2
update_env "DOCKERHUB_USER" "$DOCKERHUB_USER"
update_env "PULL_POLICY" "never"
build_local_image
sudo rm -rf $PLANE_INSTALL_DIR/temp > /dev/null
show_message "- Docker Images Built ✅" "replace_last_line" >&2
sudo cd $CURR_DIR
fi
sudo sed -i "s|- pgdata:|- $DATA_DIR/postgres:|g" $PLANE_INSTALL_DIR/docker-compose.yaml
sudo sed -i "s|- redisdata:|- $DATA_DIR/redis:|g" $PLANE_INSTALL_DIR/docker-compose.yaml
sudo sed -i "s|- uploads:|- $DATA_DIR/minio:|g" $PLANE_INSTALL_DIR/docker-compose.yaml
show_message "Downloading Plane Images for $CPU_ARCH ✋" >&2
sudo docker compose -f $PLANE_INSTALL_DIR/docker-compose.yaml --env-file=$PLANE_INSTALL_DIR/.env pull
show_message "Plane Images Downloaded ✅" "replace_last_line" >&2
}
function configure_plane() {
show_message "" >&2
show_message "Configuring Plane" >&2
show_message "" >&2
exec 3>&1
nginx_port=$(read_env "NGINX_PORT")
domain_name=$(read_env "DOMAIN_NAME")
upload_limit=$(read_env "FILE_SIZE_LIMIT")
NGINX_SETTINGS=$(dialog \
--ok-label "Next" \
--cancel-label "Skip" \
--backtitle "Plane Configuration" \
--title "Nginx Settings" \
--form "" \
0 0 0 \
"Port:" 1 1 "${nginx_port:-80}" 1 10 50 0 \
"Domain:" 2 1 "${domain_name:-localhost}" 2 10 50 0 \
"Upload Limit:" 3 1 "${upload_limit:-5242880}" 3 10 15 0 \
2>&1 1>&3)
save_nginx_settings=0
if [ $? -eq 0 ]; then
save_nginx_settings=1
nginx_port=$(echo "$NGINX_SETTINGS" | sed -n 1p)
domain_name=$(echo "$NGINX_SETTINGS" | sed -n 2p)
upload_limit=$(echo "$NGINX_SETTINGS" | sed -n 3p)
fi
smtp_host=$(read_env "EMAIL_HOST")
smtp_user=$(read_env "EMAIL_HOST_USER")
smtp_password=$(read_env "EMAIL_HOST_PASSWORD")
smtp_port=$(read_env "EMAIL_PORT")
smtp_from=$(read_env "EMAIL_FROM")
smtp_tls=$(read_env "EMAIL_USE_TLS")
smtp_ssl=$(read_env "EMAIL_USE_SSL")
SMTP_SETTINGS=$(dialog \
--ok-label "Next" \
--cancel-label "Skip" \
--backtitle "Plane Configuration" \
--title "SMTP Settings" \
--form "" \
0 0 0 \
"Host:" 1 1 "$smtp_host" 1 10 80 0 \
"User:" 2 1 "$smtp_user" 2 10 80 0 \
"Password:" 3 1 "$smtp_password" 3 10 80 0 \
"Port:" 4 1 "${smtp_port:-587}" 4 10 5 0 \
"From:" 5 1 "${smtp_from:-Mailer <mailer@example.com>}" 5 10 80 0 \
"TLS:" 6 1 "${smtp_tls:-1}" 6 10 1 1 \
"SSL:" 7 1 "${smtp_ssl:-0}" 7 10 1 1 \
2>&1 1>&3)
save_smtp_settings=0
if [ $? -eq 0 ]; then
save_smtp_settings=1
smtp_host=$(echo "$SMTP_SETTINGS" | sed -n 1p)
smtp_user=$(echo "$SMTP_SETTINGS" | sed -n 2p)
smtp_password=$(echo "$SMTP_SETTINGS" | sed -n 3p)
smtp_port=$(echo "$SMTP_SETTINGS" | sed -n 4p)
smtp_from=$(echo "$SMTP_SETTINGS" | sed -n 5p)
smtp_tls=$(echo "$SMTP_SETTINGS" | sed -n 6p)
fi
external_pgdb_url=$(dialog \
--backtitle "Plane Configuration" \
--title "Using External Postgres Database ?" \
--ok-label "Next" \
--cancel-label "Skip" \
--inputbox "Enter your external database url" \
8 60 3>&1 1>&2 2>&3)
external_redis_url=$(dialog \
--backtitle "Plane Configuration" \
--title "Using External Redis Database ?" \
--ok-label "Next" \
--cancel-label "Skip" \
--inputbox "Enter your external redis url" \
8 60 3>&1 1>&2 2>&3)
aws_region=$(read_env "AWS_REGION")
aws_access_key=$(read_env "AWS_ACCESS_KEY_ID")
aws_secret_key=$(read_env "AWS_SECRET_ACCESS_KEY")
aws_bucket=$(read_env "AWS_S3_BUCKET_NAME")
AWS_S3_SETTINGS=$(dialog \
--ok-label "Next" \
--cancel-label "Skip" \
--backtitle "Plane Configuration" \
--title "AWS S3 Bucket Configuration" \
--form "" \
0 0 0 \
"Region:" 1 1 "$aws_region" 1 10 50 0 \
"Access Key:" 2 1 "$aws_access_key" 2 10 50 0 \
"Secret Key:" 3 1 "$aws_secret_key" 3 10 50 0 \
"Bucket:" 4 1 "$aws_bucket" 4 10 50 0 \
2>&1 1>&3)
save_aws_settings=0
if [ $? -eq 0 ]; then
save_aws_settings=1
aws_region=$(echo "$AWS_S3_SETTINGS" | sed -n 1p)
aws_access_key=$(echo "$AWS_S3_SETTINGS" | sed -n 2p)
aws_secret_key=$(echo "$AWS_S3_SETTINGS" | sed -n 3p)
aws_bucket=$(echo "$AWS_S3_SETTINGS" | sed -n 4p)
fi
# display dialogbox asking for confirmation to continue
CONFIRM_CONFIG=$(dialog \
--title "Confirm Configuration" \
--backtitle "Plane Configuration" \
--yes-label "Confirm" \
--no-label "Cancel" \
--yesno \
"
save_ngnix_settings: $save_nginx_settings
nginx_port: $nginx_port
domain_name: $domain_name
upload_limit: $upload_limit
save_smtp_settings: $save_smtp_settings
smtp_host: $smtp_host
smtp_user: $smtp_user
smtp_password: $smtp_password
smtp_port: $smtp_port
smtp_from: $smtp_from
smtp_tls: $smtp_tls
smtp_ssl: $smtp_ssl
save_aws_settings: $save_aws_settings
aws_region: $aws_region
aws_access_key: $aws_access_key
aws_secret_key: $aws_secret_key
aws_bucket: $aws_bucket
pdgb_url: $external_pgdb_url
redis_url: $external_redis_url
" \
0 0 3>&1 1>&2 2>&3)
if [ $? -eq 0 ]; then
if [ $save_nginx_settings == 1 ]; then
update_env "NGINX_PORT" "$nginx_port"
update_env "DOMAIN_NAME" "$domain_name"
update_env "WEB_URL" "http://$domain_name"
update_env "CORS_ALLOWED_ORIGINS" "http://$domain_name"
update_env "FILE_SIZE_LIMIT" "$upload_limit"
fi
# check enable smpt settings value
if [ $save_smtp_settings == 1 ]; then
update_env "EMAIL_HOST" "$smtp_host"
update_env "EMAIL_HOST_USER" "$smtp_user"
update_env "EMAIL_HOST_PASSWORD" "$smtp_password"
update_env "EMAIL_PORT" "$smtp_port"
update_env "EMAIL_FROM" "$smtp_from"
update_env "EMAIL_USE_TLS" "$smtp_tls"
update_env "EMAIL_USE_SSL" "$smtp_ssl"
fi
# check enable aws settings value
if [[ $save_aws_settings == 1 && $aws_access_key != "" && $aws_secret_key != "" ]] ; then
update_env "USE_MINIO" "0"
update_env "AWS_REGION" "$aws_region"
update_env "AWS_ACCESS_KEY_ID" "$aws_access_key"
update_env "AWS_SECRET_ACCESS_KEY" "$aws_secret_key"
update_env "AWS_S3_BUCKET_NAME" "$aws_bucket"
elif [[ -z $aws_access_key || -z $aws_secret_key ]] ; then
update_env "USE_MINIO" "1"
update_env "AWS_REGION" ""
update_env "AWS_ACCESS_KEY_ID" ""
update_env "AWS_SECRET_ACCESS_KEY" ""
update_env "AWS_S3_BUCKET_NAME" "uploads"
fi
if [ "$external_pgdb_url" != "" ]; then
update_env "DATABASE_URL" "$external_pgdb_url"
fi
if [ "$external_redis_url" != "" ]; then
update_env "REDIS_URL" "$external_redis_url"
fi
fi
exec 3>&-
}
function upgrade_configuration() {
upg_env_file="$PLANE_INSTALL_DIR/variables-upgrade.env"
# Check if the file exists
if [ -f "$upg_env_file" ]; then
# Read each line from the file
while IFS= read -r line; do
# Skip comments and empty lines
if [[ "$line" =~ ^\s*#.*$ ]] || [[ -z "$line" ]]; then
continue
fi
# Split the line into key and value
key=$(echo "$line" | cut -d'=' -f1)
value=$(echo "$line" | cut -d'=' -f2-)
current_value=$(read_env "$key")
if [ -z "$current_value" ]; then
update_env "$key" "$value"
fi
done < "$upg_env_file"
fi
}
function install() {
show_message ""
if [ "$(uname)" == "Linux" ]; then
OS="linux"
OS_NAME=$(sudo awk -F= '/^ID=/{print $2}' /etc/os-release)
OS_NAME=$(echo "$OS_NAME" | tr -d '"')
print_header
if [ "$OS_NAME" == "ubuntu" ] || [ "$OS_NAME" == "debian" ] ||
[ "$OS_NAME" == "centos" ] || [ "$OS_NAME" == "amazon" ]; then
OS_SUPPORTED=true
show_message "******** Installing Plane ********"
show_message ""
prepare_environment
if [ $? -eq 0 ]; then
download_plane
if [ $? -eq 0 ]; then
# create_service
check_for_docker_images
last_installed_on=$(read_config "INSTALLATION_DATE")
if [ "$last_installed_on" == "" ]; then
configure_plane
fi
printUsageInstructions
update_config "INSTALLATION_DATE" "$(date)"
show_message "Plane Installed Successfully ✅"
show_message ""
else
show_message "Download Failed ❌"
exit 1
fi
else
show_message "Initialization Failed ❌"
exit 1
fi
else
OS_SUPPORTED=false
PROGRESS_MSG="❌❌ Unsupported OS Varient Detected : $OS_NAME ❌❌"
show_message ""
exit 1
fi
else
PROGRESS_MSG="❌❌❌ Unsupported OS Detected : $(uname) ❌❌❌"
show_message ""
exit 1
fi
}
function upgrade() {
print_header
if [ "$(uname)" == "Linux" ]; then
OS="linux"
OS_NAME=$(sudo awk -F= '/^ID=/{print $2}' /etc/os-release)
OS_NAME=$(echo "$OS_NAME" | tr -d '"')
if [ "$OS_NAME" == "ubuntu" ] || [ "$OS_NAME" == "debian" ] ||
[ "$OS_NAME" == "centos" ] || [ "$OS_NAME" == "amazon" ]; then
OS_SUPPORTED=true
show_message "******** Upgrading Plane ********"
show_message ""
prepare_environment
if [ $? -eq 0 ]; then
download_plane
if [ $? -eq 0 ]; then
check_for_docker_images
upgrade_configuration
update_config "UPGRADE_DATE" "$(date)"
show_message ""
show_message "Plane Upgraded Successfully ✅"
show_message ""
printUsageInstructions
else
show_message "Download Failed ❌"
exit 1
fi
else
show_message "Initialization Failed ❌"
exit 1
fi
else
PROGRESS_MSG="❌❌ Unsupported OS Varient Detected : $OS_NAME ❌❌"
show_message ""
exit 1
fi
else
PROGRESS_MSG="❌❌❌ Unsupported OS Detected : $(uname) ❌❌❌"
show_message ""
exit 1
fi
}
function uninstall() {
print_header
if [ "$(uname)" == "Linux" ]; then
OS="linux"
OS_NAME=$(awk -F= '/^ID=/{print $2}' /etc/os-release)
OS_NAME=$(echo "$OS_NAME" | tr -d '"')
if [ "$OS_NAME" == "ubuntu" ] || [ "$OS_NAME" == "debian" ] ||
[ "$OS_NAME" == "centos" ] || [ "$OS_NAME" == "amazon" ]; then
OS_SUPPORTED=true
show_message "******** Uninstalling Plane ********"
show_message ""
stop_server
if ! [ -x "$(command -v docker)" ]; then
echo "DOCKER_NOT_INSTALLED" &> /dev/null
else
# Ask of user input to confirm uninstall docker ?
CONFIRM_DOCKER_PURGE=$(dialog --title "Uninstall Docker" --defaultno --yesno "Are you sure you want to uninstall docker ?" 8 60 3>&1 1>&2 2>&3)
if [ $? -eq 0 ]; then
show_message "- Uninstalling Docker ✋"
sudo docker images -q | xargs -r sudo docker rmi -f &> /dev/null
sudo "$PACKAGE_MANAGER" remove -y docker-engine docker docker.io docker-ce docker-ce-cli docker-compose-plugin &> /dev/null
sudo "$PACKAGE_MANAGER" autoremove -y docker-engine docker docker.io docker-ce docker-compose-plugin &> /dev/null
show_message "- Docker Uninstalled ✅" "replace_last_line" >&2
fi
fi
sudo rm $PLANE_INSTALL_DIR/.env &> /dev/null
sudo rm $PLANE_INSTALL_DIR/variables-upgrade.env &> /dev/null
sudo rm $PLANE_INSTALL_DIR/config.env &> /dev/null
sudo rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null
# rm -rf $PLANE_INSTALL_DIR &> /dev/null
show_message "- Configuration Cleaned ✅"
show_message ""
show_message "******** Plane Uninstalled ********"
show_message ""
show_message ""
show_message "Plane Configuration Cleaned with some exceptions"
show_message "- DB Data: $DATA_DIR/postgres"
show_message "- Redis Data: $DATA_DIR/redis"
show_message "- Minio Data: $DATA_DIR/minio"
show_message ""
show_message ""
show_message "Thank you for using Plane. We hope to see you again soon."
show_message ""
show_message ""
else
PROGRESS_MSG="❌❌ Unsupported OS Varient Detected : $OS_NAME ❌❌"
show_message ""
exit 1
fi
else
PROGRESS_MSG="❌❌❌ Unsupported OS Detected : $(uname) ❌❌❌"
show_message ""
exit 1
fi
}
function start_server() {
docker_compose_file="$PLANE_INSTALL_DIR/docker-compose.yaml"
env_file="$PLANE_INSTALL_DIR/.env"
# check if both the files exits
if [ -f "$docker_compose_file" ] && [ -f "$env_file" ]; then
show_message "Starting Plane Server ($APP_RELEASE) ✋"
sudo docker compose -f $docker_compose_file --env-file=$env_file up -d
# Wait for containers to be running
echo "Waiting for containers to start..."
while ! sudo docker compose -f "$docker_compose_file" --env-file="$env_file" ps --services --filter "status=running" --quiet | grep -q "."; do
sleep 1
done
show_message "Plane Server Started ($APP_RELEASE) ✅" "replace_last_line" >&2
else
show_message "Plane Server not installed. Please install Plane first ❌" "replace_last_line" >&2
fi
}
function stop_server() {
docker_compose_file="$PLANE_INSTALL_DIR/docker-compose.yaml"
env_file="$PLANE_INSTALL_DIR/.env"
# check if both the files exits
if [ -f "$docker_compose_file" ] && [ -f "$env_file" ]; then
show_message "Stopping Plane Server ($APP_RELEASE) ✋"
sudo docker compose -f $docker_compose_file --env-file=$env_file down
show_message "Plane Server Stopped ($APP_RELEASE) ✅" "replace_last_line" >&2
else
show_message "Plane Server not installed [Skipping] ✅" "replace_last_line" >&2
fi
}
function restart_server() {
docker_compose_file="$PLANE_INSTALL_DIR/docker-compose.yaml"
env_file="$PLANE_INSTALL_DIR/.env"
# check if both the files exits
if [ -f "$docker_compose_file" ] && [ -f "$env_file" ]; then
show_message "Restarting Plane Server ($APP_RELEASE) ✋"
sudo docker compose -f $docker_compose_file --env-file=$env_file restart
show_message "Plane Server Restarted ($APP_RELEASE) ✅" "replace_last_line" >&2
else
show_message "Plane Server not installed. Please install Plane first ❌" "replace_last_line" >&2
fi
}
function show_help() {
# print_header
show_message "Usage: plane-app [OPTION]" >&2
show_message "" >&2
show_message " start Start Server" >&2
show_message " stop Stop Server" >&2
show_message " restart Restart Server" >&2
show_message "" >&2
show_message "other options" >&2
show_message " -i, --install Install Plane" >&2
show_message " -c, --configure Configure Plane" >&2
show_message " -up, --upgrade Upgrade Plane" >&2
show_message " -un, --uninstall Uninstall Plane" >&2
show_message " -ui, --update-installer Update Plane Installer" >&2
show_message " -h, --help Show help" >&2
show_message "" >&2
exit 1
}
function update_installer() {
show_message "Updating Plane Installer ✋" >&2
sudo curl -H 'Cache-Control: no-cache, no-store' \
-s -o /usr/local/bin/plane-app \
https://raw.githubusercontent.com/makeplane/plane/$DEPLOY_BRANCH/deploy/1-click/plane-app?token=$(date +%s)
sudo chmod +x /usr/local/bin/plane-app > /dev/null&> /dev/null
show_message "Plane Installer Updated ✅" "replace_last_line" >&2
}
export DEPLOY_BRANCH=${BRANCH:-master}
export APP_RELEASE=$DEPLOY_BRANCH
export DOCKERHUB_USER=makeplane
export PULL_POLICY=always
if [ "$DEPLOY_BRANCH" == "master" ]; then
export APP_RELEASE=latest
fi
PLANE_INSTALL_DIR=/opt/plane
DATA_DIR=$PLANE_INSTALL_DIR/data
LOG_DIR=$PLANE_INSTALL_DIR/log
OS_SUPPORTED=false
CPU_ARCH=$(uname -m)
PROGRESS_MSG=""
USE_GLOBAL_IMAGES=0
PACKAGE_MANAGER=""
if [[ $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $DEPLOY_BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]]; then
USE_GLOBAL_IMAGES=1
fi
sudo mkdir -p $PLANE_INSTALL_DIR/{data,log}
if command -v apt-get &> /dev/null; then
PACKAGE_MANAGER="apt-get"
elif command -v yum &> /dev/null; then
PACKAGE_MANAGER="yum"
elif command -v apk &> /dev/null; then
PACKAGE_MANAGER="apk"
fi
if [ "$1" == "start" ]; then
start_server
elif [ "$1" == "stop" ]; then
stop_server
elif [ "$1" == "restart" ]; then
restart_server
elif [ "$1" == "--install" ] || [ "$1" == "-i" ]; then
install
elif [ "$1" == "--configure" ] || [ "$1" == "-c" ]; then
configure_plane
printUsageInstructions
elif [ "$1" == "--upgrade" ] || [ "$1" == "-up" ]; then
upgrade
elif [ "$1" == "--uninstall" ] || [ "$1" == "-un" ]; then
uninstall
elif [ "$1" == "--update-installer" ] || [ "$1" == "-ui" ]; then
update_installer
elif [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
show_help
else
show_help
fi
+12 -4
View File
@@ -38,10 +38,6 @@ x-app-env : &app-env
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
# OPENAI SETTINGS - Deprecated can be configured through admin panel
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
- OPENAI_API_KEY=${OPENAI_API_KEY:-""}
- GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"}
# LOGIN/SIGNUP SETTINGS - Deprecated can be configured through admin panel
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
- ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1}
@@ -122,6 +118,18 @@ services:
- plane-db
- plane-redis
migrator:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest}
pull_policy: ${PULL_POLICY:-always}
restart: no
command: >
sh -c "python manage.py wait_for_db &&
python manage.py migrate"
depends_on:
- plane-db
- plane-redis
plane-db:
<<: *app-env
image: postgres:15.2-alpine
+11 -6
View File
@@ -20,8 +20,8 @@ function buildLocalImage() {
DO_BUILD="2"
else
printf "\n" >&2
printf "${YELLOW}You are on ${ARCH} cpu architecture. ${NC}\n" >&2
printf "${YELLOW}Since the prebuilt ${ARCH} compatible docker images are not available for, we will be running the docker build on this system. ${NC} \n" >&2
printf "${YELLOW}You are on ${CPU_ARCH} cpu architecture. ${NC}\n" >&2
printf "${YELLOW}Since the prebuilt ${CPU_ARCH} compatible docker images are not available for, we will be running the docker build on this system. ${NC} \n" >&2
printf "${YELLOW}This might take ${YELLOW}5-30 min based on your system's hardware configuration. \n ${NC} \n" >&2
printf "\n" >&2
printf "${GREEN}Select an option to proceed: ${NC}\n" >&2
@@ -49,7 +49,7 @@ function buildLocalImage() {
cd $PLANE_TEMP_CODE_DIR
if [ "$BRANCH" == "master" ];
then
APP_RELEASE=latest
export APP_RELEASE=latest
fi
docker compose -f build.yml build --no-cache >&2
@@ -149,7 +149,7 @@ function upgrade() {
function askForAction() {
echo
echo "Select a Action you want to perform:"
echo " 1) Install (${ARCH})"
echo " 1) Install (${CPU_ARCH})"
echo " 2) Start"
echo " 3) Stop"
echo " 4) Restart"
@@ -193,8 +193,8 @@ function askForAction() {
}
# CPU ARCHITECHTURE BASED SETTINGS
ARCH=$(uname -m)
if [ $ARCH == "amd64" ] || [ $ARCH == "x86_64" ];
CPU_ARCH=$(uname -m)
if [[ $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]];
then
USE_GLOBAL_IMAGES=1
DOCKERHUB_USER=makeplane
@@ -205,6 +205,11 @@ else
PULL_POLICY=never
fi
if [ "$BRANCH" == "master" ];
then
export APP_RELEASE=latest
fi
# REMOVE SPECIAL CHARACTERS FROM BRANCH NAME
if [ "$BRANCH" != "master" ];
then
+15 -20
View File
@@ -8,13 +8,13 @@ NGINX_PORT=80
WEB_URL=http://localhost
DEBUG=0
NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
SENTRY_DSN=""
SENTRY_ENVIRONMENT="production"
GOOGLE_CLIENT_ID=""
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
SENTRY_DSN=
SENTRY_ENVIRONMENT=production
GOOGLE_CLIENT_ID=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
DOCKERIZED=1 # deprecated
CORS_ALLOWED_ORIGINS="http://localhost"
CORS_ALLOWED_ORIGINS=http://localhost
#DB SETTINGS
PGHOST=plane-db
@@ -31,19 +31,14 @@ REDIS_PORT=6379
REDIS_URL=redis://${REDIS_HOST}:6379/
# EMAIL SETTINGS
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_HOST=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
EMAIL_PORT=587
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
EMAIL_FROM=Team Plane <team@mailer.plane.so>
EMAIL_USE_TLS=1
EMAIL_USE_SSL=0
# OPENAI SETTINGS
OPENAI_API_BASE=https://api.openai.com/v1 # deprecated
OPENAI_API_KEY="sk-" # deprecated
GPT_ENGINE="gpt-3.5-turbo" # deprecated
# LOGIN/SIGNUP SETTINGS
ENABLE_SIGNUP=1
ENABLE_EMAIL_PASSWORD=1
@@ -52,13 +47,13 @@ SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
# DATA STORE SETTINGS
USE_MINIO=1
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
AWS_SECRET_ACCESS_KEY="secret-key"
AWS_REGION=
AWS_ACCESS_KEY_ID=access-key
AWS_SECRET_ACCESS_KEY=secret-key
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
AWS_S3_BUCKET_NAME=uploads
MINIO_ROOT_USER="access-key"
MINIO_ROOT_PASSWORD="secret-key"
MINIO_ROOT_USER=access-key
MINIO_ROOT_PASSWORD=secret-key
BUCKET_NAME=uploads
FILE_SIZE_LIMIT=5242880
+23 -3
View File
@@ -86,7 +86,7 @@ services:
- dev_env
volumes:
- ./apiserver:/code
# command: /bin/sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local"
command: ./bin/takeoff.local
env_file:
- ./apiserver/.env
depends_on:
@@ -104,7 +104,7 @@ services:
- dev_env
volumes:
- ./apiserver:/code
command: /bin/sh -c "celery -A plane worker -l info"
command: ./bin/worker
env_file:
- ./apiserver/.env
depends_on:
@@ -123,7 +123,7 @@ services:
- dev_env
volumes:
- ./apiserver:/code
command: /bin/sh -c "celery -A plane beat -l info"
command: ./bin/beat
env_file:
- ./apiserver/.env
depends_on:
@@ -131,6 +131,26 @@ services:
- plane-db
- plane-redis
migrator:
build:
context: ./apiserver
dockerfile: Dockerfile.dev
args:
DOCKER_BUILDKIT: 1
restart: "no"
networks:
- dev_env
volumes:
- ./apiserver:/code
command: >
sh -c "python manage.py wait_for_db --settings=plane.settings.local &&
python manage.py migrate --settings=plane.settings.local"
env_file:
- ./apiserver/.env
depends_on:
- plane-db
- plane-redis
proxy:
build:
context: ./nginx
+2 -2
View File
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
"version": "0.14.0",
"version": "0.15.1",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
@@ -15,7 +15,7 @@
],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"dev": "turbo run dev --concurrency=13",
"start": "turbo run start",
"lint": "turbo run lint",
"clean": "turbo run clean",
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor-core",
"version": "0.14.0",
"version": "0.15.1",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
@@ -30,7 +30,6 @@
"dependencies": {
"@tiptap/core": "^2.1.13",
"@tiptap/extension-blockquote": "^2.1.13",
"@tiptap/extension-code": "^2.1.13",
"@tiptap/extension-code-block-lowlight": "^2.1.13",
"@tiptap/extension-color": "^2.1.13",
"@tiptap/extension-image": "^2.1.13",
@@ -34,8 +34,32 @@ export const toggleUnderline = (editor: Editor, range?: Range) => {
};
export const toggleCodeBlock = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
else editor.chain().focus().toggleCodeBlock().run();
// Check if code block is active then toggle code block
if (editor.isActive("codeBlock")) {
if (range) {
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
return;
}
editor.chain().focus().toggleCodeBlock().run();
return;
}
// Check if user hasn't selected any text
const isSelectionEmpty = editor.state.selection.empty;
if (isSelectionEmpty) {
if (range) {
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
return;
}
editor.chain().focus().toggleCodeBlock().run();
} else {
if (range) {
editor.chain().focus().deleteRange(range).toggleCode().run();
return;
}
editor.chain().focus().toggleCode().run();
}
};
export const toggleOrderedList = (editor: Editor, range?: Range) => {
@@ -59,8 +83,8 @@ export const toggleStrike = (editor: Editor, range?: Range) => {
};
export const toggleBlockquote = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
else editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run();
if (range) editor.chain().focus().deleteRange(range).toggleBlockquote().run();
else editor.chain().focus().toggleBlockquote().run();
};
export const insertTableCommand = (editor: Editor, range?: Range) => {
@@ -12,7 +12,7 @@ export const EditorContainer = ({ editor, editorClassNames, hideDragHandle, chil
<div
id="editor-container"
onClick={() => {
editor?.chain().focus().run();
editor?.chain().focus(undefined, { scrollIntoView: false }).run();
}}
onMouseLeave={() => {
hideDragHandle?.();
@@ -1,12 +1,80 @@
import { markInputRule, markPasteRule } from "@tiptap/core";
import Code from "@tiptap/extension-code";
import { Mark, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
export const inputRegex = /(?<!`)`([^`]*)`(?!`)/;
export const pasteRegex = /(?<!`)`([^`]+)`(?!`)/g;
export interface CodeOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
code: {
/**
* Set a code mark
*/
setCode: () => ReturnType;
/**
* Toggle inline code
*/
toggleCode: () => ReturnType;
/**
* Unset a code mark
*/
unsetCode: () => ReturnType;
};
}
}
export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/;
export const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/g;
export const CustomCodeInlineExtension = Mark.create<CodeOptions>({
name: "code",
addOptions() {
return {
HTMLAttributes: {
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-[2px] font-mono font-medium text-custom-text-1000",
spellcheck: "false",
},
};
},
excludes: "_",
code: true,
export const CustomCodeInlineExtension = Code.extend({
exitable: true,
inclusive: false,
parseHTML() {
return [{ tag: "code" }];
},
renderHTML({ HTMLAttributes }) {
return ["code", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
},
addCommands() {
return {
setCode:
() =>
({ commands }) =>
commands.setMark(this.name),
toggleCode:
() =>
({ commands }) =>
commands.toggleMark(this.name),
unsetCode:
() =>
({ commands }) =>
commands.unsetMark(this.name),
};
},
addKeyboardShortcuts() {
return {
"Mod-e": () => this.editor.commands.toggleCode(),
};
},
addInputRules() {
return [
markInputRule({
@@ -15,6 +83,7 @@ export const CustomCodeInlineExtension = Code.extend({
}),
];
},
addPasteRules() {
return [
markPasteRule({
@@ -23,9 +92,4 @@ export const CustomCodeInlineExtension = Code.extend({
}),
];
},
}).configure({
HTMLAttributes: {
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-[2px] font-mono font-medium text-custom-text-1000",
spellcheck: "false",
},
});
@@ -1,109 +0,0 @@
import { TextSelection } from "prosemirror-state";
import { InputRule, mergeAttributes, Node, nodeInputRule, wrappingInputRule } from "@tiptap/core";
/**
* Extension based on:
* - Tiptap HorizontalRule extension (https://tiptap.dev/api/nodes/horizontal-rule)
*/
export interface HorizontalRuleOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
horizontalRule: {
/**
* Add a horizontal rule
*/
setHorizontalRule: () => ReturnType;
};
}
}
export const HorizontalRule = Node.create<HorizontalRuleOptions>({
name: "horizontalRule",
addOptions() {
return {
HTMLAttributes: {},
};
},
group: "block",
addAttributes() {
return {
color: {
default: "#dddddd",
},
};
},
parseHTML() {
return [
{
tag: `div[data-type="${this.name}"]`,
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name,
}),
["div", {}],
];
},
addCommands() {
return {
setHorizontalRule:
() =>
({ chain }) => {
return (
chain()
.insertContent({ type: this.name })
// set cursor after horizontal rule
.command(({ tr, dispatch }) => {
if (dispatch) {
const { $to } = tr.selection;
const posAfter = $to.end();
if ($to.nodeAfter) {
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
} else {
// add node after horizontal rule if its the end of the document
const node = $to.parent.type.contentMatch.defaultType?.create();
if (node) {
tr.insert(posAfter, node);
tr.setSelection(TextSelection.create(tr.doc, posAfter));
}
}
tr.scrollIntoView();
}
return true;
})
.run()
);
},
};
},
addInputRules() {
return [
new InputRule({
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
handler: ({ state, range, match }) => {
state.tr.replaceRangeWith(range.from, range.to, this.type.create());
},
}),
];
},
});
@@ -1,26 +1,25 @@
import StarterKit from "@tiptap/starter-kit";
import TiptapUnderline from "@tiptap/extension-underline";
import TextStyle from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color";
import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
import TextStyle from "@tiptap/extension-text-style";
import TiptapUnderline from "@tiptap/extension-underline";
import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown";
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { Table } from "src/ui/extensions/table/table";
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
import { ImageExtension } from "src/ui/extensions/image";
import { isValidHttpUrl } from "src/lib/utils";
import { Mentions } from "src/ui/mentions";
import { CustomKeymap } from "src/ui/extensions/keymap";
import { CustomCodeBlockExtension } from "src/ui/extensions/code";
import { CustomQuoteExtension } from "src/ui/extensions/quote";
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
import { CustomKeymap } from "src/ui/extensions/keymap";
import { CustomQuoteExtension } from "src/ui/extensions/quote";
import { DeleteImage } from "src/types/delete-image";
import { IMentionSuggestion } from "src/types/mention-suggestion";
@@ -53,14 +52,11 @@ export const CoreEditorExtensions = (
class: "leading-normal -mb-2",
},
},
hardBreak: {
HTMLAttributes: {
class: "p-2",
},
},
code: false,
codeBlock: false,
horizontalRule: false,
horizontalRule: {
HTMLAttributes: { class: "mt-4 mb-4" },
},
blockquote: false,
dropcursor: {
color: "rgba(var(--color-text-100))",
@@ -109,7 +105,6 @@ export const CoreEditorExtensions = (
transformCopiedText: true,
transformPastedText: true,
}),
HorizontalRule,
Table,
TableHeader,
TableCell,
@@ -10,6 +10,11 @@ export interface CustomMentionOptions extends MentionOptions {
}
export const CustomMention = Mention.extend<CustomMentionOptions>({
addStorage(this) {
return {
mentionsOpen: false,
};
},
addAttributes() {
return {
id: {
@@ -14,6 +14,7 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
props.editor.storage.mentionsOpen = true;
reactRenderer = new ReactRenderer(MentionList, {
props,
editor: props.editor,
@@ -45,10 +46,18 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
return true;
}
// @ts-ignore
return reactRenderer?.ref?.onKeyDown(props);
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
if (navigationKeys.includes(props.event.key)) {
// @ts-ignore
reactRenderer?.ref?.onKeyDown(props);
event?.stopPropagation();
return true;
}
return false;
},
onExit: () => {
onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
props.editor.storage.mentionsOpen = false;
popup?.[0].destroy();
reactRenderer?.destroy();
},
@@ -106,7 +106,7 @@ export const TodoListItem = (editor: Editor): EditorMenuItem => ({
export const CodeItem = (editor: Editor): EditorMenuItem => ({
name: "code",
isActive: () => editor?.isActive("code"),
isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"),
command: () => toggleCodeBlock(editor),
icon: CodeIcon,
});
@@ -120,7 +120,7 @@ export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
export const QuoteItem = (editor: Editor): EditorMenuItem => ({
name: "quote",
isActive: () => editor?.isActive("quote"),
isActive: () => editor?.isActive("blockquote"),
command: () => toggleBlockquote(editor),
icon: QuoteIcon,
});
@@ -11,7 +11,6 @@ import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { Table } from "src/ui/extensions/table/table";
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image";
import { isValidHttpUrl } from "src/lib/utils";
@@ -51,7 +50,9 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
},
},
codeBlock: false,
horizontalRule: false,
horizontalRule: {
HTMLAttributes: { class: "mt-4 mb-4" },
},
dropcursor: {
color: "rgba(var(--color-text-100))",
width: 2,
@@ -72,7 +73,6 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
class: "rounded-lg border border-custom-border-300",
},
}),
HorizontalRule,
TiptapUnderline,
TextStyle,
Color,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/document-editor",
"version": "0.14.0",
"version": "0.15.1",
"description": "Package that powers Plane's Pages Editor",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
@@ -6,10 +6,16 @@ import { scrollSummary } from "src/utils/editor-summary-utils";
interface ContentBrowserProps {
editor: Editor;
markings: IMarking[];
setSidePeekVisible?: (sidePeekState: boolean) => void;
}
export const ContentBrowser = (props: ContentBrowserProps) => {
const { editor, markings } = props;
const { editor, markings, setSidePeekVisible } = props;
const handleOnClick = (marking: IMarking) => {
scrollSummary(editor, marking);
if (setSidePeekVisible) setSidePeekVisible(false);
}
return (
<div className="flex h-full flex-col overflow-hidden">
@@ -18,11 +24,11 @@ export const ContentBrowser = (props: ContentBrowserProps) => {
{markings.length !== 0 ? (
markings.map((marking) =>
marking.level === 1 ? (
<HeadingComp onClick={() => scrollSummary(editor, marking)} heading={marking.text} />
<HeadingComp onClick={() => handleOnClick(marking)} heading={marking.text} />
) : marking.level === 2 ? (
<SubheadingComp onClick={() => scrollSummary(editor, marking)} subHeading={marking.text} />
<SubheadingComp onClick={() => handleOnClick(marking)} subHeading={marking.text} />
) : (
<HeadingThreeComp heading={marking.text} onClick={() => scrollSummary(editor, marking)} />
<HeadingThreeComp heading={marking.text} onClick={() => handleOnClick(marking)} />
)
)
) : (
@@ -42,8 +42,8 @@ export const EditorHeader = (props: IEditorHeader) => {
} = props;
return (
<div className="flex items-center border-b border-custom-border-200 px-5 py-2">
<div className="w-56 flex-shrink-0 lg:w-72">
<div className="flex items-center border-b border-custom-border-200 md:px-5 px-3 py-2">
<div className="md:w-56 flex-shrink-0 lg:w-72 w-fit">
<SummaryPopover
editor={editor}
markings={markings}
@@ -52,7 +52,7 @@ export const EditorHeader = (props: IEditorHeader) => {
/>
</div>
<div className="flex-shrink-0">
<div className="flex-shrink-0 hidden md:flex">
{!readonly && uploadFile && (
<FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />
)}
@@ -27,11 +27,20 @@ type IPageRenderer = {
}) => void;
editorClassNames: string;
editorContentCustomClassNames?: string;
hideDragHandle?: () => void;
readonly: boolean;
};
export const PageRenderer = (props: IPageRenderer) => {
const { documentDetails, editor, editorClassNames, editorContentCustomClassNames, updatePageTitle, readonly } = props;
const {
documentDetails,
editor,
editorClassNames,
editorContentCustomClassNames,
updatePageTitle,
readonly,
hideDragHandle,
} = props;
const [pageTitle, setPagetitle] = useState(documentDetails.title);
@@ -65,14 +74,6 @@ export const PageRenderer = (props: IPageRenderer) => {
setIsOpen(false);
};
const switchLinkView = (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => {
if (!linkViewProps) return;
setLinkViewProps({
...linkViewProps,
view: view,
});
};
const handleLinkHover = useCallback(
(event: React.MouseEvent) => {
if (!editor) return;
@@ -151,7 +152,7 @@ export const PageRenderer = (props: IPageRenderer) => {
);
return (
<div className="w-full pb-64 pl-7 pt-5 page-renderer">
<div className="w-full pb-64 md:pl-7 pl-3 pt-5 page-renderer">
{!readonly ? (
<input
onChange={(e) => handlePageTitleChange(e.target.value)}
@@ -167,7 +168,7 @@ export const PageRenderer = (props: IPageRenderer) => {
/>
)}
<div className="flex relative h-full w-full flex-col pr-5 editor-renderer" onMouseOver={handleLinkHover}>
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
<EditorContainer hideDragHandle={hideDragHandle} editor={editor} editorClassNames={editorClassNames}>
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
</EditorContainer>
</div>
@@ -33,23 +33,36 @@ export const SummaryPopover: React.FC<Props> = (props) => {
<button
type="button"
ref={setReferenceElement}
className={`grid h-7 w-7 place-items-center rounded ${
sidePeekVisible ? "bg-custom-primary-100/20 text-custom-primary-100" : "text-custom-text-300"
}`}
className={`grid h-7 w-7 place-items-center rounded ${sidePeekVisible ? "bg-custom-primary-100/20 text-custom-primary-100" : "text-custom-text-300"
}`}
onClick={() => setSidePeekVisible(!sidePeekVisible)}
>
<List className="h-4 w-4" />
</button>
{!sidePeekVisible && (
<div
className="z-10 hidden max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg group-hover/summary-popover:block"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<ContentBrowser editor={editor} markings={markings} />
</div>
)}
<div className="md:hidden block">
{sidePeekVisible && (
<div
className="z-10 max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<ContentBrowser setSidePeekVisible={setSidePeekVisible} editor={editor} markings={markings} />
</div>
)}
</div>
<div className="hidden md:block">
{!sidePeekVisible && (
<div
className="z-10 hidden max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg group-hover/summary-popover:block"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<ContentBrowser editor={editor} markings={markings} />
</div>
)}
</div>
</div>
);
};
@@ -1,55 +1,29 @@
import Placeholder from "@tiptap/extension-placeholder";
import { IssueWidgetExtension } from "src/ui/extensions/widgets/issue-embed-widget";
import { IIssueEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types";
import { IssueWidgetPlaceholder } from "src/ui/extensions/widgets/issue-embed-widget";
import { SlashCommand, DragAndDrop } from "@plane/editor-extensions";
import { ISlashCommandItem, UploadImage } from "@plane/editor-core";
import { IssueSuggestions } from "src/ui/extensions/widgets/issue-embed-suggestion-list";
import { LayersIcon } from "@plane/ui";
import { UploadImage } from "@plane/editor-core";
export const DocumentEditorExtensions = (
uploadFile: UploadImage,
issueEmbedConfig?: IIssueEmbedConfig,
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
) => {
const additionalOptions: ISlashCommandItem[] = [
{
key: "issue_embed",
title: "Issue embed",
description: "Embed an issue from the project.",
searchTerms: ["issue", "link", "embed"],
icon: <LayersIcon className="h-3.5 w-3.5" />,
command: ({ editor, range }) => {
editor
.chain()
.focus()
.insertContentAt(
range,
"<p class='text-sm bg-gray-300 w-fit pl-3 pr-3 pt-1 pb-1 rounded shadow-sm'>#issue_</p>\n"
)
.run();
},
) => [
SlashCommand(uploadFile, setIsSubmitting),
DragAndDrop(setHideDragHandle),
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === "heading") {
return `Heading ${node.attrs.level}`;
}
if (node.type.name === "image" || node.type.name === "table") {
return "";
}
return "Press '/' for commands...";
},
];
includeChildren: true,
}),
IssueWidgetPlaceholder(),
];
return [
SlashCommand(uploadFile, setIsSubmitting, additionalOptions),
DragAndDrop,
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === "heading") {
return `Heading ${node.attrs.level}`;
}
if (node.type.name === "image" || node.type.name === "table") {
return "";
}
return "Press '/' for commands...";
},
includeChildren: true,
}),
IssueWidgetExtension({ issueEmbedConfig }),
IssueSuggestions(issueEmbedConfig ? issueEmbedConfig.issues : []),
];
};
@@ -78,7 +78,6 @@ const IssueSuggestionList = ({
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter", "Tab"];
const onKeyDown = (e: KeyboardEvent) => {
if (navigationKeys.includes(e.key)) {
e.preventDefault();
// if (editor.isFocused) {
// editor.chain().blur();
// commandListContainer.current?.focus();
@@ -87,7 +86,6 @@ const IssueSuggestionList = ({
setSelectedIndex(
(selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length
);
e.stopPropagation();
return true;
}
if (e.key === "ArrowDown") {
@@ -102,12 +100,10 @@ const IssueSuggestionList = ({
[currentSection]: [...prevItems[currentSection], ...nextItems],
}));
}
e.stopPropagation();
return true;
}
if (e.key === "Enter") {
selectItem(currentSection, selectedIndex);
e.stopPropagation();
return true;
}
if (e.key === "Tab") {
@@ -115,7 +111,6 @@ const IssueSuggestionList = ({
const nextSectionIndex = (currentSectionIndex + 1) % sections.length;
setCurrentSection(sections[nextSectionIndex]);
setSelectedIndex(0);
e.stopPropagation();
return true;
}
return false;
@@ -150,7 +145,7 @@ const IssueSuggestionList = ({
<div
id="issue-list-container"
ref={commandListContainer}
className="fixed z-[10] max-h-80 w-60 overflow-y-auto overflow-x-hidden rounded-md border border-custom-border-100 bg-custom-background-100 px-1 shadow-custom-shadow-xs transition-all"
className=" fixed z-[10] max-h-80 w-60 overflow-y-auto overflow-x-hidden rounded-md border border-custom-border-100 bg-custom-background-100 px-1 shadow-custom-shadow-xs transition-all"
>
{sections.map((section) => {
const sectionItems = displayedItems[section];
@@ -193,29 +188,35 @@ const IssueSuggestionList = ({
</div>
) : null;
};
export const IssueListRenderer = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
const container = document.querySelector(".frame-renderer") as HTMLElement;
component = new ReactRenderer(IssueSuggestionList, {
props,
// @ts-ignore
editor: props.editor,
});
// @ts-ignore
popup = tippy("body", {
popup = tippy(".frame-renderer", {
flipbehavior: ["bottom", "top"],
appendTo: () => document.querySelector(".frame-renderer") as HTMLElement,
flip: true,
flipOnUpdate: true,
getReferenceClientRect: props.clientRect,
appendTo: () => document.querySelector("#editor-container"),
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
container.addEventListener("scroll", () => {
popup?.[0].destroy();
});
},
onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
component?.updateProps(props);
@@ -230,10 +231,20 @@ export const IssueListRenderer = () => {
popup?.[0].hide();
return true;
}
// @ts-ignore
return component?.ref?.onKeyDown(props);
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter", "Tab"];
if (navigationKeys.includes(props.event.key)) {
// @ts-ignore
component?.ref?.onKeyDown(props);
return true;
}
return false;
},
onExit: (e) => {
const container = document.querySelector(".frame-renderer") as HTMLElement;
if (container) {
container.removeEventListener("scroll", () => {});
}
popup?.[0].destroy();
setTimeout(() => {
component?.destroy();
@@ -1,11 +1,3 @@
import { IssueWidget } from "src/ui/extensions/widgets/issue-embed-widget/issue-widget-node";
import { IIssueEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types";
interface IssueWidgetExtensionProps {
issueEmbedConfig?: IIssueEmbedConfig;
}
export const IssueWidgetExtension = ({ issueEmbedConfig }: IssueWidgetExtensionProps) =>
IssueWidget.configure({
issueEmbedConfig,
});
export const IssueWidgetPlaceholder = () => IssueWidget.configure({});
@@ -1,77 +1,33 @@
// @ts-nocheck
import { useState, useEffect } from "react";
import { Button } from "@plane/ui";
import { NodeViewWrapper } from "@tiptap/react";
import { Avatar, AvatarGroup, Loader, PriorityIcon } from "@plane/ui";
import { Calendar, AlertTriangle } from "lucide-react";
import { Crown } from "lucide-react";
export const IssueWidgetCard = (props) => {
const [loading, setLoading] = useState<number>(1);
const [issueDetails, setIssueDetails] = useState();
useEffect(() => {
props.issueEmbedConfig
.fetchIssue(props.node.attrs.entity_identifier)
.then((issue) => {
setIssueDetails(issue);
setLoading(0);
})
.catch(() => {
setLoading(-1);
});
}, []);
const completeIssueEmbedAction = () => {
props.issueEmbedConfig.clickAction(issueDetails.id, props.node.attrs.title);
};
return (
<NodeViewWrapper className="issue-embed-component m-2">
{loading == 0 ? (
<div
onClick={completeIssueEmbedAction}
className={`${
props.selected ? "border-custom-primary-200 border-[2px]" : ""
} w-full cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs`}
>
<h5 className="text-xs text-custom-text-300">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
</h5>
<h4 className="break-words text-sm font-medium">{issueDetails.name}</h4>
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<div>
<PriorityIcon priority={issueDetails.priority} />
export const IssueWidgetCard = (props) => (
<NodeViewWrapper className="issue-embed-component m-2">
<div
className={`${
props.selected ? "border-custom-primary-200 border-[2px]" : ""
} w-full h-[100px] cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 shadow-custom-shadow-2xs`}
>
<h5 className="h-[20%] text-xs text-custom-text-300 p-2">
{props.node.attrs.project_identifier}-{props.node.attrs.sequence_id}
</h5>
<div className="relative h-[71%]">
<div className="h-full backdrop-filter backdrop-blur-[30px] bg-custom-background-80 bg-opacity-30 flex items-center w-full justify-between gap-5 mt-2.5 pl-4 pr-5 py-3 max-md:max-w-full max-md:flex-wrap relative">
<div className="flex gap-2 items-center">
<div className="rounded">
<Crown className="m-2" size={16} color="#FFBA18" />
</div>
<div>
<AvatarGroup size="sm">
{issueDetails.assignee_details.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} className={"m-0"} />
))}
</AvatarGroup>
<div className="text-custom-text text-sm">
Embed and access issues in pages seamlessly, upgrade to plane pro now.
</div>
{issueDetails.target_date && (
<div className="flex h-5 items-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs text-custom-text-100">
<Calendar className="h-3 w-3" strokeWidth={1.5} />
{new Date(issueDetails.target_date).toLocaleDateString()}
</div>
)}
</div>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
<Button>Upgrade</Button>
</a>
</div>
) : loading == -1 ? (
<div className="flex items-center gap-[8px] rounded border-2 border-[#D97706] bg-[#FFFBEB] pb-[10px] pl-[13px] pt-[10px] text-[#D97706]">
<AlertTriangle color={"#D97706"} />
{"This Issue embed is not found in any project. It can no longer be updated or accessed from here."}
</div>
) : (
<div className="w-full space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs">
<Loader className={"px-6"}>
<Loader.Item height={"30px"} />
<div className={"mt-3 space-y-2"}>
<Loader.Item height={"20px"} width={"70%"} />
<Loader.Item height={"20px"} width={"60%"} />
</div>
</Loader>
</div>
)}
</NodeViewWrapper>
);
};
</div>
</div>
</NodeViewWrapper>
);
@@ -34,9 +34,7 @@ export const IssueWidget = Node.create({
},
addNodeView() {
return ReactNodeViewRenderer((props: Object) => (
<IssueWidgetCard {...props} issueEmbedConfig={this.options.issueEmbedConfig} />
));
return ReactNodeViewRenderer((props: Object) => <IssueWidgetCard {...props} />);
},
parseHTML() {
@@ -1,9 +0,0 @@
export interface IEmbedConfig {
issueEmbedConfig: IIssueEmbedConfig;
}
export interface IIssueEmbedConfig {
fetchIssue: (issueId: string) => Promise<any>;
clickAction: (issueId: string, issueTitle: string) => void;
issues: Array<any>;
}
@@ -10,7 +10,7 @@ import { DocumentDetails } from "src/types/editor-types";
import { PageRenderer } from "src/ui/components/page-renderer";
import { getMenuOptions } from "src/utils/menu-options";
import { useRouter } from "next/router";
import { IEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types";
import { FixedMenu } from "src";
interface IDocumentEditor {
// document info
@@ -47,7 +47,6 @@ interface IDocumentEditor {
duplicationConfig?: IDuplicationConfig;
pageLockConfig?: IPageLockConfig;
pageArchiveConfig?: IPageArchiveConfig;
embedConfig?: IEmbedConfig;
}
interface DocumentEditorProps extends IDocumentEditor {
forwardedRef?: React.Ref<EditorHandle>;
@@ -75,17 +74,23 @@ const DocumentEditor = ({
duplicationConfig,
pageLockConfig,
pageArchiveConfig,
embedConfig,
updatePageTitle,
cancelUploadImage,
onActionCompleteHandler,
rerenderOnPropsChange,
}: IDocumentEditor) => {
// const [alert, setAlert] = useState<string>("")
const { markings, updateMarkings } = useEditorMarkings();
const [sidePeekVisible, setSidePeekVisible] = useState(true);
const router = useRouter();
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = React.useState<() => void>(() => {});
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
// loads such that we can invoke it from react when the cursor leaves the container
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
};
const editor = useEditor({
onChange(json, html) {
updateMarkings(json);
@@ -104,7 +109,7 @@ const DocumentEditor = ({
cancelUploadImage,
rerenderOnPropsChange,
forwardedRef,
extensions: DocumentEditorExtensions(uploadFile, embedConfig?.issueEmbedConfig, setIsSubmitting),
extensions: DocumentEditorExtensions(uploadFile, setHideDragHandleFunction, setIsSubmitting),
});
if (!editor) {
@@ -145,13 +150,17 @@ const DocumentEditor = ({
documentDetails={documentDetails}
isSubmitting={isSubmitting}
/>
<div className="flex h-full w-full overflow-y-auto">
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-72">
<div className="flex-shrink-0 md:hidden border-b border-custom-border-200 pl-3 py-2">
{uploadFile && <FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />}
</div>
<div className="flex h-full w-full overflow-y-auto frame-renderer">
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-72 hidden md:block">
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
</div>
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
<div className="h-full w-full md:w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)] page-renderer">
<PageRenderer
onActionCompleteHandler={onActionCompleteHandler}
hideDragHandle={hideDragHandleOnMouseLeave}
readonly={false}
editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames}
@@ -77,7 +77,7 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
}
return (
<div className="flex items-center divide-x divide-custom-border-200">
<div className="flex flex-wrap items-center divide-x divide-custom-border-200">
<div className="flex items-center gap-0.5 pr-2">
{basicMarkItems.map((item) => (
<button
@@ -4,12 +4,11 @@ import { useState, forwardRef, useEffect } from "react";
import { EditorHeader } from "src/ui/components/editor-header";
import { PageRenderer } from "src/ui/components/page-renderer";
import { SummarySideBar } from "src/ui/components/summary-side-bar";
import { IssueWidgetExtension } from "src/ui/extensions/widgets/issue-embed-widget";
import { IEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types";
import { useEditorMarkings } from "src/hooks/use-editor-markings";
import { DocumentDetails } from "src/types/editor-types";
import { IPageArchiveConfig, IPageLockConfig, IDuplicationConfig } from "src/types/menu-actions";
import { getMenuOptions } from "src/utils/menu-options";
import { IssueWidgetPlaceholder } from "../extensions/widgets/issue-embed-widget";
interface IDocumentReadOnlyEditor {
value: string;
@@ -29,7 +28,6 @@ interface IDocumentReadOnlyEditor {
message: string;
type: "success" | "error" | "warning" | "info";
}) => void;
embedConfig?: IEmbedConfig;
}
interface DocumentReadOnlyEditorProps extends IDocumentReadOnlyEditor {
@@ -51,7 +49,6 @@ const DocumentReadOnlyEditor = ({
pageDuplicationConfig,
pageLockConfig,
pageArchiveConfig,
embedConfig,
rerenderOnPropsChange,
onActionCompleteHandler,
}: DocumentReadOnlyEditorProps) => {
@@ -63,7 +60,7 @@ const DocumentReadOnlyEditor = ({
value,
forwardedRef,
rerenderOnPropsChange,
extensions: [IssueWidgetExtension({ issueEmbedConfig: embedConfig?.issueEmbedConfig })],
extensions: [IssueWidgetPlaceholder()],
});
useEffect(() => {
@@ -105,11 +102,11 @@ const DocumentReadOnlyEditor = ({
documentDetails={documentDetails}
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
/>
<div className="flex h-full w-full overflow-y-auto">
<div className="flex h-full w-full overflow-y-auto frame-renderer">
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-80">
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
</div>
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)] page-renderer">
<PageRenderer
onActionCompleteHandler={onActionCompleteHandler}
updatePageTitle={() => Promise.resolve()}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor-extensions",
"version": "0.14.0",
"version": "0.15.1",
"description": "Package that powers Plane's Editor with extensions",
"private": true,
"main": "./dist/index.mjs",

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