Compare commits

...
Author SHA1 Message Date
sriram veeraghantaandGitHub a0b9596cb4 Merge pull request #4239 from makeplane/preview
chore:version update
2024-04-19 12:01:15 +05:30
sriram veeraghanta c1e5c0d4eb chore:version update 2024-04-19 11:59:30 +05:30
sriram veeraghantaandGitHub f71e8a3a0f Merge pull request #4238 from makeplane/preview
release: v0.18-dev
2024-04-19 11:56:03 +05:30
Prateek ShouryaandGitHub 086d14609c [WEB-1028] fix: enable comments and issue reactions for guest/ viewer. (#4231) 2024-04-18 20:36:24 +05:30
NikhilandGitHub cb9fda8557 fix: caching login when member joins a workspace from the email (#4237) 2024-04-18 20:34:59 +05:30
sriram veeraghanta 7da2cb143f Merge branch 'preview' of github.com:makeplane/plane into develop 2024-04-17 20:33:18 +05:30
Aaryan KhandelwalandGitHub 0d70960e4e [WEB-1026] chore: clear search query on project change (#4225)
* chore: clear search query on project change

* fix: project query search
2024-04-17 20:32:25 +05:30
Anmol Singh BhatiaandGitHub 0c0bdd6969 [WEB-999] fix: inbox sentry error and accept modal fix (#4226)
* fix: inbox issue parent id sentry error

* fix: inbox issue accept parent id fix
2024-04-17 20:31:47 +05:30
Anmol Singh BhatiaandGitHub cdc73cedab chore: sign in page improvement (#4224) 2024-04-17 19:46:35 +05:30
bf9049a7a2 [WEB-987] fix: inbox issue activity (#4208)
* chore: inbox issue activity

* chore: dummy data script

* chore: inbox issue activity implementation

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-04-17 19:43:22 +05:30
Aaryan KhandelwalandGitHub f0cb48006f [WEB-1024] fix: textarea component auto-resize (#4221)
* chore: updated resize hook logic

* fix: page title overflow issue

* chore: add length validation to page title
2024-04-17 19:41:34 +05:30
rahulrameshaandGitHub 10ed12e589 [WEB-1005] chore: pragmatic drag n drop implementation for labels (#4223)
* pragmatic drag n drop implementation for labels

* minor code quality improvements
2024-04-17 18:20:02 +05:30
Bavisetti NarayanandGitHub 68c870b791 [WEB-1023] chore: page mention transactions (#4222)
* chore: null validation for old value in pages

* chore: page transaction activity

* chore: serialized description_html
2024-04-17 18:18:14 +05:30
Anmol Singh BhatiaandGitHub 8e764004f0 fix: draft issue parent select query params (#4218) 2024-04-17 14:53:24 +05:30
1880eb7704 [WEB-1019] chore: error state for unauthorized pages (#4219)
* chore: private page access

* chore: add error state for private pages

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-04-17 14:53:01 +05:30
M. PalanikannanandGitHub 1080094b01 fix: tab key behaviour fixed for tabIndex containing editors with lists' and code blocks' tab key (#4216) 2024-04-16 21:48:30 +05:30
Anmol Singh BhatiaandGitHub d307c727ea fix: profile activity application error (#4214) 2024-04-16 20:35:37 +05:30
M. PalanikannanandGitHub ec5667194f fix: unnecessary focus in editor on swr sync (#4215) 2024-04-16 20:35:10 +05:30
Anmol Singh BhatiaandGitHub 1549f3a808 chore: calendar layout quick add menu placement improvement (#4213) 2024-04-16 20:24:13 +05:30
sriram veeraghanta 0705d2559b fix: issue template version from text area to input 2024-04-16 18:58:05 +05:30
sriram veeraghanta a36836abd7 fix: issue template update 2024-04-16 18:55:51 +05:30
27db8699c8 [WEB-480] fix: code block paste from VSCode and indentation (#4198)
* fix: stroing the transactions in page

* fix: page details changes

* chore: page response change

* chore: removed duplicated endpoints

* chore: optimised the urls

* chore: removed archived and favorite pages

* chore: revamping pages store and components

* mentions loading state part done

* fixed mentions not showing in modals

* removed comments and cleaned up types

* removed unused types

* reset: head

* chore: pages store and component updates

* style: pages list item UI

* fix: improved colors and drag handle width

* fix: slash commands are no more shown in the code blocks

* fix: cleanup/hide drag handles post drop

* fix: hide/cleanup drag handles post drag start

* fix: aligning the drag handles better with the node post css changes of the length

* fix: juggling back and forth of drag handles in ordered and unordered lists

* chore: fix imports, ts errors and other things

* fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes

For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes

* chore: clearNodes after delete in case of selections being present

* fix: hiding link selector in the bubble menu if inline code block is selected

* chore: filtering, ordering and searching implemented

* chore: updated pages store and updated UI

* chore: new core editor just for document editor created

* chore: removed setIsSubmitting prop in doc editor

* fix: fixed submitting state for image uploads

* refactor: setShouldShowAlert removed

* refactor: rerenderOnPropsChange prop removed

* chore: type inference magic in ref to expose an api for controlling editor menu items from outside

* fix: naming imports

* chore: change names of the exposed functions and removing old types

* refactor: remove debouncedUpdatesEnabled prop;

* refactor: editor heading markings now parsed using html

* chore: removed unrelated components from the document editor

* refactor: page details granular components

* fix: remove onActionCompleteHandler

* refactor: removed rerenderOnProps change prop

* feat: added getMarkDown function

* chore: update dropdown option actions

* fix: sidebar markings update logic

* chore: add image and to-do list actions to the toolbar

* fix: handling refs and populating them via callbacks

* feat: scroll to node api exposed

* cleaning up editor refs when the editor is destroyed

* feat: scrolling added to read only instance of the editor

* fix: markings logic

* fix: build errors with types

* fix: build erros

* fix: subscribing to transactions of editor via ref

* chore: remove debug statements

* fix: type errors

* fix: temporary different slash commands for document editor

* chore: inline code extension style

* chore: remove border from readOnly editor

* fix: editor bottom padding

* chore: pages improvements

* chore: handle Enter key on the page title

* feat: added loading indicator logic in mentions

* fix: mentions and slash commands now work well with multiple editors in one place

* refactor: page store structure, filtering logic

* feat: added better seperation in inline code blocks

* feat: list autojoining added

* fix: pages folder structure

* fix: image refocus from external parts

* working lists somewhat

* chore: implement page reactions

* fix: build errors

* fix: build errors

* fixed drag handles stuff

* task list item fixed

* working

* fix: working on multiple nested lists

* chore: remove debug statements

* fix: Tab key on first list item handled to not go out of editor focus

* feat: threshold auto scroll support added and multi nested list selection fixed

* fix: caret color bug with improved inline code blocks

* fix: node range error when bulk deleting with list

* fix: removed slash commands from working in code blocks

* chore: update typography margins

* chore: new field added in page model

* fix: better type inference in slash commands

* chore: code block UI

* feat: image insertion at correct position using ref added

* feat: added improved mentions support for space

* fix: type errors in mentions for comments in web app

* sync: core with document-core

* fix: build errors

* fix: fallback for appendTo not being able to find active container instantly

* fix: page store

* fix: page description

* fix: css quality issues

* chore: code cleanup

* chore: removed placeholder text in codeblocks

* chore: archived pages response change

* chore: archived pages response change

* fix: initial pages list fetch

* fix: pages list filters and ordering

* chore: add access change option in the quick actions dropdown

* fix: inline code block caret fixed

* regression: removing extra text

* chore: caret color removed

* feat: copy code button added in code blocks

* fix: initial load of page details

* fix: initial load of page details

* fix: image resizing weird behavior on click/expanding it too much fixed now

* chore: copy page response

* fix: todo list spacing

* chore: description html in the copy page

* chore: handle latest description on refetch

* fix: saner scroll behaviours

* fix: block menu positioning

* fix: updated empty string description

* feat: tab change sync support added

* fix: infinite rerendering with markings

* fix: block menu finally

* fix: intial load on reload bug fixed

* fix: nested lists alignment

* fix: editor padding

* fix: first level list items copyable

* chore: list spacing

* fix: title change

* fix: pages list block items interaction

* fix: saving chip position

* fix: delete action from block menu to focus properly

* fix: margin-bottom as 0 to avoid weird spacing when a paragraph node follows a list node

* style: table, chore: lite text editor toolbar

* fix: page description tab sync

* fix: lists spacing and alignment

* refactor: document editor props

* feat: rich text editor wrapper created and migrated core

* feat: created wrapper around lite text editor and merged core

* chore: add lite text editor toolbar

* fix: build errors

* fix: type errors and addead live updation of toolbar

* chore: pages migration

* fix: inbox issue

* refactor: remove redundant package

* refactor: unused files

* fix: add dompurify to space app

* fix: inline code margin

* fix: editor className props

* fix: build errors

* fix: traversing up the tree before assuming the parent is not a list item

* fix: drag handle positions for list items fixed

* fix: removed focus at end logic after deleting block

* fix: image wrapper overflow scroll fix with block menu's position

* fix: selection and deletion logic for nested lists fixed!!

* fix: hiding the block menu while scrolling in the document/app

* fix: merge conflicts resolved from develop

* fix: inbox issue description

* chore: move page title to the web app

* fix: handling edge cases for table selection

* chore: lint issues

* refactor: list item functions moved to same file

* refactor: use mention hook

* fix: added try catch blocks for mention suggestions

* chore: remove unused code

* fix: remove console logs

* fix: remove console logs

* fix: code block paste handler

* fix: tracking image uploading status to prevent sync rerenders

* chore: remove unnecessary props

* fix: code block pasting logic from vs code handled properly

* feat: additional checks for doc bounds

* fix: type of cancel button changed to button instead of default submit

* feat: editor focus on saved position while syncing via swr

* fix: type errors

* fix: changing names of plugins and removing packages

* fix: readonly editor synced

* removed console logs

* fix: stringifying instead of dom injection

* fix: use-editor try catch error handling

* fix: editor container click in try catch

* fix: some more error handling

* chore: removed commented out code

* fix: backspace error handling

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2024-04-16 18:50:45 +05:30
Anmol Singh BhatiaandGitHub 59772be014 fix: issue comment validation (#4211) 2024-04-16 18:50:26 +05:30
Anmol Singh BhatiaandGitHub 2ab4d99c15 chore: issue kanban cursor improvement (#4212) 2024-04-16 18:49:46 +05:30
Bavisetti NarayanandGitHub 50825b574b chore: instance admin script (#4210) 2024-04-16 18:06:46 +05:30
rahulrameshaandGitHub e952d88905 fix spreadsheet labels dropdown keyboard navigation (#4209) 2024-04-16 18:06:02 +05:30
sriram veeraghanta 247720b0d4 Merge branch 'develop' of github.com:makeplane/plane into develop 2024-04-16 16:41:29 +05:30
sriram veeraghanta dce3f4bce8 fix: upgrading turbo 2024-04-16 16:41:15 +05:30
c18265c7cf [WEB-480] fix: image uploading with swr sync fixed for pages (#4187)
* fix: stroing the transactions in page

* fix: page details changes

* chore: page response change

* chore: removed duplicated endpoints

* chore: optimised the urls

* chore: removed archived and favorite pages

* chore: revamping pages store and components

* mentions loading state part done

* fixed mentions not showing in modals

* removed comments and cleaned up types

* removed unused types

* reset: head

* chore: pages store and component updates

* style: pages list item UI

* fix: improved colors and drag handle width

* fix: slash commands are no more shown in the code blocks

* fix: cleanup/hide drag handles post drop

* fix: hide/cleanup drag handles post drag start

* fix: aligning the drag handles better with the node post css changes of the length

* fix: juggling back and forth of drag handles in ordered and unordered lists

* chore: fix imports, ts errors and other things

* fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes

For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes

* chore: clearNodes after delete in case of selections being present

* fix: hiding link selector in the bubble menu if inline code block is selected

* chore: filtering, ordering and searching implemented

* chore: updated pages store and updated UI

* chore: new core editor just for document editor created

* chore: removed setIsSubmitting prop in doc editor

* fix: fixed submitting state for image uploads

* refactor: setShouldShowAlert removed

* refactor: rerenderOnPropsChange prop removed

* chore: type inference magic in ref to expose an api for controlling editor menu items from outside

* fix: naming imports

* chore: change names of the exposed functions and removing old types

* refactor: remove debouncedUpdatesEnabled prop;

* refactor: editor heading markings now parsed using html

* chore: removed unrelated components from the document editor

* refactor: page details granular components

* fix: remove onActionCompleteHandler

* refactor: removed rerenderOnProps change prop

* feat: added getMarkDown function

* chore: update dropdown option actions

* fix: sidebar markings update logic

* chore: add image and to-do list actions to the toolbar

* fix: handling refs and populating them via callbacks

* feat: scroll to node api exposed

* cleaning up editor refs when the editor is destroyed

* feat: scrolling added to read only instance of the editor

* fix: markings logic

* fix: build errors with types

* fix: build erros

* fix: subscribing to transactions of editor via ref

* chore: remove debug statements

* fix: type errors

* fix: temporary different slash commands for document editor

* chore: inline code extension style

* chore: remove border from readOnly editor

* fix: editor bottom padding

* chore: pages improvements

* chore: handle Enter key on the page title

* feat: added loading indicator logic in mentions

* fix: mentions and slash commands now work well with multiple editors in one place

* refactor: page store structure, filtering logic

* feat: added better seperation in inline code blocks

* feat: list autojoining added

* fix: pages folder structure

* fix: image refocus from external parts

* working lists somewhat

* chore: implement page reactions

* fix: build errors

* fix: build errors

* fixed drag handles stuff

* task list item fixed

* working

* fix: working on multiple nested lists

* chore: remove debug statements

* fix: Tab key on first list item handled to not go out of editor focus

* feat: threshold auto scroll support added and multi nested list selection fixed

* fix: caret color bug with improved inline code blocks

* fix: node range error when bulk deleting with list

* fix: removed slash commands from working in code blocks

* chore: update typography margins

* chore: new field added in page model

* fix: better type inference in slash commands

* chore: code block UI

* feat: image insertion at correct position using ref added

* feat: added improved mentions support for space

* fix: type errors in mentions for comments in web app

* sync: core with document-core

* fix: build errors

* fix: fallback for appendTo not being able to find active container instantly

* fix: page store

* fix: page description

* fix: css quality issues

* chore: code cleanup

* chore: removed placeholder text in codeblocks

* chore: archived pages response change

* chore: archived pages response change

* fix: initial pages list fetch

* fix: pages list filters and ordering

* chore: add access change option in the quick actions dropdown

* fix: inline code block caret fixed

* regression: removing extra text

* chore: caret color removed

* feat: copy code button added in code blocks

* fix: initial load of page details

* fix: initial load of page details

* fix: image resizing weird behavior on click/expanding it too much fixed now

* chore: copy page response

* fix: todo list spacing

* chore: description html in the copy page

* chore: handle latest description on refetch

* fix: saner scroll behaviours

* fix: block menu positioning

* fix: updated empty string description

* feat: tab change sync support added

* fix: infinite rerendering with markings

* fix: block menu finally

* fix: intial load on reload bug fixed

* fix: nested lists alignment

* fix: editor padding

* fix: first level list items copyable

* chore: list spacing

* fix: title change

* fix: pages list block items interaction

* fix: saving chip position

* fix: delete action from block menu to focus properly

* fix: margin-bottom as 0 to avoid weird spacing when a paragraph node follows a list node

* style: table, chore: lite text editor toolbar

* fix: page description tab sync

* fix: lists spacing and alignment

* refactor: document editor props

* feat: rich text editor wrapper created and migrated core

* feat: created wrapper around lite text editor and merged core

* chore: add lite text editor toolbar

* fix: build errors

* fix: type errors and addead live updation of toolbar

* chore: pages migration

* fix: inbox issue

* refactor: remove redundant package

* refactor: unused files

* fix: add dompurify to space app

* fix: inline code margin

* fix: editor className props

* fix: build errors

* fix: traversing up the tree before assuming the parent is not a list item

* fix: drag handle positions for list items fixed

* fix: removed focus at end logic after deleting block

* fix: image wrapper overflow scroll fix with block menu's position

* fix: selection and deletion logic for nested lists fixed!!

* fix: hiding the block menu while scrolling in the document/app

* fix: merge conflicts resolved from develop

* fix: inbox issue description

* chore: move page title to the web app

* fix: handling edge cases for table selection

* chore: lint issues

* refactor: list item functions moved to same file

* refactor: use mention hook

* fix: added try catch blocks for mention suggestions

* chore: remove unused code

* fix: remove console logs

* fix: remove console logs

* fix: tracking image uploading status to prevent sync rerenders

* chore: remove unnecessary props

* fix: type of cancel button changed to button instead of default submit

* feat: editor focus on saved position while syncing via swr

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2024-04-16 15:52:31 +05:30
M. PalanikannanandGitHub 480aa906de [WEB - 480] fix: horizontal rule drag and drop (#4200)
* fix: horizontal rule parsing (with backwards compatibility) and ensuring correct selections

* fix: table drag handle position

* fix: code block drag handle positioning fixes

* fix: blockquote duplication, selection and deletion fixed

* fix: possible range errors and type errors
2024-04-16 15:51:05 +05:30
rahulrameshaandGitHub b9314889d7 fix empty blocks due to virtualization in rare cases (#4207) 2024-04-16 15:50:06 +05:30
sriram veeraghanta 01f7328714 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-04-16 15:49:22 +05:30
rahulrameshaandGitHub 2311bd4f78 add a small min height for dropping issues incase tger are zero issues in the entire row of columns (#4206) 2024-04-16 15:48:32 +05:30
CoolCuandGitHub f729e46bb0 chore: fix some typos in comments (#4205)
Signed-off-by: CoolCu <coolcui@qq.com>
2024-04-16 15:48:11 +05:30
sriram veeraghanta 41d8005f0b Merge branch 'preview' of github.com:makeplane/plane into develop 2024-04-16 13:28:03 +05:30
17347013442andGitHub 320cb94e35 Update issue_details.ts (#4199)
You should implement the interface here, not yourself
2024-04-16 13:24:54 +05:30
Aaryan KhandelwalandGitHub 6eed2c202b chore: created a global mark as favorite component (#4203) 2024-04-16 13:23:50 +05:30
Anmol Singh BhatiaandGitHub 0a6d7713fb chore: issue comment empty state added (#4202) 2024-04-16 13:22:27 +05:30
Anmol Singh BhatiaandGitHub 18b573531f chore: member invite modal improvement (#4201) 2024-04-16 13:21:56 +05:30
sriram veeraghanta 963d078aba fix: codeql workflow update 2024-04-15 20:02:57 +05:30
Aaryan KhandelwalandGitHub abce0ed946 [WEB-735] chore: added a custom placeholder prop to all the editors (#4194)
* chore: added a custom placeholder prop to all the editors

* chore: inbox issue create modal placeholder
2024-04-15 20:00:02 +05:30
Anmol Singh BhatiaandGitHub 77b323f562 chore: cycle list page improvement (#4195) 2024-04-15 19:49:16 +05:30
Anmol Singh BhatiaandGitHub f4c528f535 chore: inbox issue attachment added (#4190) 2024-04-15 19:48:50 +05:30
NikhilandGitHub c670a31836 fix: remove caching when joining workspace (#4192) 2024-04-15 19:46:44 +05:30
54196aafc0 [WEB-989] chore: archived module and cycle sidebar details (#4191)
* chore: module and cycle archive

* fix: archived cycle sidebar details fetch

* fix: archived module sidebar details fetch

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-04-15 19:46:14 +05:30
7a4ee509da [WEB-999] chore: inbox issue bug fixes and improvement (#4188)
* chore: duplicate inbox issue header improvement

* chore: snooze inbox issue header action improvement

* fix: project inbox exception error

* chore: inbox issue modal improvement

* chore: code refactor

* chore: Updated the inbox issue mutation

---------

Co-authored-by: guru_sainath <gurusainath007@gmail.com>
2024-04-15 19:45:47 +05:30
Aaryan KhandelwalandGitHub 8454e4f1e0 [WEB-986] fix: editor slash command positioning (#4186)
* fix: table selected cell border

* chore: add syncing message when revalidating page data

* fix: slash command positioning

* fix: mentions list dropdown positioning
2024-04-15 19:45:03 +05:30
rahulrameshaandGitHub 7a21855ab6 [WEB-1004] feat: Pragmatic dnd implementation for Kanban (#4189)
* Pragmatic drag and drop implmentation of Kanban

* refactor pragmatic dnd implementation and fix bugs

* fix dnd for modules, cycles, draft and project views
2024-04-15 17:02:53 +05:30
sriram veeraghanta 384624a21b Merge branch 'develop' of github.com:makeplane/plane into preview 2024-04-15 12:54:06 +05:30
Aaryan KhandelwalandGitHub 7507cb0a0f [WEB-994] fix: pages list mutation between projects (#4179)
* fix: pages list mutation between projects

* fix: page tab logic

* chore: remove pageType from the project pages store

* chore: rename computed helper functions
2024-04-15 12:50:22 +05:30
Bavisetti NarayanandGitHub d58bc03db6 fix: pages and inbox issue in dummy data (#4177) 2024-04-15 12:49:58 +05:30
20b0edeaa6 [WEB-999] chore: updated UI improvements and workflow updates in the project inbox (#4180)
* chore: snoozed filter in the issue inbox filter

* chore: navigating to the next or previous issue when we accept, decline, or duplicate the issue in inbox

* chore: Implemented state, label, assignee and target_date in the inbox issue description and Implemented issue edit confirmation once we click accept the inbox issue

* chore: removed logs

* chore: inbox issue create response

* chore: update inbox issue response

* chore: updated inbox issue accept workflow and added issue properties in inbox issue create modal

* chore: resolved build errors and upgraded lucide react

* chore: updated inbox issue store hook

* chore: code cleanup and removed validation for inbox description

* fix: renamed the variable isLoading to loader in project-inbox store

* fix: updated set function for issue property update

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-04-15 12:49:14 +05:30
guru_sainathandGitHub a44a032683 [WEB-984] feat: integrated state filter in inbox issues filter (#4182)
* chore: added state filtering in the inbox filters

* chore: Clearing the issues when we apply filters
2024-04-15 12:48:43 +05:30
guru_sainathandGitHub 251a432d8a [WEB-1000] fix: handled edge case for color on custom analytics state_group (#4185)
* chore: handled edge case for color on custom analytics state_group

* fix: error handler
2024-04-13 22:18:34 +05:30
3e2355e223 [WEB-460] refactor: editors, chore: pages list improvement (#4090)
* fix: stroing the transactions in page

* fix: page details changes

* chore: page response change

* chore: removed duplicated endpoints

* chore: optimised the urls

* chore: removed archived and favorite pages

* chore: revamping pages store and components

* mentions loading state part done

* fixed mentions not showing in modals

* removed comments and cleaned up types

* removed unused types

* reset: head

* chore: pages store and component updates

* style: pages list item UI

* fix: improved colors and drag handle width

* fix: slash commands are no more shown in the code blocks

* fix: cleanup/hide drag handles post drop

* fix: hide/cleanup drag handles post drag start

* fix: aligning the drag handles better with the node post css changes of the length

* fix: juggling back and forth of drag handles in ordered and unordered lists

* chore: fix imports, ts errors and other things

* fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes

For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes

* chore: clearNodes after delete in case of selections being present

* fix: hiding link selector in the bubble menu if inline code block is selected

* chore: filtering, ordering and searching implemented

* chore: updated pages store and updated UI

* chore: new core editor just for document editor created

* chore: removed setIsSubmitting prop in doc editor

* fix: fixed submitting state for image uploads

* refactor: setShouldShowAlert removed

* refactor: rerenderOnPropsChange prop removed

* chore: type inference magic in ref to expose an api for controlling editor menu items from outside

* fix: naming imports

* chore: change names of the exposed functions and removing old types

* refactor: remove debouncedUpdatesEnabled prop;

* refactor: editor heading markings now parsed using html

* chore: removed unrelated components from the document editor

* refactor: page details granular components

* fix: remove onActionCompleteHandler

* refactor: removed rerenderOnProps change prop

* feat: added getMarkDown function

* chore: update dropdown option actions

* fix: sidebar markings update logic

* chore: add image and to-do list actions to the toolbar

* fix: handling refs and populating them via callbacks

* feat: scroll to node api exposed

* cleaning up editor refs when the editor is destroyed

* feat: scrolling added to read only instance of the editor

* fix: markings logic

* fix: build errors with types

* fix: build erros

* fix: subscribing to transactions of editor via ref

* chore: remove debug statements

* fix: type errors

* fix: temporary different slash commands for document editor

* chore: inline code extension style

* chore: remove border from readOnly editor

* fix: editor bottom padding

* chore: pages improvements

* chore: handle Enter key on the page title

* feat: added loading indicator logic in mentions

* fix: mentions and slash commands now work well with multiple editors in one place

* refactor: page store structure, filtering logic

* feat: added better seperation in inline code blocks

* feat: list autojoining added

* fix: pages folder structure

* fix: image refocus from external parts

* working lists somewhat

* chore: implement page reactions

* fix: build errors

* fix: build errors

* fixed drag handles stuff

* task list item fixed

* working

* fix: working on multiple nested lists

* chore: remove debug statements

* fix: Tab key on first list item handled to not go out of editor focus

* feat: threshold auto scroll support added and multi nested list selection fixed

* fix: caret color bug with improved inline code blocks

* fix: node range error when bulk deleting with list

* fix: removed slash commands from working in code blocks

* chore: update typography margins

* chore: new field added in page model

* fix: better type inference in slash commands

* chore: code block UI

* feat: image insertion at correct position using ref added

* feat: added improved mentions support for space

* fix: type errors in mentions for comments in web app

* sync: core with document-core

* fix: build errors

* fix: fallback for appendTo not being able to find active container instantly

* fix: page store

* fix: page description

* fix: css quality issues

* chore: code cleanup

* chore: removed placeholder text in codeblocks

* chore: archived pages response change

* chore: archived pages response change

* fix: initial pages list fetch

* fix: pages list filters and ordering

* chore: add access change option in the quick actions dropdown

* fix: inline code block caret fixed

* regression: removing extra text

* chore: caret color removed

* feat: copy code button added in code blocks

* fix: initial load of page details

* fix: initial load of page details

* fix: image resizing weird behavior on click/expanding it too much fixed now

* chore: copy page response

* fix: todo list spacing

* chore: description html in the copy page

* chore: handle latest description on refetch

* fix: saner scroll behaviours

* fix: block menu positioning

* fix: updated empty string description

* feat: tab change sync support added

* fix: infinite rerendering with markings

* fix: block menu finally

* fix: intial load on reload bug fixed

* fix: nested lists alignment

* fix: editor padding

* fix: first level list items copyable

* chore: list spacing

* fix: title change

* fix: pages list block items interaction

* fix: saving chip position

* fix: delete action from block menu to focus properly

* fix: margin-bottom as 0 to avoid weird spacing when a paragraph node follows a list node

* style: table, chore: lite text editor toolbar

* fix: page description tab sync

* fix: lists spacing and alignment

* refactor: document editor props

* feat: rich text editor wrapper created and migrated core

* feat: created wrapper around lite text editor and merged core

* chore: add lite text editor toolbar

* fix: build errors

* fix: type errors and addead live updation of toolbar

* chore: pages migration

* fix: inbox issue

* refactor: remove redundant package

* refactor: unused files

* fix: add dompurify to space app

* fix: inline code margin

* fix: editor className props

* fix: build errors

* fix: traversing up the tree before assuming the parent is not a list item

* fix: drag handle positions for list items fixed

* fix: removed focus at end logic after deleting block

* fix: image wrapper overflow scroll fix with block menu's position

* fix: selection and deletion logic for nested lists fixed!!

* fix: hiding the block menu while scrolling in the document/app

* fix: merge conflicts resolved from develop

* fix: inbox issue description

* chore: move page title to the web app

* fix: handling edge cases for table selection

* chore: lint issues

* refactor: list item functions moved to same file

* refactor: use mention hook

* fix: added try catch blocks for mention suggestions

* chore: remove unused code

* fix: remove console logs

* fix: remove console logs

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
2024-04-11 21:28:59 +05:30
sriram veeraghanta 8b6035d315 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-04-11 20:27:44 +05:30
Bavisetti NarayanandGitHub b90ca97461 [WEB-983] chore: dummy data script (#4173)
* chore: dummy data script

* fix: lint issues

* chore: removed print statment
2024-04-11 20:27:05 +05:30
Anmol Singh BhatiaandGitHub b0ab17021f [WEB-943] chore: inbox cancelled issue header action improvement (#4172)
* chore: inbox cancelled issue header action improvement

* chore: code refactor
2024-04-11 20:26:23 +05:30
rahulrameshaandGitHub cd395fa3d2 [WEB-905] fix: make entire Kanban issue block clickable (#4175)
* make entire block clickable

* left align title tooltip on kanban block

* fix minor issue with kanban issue title

* disable click inside the quick actions
2024-04-11 20:21:03 +05:30
guru_sainathandGitHub 9e3fedd0df chore: Handled inbox issue sorting locally in inbox store (#4169) 2024-04-10 21:29:21 +05:30
Anmol Singh BhatiaandGitHub 5250c64be5 [WEB-937] fix: inbox issue hook render issue and code refactor (#4168)
* fix: inbox issue hook render issue and code refactor

* fix: inbox issue hook render issue and code refactor
2024-04-10 21:28:48 +05:30
Aaryan KhandelwalandGitHub 5c6170507e fix: drag handle position in the peek overview (#4167) 2024-04-10 21:28:06 +05:30
Anmol Singh BhatiaandGitHub 4f15c03f91 fix: api token expiry time (#4165) 2024-04-10 21:27:22 +05:30
Nico FrankeandGitHub 43534dcaed fix: email ssl setting not getting saved (#4161) 2024-04-10 20:01:22 +05:30
guru_sainathandGitHub cfbc1a91af chore: optimised issue loaders and added enum for issue status and current tab (#4164) 2024-04-10 19:55:16 +05:30
Anmol Singh BhatiaandGitHub 7aa1d750ea chore: issue comment mention user improvement (#4163) 2024-04-10 19:52:10 +05:30
Anmol Singh BhatiaandGitHub 91ed27837e chore: kanban card padding improvement (#4162) 2024-04-10 19:51:13 +05:30
f45c2d12fd [WEB-927, WEB-928] fix: inbox issue bug fixes and improvement (#4160)
* chore: inbox duplicate issue modal improvement

* chore: handled tab navigation in inbox issues and handled cross project inbox issues

* chore: fetch inbox issue activity once the issue is updated in inbox issue

* chore: disable duplicate inbox issue actions

* chore: duplicate issue mutation in the inbox issue

* chore: inbox create modal sidebar tab change updated

* chore: multiple date selection in the inbox issue filters

* chore: code refactor

* chore: removed project dependancy on the inbox store structure

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-04-10 16:08:31 +05:30
d0cb00f28a [WEB-926] fix: issue description component update handled in peek overview, issue detail, and inbox issues (#4159)
* fix: issue description mutation

* fix: implemented same issue description logic for issue detail and inbox issue description

* fix: fixed parent issue title dissapearing while loading the issue detail page

* chore: code cleanup

* chore: handled exception when issue in not available in issue detail in issue store

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-04-10 14:58:46 +05:30
sriram veeraghanta c80638090f Merge branch 'preview' of github.com:makeplane/plane into preview 2024-04-10 14:04:38 +05:30
Anmol Singh BhatiaandGitHub 549790ee8a [WEB-905] chore: kanban card icon color improvement (#4156)
* chore: kanban card icon color improvement

* chore: kanban card clickable area improvement
2024-04-10 14:03:22 +05:30
1dac70ecbe [WEB-406] chore: project inbox improvement (#4151)
* chore: inbox issue status pill improvement

* chore: loader inconsistancy resolved

* chore: accepted and decline inbox issue validation

* chore: removed clear all button in applied filters

* chore: inbox issue create label improvement

* chore: updated label filter

* chore: updated fetching activites and comments

* chore: inbox filters date

* chore: removed the print statement

* chore: inbox date filter updated

* chore: handled custom date filter in inbox issue query params

* chore: handled custom date filter in inbox issue single select

* chore: inbox custom date filter updated

* chore: inbox sidebar filter improvement

* chore: inbox sidebar filter improvement

* chore: duplicate issue detail

* chore: duplicate inbox issue improvement

* chore: lint issue resolved

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-04-10 13:52:57 +05:30
Prateek ShouryaandGitHub 3c2b2e3ed6 [WEB-924] chore: remove Add Issues button from completed cycles header. (#4153)
* [WEB-924] chore: remove `Add Issues` button from completed cycles header.

* fix: analytics button.
2024-04-10 13:02:56 +05:30
Anmol Singh BhatiaandGitHub f805acbcef chore: workspace invite loader improvement (#4152) 2024-04-09 19:18:43 +05:30
sriram veeraghantaandGitHub b349de556a chore: security policy updated 2024-04-09 17:53:09 +05:30
Anmol Singh BhatiaandGitHub 699017014e fix: workspace invite empty state flicker (#4150) 2024-04-09 15:04:16 +05:30
Prateek ShouryaandGitHub 2d4547601d [WEB-903] fix: issue subscription button mutation. (#4149) 2024-04-09 15:03:49 +05:30
Prateek ShouryaandGitHub 9b918b727a [WEB-918] style: update size of state dropdown button for consistency. (#4144) 2024-04-09 13:39:01 +05:30
Anmol Singh BhatiaandGitHub 74a88fc028 [WEB-904] chore: feature validation empty state (#4145)
* chore: disable feature empty state added

* chore: disable feature empty state updated
2024-04-09 13:38:07 +05:30
Prateek ShouryaandGitHub e86397b649 [WEB-917] style: update urgent priority icon design. (#4146) 2024-04-09 13:37:18 +05:30
NikhilandGitHub 7e0520d1cf [WEB - 922]dev: fix workspace member caching (#4147)
* dev: fix workspace member caching

* fix: caching on debug
2024-04-09 13:36:08 +05:30
rahulrameshaandGitHub 95580d0c62 [WEB-881] fix: Sentry errors from previous build (#4142)
* Sentry Fix for "Non-Error promise rejection captured with value: Route change to url was aborted"

* Sentry fix for "undefined is not an object (evaluating 'n.response')"

* Possible Sentry Fix for "TypeError Function.entries(<anonymous>)"

* Possible Sentry fix for "null is not an object (evaluating 'e.type')"
2024-04-09 13:12:14 +05:30
guru_sainathandGitHub 03df410b52 fix: updated issue description rendering when we switch between two issues via sub-issue (#4143) 2024-04-08 20:23:16 +05:30
sriram veeraghanta c843a1757f fix: chat with us key handling 2024-04-08 19:39:43 +05:30
/dev/paulandGitHub 2ea6d70fac Changed the crips-wrapper to only use crisp when env is set (#4139) 2024-04-08 19:37:06 +05:30
/dev/paulandGitHub 7bec244a67 Added fonts loaded from Google Fonts locally (#4140)
* Added all fonts locally

* Included LICENSEs

* Changed code to match linter-rules
2024-04-08 19:29:05 +05:30
ddb07dbe5f [WEB-406] chore: project inbox revamp (#4141)
* chore: removed inbox id

* fix: inbox changes

* chore: resolved merge conflicts

* chore: inbox issue response changes

* chore: inbox issue filters

* fix: inbox implementation revamp

* fix: type fixes

* fix: pagination implementation

* fix: inbox fixes

* fix: pagination fixes

* fix: inbox Issues pagination fixes

* chore: triage state change

* fix: inbox fixes

* chore: filtering using boolean

* chore: total results in the pagination

* fix: inbox main content changes

* fix: develop pull fixes

* chore: resolved build erros in inbox issues

* dev: fix migrations

* chore: module, labels and assignee in inbox

* chore: inbox issue order by

* chore: inbox filters

* chore: inbox ui revamp

* chore: inbox type updated

* chore: updated filters

* chore: updated filter menmbers and date types in inbox issue filter

* chore: inbox issue filter updated

* chore: updated date filter in the inbox issue filter

* chore: moved the current tab state from local state to store

* chore: updated the filter and fetch request in the inbox issues

* chore: updated tab change handler

* chore: handled isEmpty in the issue filters query params

* chore: inbox sidebar updated

* chore: enabled create inbox issue in mobx

* chore: replaced the key inbox_status to status

* chore: inbox sidebar pagination

* chore: updated inbox issue services

* chore: inbox sidebar total count indicator

* chore: create inbox issue updated

* chore: updated inbox issue sidebar layout

* chore: rendering issue detail in inbox issue

* chore: inbox issue content updated

* chore: create inbox issue modal description improvement

* fix: updated delete functionality in inbox store

* chore: updated multiple inbox issue creation

* chore: handled loading, empty states and inbox user access permissions

* chore: updated rendering issues in the sidebar

* chore: inbox sidebar label improvement

* chore: handled empty states

* chore: disabled inbox empty state added

* chore: module, labels and assignee in list endpoint

* chore: labels in list endpoint

* chore: inboc issue serializer

* chore: representation in serializer

* chore: super function

* chore: inbox empty state updated

* chore: implemented applied filters

* chore: inbox empty state updated

* chore: update date formats in applied filters

* chore: inbox skeleton updated

* chore: ui changes in the siebar list item

* chore: removed the module and cycle ids

* chore: inbox sidebar tab

* chore: inbox actions

* chore: updated inbox issue header actions

* chore: updated inbox issue code cleanup

* chore: loader improvement

* chore: inbox sidebar improvement

* chore: inbox sidebar empty state flicker

* fix: inbox issue delete operation

* chore: inbox issue title and description update indicator added

* fix: resolved issue property rendering in initial load

* chore: inbox sidebar and detail header improvement

* fix: handling selected filter in the issue filters and applied filters

* chore: inbox issue detail improvement

* chore: inbox issue label updated

* chore: inbox issue sidebar improvement

* fix: handling issue description update when we move between the issues in inbox

* chore: removed inbox issue helpers file

* chore: boolean checked

* chore: resolved file change requests

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-04-08 19:11:47 +05:30
NikhilandGitHub 9b0949148f [WEB - 883] fix: external apis (#3975)
* fix: external apis

* dev: remove descrypt in email password

* dev: add email as field in user serializer

* dev: fix linting errors

* dev: push commit to enable build triggers

* fix: formatting errors

* dev: remove instance value extraction
2024-04-08 18:46:05 +05:30
Prateek ShouryaandGitHub 8d009187ab [WEB-916] fix: description editor missing when using create more button in create issue modal. (#4137)
* [WEB-916] fix: description editor missing when using create more button in create issue modal.

* chore: handle edge cases.

* chore: handle edge cases.
2024-04-08 18:44:04 +05:30
Prateek ShouryaandGitHub 39b5a58ce8 chore: remove placeholder from estimate property if no estimates are selected. (#4125) 2024-04-08 18:38:39 +05:30
Anmol Singh BhatiaandGitHub 986f81e3ae [WEB-905] chore: issue peek overview and kanban layout improvement (#4135)
* chore: peek overview and kanban card improvement

* chore: peek overview improvement
2024-04-08 18:38:05 +05:30
Prateek ShouryaandGitHub fd2cacb0cd [WEB-854] style: update height of sidebar help section and quick add across all layout for consitency. (#4138) 2024-04-08 18:35:35 +05:30
NikhilandGitHub 4f138ac3f9 [WEB - 908] chore: remove user dependency in dockerfile (#4124)
* chore: remove user dependency from api container

* dev: remove user assignment from dockerfile

* dev: add logging configuration
2024-04-08 15:01:17 +05:30
NikhilandGitHub 2c8c139c62 [WEB - 898] fix: external webhooks (#4119)
* fix: project webhooks

* fix: issue and issue comment webhooks

* dev: remove module and cycle issue webhooks

* fix: linting errors
2024-04-08 14:59:50 +05:30
sriram veeraghanta 57f2445bb8 fix: ingest events 2024-04-06 17:13:24 +05:30
rahulrameshaandGitHub 90609b306f [WEB-914]: fix: Exception due to cycles and modules for undefined fields (#4127)
* fix cycle types

* fix module types
2024-04-05 20:05:55 +05:30
Bavisetti NarayanandGitHub 62dac421dc chore: archived cycles and mpdules (#4126) 2024-04-05 19:24:24 +05:30
guru_sainathandGitHub 3742ea91bf fix: issue description was not rendering in the issue create/edit modal (#4122) 2024-04-04 17:24:16 +05:30
Anmol Singh BhatiaandGitHub b4cc58d5dd [WEB-756] chore: spreadsheet layout cycle and module feature toggle validation (#4121)
* chore: spreadsheet layout cycle and module feature toggle validation added

* chore: project analytics cycle and module feature toggle validation added
2024-04-04 15:49:25 +05:30
CrsiandGitHub 71b73000d2 Dropped version info from docker-compose files (#4096)
According to the docker issue 11628
(https://github.com/docker/compose/issues/11628)
the 'version' field in docker-compose files is outdated.
It shows a warning like the following on hosts with a newer
Docker version:

```
WARN[0000] /srv/plane/docker-compose.yaml: `version` is obsolete
```

Also, the specs itself state the version was only informative:
https://github.com/compose-spec/compose-spec/blob/master/spec.md#version-and-name-top-level-elements

> The top-level `version` property is defined by the Compose
> Specification for backward compatibility. It is only informative.
> Compose doesn't use version to select an exact schema to
> validate the Compose file, but prefers the most recent schema
> when it's implemented.
2024-04-04 14:37:50 +05:30
Prateek ShouryaandGitHub 4cba7ff2f5 [WEB-848] fix: issue with parent child relation not being removed parent select dropwdown. (#4120) 2024-04-04 14:24:01 +05:30
NikhilandGitHub 840bc51537 fix: errors that were catched in sentry (#4114) 2024-04-03 21:05:33 +05:30
Prateek ShouryaandGitHub 0ab03c963c [WEB-869] chore: fix assignee and parent ui inconsistency in create issue modal. (#4113) 2024-04-03 20:57:44 +05:30
Prateek ShouryaandGitHub 1fb8791941 chore: increase data dropdown close icon size for consitency. (#4115) 2024-04-03 20:51:22 +05:30
Anmol Singh BhatiaandGitHub e9518ced89 [WEB-756] chore: module and cycle feature toggle validation (#4112)
* chore: cycle and module feature issue block validation

* chore: cycle and module feature display properties validation

* chore: cycle and module feature display filters validation

* chore: cycle and module feature project view validation
2024-04-03 20:49:02 +05:30
Anmol Singh BhatiaandGitHub bc0752f7e8 [WEB-850] chore: calendar layout add existing issue (#4094)
* chore: calendar layout add existing issue added in project issue

* chore: code refactor
2024-04-03 20:46:19 +05:30
Prateek ShouryaandGitHub 4c97098218 chore: archival of modules, cycles and issues enhancement. (#4100) 2024-04-03 18:19:34 +05:30
Anmol Singh BhatiaandGitHub 91d85ffed0 chore: issue block improvement (#4093) 2024-04-03 18:07:28 +05:30
Prateek ShouryaandGitHub d485446ee2 [WEB-652] fix: issue activities alignment in dashboard. (#4106) 2024-04-03 18:06:46 +05:30
Anmol Singh BhatiaandGitHub 92fd7b6977 chore: peek overview remove parent improvement (#4059) 2024-04-03 18:04:35 +05:30
Anmol Singh BhatiaandGitHub e7fc942514 [WEB-830] chore: project filter dropdown custom date select option improvement (#4069)
* chore: project filter dropdown custom date select option improvement

* fix: project filter custom date
2024-04-03 18:03:16 +05:30
Prateek ShouryaandGitHub 68ebcfd04e [842] chore: disable comments and reactions in archived issues. (#4101) 2024-04-03 18:02:07 +05:30
Prateek ShouryaandGitHub fed5916907 [WEB-848] fix: issue with parent child relation not being removed from update issue modal. (#4103) 2024-04-03 18:01:11 +05:30
sriram veeraghantaandGitHub 002fb4547b Merge pull request #4107 from makeplane/preview
release: v0.17-dev
2024-04-02 20:07:48 +05:30
sriram veeraghanta 204e4a8c6d fix: nextjs config sentry 2024-04-02 16:45:18 +05:30
Prateek ShouryaandGitHub bd470f7a90 chore: add Get started button to redirect to /god-mode to set up instance. (#4105) 2024-04-02 14:05:12 +05:30
Anmol Singh BhatiaandGitHub 84e73291cc fix: draft issue title logic (#4104) 2024-04-01 17:53:40 +05:30
SamandGitHub 1423955a34 docs: fix incorrect Docker guide link (#4098) 2024-04-01 16:13:36 +05:30
Prateek ShouryaandGitHub a771bab64c fix: display error message in project settings page. (#4099)
* chore: update `project title` to `project name` in create project modal.
2024-04-01 13:48:12 +05:30
M. PalanikannanandGitHub fafdac68f0 [WEB-876] fix: Multiple editor instances cause weird issues with Tippy (#4092)
* fix: tippy rendered properly when there are multiple editor instances

* enhancement: using cn to merge classes conditionally
2024-04-01 12:04:01 +05:30
Anmol Singh BhatiaandGitHub 3a466cfe40 [WEB-838] fix: cycle dropdown fetch (#4076)
* fix: workspace issue properties fetch

* fix: issue modal cycle fetch issue

* fix: issue modal cycle fetch issue

* chore: project issue properties hook updated
2024-03-28 20:29:35 +05:30
Manish GuptaandGitHub 86715f5b20 Chore: self host enhancements (#4091)
* latest changed to stable and .env changed to plane.env

* env changes

* script fix

* env changes

* sed fix

* codacy proposed fixes
2024-03-28 15:53:37 +05:30
Prateek ShouryaandGitHub 97fb08928f [WEB-834] fix: close shortcut modle on outside click. (#4078) 2024-03-28 15:04:56 +05:30
Prateek ShouryaandGitHub 8bdf8fa24f [WEB-836] fix: color picker overlapping with themes dropdown issue. (#4079) 2024-03-28 15:04:48 +05:30
Prateek ShouryaandGitHub f1530688f8 [WEB-843] fix: chat with us option not woking in command k modal. (#4080) 2024-03-28 15:04:41 +05:30
Prateek ShouryaandGitHub 265ce7778a [WEB-840] chore: remove make a copy option from draft issue quick action menu. (#4081) 2024-03-28 15:04:26 +05:30
Prateek ShouryaandGitHub 79d227b906 [WEB-839] chore: limit project identifier to 5 letters in project settings page. (#4082) 2024-03-28 15:04:16 +05:30
Prateek ShouryaandGitHub 5d4b3cea4c [WEB-833] chore: remove create a new issue option from command k modal if no projects are there. (#4083) 2024-03-28 14:32:16 +05:30
Prateek ShouryaandGitHub 2c510a7cb9 [WEB-852] chore: remove Estimate Demand data from the anaylytics. (#4084) 2024-03-28 14:31:52 +05:30
Prateek ShouryaandGitHub 478535422b [WEB-851] chore: update all instances of Dashboard with Home. (#4085) 2024-03-28 14:31:28 +05:30
sriram veeraghanta 35ca495f40 chore: remove develop builds on push 2024-03-28 14:05:04 +05:30
sriram veeraghanta 68fa7a485b chore: apiserver version changes 2024-03-28 13:59:52 +05:30
sriram veeraghanta ecfd3b68e3 chore: package version upgrade 2024-03-28 13:31:45 +05:30
Anmol Singh BhatiaandGitHub 9249e6d5b9 [WEB-849] fix: issue detail identifier and workspace settings (#4073)
* fix: issue detail identifier

* fix: workspace settings user role validation
2024-03-26 20:38:54 +05:30
NikhilandGitHub 30cee78170 dev: fix api security error (#4072) 2024-03-26 20:38:25 +05:30
NikhilandGitHub 4c46b075b4 fix: module endpoint validations (#4071) 2024-03-26 19:48:52 +05:30
sriram veeraghanta f3d9053d81 fix: unsafe external link fixes 2024-03-26 19:43:22 +05:30
sriram veeraghanta a136931168 fix: codeql workflow push and pull request chanages 2024-03-26 19:40:48 +05:30
sriram veeraghanta 5235b78cb8 fix: sentry config fixes 2024-03-26 19:24:42 +05:30
Bavisetti NarayanandGitHub 1c3619a4d6 chore: workspace estimate project filter (#4070) 2024-03-26 16:59:23 +05:30
Anmol Singh BhatiaandGitHub f3fd48dd43 chore: image picker popover improvement (#4068) 2024-03-26 16:30:57 +05:30
sriram veeraghanta c6f36a056b fix: adding workflow dispatch to codeql 2024-03-26 15:55:47 +05:30
Anmol Singh BhatiaandGitHub 51683e6b2f fix: active cycle stats empty state (#4067) 2024-03-26 15:53:52 +05:30
NikhilandGitHub fb189ca447 fix: cycle issue subquery filter when adding multiple assignees (#4066) 2024-03-26 15:49:44 +05:30
ea728a385f [WEB-831] fix: sentry issue and code refactor (#4063)
* chore: unnecessary console log removed

* chore: in_use sentry issue resolved

* chore: detail sentry issue resolved

* fix: updated project logo validation in project icon

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-03-26 15:13:26 +05:30
NikhilandGitHub 7452e40134 fix: optimize cycles endpoint for sql (#4065) 2024-03-26 15:08:00 +05:30
Bavisetti NarayanandGitHub 599809dd2e chore: removed project from workspace estimate (#4064) 2024-03-26 14:59:28 +05:30
Anmol Singh BhatiaandGitHub ad5559afe4 fix: peek overview delete modal (#4062) 2024-03-25 19:21:00 +05:30
11c3d4fbd8 chore: issue reaction keys (#4061)
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-03-25 19:20:33 +05:30
Ramesh Kumar ChandraandGitHub 20795db9e8 chore: readme update (#4060) 2024-03-25 19:19:55 +05:30
sriram veeraghanta 7e9daf8a20 fix: bad html filtering regexp 2024-03-25 18:54:55 +05:30
igeniandGitHub 07106f9161 fixed bug with missed color #FFFFFF (#4050) 2024-03-25 16:22:20 +05:30
Anmol Singh BhatiaandGitHub 4b8a1353fc fix: kanban layout quick add (#4058) 2024-03-25 16:20:26 +05:30
Anmol Singh BhatiaandGitHub 8205961d2a [WEB-826] chore: integrations (#4057)
* fix: workspace integration page

* chore: remove integrations and code refactor
2024-03-25 16:19:45 +05:30
sriram veeraghanta 53c63c8a2b Merge branch 'preview' of github.com:makeplane/plane into develop 2024-03-25 14:19:35 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
564d64a30e chore(deps): bump follow-redirects from 1.15.4 to 1.15.6 (#4056)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
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-03-25 14:19:02 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
961fdc17d5 chore(deps): bump django in /apiserver/requirements (#4055)
Bumps [django](https://github.com/django/django) from 4.2.10 to 4.2.11.
- [Commits](https://github.com/django/django/compare/4.2.10...4.2.11)

---
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-03-25 14:18:24 +05:30
Anmol Singh BhatiaandGitHub 7e0037b06b [WEB-823] fix: quick add flicker and dropdown placement in calendar layout (#4054)
* chore: placement prop added in quick action menu

* fix: calenar layout quick action menu placement

* fix: calendar quick add flicker
2024-03-25 14:17:34 +05:30
Anmol Singh BhatiaandGitHub 9f9e508bb7 [WEB-812] chore: project active cycle stats empty state (#4053)
* chore: empty state asset and config file updated

* chore: empty state asset and config file updated

* chore: active cycle empty state implementation
2024-03-25 13:16:53 +05:30
Bavisetti NarayanandGitHub 2429ac053d [WEB-820] fix: state count in cycle and module (#4052)
* fix: state count in cycle and module

* fix: issue manager change
2024-03-25 13:15:53 +05:30
Aaryan KhandelwalandGitHub 31b2fa2c14 fix: profile activity load more button (#4049) 2024-03-22 20:46:13 +05:30
guru_sainathandGitHub ec837a42d5 fix: handled undefined condition on issue title when we are converting into draft (#4048) 2024-03-22 20:06:49 +05:30
guru_sainathandGitHub b5ed602e05 fix: disbaled the issue property check in the issue create/edit modal (#4038) 2024-03-22 18:46:38 +05:30
sriram veeraghantaandGitHub 9652d56e49 chore: removing deepsource.toml file (#4046) 2024-03-22 18:42:33 +05:30
Prateek ShouryaandGitHub 2f637fc9f1 fix: project total issue count mutation. (#4045) 2024-03-22 18:39:26 +05:30
Prateek ShouryaandGitHub 95d2b6f1c1 style: hide cycle and module dropdown placeholder from list and kanban view. (#4044) 2024-03-22 18:39:06 +05:30
5c7886d7f3 [WEB-811] chore: filter dropdown custom date select improvement (#4043)
* fix: handled custom start_date and target_date custom filter in project-issues

* chore: filter dropdown improvement

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-03-22 18:38:39 +05:30
sriram veeraghanta e4211e5817 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-03-22 18:38:03 +05:30
Aaryan KhandelwalandGitHub 1caceca1e7 [WEB-818] fix: sub-issue and attachment delete not working in the peek overview (#4042)
* fix: delete attachment not working in the peek overview

* fix: sub-issue delete not working

* fix: lint errors
2024-03-22 18:37:11 +05:30
Anmol Singh BhatiaandGitHub 53f3357149 chore: cycle list page alignment (#4041) 2024-03-22 18:36:34 +05:30
Anmol Singh BhatiaandGitHub cdf86391f0 fix: module filter and order by (#4040) 2024-03-22 18:36:09 +05:30
guru_sainathandGitHub 72392d5731 fix: resetting the changesMage when the createmore toggle is turned on in issue create-edit modal (#4039) 2024-03-22 18:35:40 +05:30
guru_sainathandGitHub db8cf1fb24 fix: handled quick actions in the project draft issues (#4034) 2024-03-22 18:35:06 +05:30
Lakhan BahetiandGitHub d262eb4ffb [WEB-800] fix: issue peekview, and detail page create label colour selector autoplacement (#4032)
* fix: issue peekview, and detail label colour selector autoplacement

* fix: build errors
2024-03-22 18:30:23 +05:30
guru_sainathandGitHub 60aea62739 fix: handled inital empty state close in the issue create/edit odal (#4033) 2024-03-22 18:28:44 +05:30
Anmol Singh BhatiaandGitHub baab6ce99f fix: project order by dropdown (#4037) 2024-03-22 18:26:44 +05:30
Anmol Singh BhatiaandGitHub 5aed04eb41 fix: module and cycle list page quick action (#4031) 2024-03-21 21:16:06 +05:30
guru_sainathandGitHub 165bec9aa4 fix: handled the empty issue propety, create more in edit modal, and moving drafts from issue functionality (#4030) 2024-03-21 21:06:55 +05:30
231fd52992 [WEB-447] feat: projects archive. (#4014)
* dev: project archive response

* feat: projects archive.

* dev: response changes for cycle and module

* chore: status message changed

* chore: update clear all applied display filters logic.

* style: archived project card UI update.

* chore: archive/ restore taost message update.

* fix: clear all applied display filter logic.

* chore: project empty state update to handle archived projects.

* chore: minor typo fix in cycles and modules archive.

* chore: close cycle/ module overview sidebar if it's already open when clicked on overview button.

* chore: optimize current workspace applied display filter logic.

* chore: update all `archived_at` type from `Date` to `string`.

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-03-21 20:59:34 +05:30
Lakhan BahetiandGitHub 9642b761b7 fix: issue card assignee empty state tooltip (#4029) 2024-03-21 20:59:08 +05:30
guru_sainathandGitHub 86ae79f851 fix: resolved rendering issues in list and kanban layout when we apply group-by filter (#4028) 2024-03-21 20:47:36 +05:30
Anmol Singh BhatiaandGitHub 991b8e7703 chore: custom month select modal validation added (#4027) 2024-03-21 20:47:22 +05:30
Anmol Singh BhatiaandGitHub 56d4e25431 fix: project list view dropdown placement (#4026) 2024-03-21 20:47:07 +05:30
Prateek ShouryaandGitHub 23f77328d4 fix: prevent custom menu button from opening cycle/ module details page when closed. (#4025) 2024-03-21 20:46:33 +05:30
guru_sainathandGitHub 58a45c96c0 fix: rendering default state in the issue create update modal whenever we switch between the projects in the issue modal (#4024) 2024-03-21 20:46:18 +05:30
Anmol Singh BhatiaandGitHub 15e04e55bb [WEB-692] fix: project modal cover image popover z-index fix (#4018)
* fix: project modal cover image popover z-index fix

* fix: project modal cover image popover z-index fix
2024-03-21 20:46:02 +05:30
Anmol Singh BhatiaandGitHub 8911eaf676 fix: profile cover fix (#4010) 2024-03-21 15:36:16 +05:30
sriram veeraghanta 0d2a8cca50 chore: project structuring 2024-03-21 14:58:16 +05:30
061be85a5d feat: cycles and modules archive. (#4005)
* fix: GET request changes

* fix: filtering changes

* feat: cycles and modules archive.

* chore: disable fetching of cycle/ module details when clicked on the card in archives page.

* chore: remove copy link button from archived modules/ cycles.

* fix: archived cycle and module loading fliker issue.

* chore: add validation to only archive completed cycles.

* chore: add validation to only archive completed or cancelled module.

* chore: archived issues/ cycles/ modules empty state update.

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-03-20 21:02:58 +05:30
Bavisetti NarayanandGitHub 4d1b5adfc4 dev: assignee leave changes (#4009) 2024-03-20 20:42:14 +05:30
3ff0f6187a [WEB-762] chore: cycle, module modal improvement and cycle, module (#4000)
* chore: cycle modal description improvement

* chore: updated the description ui in cycles and modules

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-03-20 20:41:49 +05:30
Anmol Singh BhatiaandGitHub 37c5ce54d5 fix: peek overview sub issue operation (#4002) 2024-03-20 20:00:34 +05:30
Anmol Singh BhatiaandGitHub 054dd2bb7d fix: module empty state (#4004) 2024-03-20 20:00:13 +05:30
Bavisetti NarayanandGitHub 180f1d74e8 dev: removed double issue count (#4008) 2024-03-20 19:59:31 +05:30
Lakhan BahetiandGitHub 6ab8588afb fix: create label inline overflow (#4006) 2024-03-20 19:32:42 +05:30
NikhilandGitHub e1f04356fc dev: add -p flag for the logs folder (#4007) 2024-03-20 19:28:19 +05:30
Lakhan BahetiandGitHub 4eefc7f692 [WEB-784] chore: create workspace fields as required indication (#4003)
* chore: create workspace fields as required indication

* style: gap between asterisk & text
2024-03-20 19:22:02 +05:30
7142889c23 [WEB-413] chore: project active cycle UI revamp (#3997)
* chore: project active cycle ui revamp

* chore: resolved liniting issues

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-03-20 19:19:14 +05:30
guru_sainathandGitHub bca3e13242 fix: project inifinite loader when we go the workspace projects for the first time (#3999) 2024-03-20 18:08:42 +05:30
guru_sainathandGitHub e02fa4d9e7 fix: autofocus on the title element when we are creating the bulk issues in the issue create modal (#3998) 2024-03-20 18:08:21 +05:30
guru_sainathandGitHub 621624e29f [WEB-383] fix: rendering state and members from different projects in sub-issues (#3996)
* fix: rendering the state and memebers from different projects in sub issues and exception handling while creating an issue from different project

* chore: handled different project issue properties in a new function handler
2024-03-20 18:07:35 +05:30
Lakhan BahetiandGitHub 4ea616f1cd fix: Labels overflow in peek, detail view (#3995) 2024-03-20 18:07:11 +05:30
Lakhan BahetiandGitHub 901a7703ff fix: project, module, cycle filter store infinite loop (#3994) 2024-03-20 18:06:45 +05:30
Bavisetti NarayanandGitHub 2f883e4939 [WEB-780]feat: project cycle module archive (#3990)
* dev: project cycle module archive

* fix: update filter logic updated
2024-03-20 13:59:31 +05:30
rahulrameshaandGitHub cb4cfa1dd5 [WEB-782] fix: Date timezone changes error (#3992)
* fix date related exceptions

* replace new Date from develop branch changes as well

* changes from self review

* wrap getDate logic with try catch to handle in case of error scenarios

* fix formatted time
2024-03-20 13:44:08 +05:30
guru_sainathandGitHub 7d3a96b3d0 [WEB-763] fix: workspace remains listed after leaving the workspace in the user profile (#3993)
* chore: build error

* fix: workspace not getting removed when user leaves the workspace
2024-03-20 13:43:18 +05:30
Wiebe VerweijandGitHub 0f79c6d7d8 Fix test_email command to use new SSL setting (#3988)
This was missed in pull request #3732 and/or the command didn't exist yet then ...
2024-03-20 12:46:40 +05:30
Lakhan BahetiandGitHub 6a245e121a fix: analytics dialog close on outside click (#3987) 2024-03-20 12:42:10 +05:30
NikhilandGitHub 293854fb3a fix: user caching when the user leaves/deletes the workspace (#3989) 2024-03-20 12:38:00 +05:30
sriram veeraghanta 53ddef1cd5 fix: eslint fixes and file formatting 2024-03-19 20:08:35 +05:30
sriram veeraghanta 473dfc7a5b fix: merge conflicts resolved 2024-03-19 18:00:43 +05:30
rahulrameshaandGitHub 1a462711e1 [WEB-761] fix: Time zone date misalignment (#3986)
* fix date time misalignment

* fix failing build

* fix gantt chart view position fix

* comments for getDate method

* remove new Date() where not required

* changes from my self review
2024-03-19 17:40:20 +05:30
sriram veeraghanta 3139f8e109 fix: merge conflicts resolved 2024-03-18 19:58:39 +05:30
sriram veeraghanta aa3702cd46 fix: codeql check workflow changes 2024-03-18 19:50:58 +05:30
sriram veeraghanta aed87ef472 fix: build branch workflow dispatch trigger changes 2024-03-18 19:48:44 +05:30
roujaandGitHub 1b742f66c6 Fix email configuration issue (#3732) 2024-03-18 19:43:00 +05:30
Lakhan BahetiandGitHub 568701881e fix: calendar-layout draggable wrapper condition (#3984) 2024-03-18 19:35:17 +05:30
Lakhan BahetiandGitHub 2dbea54d2a fix: Mobile header visibility (#3983) 2024-03-18 16:54:45 +05:30
NikhilandGitHub 82ba9833f2 dev: enable api logging (#2538)
* dev: enable api logging and control worker count through env

* dev: enable logger instead of printing

* dev: remove worker counts

* dev: enable global level log settings

* dev: add rotating logger

* fix: logging configuration

* dev: api logging and moving the capture exception to utils for logging and then capturing

* fix: information leaking through print logs

* dev: linting fix

* dev: logging configuration for django

* fix: linting errors

* dev: add logs for migrator

* dev: logging cofiguration

* dev: add permision for captain user in Plane

* dev: add log paths in compose

* dev: create directory for logs

* dev: fix linting errors
2024-03-18 14:27:02 +05:30
M. PalanikannanandGitHub 0759666b75 [WEB-755] fix: Clearing nodes to default node i.e. paragraph before converting it to other type (#3974)
* fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes

For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes

* chore: clearNodes after delete in case of selections being present

* fix: hiding link selector in the bubble menu if inline code block is selected
2024-03-18 12:51:35 +05:30
Lakhan BahetiandGitHub e9774e1af3 [WEB-759] style: added calendar layout responsiveness (#3969)
* style: added calendar layout responsiveness

* fix: quick-add-form

* fix: popover menu close on item select

* fix: class conditiion

* code review
2024-03-18 12:51:19 +05:30
Ramesh Kumar ChandraandGitHub 4a93fdbdb4 [WEB-700] chore: sidebar hamburger refactor (#3960) 2024-03-18 12:50:33 +05:30
Aaryan KhandelwalandGitHub 861a1c4132 [WEB-738] chore: conditionally render projects in the dropdown (#3952)
* chore: show authorized projects in the dropdown

* refactor: command palette logic

* chore: add helper function to check for project role

* fix: add preventDefault for shortcuts
2024-03-15 17:44:01 +05:30
Anmol Singh BhatiaandGitHub 5244ba72c9 [WEB-746] chore: draft issue modal improvement (#3966)
* chore: draft issue modal improvement

* chore: draft issue modal improvement

* chore: draft issue modal improvement

* chore: draft issue modal improvement
2024-03-15 17:43:42 +05:30
Anmol Singh BhatiaandGitHub 36bc7bf996 chore: app sidebar collapsed state alignment improvement (#3976) 2024-03-15 17:36:20 +05:30
Aaryan KhandelwalandGitHub 0789238282 fix: cycle layout not getting initialized (#3961) 2024-03-15 17:31:28 +05:30
Anmol Singh BhatiaandGitHub c17748eec2 chore: completed cycle empty state valdidation updated (#3959) 2024-03-15 17:30:26 +05:30
Anmol Singh BhatiaandGitHub 94f1e6d957 [WEB-698] chore: profile activity page scrollbar added (#3958)
* chore: profile activity page scrollbar added

* chore: profile activity page padding improvement
2024-03-15 17:29:46 +05:30
Aaryan KhandelwalandGitHub 13bbb9cde4 [WEB-699] chore: implement sub-issues and attachments in the peek overview (#3956)
* chore: implement sub-issues and attachments in the peek overview

* chore: add the same to full-screen view
2024-03-15 17:29:22 +05:30
Anmol Singh BhatiaandGitHub 92a077dce1 [WEB-504] chore: command k and issue relation modal empty state (#3955)
* chore: empty state asset updated

* chore: empty state asset updated

* chore: empty state config file updated

* chore: notification empty state updated

* chore: command-k, bulk delete and issue relation modal empty state updated

* chore: code refactor

* chore: code refactor
2024-03-15 17:28:45 +05:30
Lakhan BahetiandGitHub ed2e4ad6f7 fix: cycle & module peekview scroll (#3953) 2024-03-15 17:28:03 +05:30
Anmol Singh BhatiaandGitHub cbe5d9a047 chore: issue reaction response updated (#3951) 2024-03-15 17:26:38 +05:30
Ramesh Kumar ChandraandGitHub d25fcfdd88 [WEB-715] chore: closing custom dropdown on button click (#3950) 2024-03-15 17:25:57 +05:30
Aaryan KhandelwalandGitHub aaad37575b fix: cycle date check wrong projectId (#3972) 2024-03-14 20:11:10 +05:30
Aaryan KhandelwalandGitHub 6bc133e3b1 chore: swapped dates and status properties in the cycle list item (#3957) 2024-03-14 20:02:18 +05:30
Bavisetti NarayanandGitHub 42b524b16a chore: project active filter (#3971) 2024-03-14 19:22:54 +05:30
Anmol Singh BhatiaandGitHub 0bc4b6cece chore: ai modal disclamier added (#3970) 2024-03-14 19:16:07 +05:30
guru_sainathandGitHub 43c75f4457 fix: rendering Issue in kanban swimlanes view for unassigned cycle or Modules (#3965) 2024-03-14 12:43:29 +05:30
sriram veeraghanta 80761d3507 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-03-14 12:42:33 +05:30
sriram veeraghanta 9c13dbd957 chore: 1click docs readme update 2024-03-13 17:26:52 +05:30
sriram veeraghanta 8d9adf4d87 chore: 1click docs readme update 2024-03-13 17:26:04 +05:30
sriram veeraghanta 4ccb505f36 chore: 1click docs changes 2024-03-13 17:24:37 +05:30
sriram veeraghanta 884856b021 chore: 1click docs update 2024-03-13 17:18:11 +05:30
sriram veeraghanta 552c66457a chore: 1click docs update 2024-03-13 17:17:40 +05:30
P BandGitHub 1363ef0b2d Updated one-click deploy's readme for readability and presentation (#3962)
Changed title, first paragraph, and bullets.
2024-03-13 17:13:11 +05:30
guru_sainathandGitHub 898cf98be3 [WEB-749] fix: rendering empty states with "showEmptyGroup" filter in issue grouping (#3954)
* fix: Fixed show empty states in groupBy and subGroupBy in kanban

* lint: lint issue resolved
2024-03-13 16:59:12 +05:30
cb632408f9 [WEB-713] style: remove tooltips in mobile responsiveness (#3948)
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-03-12 20:39:36 +05:30
b930d98665 [WEB-554] feat: modules filtering, searching and ordering (#3947)
* feat: modules filtering, searching and ordering implemented

* fix: modules ordering

* chore: total issues in list endpoint

* fix: modules ordering

* fix: build errors

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-03-12 20:24:21 +05:30
Aaryan KhandelwalandGitHub 69e110f4a8 [WEB-578] feat: projects list filtering and ordering (#3926)
* style: project card UI updated

* dev: initialize project filter store and types

* chore: implemented filtering logic

* chore: implemented ordering

* chore: my projects filter added

* chore: update created at date filter options

* refactor: order by dropdown

* style: revert project card UI

* fix: project card z-index

* fix: members filtering

* fix: build errors
2024-03-12 19:36:40 +05:30
Lakhan BahetiandGitHub c3c6ef8830 chore: removed module tooltip from peek-overview & issue detail (#3946) 2024-03-12 19:13:04 +05:30
Anmol Singh BhatiaandGitHub 8aca74c68d [WEB-720] chore: reaction tooltip added (#3945)
* chore: reaction tooltip added

* chore: reaction tooltip updated

* chore: issue reaction tooltip updated

* chore: helper function updated
2024-03-12 19:12:25 +05:30
Prateek ShouryaandGitHub c97b994311 [WEB-717] fix: delete issue modal UI fixes. (#3944)
* fix: avoid closing the peek overview when the close button within the delete model popup is clicked.

* fix: distortion in issue detail sidebar UI when any modal is open.
2024-03-12 19:11:36 +05:30
Lakhan BahetiandGitHub 443b93f897 fix: label mutation in peek-overview & issue detail (#3942) 2024-03-12 19:10:35 +05:30
Aaryan KhandelwalandGitHub 730e556bea fix: emoji picker unnecessary scroll (#3941) 2024-03-12 19:09:51 +05:30
Aaryan KhandelwalandGitHub f77f4b8221 fix: gantt sidebar links (#3940) 2024-03-12 19:07:12 +05:30
Anmol Singh BhatiaandGitHub 5c4c3f5c04 fix: project page empty state (#3939) 2024-03-12 19:05:15 +05:30
Ramesh Kumar ChandraandGitHub 73c91654eb [WEB-716] style: list view issues responsiveness (#3938) 2024-03-12 12:29:00 +05:30
Bavisetti NarayanandGitHub 578bd29f6f chore: active issue total count (#3936) 2024-03-12 12:24:58 +05:30
NikhilandGitHub 2e5e14556d feat: test email script (#3937)
* feat: script for testing emails

* chore: rename subject and body for the mail
2024-03-12 12:00:18 +05:30
sriram veeraghanta ecac45e885 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-03-12 01:12:57 +05:30
sriram veeraghanta 6ec9c64f7c fix: build branch optional builds workflow changes 2024-03-12 01:06:37 +05:30
sriram veeraghanta f493a03f56 fix: adding github workflow events 2024-03-12 01:01:03 +05:30
Anmol Singh BhatiaandGitHub 48c9b78397 [WEB-545] chore: completed cycle empty state (#3931)
* chore: completed cycle issue transfer empty state added

* chore: cycle empty state variable updated

* chore: empty state config file updated
2024-03-11 21:23:09 +05:30
Anmol Singh BhatiaandGitHub 9c29ad1a28 chore: list layout scrollbar improvement (#3933) 2024-03-11 21:12:59 +05:30
Aaryan KhandelwalandGitHub e3ac075ee2 chore: error state for the issues widgets (#3934) 2024-03-11 21:12:28 +05:30
Anmol Singh BhatiaandGitHub 8c9d328c24 chore: profile setting scroll fix and scrollbar added in activity section (#3929) 2024-03-11 21:09:41 +05:30
Aaryan KhandelwalandGitHub b2146098e2 chore: added view less button to the collaborators widget (#3928) 2024-03-11 21:09:16 +05:30
NikhilandGitHub 4a06572f73 chore: remove deprecated envs (#3927) 2024-03-11 21:08:10 +05:30
M. PalanikannanandGitHub d34df5c805 fix: updated extensions in read only instance of the editor (#3925) 2024-03-11 21:05:27 +05:30
01702e9f66 [WEB-402] chore: project issues count (#3911)
* chore: added project issues count

* chore: project module and cycle issue count

* chore: resolved merge conflicts

* chore: added import statement

* chore: issue count type added

* chore: issue count added in project, cycle and module issues

* fix: lint fixes

* chore: tooltip added in issue count badge

* chore: tooltip added in issue count badge

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-03-11 21:05:00 +05:30
NikhilandGitHub b57c389c75 fix: caching (#3923) 2024-03-11 21:04:43 +05:30
Lakhan BahetiandGitHub 27acd1bec1 fix: cycle & module sidebar progress-stats scroll (#3932) 2024-03-11 21:03:30 +05:30
Anmol Singh BhatiaandGitHub 539afb0792 chore: issue quick action dropdown max height updated (#3919) 2024-03-11 21:02:28 +05:30
Anmol Singh BhatiaandGitHub 6c7ba3bc79 chore: applied filter ui improvement (#3918) 2024-03-11 21:01:59 +05:30
Anmol Singh BhatiaandGitHub c8ab650008 fix: module empty state create issue (#3916) 2024-03-11 21:01:27 +05:30
Anmol Singh BhatiaandGitHub 89d019df57 fix: parent select modal params updated (#3914) 2024-03-11 21:01:07 +05:30
535731141f [WEB-682] feat: cycles list filtering and searching (#3910)
* chore: implemented cycles list filters and ordering

* chore: active cycle tab updated

* refactor: cycles folder structure

* fix: name search inout auto-focus

* fix: cycles ordering

* refactor: move cycle filters logic to mobx store from local storage

* chore: show completed cycles in a disclosure

* chore: added completed cycles count

* refactor: cycles mapping logic

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-03-11 21:00:05 +05:30
4b30339a59 [WEB-638] feat: add tabIndex prop support to navigate using Tab key in all the editors (#3902)
* feat: added tab index support to navigate using Tab key in all the editors

* chore: changed the name of Table of Contents in Pages

* chore: file formatting

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-03-11 20:58:23 +05:30
899771a678 [WEB-434] feat: add support to insert a new empty line on clicking at bottom of the editor (#3856)
* fix: horizontal rule no more causes issues on last node

* fixed the mismatched transaction by using native tiptap stuff

* added support to add new line onclick at bottom if last node is an image

TODO: blockquote at last node

* fix: simplified adding node at end of the document logic

* feat: rewrite entire logic handling all cases

* feat: arrow down and arrow up keys add empty node at top and bottom of doc from first/last row's cells

* feat: added arrow up and down key support to images too

* remove unnecessary console logs

* chore: formatting components

* fix: reduced bottom padding to increase onclick area

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-03-11 20:55:24 +05:30
sriram veeraghanta 8997ee2e3e fix: eslint fixes 2024-03-11 20:45:51 +05:30
sriram veeraghanta cb78ccad1f fix: 1click deployment fixes 2024-03-08 16:58:37 +05:30
Manish Gupta 6c6b7156bb helm variable update 2024-03-08 16:00:18 +05:30
Manish Gupta 8cc372679c Web Build fixes 2024-03-08 15:56:05 +05:30
Manish GuptaandGitHub 94327b8311 chore: feature build process optimization (#3907)
* process changed to build tar and use for deployment

* fixes
2024-03-08 15:16:32 +05:30
sriram veeraghantaandGitHub c1b1ba35c1 Merge pull request #3878 from makeplane/preview
release: v0.16-dev
2024-03-05 20:04:08 +05:30
sriram veeraghantaandGitHub 4566d6e80c Merge pull request #3697 from makeplane/preview
release: 0.15.4-dev
2024-02-19 19:30:06 +05:30
sriram veeraghantaandGitHub e8d359e625 Merge pull request #3674 from makeplane/preview
fix: build branch docker images push on release
2024-02-15 14:35:32 +05:30
sriram veeraghantaandGitHub 351eba8d61 Merge pull request #3671 from makeplane/preview
release: peek overview issue description initial load bug (#3670)
2024-02-15 03:25:30 +05:30
sriram veeraghantaandGitHub 1e27e37b51 Merge pull request #3666 from makeplane/preview
release: v0.15.2-dev
2024-02-14 19:41:55 +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
sriram veeraghantaandGitHub c6e3f1b932 Merge pull request #3535 from makeplane/preview
release: 0.15-dev
2024-02-01 15:01:49 +05:30
1520 changed files with 36878 additions and 19224 deletions
-23
View File
@@ -1,23 +0,0 @@
version = 1
exclude_patterns = [
"bin/**",
"**/node_modules/",
"**/*.min.js"
]
[[analyzers]]
name = "shell"
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
[[analyzers]]
name = "python"
[analyzers.meta]
runtime_version = "3.x.x"
+9 -2
View File
@@ -55,12 +55,19 @@ body:
- Safari
- Other
- type: dropdown
id: version
id: variant
attributes:
label: Version
label: Variant
options:
- Cloud
- Self-hosted
- Local
validations:
required: true
- type: input
id: version
attributes:
label: Version
placeholder: v0.17.0-dev
validations:
required: true
+5 -7
View File
@@ -6,7 +6,6 @@ on:
branches:
- master
- preview
- develop
release:
types: [released, prereleased]
@@ -18,7 +17,7 @@ jobs:
name: Build-Push Web/Space/API/Proxy Docker Image
runs-on: ubuntu-latest
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
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 }}
@@ -74,7 +73,7 @@ jobs:
- nginx/**
branch_build_push_frontend:
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -126,7 +125,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_space:
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -178,7 +177,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_backend:
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -230,7 +229,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_proxy:
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -280,4 +279,3 @@ jobs:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
+32 -33
View File
@@ -1,13 +1,13 @@
name: "CodeQL"
on:
workflow_dispatch:
push:
branches: [ 'develop', 'preview', 'master' ]
branches: ["preview", "master"]
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'develop', 'preview', 'master' ]
branches: ["develop", "preview", "master"]
schedule:
- cron: '53 19 * * 5'
- cron: "53 19 * * 5"
jobs:
analyze:
@@ -21,45 +21,44 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'python', 'javascript' ]
language: ["python", "javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
+157 -31
View File
@@ -4,70 +4,196 @@ on:
workflow_dispatch:
inputs:
web-build:
required: true
required: false
description: 'Build Web'
type: boolean
default: true
space-build:
required: true
required: false
description: 'Build Space'
type: boolean
default: false
env:
BUILD_WEB: ${{ github.event.inputs.web-build }}
BUILD_SPACE: ${{ github.event.inputs.space-build }}
jobs:
setup-feature-build:
name: Feature Build Setup
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
echo "BUILD_WEB=$BUILD_WEB"
echo "BUILD_SPACE=$BUILD_SPACE"
outputs:
web-build: ${{ env.BUILD_WEB}}
space-build: ${{env.BUILD_SPACE}}
feature-build-web:
if: ${{ needs.setup-feature-build.outputs.web-build == 'true' }}
needs: setup-feature-build
name: Feature Build Web
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install AWS cli
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Checkout
uses: actions/checkout@v4
with:
path: plane
- name: Install Dependencies
run: |
cd $GITHUB_WORKSPACE/plane
yarn install
- name: Build Web
id: build-web
run: |
cd $GITHUB_WORKSPACE/plane
yarn build --filter=web
cd $GITHUB_WORKSPACE
TAR_NAME="web.tar.gz"
tar -czf $TAR_NAME ./plane
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
feature-build-space:
if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }}
needs: setup-feature-build
name: Feature Build Space
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs:
do-build: ${{ needs.setup-feature-build.outputs.space-build }}
s3-url: ${{ steps.build-space.outputs.S3_PRESIGNED_URL }}
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install AWS cli
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Checkout
uses: actions/checkout@v4
with:
path: plane
- name: Install Dependencies
run: |
cd $GITHUB_WORKSPACE/plane
yarn install
- name: Build Space
id: build-space
run: |
cd $GITHUB_WORKSPACE/plane
yarn build --filter=space
cd $GITHUB_WORKSPACE
TAR_NAME="space.tar.gz"
tar -czf $TAR_NAME ./plane
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
feature-deploy:
if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true') }}
needs: [feature-build-web, feature-build-space]
name: Feature Deploy
runs-on: ubuntu-latest
env:
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG }}
BUILD_WEB: ${{ (github.event.inputs.web-build == '' && true) || github.event.inputs.web-build }}
BUILD_SPACE: ${{ (github.event.inputs.space-build == '' && false) || github.event.inputs.space-build }}
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
KUBE_CONFIG_FILE: ${{ secrets.FEATURE_PREVIEW_KUBE_CONFIG }}
steps:
- name: Install AWS cli
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Tailscale
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
tags: tag:ci
- name: Kubectl Setup
run: |
curl -LO "https://dl.k8s.io/release/${{secrets.KUBE_VERSION}}/bin/linux/amd64/kubectl"
curl -LO "https://dl.k8s.io/release/${{ vars.FEATURE_PREVIEW_KUBE_VERSION }}/bin/linux/amd64/kubectl"
chmod +x kubectl
mkdir -p ~/.kube
echo "$KUBE_CONFIG_FILE" > ~/.kube/config
chmod 600 ~/.kube/config
- name: HELM Setup
run: |
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
- name: App Deploy
run: |
helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ secrets.FEATURE_PREVIEW_HELM_CHART_URL }}
GIT_BRANCH=${{ github.ref_name }}
APP_NAMESPACE=${{ secrets.FEATURE_PREVIEW_NAMESPACE }}
WEB_S3_URL=""
if [ ${{ env.BUILD_WEB }} == true ]; then
WEB_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/web.tar.gz --expires-in 3600)
fi
METADATA=$(helm install feature-preview/${{ secrets.FEATURE_PREVIEW_HELM_CHART_NAME }} \
--kube-insecure-skip-tls-verify \
--generate-name \
--namespace $APP_NAMESPACE \
--set shared_config.git_repo=${{github.server_url}}/${{ github.repository }}.git \
--set shared_config.git_branch="$GIT_BRANCH" \
--set web.enabled=${{ env.BUILD_WEB }} \
--set space.enabled=${{ env.BUILD_SPACE }} \
--output json \
--timeout 1000s)
SPACE_S3_URL=""
if [ ${{ env.BUILD_SPACE }} == true ]; then
SPACE_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/space.tar.gz --expires-in 3600)
fi
APP_NAME=$(echo $METADATA | jq -r '.name')
if [ ${{ env.BUILD_WEB }} == true ] || [ ${{ env.BUILD_SPACE }} == true ]; then
INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \
-o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \
jq -r '.spec.rules[0].host')
helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ vars.FEATURE_PREVIEW_HELM_CHART_URL }}
echo "****************************************"
echo "APP NAME ::: $APP_NAME"
echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME"
echo "****************************************"
APP_NAMESPACE="${{ vars.FEATURE_PREVIEW_NAMESPACE }}"
DEPLOY_SCRIPT_URL="${{ vars.FEATURE_PREVIEW_DEPLOY_SCRIPT_URL }}"
METADATA=$(helm --kube-insecure-skip-tls-verify install feature-preview/${{ vars.FEATURE_PREVIEW_HELM_CHART_NAME }} \
--generate-name \
--namespace $APP_NAMESPACE \
--set ingress.primaryDomain=${{vars.FEATURE_PREVIEW_PRIMARY_DOMAIN || 'feature.plane.tools' }} \
--set web.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \
--set web.enabled=${{ env.BUILD_WEB || false }} \
--set web.artifact_url=$WEB_S3_URL \
--set space.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \
--set space.enabled=${{ env.BUILD_SPACE || false }} \
--set space.artifact_url=$SPACE_S3_URL \
--set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \
--set shared_config.api_base_url=${{vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL}} \
--output json \
--timeout 1000s)
APP_NAME=$(echo $METADATA | jq -r '.name')
INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \
-o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \
jq -r '.spec.rules[0].host')
echo "****************************************"
echo "APP NAME ::: $APP_NAME"
echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME"
echo "****************************************"
fi
+1
View File
@@ -51,6 +51,7 @@ staticfiles
mediafiles
.env
.DS_Store
logs/
node_modules/
assets/dist/
+5 -10
View File
@@ -27,7 +27,7 @@ RUN yarn install
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build
@@ -89,21 +89,17 @@ RUN chmod -R 777 /code
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 plane
RUN adduser --system --uid 1001 captain
COPY --from=installer /app/apps/app/next.config.js .
COPY --from=installer /app/apps/app/package.json .
COPY --from=installer /app/apps/space/next.config.js .
COPY --from=installer /app/apps/space/package.json .
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
COPY --from=installer /app/apps/app/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
COPY --from=installer /app/apps/app/.next/static ./apps/app/.next/static
COPY --from=installer --chown=captain:plane /app/apps/space/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/space/.next ./apps/space/.next
COPY --from=installer /app/apps/space/.next/standalone ./
COPY --from=installer /app/apps/space/.next ./apps/space/.next
ENV NEXT_TELEMETRY_DISABLED 1
@@ -118,7 +114,6 @@ ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
USER root
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
+12 -12
View File
@@ -17,10 +17,10 @@
</p>
<p align="center">
<a href="http://www.plane.so"><b>Website</b></a> •
<a href="https://github.com/makeplane/plane/releases"><b>Releases</b></a> •
<a href="https://twitter.com/planepowers"><b>Twitter</b></a> •
<a href="https://docs.plane.so/"><b>Documentation</b></a>
<a href="https://dub.sh/plane-website-readme"><b>Website</b></a> •
<a href="https://git.new/releases"><b>Releases</b></a> •
<a href="https://dub.sh/planepowershq"><b>Twitter</b></a> •
<a href="https://dub.sh/planedocs"><b>Documentation</b></a>
</p>
<p>
@@ -40,28 +40,28 @@
</a>
</p>
Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind. 🧘‍♀️
Meet [Plane](https://dub.sh/plane-website-readme). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind. 🧘‍♀️
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve in our upcoming releases.
## ⚡ Installation
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users.
If you want more control over your data prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose).
If you want more control over your data, prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose).
| Installation Methods | Documentation Link |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
## 🚀 Features
- **Issues**: Quickly create issues and add details using a powerful, rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking.
- **Issues**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking.
- **Cycles**
- **Cycles**:
Keep up your team's momentum with Cycles. Gain insights into your project's progress with burn-down charts and other valuable features.
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to track and plan your project's progress easily.
@@ -74,11 +74,11 @@ If you want more control over your data prefer to self-host Plane, please refer
- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
## 🛠️ Contributors Quick Start
## 🛠️ Quick start for contributors
> Development system must have docker engine installed and running.
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute -
1. Clone the code locally using:
```
+44
View File
@@ -0,0 +1,44 @@
# Security Policy
This document outlines security procedures and vulnerabilities reporting for the Plane project.
At Plane, we safeguarding the security of our systems with top priority. Despite our efforts, vulnerabilities may still exist. We greatly appreciate your assistance in identifying and reporting any such vulnerabilities to help us maintain the integrity of our systems and protect our clients.
To report a security vulnerability, please email us directly at security@plane.so with a detailed description of the vulnerability and steps to reproduce it. Please refrain from disclosing the vulnerability publicly until we have had an opportunity to review and address it.
## Out of Scope Vulnerabilities
We appreciate your help in identifying vulnerabilities. However, please note that the following types of vulnerabilities are considered out of scope:
- Attacks requiring MITM or physical access to a user's device.
- Content spoofing and text injection issues without demonstrating an attack vector or ability to modify HTML/CSS.
- Email spoofing.
- Missing DNSSEC, CAA, CSP headers.
- Lack of Secure or HTTP only flag on non-sensitive cookies.
## Reporting Process
If you discover a vulnerability, please adhere to the following reporting process:
1. Email your findings to security@plane.so.
2. Refrain from running automated scanners on our infrastructure or dashboard without prior consent. Contact us to set up a sandbox environment if necessary.
3. Do not exploit the vulnerability for malicious purposes, such as downloading excessive data or altering user data.
4. Maintain confidentiality and refrain from disclosing the vulnerability until it has been resolved.
5. Avoid using physical security attacks, social engineering, distributed denial of service, spam, or third-party applications.
When reporting a vulnerability, please provide sufficient information to allow us to reproduce and address the issue promptly. Include the IP address or URL of the affected system, along with a detailed description of the vulnerability.
## Our Commitment
We are committed to promptly addressing reported vulnerabilities and maintaining open communication throughout the resolution process. Here's what you can expect from us:
- **Response Time:** We will acknowledge receipt of your report within three business days and provide an expected resolution date.
- **Legal Protection:** We will not pursue legal action against you for reporting vulnerabilities, provided you adhere to the reporting guidelines.
- **Confidentiality:** Your report will be treated with strict confidentiality. We will not disclose your personal information to third parties without your consent.
- **Progress Updates:** We will keep you informed of our progress in resolving the reported vulnerability.
- **Recognition:** With your permission, we will publicly acknowledge you as the discoverer of the vulnerability.
- **Timely Resolution:** We strive to resolve all reported vulnerabilities promptly and will actively participate in the publication process once the issue is resolved.
We appreciate your cooperation in helping us maintain the security of our systems and protecting our clients. Thank you for your contributions to our security efforts.
reference: https://supabase.com/.well-known/security.txt
-20
View File
@@ -14,10 +14,6 @@ POSTGRES_HOST="plane-db"
POSTGRES_DB="plane"
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DB}
# Oauth variables
GOOGLE_CLIENT_ID=""
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Redis Settings
REDIS_HOST="plane-redis"
@@ -34,11 +30,6 @@ AWS_S3_BUCKET_NAME="uploads"
# Maximum file upload limit
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
OPENAI_API_KEY="sk-" # deprecated
GPT_ENGINE="gpt-3.5-turbo" # deprecated
# Settings related to Docker
DOCKERIZED=1 # deprecated
@@ -48,19 +39,8 @@ USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# SignUps
ENABLE_SIGNUP="1"
# Enable Email/Password Signup
ENABLE_EMAIL_PASSWORD="1"
# Enable Magic link Login
ENABLE_MAGIC_LINK_LOGIN="0"
# Email redirections and minio domain settings
WEB_URL="http://localhost"
# Gunicorn Workers
GUNICORN_WORKERS=2
+2 -10
View File
@@ -32,27 +32,19 @@ RUN apk add --no-cache --virtual .build-deps \
apk del .build-deps
RUN addgroup -S plane && \
adduser -S captain -G plane
RUN chown captain.plane /code
USER captain
# Add in Django deps and generate Django's static files
COPY manage.py manage.py
COPY plane plane/
COPY templates templates/
COPY package.json package.json
USER root
RUN apk --no-cache add "bash~=5.2"
COPY ./bin ./bin/
RUN mkdir -p /code/plane/logs
RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
RUN chmod -R 777 /code
USER captain
# Expose container port and run entry point script
EXPOSE 8000
+1 -4
View File
@@ -30,16 +30,13 @@ ADD requirements ./requirements
# Install the local development settings
RUN pip install -r requirements/local.txt --compile --no-cache-dir
RUN addgroup -S plane && \
adduser -S captain -G plane
COPY . .
RUN chown -R captain.plane /code
RUN mkdir -p /code/plane/logs
RUN chmod -R +x /code/bin
RUN chmod -R 777 /code
USER captain
# Expose container port and run entry point script
EXPOSE 8000
+1 -1
View File
@@ -182,7 +182,7 @@ def update_label_color():
labels = Label.objects.filter(color="")
updated_labels = []
for label in labels:
label.color = "#" + "%06x" % random.randint(0, 0xFFFFFF)
label.color = f"#{random.randint(0, 0xFFFFFF+1):06X}"
updated_labels.append(label)
Label.objects.bulk_update(updated_labels, ["color"], batch_size=100)
+6 -2
View File
@@ -21,11 +21,15 @@ SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256
export MACHINE_SIGNATURE=$SIGNATURE
# Register instance
python manage.py register_instance $MACHINE_SIGNATURE
python manage.py register_instance "$MACHINE_SIGNATURE"
# Load the configuration variable
python manage.py configure_instance
# Create the default bucket
python manage.py create_bucket
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:${PORT:-8000} --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
# Clear Cache before starting to remove stale values
python manage.py clear_cache
exec gunicorn -w "$GUNICORN_WORKERS" -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:"${PORT:-8000}" --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
+4 -1
View File
@@ -21,12 +21,15 @@ SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256
export MACHINE_SIGNATURE=$SIGNATURE
# Register instance
python manage.py register_instance $MACHINE_SIGNATURE
python manage.py register_instance "$MACHINE_SIGNATURE"
# Load the configuration variable
python manage.py configure_instance
# Create the default bucket
python manage.py create_bucket
# Clear Cache before starting to remove stale values
python manage.py clear_cache
python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.16.0"
"version": "0.18.0"
}
+4 -4
View File
@@ -66,11 +66,11 @@ class BaseSerializer(serializers.ModelSerializer):
if expand in self.fields:
# Import all the expandable serializers
from . import (
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
ProjectLiteSerializer,
StateLiteSerializer,
UserLiteSerializer,
WorkspaceLiteSerializer,
)
# Expansion mapper
+18 -17
View File
@@ -1,32 +1,33 @@
from lxml import html
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
# Django imports
from django.utils import timezone
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from lxml import html
# Third party imports
from rest_framework import serializers
# Module imports
from plane.db.models import (
User,
Issue,
State,
IssueActivity,
IssueAssignee,
Label,
IssueAttachment,
IssueComment,
IssueLabel,
IssueLink,
IssueComment,
IssueAttachment,
IssueActivity,
Label,
ProjectMember,
State,
User,
)
from .base import BaseSerializer
from .cycle import CycleSerializer, CycleLiteSerializer
from .module import ModuleSerializer, ModuleLiteSerializer
from .user import UserLiteSerializer
from .cycle import CycleLiteSerializer, CycleSerializer
from .module import ModuleLiteSerializer, ModuleSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
class IssueSerializer(BaseSerializer):
@@ -78,8 +79,8 @@ class IssueSerializer(BaseSerializer):
parsed_str = html.tostring(parsed, encoding="unicode")
data["description_html"] = parsed_str
except Exception as e:
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
except Exception:
raise serializers.ValidationError("Invalid HTML passed")
# Validate assignees are from project
if data.get("assignees", []):
@@ -294,7 +295,7 @@ class IssueLinkSerializer(BaseSerializer):
raise serializers.ValidationError("Invalid URL format.")
# Check URL scheme
if not value.startswith(('http://', 'https://')):
if not value.startswith(("http://", "https://")):
raise serializers.ValidationError("Invalid URL scheme.")
return value
@@ -365,8 +366,8 @@ class IssueCommentSerializer(BaseSerializer):
parsed_str = html.tostring(parsed, encoding="unicode")
data["comment_html"] = parsed_str
except Exception as e:
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
except Exception:
raise serializers.ValidationError("Invalid HTML passed")
return data
@@ -7,6 +7,7 @@ from plane.db.models import (
ProjectIdentifier,
WorkspaceMember,
)
from .base import BaseSerializer
+3
View File
@@ -1,5 +1,6 @@
# Module imports
from plane.db.models import User
from .base import BaseSerializer
@@ -10,7 +11,9 @@ class UserLiteSerializer(BaseSerializer):
"id",
"first_name",
"last_name",
"email",
"avatar",
"display_name",
"email",
]
read_only_fields = fields
+11
View File
@@ -4,6 +4,7 @@ from plane.api.views.cycle import (
CycleAPIEndpoint,
CycleIssueAPIEndpoint,
TransferCycleIssueAPIEndpoint,
CycleArchiveUnarchiveAPIEndpoint,
)
urlpatterns = [
@@ -32,4 +33,14 @@ urlpatterns = [
TransferCycleIssueAPIEndpoint.as_view(),
name="transfer-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/archive/",
CycleArchiveUnarchiveAPIEndpoint.as_view(),
name="cycle-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-cycles/",
CycleArchiveUnarchiveAPIEndpoint.as_view(),
name="cycle-archive-unarchive",
),
]
+15 -1
View File
@@ -1,6 +1,10 @@
from django.urls import path
from plane.api.views import ModuleAPIEndpoint, ModuleIssueAPIEndpoint
from plane.api.views import (
ModuleAPIEndpoint,
ModuleIssueAPIEndpoint,
ModuleArchiveUnarchiveAPIEndpoint,
)
urlpatterns = [
path(
@@ -23,4 +27,14 @@ urlpatterns = [
ModuleIssueAPIEndpoint.as_view(),
name="module-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:pk>/archive/",
ModuleArchiveUnarchiveAPIEndpoint.as_view(),
name="module-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-modules/",
ModuleArchiveUnarchiveAPIEndpoint.as_view(),
name="module-archive-unarchive",
),
]
+10 -2
View File
@@ -1,6 +1,9 @@
from django.urls import path
from plane.api.views import ProjectAPIEndpoint
from plane.api.views import (
ProjectAPIEndpoint,
ProjectArchiveUnarchiveAPIEndpoint,
)
urlpatterns = [
path(
@@ -9,8 +12,13 @@ urlpatterns = [
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/",
"workspaces/<str:slug>/projects/<uuid:pk>/",
ProjectAPIEndpoint.as_view(),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archive/",
ProjectArchiveUnarchiveAPIEndpoint.as_view(),
name="project-archive-unarchive",
),
]
+7 -2
View File
@@ -1,4 +1,4 @@
from .project import ProjectAPIEndpoint
from .project import ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint
from .state import StateAPIEndpoint
@@ -14,8 +14,13 @@ from .cycle import (
CycleAPIEndpoint,
CycleIssueAPIEndpoint,
TransferCycleIssueAPIEndpoint,
CycleArchiveUnarchiveAPIEndpoint,
)
from .module import ModuleAPIEndpoint, ModuleIssueAPIEndpoint
from .module import (
ModuleAPIEndpoint,
ModuleIssueAPIEndpoint,
ModuleArchiveUnarchiveAPIEndpoint,
)
from .inbox import InboxIssueAPIEndpoint
+18 -16
View File
@@ -1,26 +1,27 @@
# Python imports
import zoneinfo
from urllib.parse import urlparse
import zoneinfo
# Django imports
from django.conf import settings
from django.db import IntegrityError
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import IntegrityError
from django.urls import resolve
from django.utils import timezone
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
# Third party imports
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from plane.api.middleware.api_authentication import APIKeyAuthentication
from plane.api.rate_limit import ApiKeyRateThrottle
from plane.utils.paginator import BasePaginator
from plane.bgtasks.webhook_task import send_webhook
from plane.utils.exception_logger import log_exception
from plane.utils.paginator import BasePaginator
class TimezoneMixin:
@@ -106,27 +107,23 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
if isinstance(e, ValidationError):
return Response(
{
"error": "The provided payload is not valid please try with a valid payload"
},
{"error": "Please provide valid detail"},
status=status.HTTP_400_BAD_REQUEST,
)
if isinstance(e, ObjectDoesNotExist):
return Response(
{"error": "The required object does not exist."},
{"error": "The requested resource does not exist."},
status=status.HTTP_404_NOT_FOUND,
)
if isinstance(e, KeyError):
return Response(
{"error": " The required key does not exist."},
{"error": "The required key does not exist."},
status=status.HTTP_400_BAD_REQUEST,
)
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -169,7 +166,12 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property
def project_id(self):
return self.kwargs.get("project_id", None)
project_id = self.kwargs.get("project_id", None)
if project_id:
return project_id
if resolve(self.request.path_info).url_name == "project":
return self.kwargs.get("pk", None)
@property
def fields(self):
+382 -18
View File
@@ -2,29 +2,31 @@
import json
# Django imports
from django.db.models import Q, Count, Sum, F, OuterRef, Func
from django.utils import timezone
from django.core import serializers
from django.db.models import Count, F, Func, OuterRef, Q, Sum
from django.utils import timezone
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .base import BaseAPIView, WebhookMixin
from plane.db.models import (
Cycle,
Issue,
CycleIssue,
IssueLink,
IssueAttachment,
from plane.api.serializers import (
CycleIssueSerializer,
CycleSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.api.serializers import (
CycleSerializer,
CycleIssueSerializer,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.db.models import (
Cycle,
CycleIssue,
Issue,
IssueAttachment,
IssueLink,
)
from plane.utils.analytics_plot import burndown_plot
from .base import BaseAPIView, WebhookMixin
class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
@@ -140,7 +142,9 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
def get(self, request, slug, project_id, pk=None):
if pk:
queryset = self.get_queryset().get(pk=pk)
queryset = (
self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
)
data = CycleSerializer(
queryset,
fields=self.fields,
@@ -150,7 +154,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
data,
status=status.HTTP_200_OK,
)
queryset = self.get_queryset()
queryset = self.get_queryset().filter(archived_at__isnull=True)
cycle_view = request.GET.get("cycle_view", "all")
# Current Cycle
@@ -291,6 +295,11 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if cycle.archived_at:
return Response(
{"error": "Archived cycle cannot be edited"},
status=status.HTTP_400_BAD_REQUEST,
)
request_data = request.data
@@ -368,6 +377,144 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
Cycle.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(archived_at__isnull=False)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.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,
),
)
)
.order_by(self.kwargs.get("order_by", "-created_at"))
.distinct()
)
def get(self, request, slug, project_id):
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
def post(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.get(
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
if cycle.end_date >= timezone.now().date():
return Response(
{"error": "Only completed cycles can be archived"},
status=status.HTTP_400_BAD_REQUEST,
)
cycle.archived_at = timezone.now()
cycle.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.get(
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
cycle.archived_at = None
cycle.save()
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`,
@@ -409,7 +556,21 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
.distinct()
)
def get(self, request, slug, project_id, cycle_id):
def get(self, request, slug, project_id, cycle_id, issue_id=None):
# Get
if issue_id:
cycle_issue = CycleIssue.objects.get(
workspace__slug=slug,
project_id=project_id,
cycle_id=cycle_id,
issue_id=issue_id,
)
serializer = CycleIssueSerializer(
cycle_issue, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK)
# List
order_by = request.GET.get("order_by", "created_at")
issues = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
@@ -585,7 +746,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
class TransferCycleIssueAPIEndpoint(BaseAPIView):
"""
This viewset provides `create` actions for transfering the issues into a particular cycle.
This viewset provides `create` actions for transferring the issues into a particular cycle.
"""
@@ -606,6 +767,209 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
)
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,
),
)
)
)
# 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,
)
# Get the assignee distribution
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")
)
# assignee distribution serialized
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
]
# Get the label distribution
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")
)
# Label distribution serilization
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()
if current_cycle:
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,
"distribution": {
"labels": label_distribution_data,
"assignees": assignee_distribution_data,
"completion_chart": completion_chart,
},
}
# Save the snapshot of the current cycle
current_cycle.save(update_fields=["progress_snapshot"])
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now().date()
+30 -10
View File
@@ -2,27 +2,28 @@
import json
# Django improts
from django.utils import timezone
from django.db.models import Q
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Q
from django.utils import timezone
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .base import BaseAPIView
from plane.app.permissions import ProjectLitePermission
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
from plane.app.permissions import ProjectLitePermission
from plane.bgtasks.issue_activites_task import issue_activity
from plane.db.models import (
Inbox,
InboxIssue,
Issue,
State,
ProjectMember,
Project,
Inbox,
ProjectMember,
State,
)
from plane.bgtasks.issue_activites_task import issue_activity
from .base import BaseAPIView
class InboxIssueAPIEndpoint(BaseAPIView):
@@ -134,10 +135,11 @@ class InboxIssueAPIEndpoint(BaseAPIView):
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="backlog",
group="triage",
description="Default state for managing all Inbox Issues",
project_id=project_id,
color="#ff7700",
is_triage=True,
)
# create an issue
@@ -270,6 +272,9 @@ class InboxIssueAPIEndpoint(BaseAPIView):
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
)
current_instance = json.dumps(
InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
)
if serializer.is_valid():
serializer.save()
@@ -298,7 +303,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
)
# Update the issue state only if it is in triage state
if issue.state.name == "Triage":
if issue.state.is_triage:
# Move to default state
state = State.objects.filter(
workspace__slug=slug,
@@ -309,6 +314,21 @@ class InboxIssueAPIEndpoint(BaseAPIView):
issue.state = state
issue.save()
# create a activity for status change
issue_activity.delay(
type="inbox.activity.created",
requested_data=json.dumps(
request.data, cls=DjangoJSONEncoder
),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=False,
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
+4 -2
View File
@@ -308,8 +308,6 @@ 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()),
)
@@ -357,6 +355,7 @@ class LabelAPIEndpoint(BaseAPIView):
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(project__archived_at__isnull=True)
.select_related("project")
.select_related("workspace")
.select_related("parent")
@@ -489,6 +488,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(project__archived_at__isnull=True)
.order_by(self.kwargs.get("order_by", "-created_at"))
.distinct()
)
@@ -618,6 +618,7 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(project__archived_at__isnull=True)
.select_related("workspace", "project", "issue", "actor")
.annotate(
is_member=Exists(
@@ -793,6 +794,7 @@ class IssueActivityAPIEndpoint(BaseAPIView):
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(project__archived_at__isnull=True)
.select_related("actor", "workspace", "issue", "project")
).order_by(request.GET.get("order_by", "created_at"))
+157 -15
View File
@@ -2,32 +2,33 @@
import json
# Django imports
from django.db.models import Count, Prefetch, Q, F, Func, OuterRef
from django.utils import timezone
from django.core import serializers
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
from django.utils import timezone
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .base import BaseAPIView, WebhookMixin
from plane.api.serializers import (
IssueSerializer,
ModuleIssueSerializer,
ModuleSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.bgtasks.issue_activites_task import issue_activity
from plane.db.models import (
Project,
Module,
ModuleLink,
Issue,
ModuleIssue,
IssueAttachment,
IssueLink,
Module,
ModuleIssue,
ModuleLink,
Project,
)
from plane.api.serializers import (
ModuleSerializer,
ModuleIssueSerializer,
IssueSerializer,
)
from plane.bgtasks.issue_activites_task import issue_activity
from .base import BaseAPIView, WebhookMixin
class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
@@ -67,6 +68,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
),
)
.annotate(
@@ -77,6 +79,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
@@ -87,6 +90,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
@@ -97,6 +101,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
@@ -107,6 +112,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
@@ -117,6 +123,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.order_by(self.kwargs.get("order_by", "-created_at"))
@@ -165,6 +172,11 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
if module.archived_at:
return Response(
{"error": "Archived module cannot be edited"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ModuleSerializer(
module,
data=request.data,
@@ -197,7 +209,9 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
def get(self, request, slug, project_id, pk=None):
if pk:
queryset = self.get_queryset().get(pk=pk)
queryset = (
self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
)
data = ModuleSerializer(
queryset,
fields=self.fields,
@@ -209,7 +223,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
)
return self.paginate(
request=request,
queryset=(self.get_queryset()),
queryset=(self.get_queryset().filter(archived_at__isnull=True)),
on_results=lambda modules: ModuleSerializer(
modules,
many=True,
@@ -279,6 +293,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(project__archived_at__isnull=True)
.select_related("project")
.select_related("workspace")
.select_related("module")
@@ -446,3 +461,130 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
epoch=int(timezone.now().timestamp()),
)
return Response(status=status.HTTP_204_NO_CONTENT)
class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
Module.objects.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(archived_at__isnull=False)
.select_related("project")
.select_related("workspace")
.select_related("lead")
.prefetch_related("members")
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related(
"module", "created_by"
),
)
)
.annotate(
total_issues=Count(
"issue_module",
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
),
)
.annotate(
completed_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="completed",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
cancelled_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="cancelled",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
started_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="started",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
unstarted_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="unstarted",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
backlog_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="backlog",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.order_by(self.kwargs.get("order_by", "-created_at"))
)
def get(self, request, slug, project_id, pk):
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda modules: ModuleSerializer(
modules,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
def post(self, request, slug, project_id, pk):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
if module.status not in ["completed", "cancelled"]:
return Response(
{
"error": "Only completed or cancelled modules can be archived"
},
status=status.HTTP_400_BAD_REQUEST,
)
module.archived_at = timezone.now()
module.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id, pk):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
module.archived_at = None
module.save()
return Response(status=status.HTTP_204_NO_CONTENT)
+52 -20
View File
@@ -1,26 +1,29 @@
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, OuterRef, Q, F, Func, Subquery, Prefetch
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
from django.utils import timezone
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from plane.api.serializers import ProjectSerializer
from plane.app.permissions import ProjectBasePermission
# Module imports
from plane.db.models import (
Workspace,
Project,
ProjectMember,
ProjectDeployBoard,
State,
Cycle,
Module,
IssueProperty,
Inbox,
IssueProperty,
Module,
Project,
ProjectDeployBoard,
ProjectMember,
State,
Workspace,
)
from plane.app.permissions import ProjectBasePermission
from plane.api.serializers import ProjectSerializer
from .base import BaseAPIView, WebhookMixin
@@ -39,7 +42,10 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
return (
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(
Q(project_projectmember__member=self.request.user)
Q(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
| Q(network=2)
)
.select_related(
@@ -99,8 +105,8 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
.distinct()
)
def get(self, request, slug, project_id=None):
if project_id is None:
def get(self, request, slug, pk=None):
if pk is None:
sort_order_query = ProjectMember.objects.filter(
member=request.user,
project_id=OuterRef("pk"),
@@ -131,7 +137,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
expand=self.expand,
).data,
)
project = self.get_queryset().get(workspace__slug=slug, pk=project_id)
project = self.get_queryset().get(workspace__slug=slug, pk=pk)
serializer = ProjectSerializer(
project,
fields=self.fields,
@@ -255,10 +261,16 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
status=status.HTTP_410_GONE,
)
def patch(self, request, slug, project_id=None):
def patch(self, request, slug, pk):
try:
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=project_id)
project = Project.objects.get(pk=pk)
if project.archived_at:
return Response(
{"error": "Archived project cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ProjectSerializer(
project,
@@ -279,10 +291,11 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
# Create the triage state in Backlog group
State.objects.get_or_create(
name="Triage",
group="backlog",
group="triage",
description="Default state for managing all Inbox Issues",
project_id=project_id,
project_id=pk,
color="#ff7700",
is_triage=True,
)
project = (
@@ -312,7 +325,26 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
status=status.HTTP_410_GONE,
)
def delete(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
def delete(self, request, slug, pk):
project = Project.objects.get(pk=pk, workspace__slug=slug)
project.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProjectArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]
def post(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project.archived_at = timezone.now()
project.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project.archived_at = None
project.save()
return Response(status=status.HTTP_204_NO_CONTENT)
+13 -8
View File
@@ -1,16 +1,16 @@
# Django imports
from django.db import IntegrityError
from django.db.models import Q
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from rest_framework.response import Response
from plane.api.serializers import StateSerializer
from plane.app.permissions import ProjectEntityPermission
from plane.db.models import Issue, State
# Module imports
from .base import BaseAPIView
from plane.api.serializers import StateSerializer
from plane.app.permissions import ProjectEntityPermission
from plane.db.models import State, Issue
class StateAPIEndpoint(BaseAPIView):
@@ -28,7 +28,8 @@ class StateAPIEndpoint(BaseAPIView):
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(~Q(name="Triage"))
.filter(is_triage=False)
.filter(project__archived_at__isnull=True)
.select_related("project")
.select_related("workspace")
.distinct()
@@ -85,7 +86,11 @@ class StateAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, state_id=None):
if state_id:
serializer = StateSerializer(self.get_queryset().get(pk=state_id))
serializer = StateSerializer(
self.get_queryset().get(pk=state_id),
fields=self.fields,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)
return self.paginate(
request=request,
@@ -100,7 +105,7 @@ class StateAPIEndpoint(BaseAPIView):
def delete(self, request, slug, project_id, state_id):
state = State.objects.get(
~Q(name="Triage"),
is_triage=False,
pk=state_id,
project_id=project_id,
workspace__slug=slug,
+2 -2
View File
@@ -1,8 +1,8 @@
# Third Party imports
from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.permissions import SAFE_METHODS, BasePermission
# Module import
from plane.db.models import WorkspaceMember, ProjectMember
from plane.db.models import ProjectMember, WorkspaceMember
# Permission Mappings
Admin = 20
@@ -59,6 +59,7 @@ from .issue import (
IssueFlatSerializer,
IssueStateSerializer,
IssueLinkSerializer,
IssueInboxSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
IssueSubscriberSerializer,
@@ -92,6 +93,7 @@ from .page import (
PageSerializer,
PageLogSerializer,
SubPageSerializer,
PageDetailSerializer,
PageFavoriteSerializer,
)
@@ -107,6 +109,7 @@ from .inbox import (
InboxIssueSerializer,
IssueStateInboxSerializer,
InboxIssueLiteSerializer,
InboxIssueDetailSerializer,
)
from .analytic import AnalyticViewSerializer
+1
View File
@@ -31,6 +31,7 @@ class CycleWriteSerializer(BaseSerializer):
"workspace",
"project",
"owned_by",
"archived_at",
]
+53 -4
View File
@@ -3,7 +3,11 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from .issue import (
IssueInboxSerializer,
LabelLiteSerializer,
IssueDetailSerializer,
)
from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
@@ -24,17 +28,62 @@ class InboxSerializer(BaseSerializer):
class InboxIssueSerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(source="issue", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
issue = IssueInboxSerializer(read_only=True)
class Meta:
model = InboxIssue
fields = "__all__"
fields = [
"id",
"status",
"duplicate_to",
"snoozed_till",
"source",
"issue",
"created_by",
]
read_only_fields = [
"project",
"workspace",
]
def to_representation(self, instance):
# Pass the annotated fields to the Issue instance if they exist
if hasattr(instance, "label_ids"):
instance.issue.label_ids = instance.label_ids
return super().to_representation(instance)
class InboxIssueDetailSerializer(BaseSerializer):
issue = IssueDetailSerializer(read_only=True)
duplicate_issue_detail = IssueInboxSerializer(
read_only=True, source="duplicate_to"
)
class Meta:
model = InboxIssue
fields = [
"id",
"status",
"duplicate_to",
"snoozed_till",
"duplicate_issue_detail",
"source",
"issue",
]
read_only_fields = [
"project",
"workspace",
]
def to_representation(self, instance):
# Pass the annotated fields to the Issue instance if they exist
if hasattr(instance, "assignee_ids"):
instance.issue.assignee_ids = instance.assignee_ids
if hasattr(instance, "label_ids"):
instance.issue.label_ids = instance.label_ids
return super().to_representation(instance)
class InboxIssueLiteSerializer(BaseSerializer):
class Meta:
+23 -3
View File
@@ -533,8 +533,8 @@ class IssueReactionLiteSerializer(DynamicBaseSerializer):
model = IssueReaction
fields = [
"id",
"actor_id",
"issue_id",
"actor",
"issue",
"reaction",
]
@@ -620,6 +620,26 @@ class IssueStateSerializer(DynamicBaseSerializer):
fields = "__all__"
class IssueInboxSerializer(DynamicBaseSerializer):
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
class Meta:
model = Issue
fields = [
"id",
"name",
"priority",
"sequence_id",
"project_id",
"created_at",
"label_ids",
]
read_only_fields = fields
class IssueSerializer(DynamicBaseSerializer):
# ids
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
@@ -688,7 +708,7 @@ class IssueLiteSerializer(DynamicBaseSerializer):
class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField()
is_subscribed = serializers.BooleanField()
is_subscribed = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + [
+3 -1
View File
@@ -39,6 +39,7 @@ class ModuleWriteSerializer(BaseSerializer):
"updated_by",
"created_at",
"updated_at",
"archived_at",
]
def to_representation(self, instance):
@@ -215,9 +216,10 @@ class ModuleSerializer(DynamicBaseSerializer):
class ModuleDetailSerializer(ModuleSerializer):
link_module = ModuleLinkSerializer(read_only=True, many=True)
sub_issues = serializers.IntegerField(read_only=True)
class Meta(ModuleSerializer.Meta):
fields = ModuleSerializer.Meta.fields + ["link_module"]
fields = ModuleSerializer.Meta.fields + ["link_module", "sub_issues"]
class ModuleFavoriteSerializer(BaseSerializer):
+31 -12
View File
@@ -3,9 +3,6 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import LabelLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import (
Page,
PageLog,
@@ -17,22 +14,33 @@ from plane.db.models import (
class PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
class Meta:
model = Page
fields = "__all__"
fields = [
"id",
"name",
"owned_by",
"access",
"color",
"labels",
"parent",
"is_favorite",
"is_locked",
"archived_at",
"workspace",
"project",
"created_at",
"updated_at",
"created_by",
"updated_by",
"view_props",
]
read_only_fields = [
"workspace",
"project",
@@ -48,8 +56,12 @@ class PageSerializer(BaseSerializer):
labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]
description_html = self.context["description_html"]
page = Page.objects.create(
**validated_data, project_id=project_id, owned_by_id=owned_by_id
**validated_data,
description_html=description_html,
project_id=project_id,
owned_by_id=owned_by_id,
)
if labels is not None:
@@ -91,6 +103,13 @@ class PageSerializer(BaseSerializer):
return super().update(instance, validated_data)
class PageDetailSerializer(PageSerializer):
description_html = serializers.CharField()
class Meta(PageSerializer.Meta):
fields = PageSerializer.Meta.fields + ["description_html"]
class SubPageSerializer(BaseSerializer):
entity_details = serializers.SerializerMethodField()
@@ -102,6 +102,12 @@ class ProjectLiteSerializer(BaseSerializer):
class ProjectListSerializer(DynamicBaseSerializer):
total_issues = serializers.IntegerField(read_only=True)
archived_issues = serializers.IntegerField(read_only=True)
archived_sub_issues = serializers.IntegerField(read_only=True)
draft_issues = serializers.IntegerField(read_only=True)
draft_sub_issues = serializers.IntegerField(read_only=True)
sub_issues = serializers.IntegerField(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
+16
View File
@@ -8,6 +8,7 @@ from plane.app.views import (
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
CycleArchiveUnarchiveEndpoint,
)
@@ -90,4 +91,19 @@ urlpatterns = [
CycleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/archive/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-cycles/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-cycles/<uuid:pk>/",
CycleArchiveUnarchiveEndpoint.as_view(),
name="cycle-archive-unarchive",
),
]
+2 -2
View File
@@ -30,7 +30,7 @@ urlpatterns = [
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
InboxIssueViewSet.as_view(
{
"get": "list",
@@ -40,7 +40,7 @@ urlpatterns = [
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:issue_id>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
InboxIssueViewSet.as_view(
{
"get": "retrieve",
+16
View File
@@ -7,6 +7,7 @@ from plane.app.views import (
ModuleLinkViewSet,
ModuleFavoriteViewSet,
ModuleUserPropertiesEndpoint,
ModuleArchiveUnarchiveEndpoint,
)
@@ -110,4 +111,19 @@ urlpatterns = [
ModuleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/archive/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-modules/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-modules/<uuid:pk>/",
ModuleArchiveUnarchiveEndpoint.as_view(),
name="module-archive-unarchive",
),
]
+13 -64
View File
@@ -31,102 +31,51 @@ urlpatterns = [
),
name="project-pages",
),
# favorite pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
"workspaces/<str:slug>/projects/<uuid:project_id>/favorite-pages/<uuid:pk>/",
PageFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/<uuid:page_id>/",
PageFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-pages",
),
# archived pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/archive/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/archive/",
PageViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
name="project-page-archive",
name="project-page-archive-unarchive",
),
# lock and unlock
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unarchive/",
PageViewSet.as_view(
{
"post": "unarchive",
}
),
name="project-page-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
PageViewSet.as_view(
{
"get": "archive_list",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/lock/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/lock/",
PageViewSet.as_view(
{
"post": "lock",
"delete": "unlock",
}
),
name="project-pages",
name="project-pages-lock-unlock",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unlock/",
PageViewSet.as_view(
{
"post": "unlock",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/transactions/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/transactions/<uuid:transaction>/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/sub-pages/",
SubPagesEndpoint.as_view(),
name="sub-page",
),
+6
View File
@@ -14,6 +14,7 @@ from plane.app.views import (
ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet,
UserProjectRolesEndpoint,
ProjectArchiveUnarchiveEndpoint,
)
@@ -175,4 +176,9 @@ urlpatterns = [
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archive/",
ProjectArchiveUnarchiveEndpoint.as_view(),
name="project-archive-unarchive",
),
]
+3
View File
@@ -5,6 +5,7 @@ from .project.base import (
ProjectFavoritesViewSet,
ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet,
ProjectArchiveUnarchiveEndpoint,
)
from .project.invite import (
@@ -90,6 +91,7 @@ from .cycle.base import (
CycleDateCheckEndpoint,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleArchiveUnarchiveEndpoint,
CycleUserPropertiesEndpoint,
)
from .cycle.issue import (
@@ -168,6 +170,7 @@ from .module.base import (
ModuleViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
ModuleArchiveUnarchiveEndpoint,
ModuleUserPropertiesEndpoint,
)
+13 -15
View File
@@ -1,27 +1,27 @@
# Python imports
import zoneinfo
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import IntegrityError
# Django imports
from django.urls import resolve
from django.conf import settings
from django.utils import timezone
from django.db import IntegrityError
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django_filters.rest_framework import DjangoFilterBackend
# Third part imports
from rest_framework import status
from rest_framework.viewsets import ModelViewSet
from rest_framework.response import Response
from rest_framework.exceptions import APIException
from rest_framework.views import APIView
from rest_framework.filters import SearchFilter
from rest_framework.permissions import IsAuthenticated
from sentry_sdk import capture_exception
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
# Module imports
from plane.utils.paginator import BasePaginator
from plane.bgtasks.webhook_task import send_webhook
from plane.utils.exception_logger import log_exception
from plane.utils.paginator import BasePaginator
class TimezoneMixin:
@@ -87,7 +87,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
try:
return self.model.objects.all()
except Exception as e:
capture_exception(e)
log_exception(e)
raise APIException(
"Please check the view", status.HTTP_400_BAD_REQUEST
)
@@ -121,13 +121,13 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
)
if isinstance(e, KeyError):
capture_exception(e)
log_exception(e)
return Response(
{"error": "The required key does not exist."},
status=status.HTTP_400_BAD_REQUEST,
)
capture_exception(e)
log_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -233,9 +233,7 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
status=status.HTTP_400_BAD_REQUEST,
)
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+457 -57
View File
@@ -1,62 +1,56 @@
# Python imports
import json
# Django imports
from django.db.models import (
Func,
F,
Q,
Exists,
OuterRef,
Count,
Prefetch,
Case,
When,
Value,
CharField,
)
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.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import UUIDField
# Django imports
from django.db.models import (
Case,
CharField,
Count,
Exists,
F,
Func,
OuterRef,
Prefetch,
Q,
UUIDField,
Value,
When,
)
from django.db.models.functions import Coalesce
from django.utils import timezone
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .. import BaseViewSet, BaseAPIView, WebhookMixin
from plane.app.serializers import (
CycleSerializer,
CycleIssueSerializer,
CycleFavoriteSerializer,
IssueSerializer,
CycleWriteSerializer,
CycleUserPropertiesSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
ProjectLitePermission,
)
from plane.db.models import (
User,
Cycle,
CycleIssue,
Issue,
CycleFavorite,
IssueLink,
IssueAttachment,
Label,
CycleUserProperties,
from plane.app.serializers import (
CycleFavoriteSerializer,
CycleSerializer,
CycleUserPropertiesSerializer,
CycleWriteSerializer,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
from plane.db.models import (
Cycle,
CycleFavorite,
CycleIssue,
CycleUserProperties,
Issue,
Label,
User,
)
from plane.utils.analytics_plot import burndown_plot
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
class CycleViewSet(WebhookMixin, BaseViewSet):
serializer_class = CycleSerializer
@@ -88,6 +82,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(project__archived_at__isnull=True)
.select_related("project", "workspace", "owned_by")
.prefetch_related(
Prefetch(
@@ -108,7 +103,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
.annotate(is_favorite=Exists(favorite_subquery))
.annotate(
total_issues=Count(
"issue_cycle",
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
@@ -117,7 +113,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
@@ -127,7 +124,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
@@ -137,7 +135,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
@@ -147,7 +146,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
@@ -157,7 +157,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
@@ -201,7 +202,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
def list(self, request, slug, project_id):
queryset = self.get_queryset()
queryset = self.get_queryset().filter(archived_at__isnull=True)
cycle_view = request.GET.get("cycle_view", "all")
# Update the order by
@@ -327,13 +328,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
}
if data[0]["start_date"] and data[0]["end_date"]:
data[0]["distribution"][
"completion_chart"
] = burndown_plot(
queryset=queryset.first(),
slug=slug,
project_id=project_id,
cycle_id=data[0]["id"],
data[0]["distribution"]["completion_chart"] = (
burndown_plot(
queryset=queryset.first(),
slug=slug,
project_id=project_id,
cycle_id=data[0]["id"],
)
)
return Response(data, status=status.HTTP_200_OK)
@@ -402,8 +403,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"progress_snapshot",
# meta fields
"is_favorite",
"total_issues",
"cancelled_issues",
"total_issues",
"completed_issues",
"started_issues",
"unstarted_issues",
@@ -430,6 +431,11 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
workspace__slug=slug, project_id=project_id, pk=pk
)
cycle = queryset.first()
if cycle.archived_at:
return Response(
{"error": "Archived cycle cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
request_data = request.data
if (
@@ -487,10 +493,22 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, pk):
queryset = self.get_queryset().filter(pk=pk)
queryset = (
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
)
data = (
self.get_queryset()
.filter(pk=pk)
.annotate(
sub_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("project_id"),
parent__isnull=False,
issue_cycle__cycle_id=pk,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.values(
# necessary fields
"id",
@@ -507,6 +525,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"external_source",
"external_id",
"progress_snapshot",
"sub_issues",
# meta fields
"is_favorite",
"total_issues",
@@ -521,6 +540,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
.first()
)
queryset = queryset.first()
if data is None:
return Response(
{"error": "Cycle does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
# Assignee Distribution
assignee_distribution = (
Issue.objects.filter(
@@ -660,6 +686,380 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleArchiveUnarchiveEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
favorite_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"),
)
return (
Cycle.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(archived_at__isnull=False)
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(project__archived_at__isnull=True)
.select_related("project", "workspace", "owned_by")
.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(),
)
)
.annotate(is_favorite=Exists(favorite_subquery))
.annotate(
total_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
completed_issues=Count(
"issue_cycle__issue__id",
distinct=True,
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__id",
distinct=True,
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__id",
distinct=True,
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__id",
distinct=True,
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__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group="backlog",
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(),
)
)
.annotate(
assignee_ids=Coalesce(
ArrayAgg(
"issue_cycle__issue__assignees__id",
distinct=True,
filter=~Q(
issue_cycle__issue__assignees__id__isnull=True
),
),
Value([], output_field=ArrayField(UUIDField())),
)
)
.order_by("-is_favorite", "name")
.distinct()
)
def get(self, request, slug, project_id, pk=None):
if pk is None:
queryset = (
self.get_queryset()
.annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.values(
# necessary fields
"id",
"workspace_id",
"project_id",
# model fields
"name",
"description",
"start_date",
"end_date",
"owned_by_id",
"view_props",
"sort_order",
"external_source",
"external_id",
"progress_snapshot",
# meta fields
"total_issues",
"is_favorite",
"cancelled_issues",
"completed_issues",
"started_issues",
"unstarted_issues",
"backlog_issues",
"assignee_ids",
"status",
"archived_at",
)
).order_by("-is_favorite", "-created_at")
return Response(queryset, status=status.HTTP_200_OK)
else:
queryset = (
self.get_queryset()
.filter(archived_at__isnull=False)
.filter(pk=pk)
)
data = (
self.get_queryset()
.filter(pk=pk)
.annotate(
sub_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("project_id"),
parent__isnull=False,
issue_cycle__cycle_id=pk,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.values(
# necessary fields
"id",
"workspace_id",
"project_id",
# model fields
"name",
"description",
"start_date",
"end_date",
"owned_by_id",
"view_props",
"sort_order",
"external_source",
"external_id",
"progress_snapshot",
"sub_issues",
# meta fields
"is_favorite",
"total_issues",
"cancelled_issues",
"completed_issues",
"started_issues",
"unstarted_issues",
"backlog_issues",
"assignee_ids",
"status",
)
.first()
)
queryset = queryset.first()
if data is None:
return Response(
{"error": "Cycle does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
# Assignee Distribution
assignee_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=pk,
workspace__slug=slug,
project_id=project_id,
)
.annotate(first_name=F("assignees__first_name"))
.annotate(last_name=F("assignees__last_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.annotate(display_name=F("assignees__display_name"))
.values(
"first_name",
"last_name",
"assignee_id",
"avatar",
"display_name",
)
.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("first_name", "last_name")
)
# Label Distribution
label_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=pk,
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")
)
data["distribution"] = {
"assignees": assignee_distribution,
"labels": label_distribution,
"completion_chart": {},
}
if queryset.start_date and queryset.end_date:
data["distribution"]["completion_chart"] = burndown_plot(
queryset=queryset,
slug=slug,
project_id=project_id,
cycle_id=pk,
)
return Response(
data,
status=status.HTTP_200_OK,
)
def post(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.get(
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
if cycle.end_date >= timezone.now().date():
return Response(
{"error": "Only completed cycles can be archived"},
status=status.HTTP_400_BAD_REQUEST,
)
cycle.archived_at = timezone.now()
cycle.save()
return Response(
{"archived_at": str(cycle.archived_at)},
status=status.HTTP_200_OK,
)
def delete(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.get(
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
cycle.archived_at = None
cycle.save()
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleDateCheckEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
+3 -1
View File
@@ -74,6 +74,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(project__archived_at__isnull=True)
.filter(cycle_id=self.kwargs.get("cycle_id"))
.select_related("project")
.select_related("workspace")
@@ -142,7 +143,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
+9 -3
View File
@@ -38,7 +38,6 @@ from plane.db.models import (
IssueLink,
IssueAttachment,
IssueRelation,
IssueAssignee,
User,
)
from plane.app.serializers import (
@@ -150,7 +149,8 @@ def dashboard_assigned_issues(self, request, slug):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -304,7 +304,8 @@ def dashboard_created_issues(self, request, slug):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -472,6 +473,7 @@ def dashboard_recent_activity(self, request, slug):
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
actor=request.user,
).select_related("actor", "workspace", "issue", "project")[:8]
@@ -487,6 +489,7 @@ def dashboard_recent_projects(self, request, slug):
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
actor=request.user,
)
.values_list("project_id", flat=True)
@@ -501,6 +504,7 @@ def dashboard_recent_projects(self, request, slug):
additional_projects = Project.objects.filter(
project_projectmember__member=request.user,
project_projectmember__is_active=True,
archived_at__isnull=True,
workspace__slug=slug,
).exclude(id__in=unique_project_ids)
@@ -523,6 +527,7 @@ def dashboard_recent_collaborators(self, request, slug):
actor=OuterRef("member"),
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.values("actor")
.annotate(num_activities=Count("pk"))
@@ -535,6 +540,7 @@ def dashboard_recent_collaborators(self, request, slug):
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.annotate(
num_activities=Coalesce(
+4 -1
View File
@@ -29,7 +29,10 @@ class ExportIssuesEndpoint(BaseAPIView):
if provider in ["csv", "xlsx", "json"]:
if not project_ids:
project_ids = Project.objects.filter(
workspace__slug=slug
workspace__slug=slug,
project_projectmember__member=request.user,
project_projectmember__is_active=True,
archived_at__isnull=True,
).values_list("id", flat=True)
project_ids = [str(project_id) for project_id in project_ids]
+229 -133
View File
@@ -3,7 +3,7 @@ import json
# Django import
from django.utils import timezone
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch, Exists
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
@@ -24,16 +24,15 @@ from plane.db.models import (
State,
IssueLink,
IssueAttachment,
Project,
ProjectMember,
IssueReaction,
IssueSubscriber,
)
from plane.app.serializers import (
IssueCreateSerializer,
IssueSerializer,
InboxSerializer,
InboxIssueSerializer,
IssueDetailSerializer,
InboxIssueDetailSerializer,
)
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.issue_activites_task import issue_activity
@@ -64,13 +63,20 @@ class InboxViewSet(BaseViewSet):
.select_related("workspace", "project")
)
def list(self, request, slug, project_id):
inbox = self.get_queryset().first()
return Response(
InboxSerializer(inbox).data,
status=status.HTTP_200_OK,
)
def perform_create(self, serializer):
serializer.save(project_id=self.kwargs.get("project_id"))
def destroy(self, request, slug, project_id, pk):
inbox = Inbox.objects.get(
inbox = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id, pk=pk
)
).first()
# Handle default inbox delete
if inbox.is_default:
return Response(
@@ -98,7 +104,6 @@ class InboxIssueViewSet(BaseViewSet):
Issue.objects.filter(
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
issue_inbox__inbox_id=self.kwargs.get("inbox_id"),
)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
@@ -146,7 +151,8 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -161,51 +167,49 @@ class InboxIssueViewSet(BaseViewSet):
)
).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")
)
if self.expand:
issues = IssueSerializer(
issue_queryset, expand=self.expand, many=True
).data
else:
issues = issue_queryset.values(
"id",
"name",
"state_id",
"sort_order",
"completed_at",
"estimate_point",
"priority",
"start_date",
"target_date",
"sequence_id",
"project_id",
"parent_id",
"cycle_id",
"module_ids",
"label_ids",
"assignee_ids",
"sub_issues_count",
"created_at",
"updated_at",
"created_by",
"updated_by",
"attachment_count",
"link_count",
"is_draft",
"archived_at",
def list(self, request, slug, project_id):
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
filters = issue_filters(request.GET, "GET", "issue__")
inbox_issue = (
InboxIssue.objects.filter(
inbox_id=inbox_id.id, project_id=project_id, **filters
)
return Response(
issues,
status=status.HTTP_200_OK,
.select_related("issue")
.prefetch_related(
"issue__labels",
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
)
)
).order_by(request.GET.get("order_by", "-issue__created_at"))
# inbox status filter
inbox_status = [
item
for item in request.GET.get("status", "-2").split(",")
if item != "null"
]
if inbox_status:
inbox_issue = inbox_issue.filter(status__in=inbox_status)
return self.paginate(
request=request,
queryset=(inbox_issue),
on_results=lambda inbox_issues: InboxIssueSerializer(
inbox_issues,
many=True,
).data,
)
def create(self, request, slug, project_id, inbox_id):
def create(self, request, slug, project_id):
if not request.data.get("issue", {}).get("name", False):
return Response(
{"error": "Name is required"},
@@ -228,49 +232,88 @@ class InboxIssueViewSet(BaseViewSet):
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="backlog",
group="triage",
description="Default state for managing all Inbox Issues",
project_id=project_id,
color="#ff7700",
is_triage=True,
)
# create an issue
issue = Issue.objects.create(
name=request.data.get("issue", {}).get("name"),
description=request.data.get("issue", {}).get("description", {}),
description_html=request.data.get("issue", {}).get(
"description_html", "<p></p>"
),
priority=request.data.get("issue", {}).get("priority", "low"),
project_id=project_id,
state=state,
project = Project.objects.get(pk=project_id)
serializer = IssueCreateSerializer(
data=request.data.get("issue"),
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
"default_assignee_id": project.default_assignee_id,
},
)
if serializer.is_valid():
serializer.save()
# Create an Issue Activity
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(serializer.data["id"]),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox_id.id,
project_id=project_id,
issue_id=serializer.data["id"],
source=request.data.get("source", "in-app"),
)
inbox_issue = (
InboxIssue.objects.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"issue__assignees__id",
distinct=True,
filter=~Q(issue__assignees__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.get(
inbox_id=inbox_id.id,
issue_id=serializer.data["id"],
project_id=project_id,
)
)
serializer = InboxIssueDetailSerializer(inbox_issue)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
# Create an Issue Activity
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(issue.id),
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(
inbox_id=inbox_id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "in-app"),
)
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):
def partial_update(self, request, slug, project_id, issue_id):
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
inbox_issue = InboxIssue.objects.get(
issue_id=issue_id,
workspace__slug=slug,
@@ -295,9 +338,12 @@ class InboxIssueViewSet(BaseViewSet):
# Get issue data
issue_data = request.data.pop("issue", False)
if bool(issue_data):
issue = self.get_queryset().filter(pk=inbox_issue.issue_id).first()
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
# Only allow guests and viewers to edit name and description
if project_member.role <= 10:
# viewers and guests since only viewers and guests
@@ -345,7 +391,9 @@ class InboxIssueViewSet(BaseViewSet):
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
)
current_instance = json.dumps(
InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
)
if serializer.is_valid():
serializer.save()
# Update the issue state if the issue is rejected or marked as duplicate
@@ -373,7 +421,7 @@ class InboxIssueViewSet(BaseViewSet):
)
# Update the issue state only if it is in triage state
if issue.state.name == "Triage":
if issue.state.is_triage:
# Move to default state
state = State.objects.filter(
workspace__slug=slug,
@@ -383,60 +431,108 @@ class InboxIssueViewSet(BaseViewSet):
if state is not None:
issue.state = state
issue.save()
return Response(status=status.HTTP_204_NO_CONTENT)
# create a activity for status change
issue_activity.delay(
type="inbox.activity.created",
requested_data=json.dumps(
request.data, cls=DjangoJSONEncoder
),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
)
inbox_issue = (
InboxIssue.objects.filter(
inbox_id=inbox_id.id,
issue_id=serializer.data["id"],
project_id=project_id,
)
.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True),
),
Value(
[],
output_field=ArrayField(UUIDField()),
),
),
assignee_ids=Coalesce(
ArrayAgg(
"issue__assignees__id",
distinct=True,
filter=~Q(issue__assignees__id__isnull=True),
),
Value(
[],
output_field=ArrayField(UUIDField()),
),
),
).first()
)
serializer = InboxIssueDetailSerializer(inbox_issue).data
return Response(serializer, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
issue = self.get_queryset().filter(pk=issue_id).first()
serializer = IssueSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
serializer = InboxIssueDetailSerializer(inbox_issue).data
return Response(serializer, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
issue = (
self.get_queryset()
.filter(pk=issue_id)
def retrieve(self, request, slug, project_id, issue_id):
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
inbox_issue = (
InboxIssue.objects.select_related("issue")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
)
)
.prefetch_related(
Prefetch(
"issue_attachment",
queryset=IssueAttachment.objects.select_related("issue"),
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
)
"issue__labels",
"issue__assignees",
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=OuterRef("pk"),
subscriber=request.user,
)
)
label_ids=Coalesce(
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"issue__assignees__id",
distinct=True,
filter=~Q(issue__assignees__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.get(
inbox_id=inbox_id.id, issue_id=issue_id, project_id=project_id
)
)
issue = InboxIssueDetailSerializer(inbox_issue).data
return Response(
issue,
status=status.HTTP_200_OK,
)
def destroy(self, request, slug, project_id, issue_id):
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
if issue is None:
return Response(
{"error": "Requested object was not found"},
status=status.HTTP_404_NOT_FOUND,
)
serializer = IssueDetailSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, issue_id):
inbox_issue = InboxIssue.objects.get(
issue_id=issue_id,
workspace__slug=slug,
@@ -44,6 +44,7 @@ class IssueActivityEndpoint(BaseAPIView):
~Q(field__in=["comment", "vote", "reaction", "draft"]),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
workspace__slug=slug,
)
.filter(**filters)
@@ -54,6 +55,7 @@ class IssueActivityEndpoint(BaseAPIView):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
workspace__slug=slug,
)
.filter(**filters)
+2 -1
View File
@@ -105,7 +105,8 @@ class IssueArchiveViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
+42 -65
View File
@@ -1,84 +1,59 @@
# Python imports
import json
import random
from itertools import chain
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import (
Case,
CharField,
Exists,
F,
Func,
Max,
OuterRef,
Prefetch,
Q,
UUIDField,
Value,
When,
)
from django.db.models.functions import Coalesce
# Django imports
from django.utils import timezone
from django.db.models import (
Prefetch,
OuterRef,
Func,
F,
Q,
Case,
Value,
CharField,
When,
Exists,
Max,
)
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.db import IntegrityError
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import Value, UUIDField
from django.db.models.functions import Coalesce
from rest_framework import status
# Third Party imports
from rest_framework.response import Response
from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
# Module imports
from .. import BaseViewSet, BaseAPIView, WebhookMixin
from plane.app.serializers import (
IssueActivitySerializer,
IssueCommentSerializer,
IssuePropertySerializer,
IssueSerializer,
IssueCreateSerializer,
LabelSerializer,
IssueFlatSerializer,
IssueLinkSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
IssueSubscriberSerializer,
ProjectMemberLiteSerializer,
IssueReactionSerializer,
CommentReactionSerializer,
IssueRelationSerializer,
RelatedIssueSerializer,
IssueDetailSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
WorkSpaceAdminPermission,
ProjectMemberPermission,
ProjectLitePermission,
)
from plane.db.models import (
Project,
Issue,
IssueActivity,
IssueComment,
IssueProperty,
Label,
IssueLink,
IssueAttachment,
IssueSubscriber,
ProjectMember,
IssueReaction,
CommentReaction,
IssueRelation,
from plane.app.serializers import (
IssueCreateSerializer,
IssueDetailSerializer,
IssuePropertySerializer,
IssueSerializer,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
from plane.db.models import (
Issue,
IssueAttachment,
IssueLink,
IssueProperty,
IssueReaction,
IssueSubscriber,
Project,
)
from plane.utils.issue_filters import issue_filters
from collections import defaultdict
from plane.utils.cache import invalidate_cache
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
class IssueListEndpoint(BaseAPIView):
@@ -142,7 +117,8 @@ class IssueListEndpoint(BaseAPIView):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -336,7 +312,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -48,6 +48,7 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.select_related("project")
.select_related("workspace")
@@ -163,6 +164,7 @@ class CommentReactionViewSet(BaseViewSet):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.order_by("-created_at")
.distinct()
+41 -44
View File
@@ -1,52 +1,54 @@
# Python imports
import json
# Django imports
from django.utils import timezone
from django.db.models import (
Prefetch,
OuterRef,
Func,
F,
Q,
Case,
Value,
CharField,
When,
Exists,
Max,
UUIDField,
)
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import (
Case,
CharField,
Exists,
F,
Func,
Max,
OuterRef,
Prefetch,
Q,
UUIDField,
Value,
When,
)
from django.db.models.functions import Coalesce
# Django imports
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from rest_framework import status
# Third Party imports
from rest_framework.response import Response
from rest_framework import status
from plane.app.permissions import ProjectEntityPermission
from plane.app.serializers import (
IssueCreateSerializer,
IssueDetailSerializer,
IssueFlatSerializer,
IssueSerializer,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.db.models import (
Issue,
IssueAttachment,
IssueLink,
IssueReaction,
IssueSubscriber,
Project,
)
from plane.utils.issue_filters import issue_filters
# Module imports
from .. import BaseViewSet
from plane.app.serializers import (
IssueSerializer,
IssueCreateSerializer,
IssueFlatSerializer,
IssueDetailSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.db.models import (
Project,
Issue,
IssueLink,
IssueAttachment,
IssueSubscriber,
IssueReaction,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
class IssueDraftViewSet(BaseViewSet):
@@ -99,7 +101,8 @@ class IssueDraftViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -117,12 +120,6 @@ class IssueDraftViewSet(BaseViewSet):
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
filters = issue_filters(request.query_params, "GET")
fields = [
field
for field in request.GET.get("fields", "").split(",")
if field
]
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = [
+1 -1
View File
@@ -87,7 +87,7 @@ class BulkCreateIssueLabelsEndpoint(BaseAPIView):
Label(
name=label.get("name", "Migrated"),
description=label.get("description", "Migrated Issue"),
color="#" + "%06x" % random.randint(0, 0xFFFFFF),
color=f"#{random.randint(0, 0xFFFFFF+1):06X}",
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
+1
View File
@@ -35,6 +35,7 @@ class IssueLinkViewSet(BaseViewSet):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.order_by("-created_at")
.distinct()
@@ -34,6 +34,7 @@ class IssueReactionViewSet(BaseViewSet):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.order_by("-created_at")
.distinct()
@@ -41,6 +41,7 @@ class IssueRelationViewSet(BaseViewSet):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.select_related("project")
.select_related("workspace")
+2 -1
View File
@@ -83,7 +83,8 @@ class SubIssuesEndpoint(BaseAPIView):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -54,6 +54,7 @@ class IssueSubscriberViewSet(BaseViewSet):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.order_by("-created_at")
.distinct()
+462 -77
View File
@@ -1,44 +1,57 @@
# Python imports
import json
# Django Imports
from django.utils import timezone
from django.db.models import Prefetch, F, OuterRef, Exists, Count, Q
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import Value, UUIDField
from django.db.models import (
Count,
Exists,
F,
Func,
IntegerField,
OuterRef,
Prefetch,
Q,
Subquery,
UUIDField,
Value,
)
from django.db.models.functions import Coalesce
# Django Imports
from django.utils import timezone
from rest_framework import status
# Third party imports
from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet, BaseAPIView, WebhookMixin
from plane.app.serializers import (
ModuleWriteSerializer,
ModuleSerializer,
ModuleLinkSerializer,
ModuleFavoriteSerializer,
ModuleUserPropertiesSerializer,
ModuleDetailSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
ProjectLitePermission,
)
from plane.db.models import (
Module,
ModuleIssue,
Project,
Issue,
ModuleLink,
ModuleFavorite,
ModuleUserProperties,
from plane.app.serializers import (
ModuleDetailSerializer,
ModuleFavoriteSerializer,
ModuleLinkSerializer,
ModuleSerializer,
ModuleUserPropertiesSerializer,
ModuleWriteSerializer,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.db.models import (
Issue,
Module,
ModuleFavorite,
ModuleIssue,
ModuleLink,
ModuleUserProperties,
Project,
)
from plane.utils.analytics_plot import burndown_plot
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
class ModuleViewSet(WebhookMixin, BaseViewSet):
model = Module
@@ -61,6 +74,59 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
cancelled_issues = (
Issue.issue_objects.filter(
state__group="cancelled",
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
completed_issues = (
Issue.issue_objects.filter(
state__group="completed",
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
started_issues = (
Issue.issue_objects.filter(
state__group="started",
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
unstarted_issues = (
Issue.issue_objects.filter(
state__group="unstarted",
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
backlog_issues = (
Issue.issue_objects.filter(
state__group="backlog",
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
total_issues = (
Issue.issue_objects.filter(
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
return (
super()
.get_queryset()
@@ -80,62 +146,39 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
)
)
.annotate(
total_issues=Count(
"issue_module",
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="completed",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
completed_issues=Coalesce(
Subquery(completed_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
cancelled_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="cancelled",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
cancelled_issues=Coalesce(
Subquery(cancelled_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
started_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="started",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
started_issues=Coalesce(
Subquery(started_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
unstarted_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="unstarted",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
unstarted_issues=Coalesce(
Subquery(unstarted_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
backlog_issues=Count(
"issue_module__issue__state__group",
filter=Q(
issue_module__issue__state__group="backlog",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
backlog_issues=Coalesce(
Subquery(backlog_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
total_issues=Coalesce(
Subquery(total_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
@@ -183,9 +226,9 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"external_id",
# computed fields
"is_favorite",
"total_issues",
"cancelled_issues",
"completed_issues",
"total_issues",
"started_issues",
"unstarted_issues",
"backlog_issues",
@@ -197,7 +240,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def list(self, request, slug, project_id):
queryset = self.get_queryset()
queryset = self.get_queryset().filter(archived_at__isnull=True)
if self.fields:
modules = ModuleSerializer(
queryset,
@@ -224,8 +267,8 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"external_source",
"external_id",
# computed fields
"is_favorite",
"total_issues",
"is_favorite",
"cancelled_issues",
"completed_issues",
"started_issues",
@@ -237,7 +280,21 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
return Response(modules, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk):
queryset = self.get_queryset().filter(pk=pk)
queryset = (
self.get_queryset()
.filter(archived_at__isnull=True)
.filter(pk=pk)
.annotate(
sub_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("project_id"),
parent__isnull=False,
issue_module__module_id=pk,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
assignee_distribution = (
Issue.objects.filter(
@@ -338,9 +395,11 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"completion_chart": {},
}
if queryset.first().start_date and queryset.first().target_date:
# Fetch the modules
modules = queryset.first()
if modules and modules.start_date and modules.target_date:
data["distribution"]["completion_chart"] = burndown_plot(
queryset=queryset.first(),
queryset=modules,
slug=slug,
project_id=project_id,
module_id=pk,
@@ -352,14 +411,20 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
)
def partial_update(self, request, slug, project_id, pk):
queryset = self.get_queryset().filter(pk=pk)
module = self.get_queryset().filter(pk=pk)
if module.first().archived_at:
return Response(
{"error": "Archived module cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ModuleWriteSerializer(
queryset.first(), data=request.data, partial=True
module.first(), data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
module = queryset.values(
module = module.values(
# Required fields
"id",
"workspace_id",
@@ -380,10 +445,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"external_id",
# computed fields
"is_favorite",
"total_issues",
"cancelled_issues",
"completed_issues",
"started_issues",
"total_issues",
"unstarted_issues",
"backlog_issues",
"created_at",
@@ -443,12 +508,332 @@ class ModuleLinkViewSet(BaseViewSet):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.order_by("-created_at")
.distinct()
)
class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
favorite_subquery = ModuleFavorite.objects.filter(
user=self.request.user,
module_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
cancelled_issues = (
Issue.issue_objects.filter(
state__group="cancelled",
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
completed_issues = (
Issue.issue_objects.filter(
state__group="completed",
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
started_issues = (
Issue.issue_objects.filter(
state__group="started",
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
unstarted_issues = (
Issue.issue_objects.filter(
state__group="unstarted",
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
backlog_issues = (
Issue.issue_objects.filter(
state__group="backlog",
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
total_issues = (
Issue.issue_objects.filter(
issue_module__module_id=OuterRef("pk"),
)
.values("issue_module__module_id")
.annotate(cnt=Count("pk"))
.values("cnt")
)
return (
Module.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(archived_at__isnull=False)
.annotate(is_favorite=Exists(favorite_subquery))
.select_related("workspace", "project", "lead")
.prefetch_related("members")
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related(
"module", "created_by"
),
)
)
.annotate(
completed_issues=Coalesce(
Subquery(completed_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
cancelled_issues=Coalesce(
Subquery(cancelled_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
started_issues=Coalesce(
Subquery(started_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
unstarted_issues=Coalesce(
Subquery(unstarted_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
backlog_issues=Coalesce(
Subquery(backlog_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
total_issues=Coalesce(
Subquery(total_issues[:1]),
Value(0, output_field=IntegerField()),
)
)
.annotate(
member_ids=Coalesce(
ArrayAgg(
"members__id",
distinct=True,
filter=~Q(members__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
)
)
.order_by("-is_favorite", "-created_at")
)
def get(self, request, slug, project_id, pk=None):
if pk is None:
queryset = self.get_queryset()
modules = queryset.values( # Required fields
"id",
"workspace_id",
"project_id",
# Model fields
"name",
"description",
"description_text",
"description_html",
"start_date",
"target_date",
"status",
"lead_id",
"member_ids",
"view_props",
"sort_order",
"external_source",
"external_id",
# computed fields
"total_issues",
"is_favorite",
"cancelled_issues",
"completed_issues",
"started_issues",
"unstarted_issues",
"backlog_issues",
"created_at",
"updated_at",
"archived_at",
)
return Response(modules, status=status.HTTP_200_OK)
else:
queryset = (
self.get_queryset()
.filter(pk=pk)
.annotate(
sub_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("project_id"),
parent__isnull=False,
issue_module__module_id=pk,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
assignee_distribution = (
Issue.objects.filter(
issue_module__module_id=pk,
workspace__slug=slug,
project_id=project_id,
)
.annotate(first_name=F("assignees__first_name"))
.annotate(last_name=F("assignees__last_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(display_name=F("assignees__display_name"))
.annotate(avatar=F("assignees__avatar"))
.values(
"first_name",
"last_name",
"assignee_id",
"avatar",
"display_name",
)
.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("first_name", "last_name")
)
label_distribution = (
Issue.objects.filter(
issue_module__module_id=pk,
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")
)
data = ModuleDetailSerializer(queryset.first()).data
data["distribution"] = {
"assignees": assignee_distribution,
"labels": label_distribution,
"completion_chart": {},
}
# Fetch the modules
modules = queryset.first()
if modules and modules.start_date and modules.target_date:
data["distribution"]["completion_chart"] = burndown_plot(
queryset=modules,
slug=slug,
project_id=project_id,
module_id=pk,
)
return Response(
data,
status=status.HTTP_200_OK,
)
def post(self, request, slug, project_id, module_id):
module = Module.objects.get(
pk=module_id, project_id=project_id, workspace__slug=slug
)
if module.status not in ["completed", "cancelled"]:
return Response(
{
"error": "Only completed or cancelled modules can be archived"
},
status=status.HTTP_400_BAD_REQUEST,
)
module.archived_at = timezone.now()
module.save()
return Response(
{"archived_at": str(module.archived_at)},
status=status.HTTP_200_OK,
)
def delete(self, request, slug, project_id, module_id):
module = Module.objects.get(
pk=module_id, project_id=project_id, workspace__slug=slug
)
module.archived_at = None
module.save()
return Response(status=status.HTTP_204_NO_CONTENT)
class ModuleFavoriteViewSet(BaseViewSet):
serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite
+2 -1
View File
@@ -93,7 +93,8 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
+69 -40
View File
@@ -1,5 +1,7 @@
# Python imports
import json
from datetime import datetime
from django.core.serializers.json import DjangoJSONEncoder
# Django imports
from django.db import connection
@@ -17,6 +19,7 @@ from plane.app.serializers import (
PageLogSerializer,
PageSerializer,
SubPageSerializer,
PageDetailSerializer,
)
from plane.db.models import (
Page,
@@ -28,6 +31,8 @@ from plane.db.models import (
# Module imports
from ..base import BaseAPIView, BaseViewSet
from plane.bgtasks.page_transaction_task import page_transaction
def unarchive_archive_page_and_descendants(page_id, archived_at):
# Your SQL query
@@ -70,6 +75,7 @@ class PageViewSet(BaseViewSet):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.filter(parent__isnull=True)
.filter(Q(owned_by=self.request.user) | Q(access=0))
@@ -86,11 +92,21 @@ class PageViewSet(BaseViewSet):
def create(self, request, slug, project_id):
serializer = PageSerializer(
data=request.data,
context={"project_id": project_id, "owned_by_id": request.user.id},
context={
"project_id": project_id,
"owned_by_id": request.user.id,
"description_html": request.data.get(
"description_html", "<p></p>"
),
},
)
if serializer.is_valid():
serializer.save()
# capture the page transaction
page_transaction.delay(request.data, None, serializer.data["id"])
page = Page.objects.get(pk=serializer.data["id"])
serializer = PageDetailSerializer(page)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -124,9 +140,25 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = PageSerializer(page, data=request.data, partial=True)
serializer = PageDetailSerializer(
page, data=request.data, partial=True
)
page_description = page.description_html
if serializer.is_valid():
serializer.save()
# capture the page transaction
if request.data.get("description_html"):
page_transaction.delay(
new_value=request.data,
old_value=json.dumps(
{
"description_html": page_description,
},
cls=DjangoJSONEncoder,
),
page_id=pk,
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
@@ -139,18 +171,30 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
def lock(self, request, slug, project_id, page_id):
def retrieve(self, request, slug, project_id, pk=None):
page = self.get_queryset().filter(pk=pk).first()
if page is None:
return Response(
{"error": "Page not found"},
status=status.HTTP_404_NOT_FOUND,
)
else:
return Response(
PageDetailSerializer(page).data, status=status.HTTP_200_OK
)
def lock(self, request, slug, project_id, pk):
page = Page.objects.filter(
pk=page_id, workspace__slug=slug, project_id=project_id
pk=pk, workspace__slug=slug, project_id=project_id
).first()
page.is_locked = True
page.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def unlock(self, request, slug, project_id, page_id):
def unlock(self, request, slug, project_id, pk):
page = Page.objects.filter(
pk=page_id, workspace__slug=slug, project_id=project_id
pk=pk, workspace__slug=slug, project_id=project_id
).first()
page.is_locked = False
@@ -159,13 +203,13 @@ class PageViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
def list(self, request, slug, project_id):
queryset = self.get_queryset().filter(archived_at__isnull=True)
queryset = self.get_queryset()
pages = PageSerializer(queryset, many=True).data
return Response(pages, status=status.HTTP_200_OK)
def archive(self, request, slug, project_id, page_id):
def archive(self, request, slug, project_id, pk):
page = Page.objects.get(
pk=page_id, workspace__slug=slug, project_id=project_id
pk=pk, workspace__slug=slug, project_id=project_id
)
# only the owner or admin can archive the page
@@ -183,13 +227,16 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
unarchive_archive_page_and_descendants(page_id, datetime.now())
unarchive_archive_page_and_descendants(pk, datetime.now())
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(
{"archived_at": str(datetime.now())},
status=status.HTTP_200_OK,
)
def unarchive(self, request, slug, project_id, page_id):
def unarchive(self, request, slug, project_id, pk):
page = Page.objects.get(
pk=page_id, workspace__slug=slug, project_id=project_id
pk=pk, workspace__slug=slug, project_id=project_id
)
# only the owner or admin can un archive the page
@@ -212,19 +259,10 @@ class PageViewSet(BaseViewSet):
page.parent = None
page.save(update_fields=["parent"])
unarchive_archive_page_and_descendants(page_id, None)
unarchive_archive_page_and_descendants(pk, None)
return Response(status=status.HTTP_204_NO_CONTENT)
def archive_list(self, request, slug, project_id):
pages = Page.objects.filter(
project_id=project_id,
workspace__slug=slug,
).filter(archived_at__isnull=False)
pages = PageSerializer(pages, many=True).data
return Response(pages, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, pk):
page = Page.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
@@ -268,29 +306,20 @@ class PageFavoriteViewSet(BaseViewSet):
serializer_class = PageFavoriteSerializer
model = PageFavorite
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(archived_at__isnull=True)
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user)
.select_related("page", "page__owned_by")
def create(self, request, slug, project_id, pk):
_ = PageFavorite.objects.create(
project_id=project_id,
page_id=pk,
user=request.user,
)
return Response(status=status.HTTP_204_NO_CONTENT)
def create(self, request, slug, project_id):
serializer = PageFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, page_id):
def destroy(self, request, slug, project_id, pk):
page_favorite = PageFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
page_id=page_id,
page_id=pk,
)
page_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+109 -5
View File
@@ -13,6 +13,7 @@ from django.db.models import (
Subquery,
)
from django.conf import settings
from django.utils import timezone
# Third Party imports
from rest_framework.response import Response
@@ -46,9 +47,11 @@ from plane.db.models import (
Inbox,
ProjectDeployBoard,
IssueProperty,
Issue,
)
from plane.utils.cache import cache_response
class ProjectViewSet(WebhookMixin, BaseViewSet):
serializer_class = ProjectListSerializer
model = Project
@@ -70,7 +73,10 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(
Q(project_projectmember__member=self.request.user)
Q(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
| Q(network=2)
)
.select_related(
@@ -171,6 +177,74 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
).data
return Response(projects, status=status.HTTP_200_OK)
def retrieve(self, request, slug, pk):
project = (
self.get_queryset()
.filter(archived_at__isnull=True)
.filter(pk=pk)
.annotate(
total_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("pk"),
parent__isnull=True,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("pk"),
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
archived_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
archived_at__isnull=False,
parent__isnull=True,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
archived_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
archived_at__isnull=False,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
draft_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
is_draft=True,
parent__isnull=True,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
draft_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
is_draft=True,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
).first()
serializer = ProjectListSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
def create(self, request, slug):
try:
workspace = Workspace.objects.get(slug=slug)
@@ -277,12 +351,12 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
{"name": "The project name is already taken"},
status=status.HTTP_410_GONE,
)
except Workspace.DoesNotExist as e:
except Workspace.DoesNotExist:
return Response(
{"error": "Workspace does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
except serializers.ValidationError as e:
except serializers.ValidationError:
return Response(
{"identifier": "The project identifier is already taken"},
status=status.HTTP_410_GONE,
@@ -294,6 +368,12 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
project = Project.objects.get(pk=pk)
if project.archived_at:
return Response(
{"error": "Archived projects cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ProjectSerializer(
project,
data={**request.data},
@@ -313,10 +393,11 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
# Create the triage state in Backlog group
State.objects.get_or_create(
name="Triage",
group="backlog",
group="triage",
description="Default state for managing all Inbox Issues",
project_id=pk,
color="#ff7700",
is_triage=True,
)
project = (
@@ -341,13 +422,35 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
{"error": "Project does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
except serializers.ValidationError as e:
except serializers.ValidationError:
return Response(
{"identifier": "The project identifier is already taken"},
status=status.HTTP_410_GONE,
)
class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]
def post(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project.archived_at = timezone.now()
project.save()
return Response(
{"archived_at": str(project.archived_at)},
status=status.HTTP_200_OK,
)
def delete(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project.archived_at = None
project.save()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProjectIdentifierEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
@@ -471,6 +574,7 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
# Cache the below api for 24 hours
@cache_response(60 * 60 * 24, user=False)
def get(self, request):
+7
View File
@@ -50,6 +50,7 @@ class GlobalSearchEndpoint(BaseAPIView):
q,
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
archived_at__isnull=True,
workspace__slug=slug,
)
.distinct()
@@ -72,6 +73,7 @@ class GlobalSearchEndpoint(BaseAPIView):
q,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
workspace__slug=slug,
)
@@ -97,6 +99,7 @@ class GlobalSearchEndpoint(BaseAPIView):
q,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
workspace__slug=slug,
)
@@ -121,6 +124,7 @@ class GlobalSearchEndpoint(BaseAPIView):
q,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
workspace__slug=slug,
)
@@ -145,6 +149,7 @@ class GlobalSearchEndpoint(BaseAPIView):
q,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
workspace__slug=slug,
)
@@ -169,6 +174,7 @@ class GlobalSearchEndpoint(BaseAPIView):
q,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
workspace__slug=slug,
)
@@ -243,6 +249,7 @@ class IssueSearchEndpoint(BaseAPIView):
workspace__slug=slug,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True
)
if workspace_search == "false":
+13 -8
View File
@@ -1,9 +1,6 @@
# Python imports
from itertools import groupby
# Django imports
from django.db.models import Q
# Third party imports
from rest_framework.response import Response
from rest_framework import status
@@ -17,6 +14,7 @@ from plane.app.permissions import (
from plane.db.models import State, Issue
from plane.utils.cache import invalidate_cache
class StateViewSet(BaseViewSet):
serializer_class = StateSerializer
model = State
@@ -33,14 +31,17 @@ class StateViewSet(BaseViewSet):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.filter(~Q(name="Triage"))
.filter(is_triage=False)
.select_related("project")
.select_related("workspace")
.distinct()
)
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
@invalidate_cache(
path="workspaces/:slug/states/", url_params=True, user=False
)
def create(self, request, slug, project_id):
serializer = StateSerializer(data=request.data)
if serializer.is_valid():
@@ -61,7 +62,9 @@ class StateViewSet(BaseViewSet):
return Response(state_dict, status=status.HTTP_200_OK)
return Response(states, status=status.HTTP_200_OK)
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
@invalidate_cache(
path="workspaces/:slug/states/", url_params=True, user=False
)
def mark_as_default(self, request, slug, project_id, pk):
# Select all the states which are marked as default
_ = State.objects.filter(
@@ -72,10 +75,12 @@ class StateViewSet(BaseViewSet):
).update(default=True)
return Response(status=status.HTTP_204_NO_CONTENT)
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
@invalidate_cache(
path="workspaces/:slug/states/", url_params=True, user=False
)
def destroy(self, request, slug, project_id, pk):
state = State.objects.get(
~Q(name="Triage"),
is_triage=False,
pk=pk,
project_id=project_id,
workspace__slug=slug,
+5 -3
View File
@@ -125,7 +125,8 @@ class GlobalViewIssuesViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -282,6 +283,7 @@ class IssueViewViewSet(BaseViewSet):
.filter(
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.select_related("project")
.select_related("workspace")
@@ -324,11 +326,11 @@ class IssueViewFavoriteViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, view_id):
view_favourite = IssueViewFavorite.objects.get(
view_favorite = IssueViewFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
view_id=view_id,
)
view_favourite.delete()
view_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+27 -23
View File
@@ -1,49 +1,51 @@
# Python imports
from datetime import date
from dateutil.relativedelta import relativedelta
import csv
import io
from datetime import date
from dateutil.relativedelta import relativedelta
from django.db import IntegrityError
from django.db.models import (
Count,
F,
Func,
OuterRef,
Prefetch,
Q,
)
from django.db.models.fields import DateField
from django.db.models.functions import Cast, ExtractDay, ExtractWeek
# Django imports
from django.http import HttpResponse
from django.db import IntegrityError
from django.utils import timezone
from django.db.models import (
Prefetch,
OuterRef,
Func,
F,
Q,
Count,
)
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
from django.db.models.fields import DateField
# Third party modules
from rest_framework import status
from rest_framework.response import Response
from plane.app.permissions import (
WorkSpaceAdminPermission,
WorkSpaceBasePermission,
WorkspaceEntityPermission,
)
# Module imports
from plane.app.serializers import (
WorkSpaceSerializer,
WorkspaceThemeSerializer,
)
from plane.app.views.base import BaseViewSet, BaseAPIView
from plane.app.views.base import BaseAPIView, BaseViewSet
from plane.db.models import (
Workspace,
IssueActivity,
Issue,
WorkspaceTheme,
IssueActivity,
Workspace,
WorkspaceMember,
)
from plane.app.permissions import (
WorkSpaceBasePermission,
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
WorkspaceTheme,
)
from plane.utils.cache import cache_response, invalidate_cache
class WorkSpaceViewSet(BaseViewSet):
model = Workspace
serializer_class = WorkSpaceSerializer
@@ -138,6 +140,7 @@ class WorkSpaceViewSet(BaseViewSet):
{"slug": "The workspace with the slug already exists"},
status=status.HTTP_410_GONE,
)
@cache_response(60 * 60 * 2)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@@ -148,7 +151,8 @@ class WorkSpaceViewSet(BaseViewSet):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/")
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True)
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
@@ -27,6 +27,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.filter(archived_at__isnull=False)
.annotate(
total_issues=Count(
"issue_cycle",
@@ -3,15 +3,10 @@ from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.app.permissions import WorkspaceEntityPermission
from plane.app.serializers import WorkspaceEstimateSerializer
from plane.app.views.base import BaseAPIView
from plane.db.models import Project, Estimate
from plane.app.permissions import WorkspaceEntityPermission
# Django imports
from django.db.models import (
Prefetch,
)
from plane.db.models import Estimate, Project
from plane.utils.cache import cache_response
@@ -25,15 +20,11 @@ class WorkspaceEstimatesEndpoint(BaseAPIView):
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=Project.objects.select_related(
"estimate", "workspace", "project"
),
)
estimates = (
Estimate.objects.filter(pk__in=estimate_ids, workspace__slug=slug)
.prefetch_related("points")
.select_related("workspace", "project")
)
serializer = WorkspaceEstimateSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
+29 -16
View File
@@ -1,36 +1,39 @@
# Python imports
import jwt
from datetime import datetime
import jwt
# Django imports
from django.conf import settings
from django.utils import timezone
from django.db.models import Count
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.db.models import Count
from django.utils import timezone
# Third party modules
from rest_framework import status
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
# Module imports
from plane.app.permissions import WorkSpaceAdminPermission
from plane.app.serializers import (
WorkSpaceMemberSerializer,
WorkSpaceMemberInviteSerializer,
WorkSpaceMemberSerializer,
)
from plane.app.views.base import BaseAPIView
from .. import BaseViewSet
from plane.bgtasks.event_tracking_task import workspace_invite_event
from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.db.models import (
User,
Workspace,
WorkspaceMemberInvite,
WorkspaceMember,
WorkspaceMemberInvite,
)
from plane.app.permissions import WorkSpaceAdminPermission
from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.bgtasks.event_tracking_task import workspace_invite_event
from plane.utils.cache import invalidate_cache
from plane.utils.cache import invalidate_cache, invalidate_cache_directly
from .. import BaseViewSet
class WorkspaceInvitationsViewset(BaseViewSet):
"""Endpoint for creating, listing and deleting workspaces"""
@@ -166,7 +169,14 @@ class WorkspaceJoinEndpoint(BaseAPIView):
"""Invitation response endpoint the user can respond to the invitation"""
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/")
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True)
@invalidate_cache(
path="/api/workspaces/:slug/members/",
user=False,
multiple=True,
url_params=True,
)
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
def post(self, request, slug, pk):
workspace_invite = WorkspaceMemberInvite.objects.get(
pk=pk, workspace__slug=slug
@@ -264,10 +274,7 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/")
@invalidate_cache(
path="/api/workspaces/:slug/members/", url_params=True, user=False
)
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True)
def create(self, request):
invitations = request.data.get("invitations", [])
workspace_invitations = WorkspaceMemberInvite.objects.filter(
@@ -276,6 +283,12 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
# If the user is already a member of workspace and was deactivated then activate the user
for invitation in workspace_invitations:
invalidate_cache_directly(
path=f"/api/workspaces/{invitation.workspace.slug}/members/",
user=False,
request=request,
multiple=True,
)
# Update the WorkspaceMember for this specific invitation
WorkspaceMember.objects.filter(
workspace_id=invitation.workspace_id, member=request.user
@@ -20,6 +20,7 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
serializer = LabelSerializer(labels, many=True).data
return Response(serializer, status=status.HTTP_200_OK)
+43 -24
View File
@@ -1,41 +1,43 @@
# Django imports
from django.db.models import (
Q,
CharField,
Count,
Q,
)
from django.db.models.functions import Cast
from django.db.models import CharField
# Third party modules
from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.app.serializers import (
WorkSpaceMemberSerializer,
TeamSerializer,
UserLiteSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
ProjectMemberRoleSerializer,
)
from plane.app.views.base import BaseAPIView
from .. import BaseViewSet
from plane.db.models import (
User,
Workspace,
Team,
ProjectMember,
Project,
WorkspaceMember,
)
from plane.app.permissions import (
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
WorkspaceUserPermission,
)
# Module imports
from plane.app.serializers import (
ProjectMemberRoleSerializer,
TeamSerializer,
UserLiteSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
WorkSpaceMemberSerializer,
)
from plane.app.views.base import BaseAPIView
from plane.db.models import (
Project,
ProjectMember,
Team,
User,
Workspace,
WorkspaceMember,
)
from plane.utils.cache import cache_response, invalidate_cache
from .. import BaseViewSet
class WorkSpaceMemberViewSet(BaseViewSet):
serializer_class = WorkspaceMemberAdminSerializer
@@ -100,7 +102,10 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache(
path="/api/workspaces/:slug/members/", url_params=True, user=False
path="/api/workspaces/:slug/members/",
url_params=True,
user=False,
multiple=True,
)
def partial_update(self, request, slug, pk):
workspace_member = WorkspaceMember.objects.get(
@@ -145,7 +150,14 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@invalidate_cache(
path="/api/workspaces/:slug/members/", url_params=True, user=False
path="/api/workspaces/:slug/members/",
url_params=True,
user=False,
multiple=True,
)
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
@invalidate_cache(
path="/api/users/me/workspaces/", user=False, multiple=True
)
def destroy(self, request, slug, pk):
# Check the user role who is deleting the user
@@ -212,7 +224,14 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
@invalidate_cache(
path="/api/workspaces/:slug/members/", url_params=True, user=False
path="/api/workspaces/:slug/members/",
url_params=True,
user=False,
multiple=True,
)
@invalidate_cache(path="/api/users/me/settings/")
@invalidate_cache(
path="api/users/me/workspaces/", user=False, multiple=True
)
def leave(self, request, slug):
workspace_member = WorkspaceMember.objects.get(
@@ -30,6 +30,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
.select_related("workspace")
.select_related("lead")
.prefetch_related("members")
.filter(archived_at__isnull=False)
.prefetch_related(
Prefetch(
"link_module",
@@ -45,6 +46,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
),
)
.annotate(
@@ -55,6 +57,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
@@ -65,6 +68,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
@@ -75,6 +79,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
@@ -85,6 +90,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.annotate(
@@ -95,6 +101,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
distinct=True,
)
)
.order_by(self.kwargs.get("order_by", "-created_at"))
@@ -20,6 +20,7 @@ class WorkspaceStatesEndpoint(BaseAPIView):
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
serializer = StateSerializer(states, many=True).data
return Response(serializer, status=status.HTTP_200_OK)
+6 -2
View File
@@ -124,7 +124,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
| Q(issue_subscribers__subscriber_id=user_id),
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True
project__project_projectmember__is_active=True,
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
@@ -165,7 +165,8 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -299,6 +300,7 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
workspace__slug=slug,
project_projectmember__member=request.user,
project_projectmember__is_active=True,
archived_at__isnull=True,
)
.annotate(
created_issues=Count(
@@ -387,6 +389,7 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
actor=user_id,
).select_related("actor", "workspace", "issue", "project")
@@ -498,6 +501,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
subscriber_id=user_id,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.filter(**filters)
.count()
+20 -20
View File
@@ -1,22 +1,22 @@
# Python imports
import csv
import io
import logging
# Third party imports
from celery import shared_task
# Django imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import Issue
from plane.utils.analytics_plot import build_graph_plot
from plane.utils.issue_filters import issue_filters
from plane.license.utils.instance_value import get_email_configuration
from plane.utils.analytics_plot import build_graph_plot
from plane.utils.exception_logger import log_exception
from plane.utils.issue_filters import issue_filters
row_mapping = {
"state__name": "State",
@@ -55,6 +55,7 @@ def send_export_email(email, slug, csv_buffer, rows):
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
@@ -64,6 +65,7 @@ def send_export_email(email, slug, csv_buffer, rows):
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
)
msg = EmailMultiAlternatives(
@@ -210,9 +212,9 @@ def generate_segmented_rows(
None,
)
if assignee:
generated_row[
0
] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
generated_row[0] = (
f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
)
if x_axis == LABEL_ID:
label = next(
@@ -279,9 +281,9 @@ def generate_segmented_rows(
None,
)
if assignee:
row_zero[
index + 2
] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
row_zero[index + 2] = (
f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
)
if segmented == LABEL_ID:
for index, segm in enumerate(row_zero[2:]):
@@ -366,9 +368,9 @@ def generate_non_segmented_rows(
None,
)
if assignee:
row[
0
] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
row[0] = (
f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}"
)
if x_axis == LABEL_ID:
label = next(
@@ -504,10 +506,8 @@ def analytic_export_task(email, data, slug):
csv_buffer = generate_csv_from_rows(rows)
send_export_email(email, slug, csv_buffer, rows)
logging.getLogger("plane").info("Email sent succesfully.")
return
except Exception as e:
print(e)
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return
+679
View File
@@ -0,0 +1,679 @@
# Python imports
import uuid
import random
from datetime import datetime, timedelta
# Django imports
from django.db.models import Max
# Third party imports
from celery import shared_task
from faker import Faker
# Module imports
from plane.db.models import (
Workspace,
User,
Project,
ProjectMember,
State,
Label,
Cycle,
Module,
Issue,
IssueSequence,
IssueAssignee,
IssueLabel,
IssueActivity,
CycleIssue,
ModuleIssue,
Page,
PageLabel,
Inbox,
InboxIssue,
)
def create_project(workspace, user_id):
fake = Faker()
name = fake.name()
unique_id = str(uuid.uuid4())[:5]
project = Project.objects.create(
workspace=workspace,
name=f"{name}_{unique_id}",
identifier=name[
: random.randint(2, 12 if len(name) - 1 >= 12 else len(name) - 1)
].upper(),
created_by_id=user_id,
inbox_view=True,
)
# Add current member as project member
_ = ProjectMember.objects.create(
project=project,
member_id=user_id,
role=20,
)
return project
def create_project_members(workspace, project, members):
members = User.objects.filter(email__in=members)
_ = ProjectMember.objects.bulk_create(
[
ProjectMember(
project=project,
workspace=workspace,
member=member,
role=20,
sort_order=random.randint(0, 65535),
)
for member in members
],
ignore_conflicts=True,
)
return
def create_states(workspace, project, user_id):
states = [
{
"name": "Backlog",
"color": "#A3A3A3",
"sequence": 15000,
"group": "backlog",
"default": True,
},
{
"name": "Todo",
"color": "#3A3A3A",
"sequence": 25000,
"group": "unstarted",
},
{
"name": "In Progress",
"color": "#F59E0B",
"sequence": 35000,
"group": "started",
},
{
"name": "Done",
"color": "#16A34A",
"sequence": 45000,
"group": "completed",
},
{
"name": "Cancelled",
"color": "#EF4444",
"sequence": 55000,
"group": "cancelled",
},
]
states = State.objects.bulk_create(
[
State(
name=state["name"],
color=state["color"],
project=project,
sequence=state["sequence"],
workspace=workspace,
group=state["group"],
default=state.get("default", False),
created_by_id=user_id,
)
for state in states
]
)
return states
def create_labels(workspace, project, user_id):
fake = Faker()
Faker.seed(0)
return Label.objects.bulk_create(
[
Label(
name=fake.color_name(),
color=fake.hex_color(),
project=project,
workspace=workspace,
created_by_id=user_id,
sort_order=random.randint(0, 65535),
)
for _ in range(0, 50)
],
ignore_conflicts=True,
)
def create_cycles(workspace, project, user_id, cycle_count):
fake = Faker()
Faker.seed(0)
cycles = []
used_date_ranges = set() # Track used date ranges
while len(cycles) <= cycle_count:
# Generate a start date, allowing for None
start_date_option = [None, fake.date_this_year()]
start_date = start_date_option[random.randint(0, 1)]
# Initialize end_date based on start_date
end_date = (
None
if start_date is None
else fake.date_between_dates(
date_start=start_date,
date_end=datetime.now().date().replace(month=12, day=31),
)
)
# Ensure end_date is strictly after start_date if start_date is not None
while start_date is not None and (
end_date <= start_date
or (start_date, end_date) in used_date_ranges
):
end_date = fake.date_this_year()
# Add the unique date range to the set
(
used_date_ranges.add((start_date, end_date))
if (end_date is not None and start_date is not None)
else None
)
# Append the cycle with unique date range
cycles.append(
Cycle(
name=fake.name(),
owned_by_id=user_id,
sort_order=random.randint(0, 65535),
start_date=start_date,
end_date=end_date,
project=project,
workspace=workspace,
)
)
return Cycle.objects.bulk_create(cycles, ignore_conflicts=True)
def create_modules(workspace, project, user_id, module_count):
fake = Faker()
Faker.seed(0)
modules = []
for _ in range(0, module_count):
start_date = [None, fake.date_this_year()][random.randint(0, 1)]
end_date = (
None
if start_date is None
else fake.date_between_dates(
date_start=start_date,
date_end=datetime.now().date().replace(month=12, day=31),
)
)
modules.append(
Module(
name=fake.name(),
sort_order=random.randint(0, 65535),
start_date=start_date,
target_date=end_date,
project=project,
workspace=workspace,
)
)
return Module.objects.bulk_create(modules, ignore_conflicts=True)
def create_pages(workspace, project, user_id, pages_count):
fake = Faker()
Faker.seed(0)
pages = []
for _ in range(0, pages_count):
text = fake.text(max_nb_chars=60000)
pages.append(
Page(
name=fake.name(),
project=project,
workspace=workspace,
owned_by_id=user_id,
access=random.randint(0, 1),
color=fake.hex_color(),
description_html=f"<p>{text}</p>",
archived_at=None,
is_locked=False,
)
)
return Page.objects.bulk_create(pages, ignore_conflicts=True)
def create_page_labels(workspace, project, user_id, pages_count):
# labels
labels = Label.objects.filter(project=project).values_list("id", flat=True)
pages = random.sample(
list(
Page.objects.filter(project=project).values_list("id", flat=True)
),
int(pages_count / 2),
)
# Bulk page labels
bulk_page_labels = []
for page in pages:
for label in random.sample(
list(labels), random.randint(0, len(labels) - 1)
):
bulk_page_labels.append(
PageLabel(
page_id=page,
label_id=label,
project=project,
workspace=workspace,
)
)
# Page labels
PageLabel.objects.bulk_create(
bulk_page_labels, batch_size=1000, ignore_conflicts=True
)
def create_issues(workspace, project, user_id, issue_count):
fake = Faker()
Faker.seed(0)
states = State.objects.filter(workspace=workspace, project=project).exclude(group="Triage").values_list("id", flat=True)
creators = ProjectMember.objects.filter(workspace=workspace, project=project).values_list("member_id", flat=True)
issues = []
# Get the maximum sequence_id
last_id = IssueSequence.objects.filter(
project=project,
).aggregate(
largest=Max("sequence")
)["largest"]
last_id = 1 if last_id is None else last_id + 1
# Get the maximum sort order
largest_sort_order = Issue.objects.filter(
project=project,
state_id=states[random.randint(0, len(states) - 1)],
).aggregate(largest=Max("sort_order"))["largest"]
largest_sort_order = (
65535 if largest_sort_order is None else largest_sort_order + 10000
)
for _ in range(0, issue_count):
start_date = [None, fake.date_this_year()][random.randint(0, 1)]
end_date = (
None
if start_date is None
else fake.date_between_dates(
date_start=start_date,
date_end=datetime.now().date().replace(month=12, day=31),
)
)
text = fake.text(max_nb_chars=60000)
issues.append(
Issue(
state_id=states[random.randint(0, len(states) - 1)],
project=project,
workspace=workspace,
name=text[:254],
description_html=f"<p>{text}</p>",
description_stripped=text,
sequence_id=last_id,
sort_order=largest_sort_order,
start_date=start_date,
target_date=end_date,
priority=["urgent", "high", "medium", "low", "none"][
random.randint(0, 4)
],
created_by_id=creators[random.randint(0, len(creators) - 1)],
)
)
largest_sort_order = largest_sort_order + random.randint(0, 1000)
last_id = last_id + 1
issues = Issue.objects.bulk_create(
issues, ignore_conflicts=True, batch_size=1000
)
# Sequences
_ = IssueSequence.objects.bulk_create(
[
IssueSequence(
issue=issue,
sequence=issue.sequence_id,
project=project,
workspace=workspace,
)
for issue in issues
],
batch_size=100,
)
# Track the issue activities
IssueActivity.objects.bulk_create(
[
IssueActivity(
issue=issue,
actor_id=user_id,
project=project,
workspace=workspace,
comment="created the issue",
verb="created",
created_by_id=user_id,
)
for issue in issues
],
batch_size=100,
)
return issues
def create_inbox_issues(workspace, project, user_id, inbox_issue_count):
issues = create_issues(workspace, project, user_id, inbox_issue_count)
inbox, create = Inbox.objects.get_or_create(
name="Inbox",
project=project,
is_default=True,
)
InboxIssue.objects.bulk_create(
[
InboxIssue(
issue=issue,
inbox=inbox,
status=(status := [-2, -1, 0, 1, 2][random.randint(0, 4)]),
snoozed_till=(
datetime.now() + timedelta(days=random.randint(1, 30))
if status == 0
else None
),
source="in-app",
workspace=workspace,
project=project,
)
for issue in issues
],
batch_size=100,
)
def create_issue_parent(workspace, project, user_id, issue_count):
parent_count = issue_count / 4
parent_issues = Issue.objects.filter(project=project).values_list(
"id", flat=True
)[: int(parent_count)]
sub_issues = Issue.objects.filter(project=project).exclude(
pk__in=parent_issues
)[: int(issue_count / 2)]
bulk_sub_issues = []
for sub_issue in sub_issues:
sub_issue.parent_id = parent_issues[
random.randint(0, int(parent_count - 1))
]
Issue.objects.bulk_update(bulk_sub_issues, ["parent"], batch_size=1000)
def create_issue_assignees(workspace, project, user_id, issue_count):
# assignees
assignees = ProjectMember.objects.filter(project=project).values_list(
"member_id", flat=True
)
issues = random.sample(
list(
Issue.objects.filter(project=project).values_list("id", flat=True)
),
int(issue_count / 2),
)
# Bulk issue
bulk_issue_assignees = []
for issue in issues:
for assignee in random.sample(
list(assignees), random.randint(0, len(assignees) - 1)
):
bulk_issue_assignees.append(
IssueAssignee(
issue_id=issue,
assignee_id=assignee,
project=project,
workspace=workspace,
)
)
# Issue assignees
IssueAssignee.objects.bulk_create(
bulk_issue_assignees, batch_size=1000, ignore_conflicts=True
)
def create_issue_labels(workspace, project, user_id, issue_count):
# labels
labels = Label.objects.filter(project=project).values_list("id", flat=True)
issues = random.sample(
list(
Issue.objects.filter(project=project).values_list("id", flat=True)
),
int(issue_count / 2),
)
# Bulk issue
bulk_issue_labels = []
for issue in issues:
for label in random.sample(
list(labels), random.randint(0, len(labels) - 1)
):
bulk_issue_labels.append(
IssueLabel(
issue_id=issue,
label_id=label,
project=project,
workspace=workspace,
)
)
# Issue labels
IssueLabel.objects.bulk_create(
bulk_issue_labels, batch_size=1000, ignore_conflicts=True
)
def create_cycle_issues(workspace, project, user_id, issue_count):
# assignees
cycles = Cycle.objects.filter(project=project).values_list("id", flat=True)
issues = random.sample(
list(
Issue.objects.filter(project=project).values_list("id", flat=True)
),
int(issue_count / 2),
)
# Bulk issue
bulk_cycle_issues = []
for issue in issues:
cycle = cycles[random.randint(0, len(cycles) - 1)]
bulk_cycle_issues.append(
CycleIssue(
cycle_id=cycle,
issue_id=issue,
project=project,
workspace=workspace,
)
)
# Issue assignees
CycleIssue.objects.bulk_create(
bulk_cycle_issues, batch_size=1000, ignore_conflicts=True
)
def create_module_issues(workspace, project, user_id, issue_count):
# assignees
modules = Module.objects.filter(project=project).values_list(
"id", flat=True
)
issues = random.sample(
list(
Issue.objects.filter(project=project).values_list("id", flat=True)
),
int(issue_count / 2),
)
# Bulk issue
bulk_module_issues = []
for issue in issues:
module = modules[random.randint(0, len(modules) - 1)]
bulk_module_issues.append(
ModuleIssue(
module_id=module,
issue_id=issue,
project=project,
workspace=workspace,
)
)
# Issue assignees
ModuleIssue.objects.bulk_create(
bulk_module_issues, batch_size=1000, ignore_conflicts=True
)
@shared_task
def create_dummy_data(
slug,
email,
members,
issue_count,
cycle_count,
module_count,
pages_count,
inbox_issue_count,
):
workspace = Workspace.objects.get(slug=slug)
user = User.objects.get(email=email)
user_id = user.id
# Create a project
project = create_project(workspace=workspace, user_id=user_id)
# create project members
create_project_members(
workspace=workspace, project=project, members=members
)
# Create states
create_states(workspace=workspace, project=project, user_id=user_id)
# Create labels
create_labels(workspace=workspace, project=project, user_id=user_id)
# create cycles
create_cycles(
workspace=workspace,
project=project,
user_id=user_id,
cycle_count=cycle_count,
)
# create modules
create_modules(
workspace=workspace,
project=project,
user_id=user_id,
module_count=module_count,
)
# create pages
create_pages(
workspace=workspace,
project=project,
user_id=user_id,
pages_count=pages_count,
)
# create page labels
create_page_labels(
workspace=workspace,
project=project,
user_id=user_id,
pages_count=pages_count,
)
# create issues
create_issues(
workspace=workspace,
project=project,
user_id=user_id,
issue_count=issue_count,
)
# create inbox issues
create_inbox_issues(
workspace=workspace,
project=project,
user_id=user_id,
inbox_issue_count=inbox_issue_count,
)
# create issue parent
create_issue_parent(
workspace=workspace,
project=project,
user_id=user_id,
issue_count=issue_count,
)
# create issue assignees
create_issue_assignees(
workspace=workspace,
project=project,
user_id=user_id,
issue_count=issue_count,
)
# create issue labels
create_issue_labels(
workspace=workspace,
project=project,
user_id=user_id,
issue_count=issue_count,
)
# create cycle issues
create_cycle_issues(
workspace=workspace,
project=project,
user_id=user_id,
issue_count=issue_count,
)
# create module issues
create_module_issues(
workspace=workspace,
project=project,
user_id=user_id,
issue_count=issue_count,
)
return
@@ -1,21 +1,29 @@
import logging
import re
from datetime import datetime
from bs4 import BeautifulSoup
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
# 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
from django.conf import settings
# Module imports
from plane.db.models import EmailNotificationLog, User, Issue
from plane.db.models import EmailNotificationLog, Issue, User
from plane.license.utils.instance_value import get_email_configuration
from plane.settings.redis import redis_instance
from plane.utils.exception_logger import log_exception
def remove_unwanted_characters(input_text):
# Keep only alphanumeric characters, spaces, and dashes.
processed_text = re.sub(r"[^a-zA-Z0-9 \-]", "", input_text)
return processed_text
# acquire and delete redis lock
@@ -69,7 +77,9 @@ def stack_email_notification():
receiver_notification.get("entity_identifier"), {}
).setdefault(
str(receiver_notification.get("triggered_by_id")), []
).append(receiver_notification.get("data"))
).append(
receiver_notification.get("data")
)
# append processed notifications
processed_notifications.append(receiver_notification.get("id"))
email_notification_ids.append(receiver_notification.get("id"))
@@ -172,7 +182,16 @@ def send_email_notification(
if acquire_lock(lock_id=lock_id):
# get the redis instance
ri = redis_instance()
base_api = ri.get(str(issue_id)).decode()
base_api = (
ri.get(str(issue_id)).decode()
if ri.get(str(issue_id))
else None
)
# Skip if base api is not present
if not base_api:
return
data = create_payload(notification_data=notification_data)
# Get email configurations
@@ -182,6 +201,7 @@ def send_email_notification(
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
@@ -251,9 +271,7 @@ def send_email_notification(
summary = "Updates were made to the issue by"
# Send the mail
subject = (
f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
)
subject = f"{issue.project.identifier}-{issue.sequence_id} {remove_unwanted_characters(issue.name)}"
context = {
"data": template_data,
"summary": summary,
@@ -285,6 +303,7 @@ def send_email_notification(
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
)
msg = EmailMultiAlternatives(
@@ -296,7 +315,9 @@ def send_email_notification(
)
msg.attach_alternative(html_content, "text/html")
msg.send()
logging.getLogger("plane").info("Email Sent Successfully")
# Update the logs
EmailNotificationLog.objects.filter(
pk__in=email_notification_ids
).update(sent_at=timezone.now())
@@ -305,15 +326,19 @@ def send_email_notification(
release_lock(lock_id=lock_id)
return
except Exception as e:
capture_exception(e)
log_exception(e)
# release the lock
release_lock(lock_id=lock_id)
return
else:
print("Duplicate task recived. Skipping...")
logging.getLogger("plane").info(
"Duplicate email received skipping"
)
return
except (Issue.DoesNotExist, User.DoesNotExist) as e:
if settings.DEBUG:
print(e)
except (Issue.DoesNotExist, User.DoesNotExist):
release_lock(lock_id=lock_id)
return
except Exception as e:
log_exception(e)
release_lock(lock_id=lock_id)
return
@@ -1,13 +1,13 @@
import uuid
import os
import uuid
# third party imports
from celery import shared_task
from sentry_sdk import capture_exception
from posthog import Posthog
# module imports
from plane.license.utils.instance_value import get_configuration_value
from plane.utils.exception_logger import log_exception
def posthogConfiguration():
@@ -51,7 +51,8 @@ def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
},
)
except Exception as e:
capture_exception(e)
log_exception(e)
return
@shared_task
@@ -77,4 +78,5 @@ def workspace_invite_event(
},
)
except Exception as e:
capture_exception(e)
log_exception(e)
return
+10 -11
View File
@@ -2,21 +2,22 @@
import csv
import io
import json
import boto3
import zipfile
import boto3
from botocore.client import Config
# Third party imports
from celery import shared_task
# Django imports
from django.conf import settings
from django.utils import timezone
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
from botocore.client import Config
from openpyxl import Workbook
# Module imports
from plane.db.models import Issue, ExporterHistory
from plane.db.models import ExporterHistory, Issue
from plane.utils.exception_logger import log_exception
def dateTimeConverter(time):
@@ -303,6 +304,7 @@ def issue_export_task(
project_id__in=project_ids,
project__project_projectmember__member=exporter_instance.initiated_by_id,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
)
.select_related(
"project", "workspace", "state", "parent", "created_by"
@@ -403,8 +405,5 @@ def issue_export_task(
exporter_instance.status = "failed"
exporter_instance.reason = str(e)
exporter_instance.save(update_fields=["status", "reason"])
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return
+10 -10
View File
@@ -1,17 +1,17 @@
# Python import
# Python imports
import logging
# Third party imports
from celery import shared_task
# Django imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@shared_task
@@ -26,6 +26,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
@@ -49,6 +50,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
)
msg = EmailMultiAlternatives(
@@ -60,10 +62,8 @@ def forgot_password(first_name, email, uidb64, token, current_site):
)
msg.attach_alternative(html_content, "text/html")
msg.send()
logging.getLogger("plane").info("Email sent successfully")
return
except Exception as e:
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return
+59 -19
View File
@@ -1,34 +1,36 @@
# Python imports
import json
import requests
# Third Party imports
from celery import shared_task
# Django imports
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone
# Third Party imports
from celery import shared_task
from sentry_sdk import capture_exception
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
# Module imports
from plane.db.models import (
User,
Issue,
Project,
Label,
IssueActivity,
State,
Cycle,
Module,
IssueReaction,
CommentReaction,
Cycle,
Issue,
IssueActivity,
IssueComment,
IssueReaction,
IssueSubscriber,
Label,
Module,
Project,
State,
User,
)
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
from plane.settings.redis import redis_instance
from plane.utils.exception_logger import log_exception
# Track Changes in name
@@ -1551,6 +1553,46 @@ def delete_draft_issue_activity(
)
def create_inbox_activity(
requested_data,
current_instance,
issue_id,
project_id,
workspace_id,
actor_id,
issue_activities,
epoch,
):
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
)
status_dict = {
-2: "Pending",
-1: "Rejected",
0: "Snoozed",
1: "Accepted",
2: "Duplicate",
}
if requested_data.get("status") is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project_id=project_id,
workspace_id=workspace_id,
comment="updated the inbox status",
field="inbox",
verb=requested_data.get("status"),
actor_id=actor_id,
epoch=epoch,
old_value=status_dict.get(current_instance.get("status")),
new_value=status_dict.get(requested_data.get("status")),
)
)
# Receive message from room group
@shared_task
def issue_activity(
@@ -1611,6 +1653,7 @@ def issue_activity(
"issue_draft.activity.created": create_draft_issue_activity,
"issue_draft.activity.updated": update_draft_issue_activity,
"issue_draft.activity.deleted": delete_draft_issue_activity,
"inbox.activity.created": create_inbox_activity,
}
func = ACTIVITY_MAPPER.get(type)
@@ -1647,7 +1690,7 @@ def issue_activity(
headers=headers,
)
except Exception as e:
capture_exception(e)
log_exception(e)
if notification:
notifications.delay(
@@ -1668,8 +1711,5 @@ def issue_activity(
return
except Exception as e:
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return
@@ -2,18 +2,17 @@
import json
from datetime import timedelta
# Django imports
from django.utils import timezone
from django.db.models import Q
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
from django.db.models import Q
# Django imports
from django.utils import timezone
# Module imports
from plane.db.models import Issue, Project, State
from plane.bgtasks.issue_activites_task import issue_activity
from plane.db.models import Issue, Project, State
from plane.utils.exception_logger import log_exception
@shared_task
@@ -96,9 +95,7 @@ def archive_old_issues():
]
return
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return
@@ -179,7 +176,5 @@ def close_old_issues():
]
return
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return
@@ -1,17 +1,17 @@
# Python imports
import logging
# Third party imports
from celery import shared_task
# Django imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@shared_task
@@ -23,6 +23,7 @@ def magic_link(email, key, token, current_site):
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
@@ -41,6 +42,7 @@ def magic_link(email, key, token, current_site):
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
)
msg = EmailMultiAlternatives(
@@ -52,11 +54,8 @@ def magic_link(email, key, token, current_site):
)
msg.attach_alternative(html_content, "text/html")
msg.send()
logging.getLogger("plane").info("Email sent successfully.")
return
except Exception as e:
print(e)
capture_exception(e)
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
log_exception(e)
return
@@ -0,0 +1,76 @@
# Python imports
import json
# Django imports
from django.utils import timezone
# Third-party imports
from bs4 import BeautifulSoup
# Module imports
from plane.db.models import Page, PageLog
from celery import shared_task
def extract_components(value, tag):
try:
mentions = []
html = value.get("description_html")
soup = BeautifulSoup(html, "html.parser")
mention_tags = soup.find_all(tag)
for mention_tag in mention_tags:
mention = {
"id": mention_tag.get("id"),
"entity_identifier": mention_tag.get("entity_identifier"),
"entity_name": mention_tag.get("entity_name"),
}
mentions.append(mention)
return mentions
except Exception:
return []
@shared_task
def page_transaction(new_value, old_value, page_id):
page = Page.objects.get(pk=page_id)
new_page_mention = PageLog.objects.filter(page_id=page_id).exists()
old_value = json.loads(old_value) if old_value else {}
new_transactions = []
deleted_transaction_ids = set()
# TODO - Add "issue-embed-component", "img", "todo" components
components = ["mention-component"]
for component in components:
old_mentions = extract_components(old_value, component)
new_mentions = extract_components(new_value, component)
new_mentions_ids = {mention["id"] for mention in new_mentions}
old_mention_ids = {mention["id"] for mention in old_mentions}
deleted_transaction_ids.update(old_mention_ids - new_mentions_ids)
new_transactions.extend(
PageLog(
transaction=mention["id"],
page_id=page_id,
entity_identifier=mention["entity_identifier"],
entity_name=mention["entity_name"],
workspace_id=page.workspace_id,
project_id=page.project_id,
created_at=timezone.now(),
updated_at=timezone.now(),
)
for mention in new_mentions
if mention["id"] not in old_mention_ids or not new_page_mention
)
# Create new PageLog objects for new transactions
PageLog.objects.bulk_create(
new_transactions, batch_size=10, ignore_conflicts=True
)
# Delete the removed transactions
PageLog.objects.filter(transaction__in=deleted_transaction_ids).delete()
@@ -1,18 +1,18 @@
# Python import
# Python imports
import logging
# Third party imports
from celery import shared_task
# Django imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import Project, User, ProjectMemberInvite
from plane.db.models import Project, ProjectMemberInvite, User
from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@shared_task
@@ -52,6 +52,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
@@ -61,6 +62,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
)
msg = EmailMultiAlternatives(
@@ -73,12 +75,10 @@ def project_invitation(email, project_id, token, current_site, invitor):
msg.attach_alternative(html_content, "text/html")
msg.send()
logging.getLogger("plane").info("Email sent successfully.")
return
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist):
return
except Exception as e:
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return
+40 -46
View File
@@ -1,44 +1,45 @@
import requests
import uuid
import hashlib
import json
import hmac
import json
import logging
import uuid
# 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
import requests
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
from plane.db.models import (
Webhook,
WebhookLog,
Project,
Issue,
Cycle,
Module,
ModuleIssue,
CycleIssue,
IssueComment,
User,
)
from plane.api.serializers import (
ProjectSerializer,
CycleSerializer,
ModuleSerializer,
CycleIssueSerializer,
ModuleIssueSerializer,
IssueCommentSerializer,
IssueExpandSerializer,
)
# Django imports
from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.serializers.json import DjangoJSONEncoder
from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Module imports
from plane.api.serializers import (
CycleIssueSerializer,
CycleSerializer,
IssueCommentSerializer,
IssueExpandSerializer,
ModuleIssueSerializer,
ModuleSerializer,
ProjectSerializer,
)
from plane.db.models import (
Cycle,
CycleIssue,
Issue,
IssueComment,
Module,
ModuleIssue,
Project,
User,
Webhook,
WebhookLog,
)
from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
SERIALIZER_MAPPER = {
"project": ProjectSerializer,
@@ -174,7 +175,7 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site):
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return
@@ -201,16 +202,7 @@ def send_webhook(event, payload, kw, action, slug, bulk, current_site):
if webhooks:
if action in ["POST", "PATCH"]:
if bulk and event in ["cycle_issue", "module_issue"]:
event_data = IssueExpandSerializer(
Issue.objects.filter(
pk__in=[
str(event.get("issue")) for event in payload
]
).prefetch_related("issue_cycle", "issue_module"),
many=True,
).data
event = "issue"
action = "PATCH"
return
else:
event_data = [
get_model_data(
@@ -218,7 +210,7 @@ def send_webhook(event, payload, kw, action, slug, bulk, current_site):
event_id=(
payload.get("id")
if isinstance(payload, dict)
else None
else kw.get("pk")
),
many=False,
)
@@ -241,7 +233,7 @@ def send_webhook(event, payload, kw, action, slug, bulk, current_site):
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return
@@ -256,6 +248,7 @@ def send_webhook_deactivation_email(
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
@@ -284,6 +277,7 @@ def send_webhook_deactivation_email(
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
)
msg = EmailMultiAlternatives(
@@ -295,8 +289,8 @@ def send_webhook_deactivation_email(
)
msg.attach_alternative(html_content, "text/html")
msg.send()
logging.getLogger("plane").info("Email sent successfully.")
return
except Exception as e:
print(e)
log_exception(e)
return
@@ -1,18 +1,18 @@
# Python imports
import logging
# Third party imports
from celery import shared_task
# Django imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import Workspace, WorkspaceMemberInvite, User
from plane.db.models import User, Workspace, WorkspaceMemberInvite
from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@shared_task
@@ -37,6 +37,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
@@ -65,6 +66,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
)
msg = EmailMultiAlternatives(
@@ -76,14 +78,12 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
)
msg.attach_alternative(html_content, "text/html")
msg.send()
logging.getLogger("plane").info("Email sent succesfully")
return
except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist):
print("Workspace or WorkspaceMember Invite Does not exists")
except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist) as e:
log_exception(e)
return
except Exception as e:
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
capture_exception(e)
log_exception(e)
return
@@ -0,0 +1,17 @@
# Django imports
from django.core.cache import cache
from django.core.management import BaseCommand
class Command(BaseCommand):
help = "Clear Cache before starting the server to remove stale values"
def handle(self, *args, **options):
try:
cache.clear()
self.stdout.write(self.style.SUCCESS("Cache Cleared"))
return
except Exception:
# Another ClientError occurred
self.stdout.write(self.style.ERROR("Failed to clear cache"))
return
@@ -0,0 +1,95 @@
# Django imports
from typing import Any
from django.core.management.base import BaseCommand, CommandError
# Module imports
from plane.db.models import User, Workspace, WorkspaceMember
class Command(BaseCommand):
help = "Create dump issues, cycles etc. for a project in a given workspace"
def handle(self, *args: Any, **options: Any) -> str | None:
try:
workspace_name = input("Workspace Name: ")
workspace_slug = input("Workspace slug: ")
if workspace_slug == "":
raise CommandError("Workspace slug is required")
if Workspace.objects.filter(slug=workspace_slug).exists():
raise CommandError("Workspace already exists")
creator = input("Your email: ")
if (
creator == ""
or not User.objects.filter(email=creator).exists()
):
raise CommandError(
"User email is required and should have signed in plane"
)
user = User.objects.get(email=creator)
members = input("Enter Member emails (comma separated): ")
members = members.split(",") if members != "" else []
# Create workspace
workspace = Workspace.objects.create(
slug=workspace_slug,
name=workspace_name,
owner=user,
)
# Create workspace member
WorkspaceMember.objects.create(
workspace=workspace, role=20, member=user
)
user_ids = User.objects.filter(email__in=members)
_ = WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace=workspace,
member=user_id,
role=20,
)
for user_id in user_ids
],
ignore_conflicts=True,
)
project_count = int(input("Number of projects to be created: "))
for i in range(project_count):
print(f"Please provide the following details for project {i+1}:")
issue_count = int(input("Number of issues to be created: "))
cycle_count = int(input("Number of cycles to be created: "))
module_count = int(input("Number of modules to be created: "))
pages_count = int(input("Number of pages to be created: "))
inbox_issue_count = int(
input("Number of inbox issues to be created: ")
)
from plane.bgtasks.dummy_data_task import create_dummy_data
create_dummy_data.delay(
slug=workspace_slug,
email=creator,
members=members,
issue_count=issue_count,
cycle_count=cycle_count,
module_count=module_count,
pages_count=pages_count,
inbox_issue_count=inbox_issue_count,
)
self.stdout.write(
self.style.SUCCESS("Data is pushed to the queue")
)
return
except Exception as e:
self.stdout.write(
self.style.ERROR(f"Command errored out {str(e)}")
)
return
@@ -0,0 +1,48 @@
# Django imports
from django.core.management.base import BaseCommand, CommandError
# Module imports
from plane.license.models import Instance, InstanceAdmin
from plane.db.models import User
class Command(BaseCommand):
help = "Add a new instance admin"
def add_arguments(self, parser):
# Positional argument
parser.add_argument(
"admin_email", type=str, help="Instance Admin Email"
)
def handle(self, *args, **options):
admin_email = options.get("admin_email", False)
if not admin_email:
raise CommandError("Please provide the email of the admin.")
user = User.objects.filter(email=admin_email).first()
if user is None:
raise CommandError("User with the provided email does not exist.")
try:
# Get the instance
instance = Instance.objects.last()
# Get or create an instance admin
_, created = InstanceAdmin.objects.get_or_create(
user=user, instance=instance, role=20
)
if not created:
raise CommandError(
"The provided email is already an instance admin."
)
self.stdout.write(
self.style.SUCCESS("Successfully created the admin")
)
except Exception as e:
print(e)
raise CommandError("Failed to create the instance admin.")
@@ -0,0 +1,63 @@
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.management import BaseCommand, CommandError
from plane.license.utils.instance_value import get_email_configuration
class Command(BaseCommand):
"""Django command to pause execution until db is available"""
def add_arguments(self, parser):
# Positional argument
parser.add_argument("to_email", type=str, help="receiver's email")
def handle(self, *args, **options):
receiver_email = options.get("to_email")
if not receiver_email:
raise CommandError("Reciever email is required")
(
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
connection = get_connection(
host=EMAIL_HOST,
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
timeout=30,
)
# Prepare email details
subject = "Email Notification from Plane"
message = (
"This is a sample email notification sent from Plane application."
)
self.stdout.write(self.style.SUCCESS("Trying to send test email..."))
# Send the email
try:
msg = EmailMultiAlternatives(
subject=subject,
body=message,
from_email=EMAIL_FROM,
to=[receiver_email],
connection=connection,
)
msg.send()
self.stdout.write(self.style.SUCCESS("Email succesfully sent"))
except Exception as e:
self.stdout.write(
self.style.ERROR(
f"Error: Email could not be delivered due to {e}"
)
)
@@ -0,0 +1,41 @@
# Generated by Django 4.2.7 on 2024-03-19 08:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0061_project_logo_props'),
]
operations = [
migrations.AddField(
model_name="cycle",
name="archived_at",
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name="module",
name="archived_at",
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name="project",
name="archived_at",
field=models.DateTimeField(null=True),
),
migrations.AlterField(
model_name="socialloginconnection",
name="medium",
field=models.CharField(
choices=[
("Google", "google"),
("Github", "github"),
("Jira", "jira"),
],
default=None,
max_length=20,
),
),
]
@@ -0,0 +1,44 @@
# Generated by Django 4.2.10 on 2024-04-02 12:18
from django.db import migrations, models
def update_project_state_group(apps, schema_editor):
State = apps.get_model("db", "State")
# Update states in bulk
State.objects.filter(group="backlog", name="Triage").update(
is_triage=True, group="triage"
)
class Migration(migrations.Migration):
dependencies = [
("db", "0062_cycle_archived_at_module_archived_at_and_more"),
]
operations = [
migrations.AddField(
model_name="state",
name="is_triage",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="state",
name="group",
field=models.CharField(
choices=[
("backlog", "Backlog"),
("unstarted", "Unstarted"),
("started", "Started"),
("completed", "Completed"),
("cancelled", "Cancelled"),
("triage", "Triage"),
],
default="backlog",
max_length=20,
),
),
migrations.RunPython(update_project_state_group),
]
@@ -0,0 +1,20 @@
# Generated by Django 4.2.10 on 2024-04-09 11:34
from django.db import migrations, models
import plane.db.models.page
class Migration(migrations.Migration):
dependencies = [
('db', '0063_state_is_triage_alter_state_group'),
]
operations = [
migrations.AddField(
model_name="page",
name="view_props",
field=models.JSONField(
default=plane.db.models.page.get_view_props
),
),
]
+1
View File
@@ -69,6 +69,7 @@ class Cycle(ProjectBaseModel):
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)
archived_at = models.DateTimeField(null=True)
class Meta:
verbose_name = "Cycle"
+3 -2
View File
@@ -91,6 +91,7 @@ class IssueManager(models.Manager):
| models.Q(issue_inbox__isnull=True)
)
.exclude(archived_at__isnull=False)
.exclude(project__archived_at__isnull=False)
.exclude(is_draft=True)
)
@@ -170,14 +171,14 @@ class Issue(ProjectBaseModel):
from plane.db.models import State
default_state = State.objects.filter(
~models.Q(name="Triage"),
~models.Q(is_triage=True),
project=self.project,
default=True,
).first()
# if there is no default state assign any random state
if default_state is None:
random_state = State.objects.filter(
~models.Q(name="Triage"), project=self.project
~models.Q(is_triage=True), project=self.project
).first()
self.state = random_state
else:

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