Compare commits

..
Author SHA1 Message Date
NarayanBavisetti f18e1e4023 fix: removed draft notifications 2023-09-14 20:16:40 +05:30
Dakshesh JainandGitHub eda4da8aed feat: draft issues (#2188)
* feat: draft issue

issues can be saved as draft

* style: modal position
2023-09-14 18:38:31 +05:30
sriram veeraghantaandGitHub 759a604cb8 fix: posthog integration (#2186) 2023-09-14 16:38:41 +05:30
sriram veeraghantaandGitHub 6659cfc8b0 fix: track events issue and env variables fixes (#2184)
* fix: track event fixes

* fix: adding env variables to trubo
2023-09-14 16:05:31 +05:30
Bavisetti NarayanandGitHub a53b428bbd chore: endpoints and history logs for issue draft (#2180)
* chore: history logs for issue draft

* fix: created seperated endpoints for issue drafts

* fix: fixed the typo
2023-09-14 15:38:11 +05:30
Bavisetti NarayanandGitHub 4e0e02522f fix: changed payload for issue subgroups (#2181)
* fix: sub groups in cycle module and my issues

* fix: changed payload for issue subgroups
2023-09-14 15:29:35 +05:30
sriram veeraghantaandGitHub f983d787b4 env and docker fixes (#2182) 2023-09-14 12:26:07 +05:30
Anmol Singh BhatiaandGitHub 87abf3ccb1 style: project setting ui revamp (#2177)
* style: project settings navigation sidebar added

* chore: emoji and image picker close on outside click added

* style: project setting general page revamp

* style: project setting member page revamp

* style: project setting features page revamp

* style: project setting state page revamp

* style: project setting integrations page revamp

* style: project setting estimates page revamp

* style: project setting automation page revamp

* style: project setting label page revamp

* chore: member select improvement for member setting page

* chore: toggle switch component improvement

* style: project automation setting ui improvement

* style: module icon added

* style: toggle switch improvement

* style: ui and spacing consistency

* style: project label setting revamp

* style: project state setting ui improvement

* chore: integration setting repo select validation

* chore: code refactor

* fix: build fix
2023-09-13 23:09:55 +05:30
d0f6ca3bac [chore] Update setup.sh, with removed replacement script & added project-level ENVs (#2115)
* chore: Updated Setup Script for Splitting Env File

* chore: updated dockerfile for using inproject env varaibles

* chore: removed husky replacement script

* chore: updated shell script using sed

* chore: updated dockerfiles with removed cp statement

* chore: added example env for apiserver

* chore: refactored secret generation for backend

* chore: removed replacement script

* chore: updated docker-compose with removed env variables

* chore: resolved comments in setup.sh and docker-compose

* chore: removed secret key placeholder in apiserver example env

* chore: updated root env for project less env variables

* chore: removed project level env update from root env logic

* chore: updated API_BASE_URL in .env.example

* chore: restored docker argument as env NEXT_PUBLIC_API_BASE_URL

* chore: added pg missing env variables

* [chore] Updated web and deploy backend configuration for reverse proxy & decoupled Plane Deploy URL generation for web (#2135)

* chore: removed api url build arg from compose

* chore: set public api default argument to black string for self hosted

* chore: updated web services to accept blank string as API URL

* chore: added env variables for pg compose service

* chore: modified space app services to use accept empty string as api base

* chore: conditionally trigger web url value based on argument

* fix: made web to use identical host with spaces suffix on absense of Deploy URL for deploy

* chore: added example env for PUBLIC_DEPLOY Env

* chore: updated web dockerfile with addition as PLANE_DEPLOY Argument

* API BASE URL global update

* API BASE URL replace with api server

* api base url fixes

* typo  fixes

---------

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

* dev: remove API_BASE_URL from environment variable

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-09-13 20:21:02 +05:30
af73bbe718 typo: changed customize to customise in project automation settings (#2153)
Co-authored-by: Neo <neo@Neos-MacBook-Pro.local>
2023-09-13 20:15:48 +05:30
Bavisetti NarayanandGitHub 9033ceb03c fix: sub groups in cycle module and my issues (#2176) 2023-09-13 19:50:34 +05:30
Dakshesh JainandGitHub 9bac7cb036 feat: issue link to create relation between issues (#2171)
* feat: issue linking

* fix: search params to filter out selected issue

* style: changed icons

* fix: build error on web-view

* fix: build error

* fix: build error on web-view component
2023-09-13 19:41:11 +05:30
Anmol Singh BhatiaandGitHub 32d08570e7 chore: peek overview for issue view and my issue view (#2172)
* chore: peak overview for issue view and my issue view

* fix: profile issue peak overview mutation fix

* chore: code refactor

* fix: image prefix url fix
2023-09-13 19:33:58 +05:30
Bavisetti NarayanandGitHub 1b1ed37405 chore: changed default props for worskpace and project members (#2175) 2023-09-13 19:13:31 +05:30
Bavisetti NarayanandGitHub 42d38f7531 feat: changed payload for swimlanes (#2173) 2023-09-13 18:25:57 +05:30
Bavisetti NarayanandGitHub 61672f47ac fix: migration files (#2169) 2023-09-13 13:23:40 +05:30
Dakshesh JainandGitHub 23e62c83eb refactor: switched priority null -> 'none' (#2166) 2023-09-13 13:22:31 +05:30
sriram veeraghantaandGitHub e58b76c8a6 fix: tailwind common config (#2168) 2023-09-13 12:50:04 +05:30
Anmol Singh BhatiaandGitHub 4ce01ca4f8 fix: calendar issues display filters loop fix (#2167) 2023-09-13 12:37:58 +05:30
Bavisetti NarayanandGitHub a34b0b059d feat: add a relation to an issue (#1995)
* feat: add issue relation to an issue

* fix: deleted the migration file

* fix: changed link to relates to in choice fields

* fix: added the migration file

* fix: changed migration file

* fix: project id issue fixed

* fix: added issue in the payload

* fix: changed the query param for blocker
2023-09-13 12:25:10 +05:30
Bavisetti NarayanandGitHub 164e0b9301 chore: changed view props (#2146)
* chore: changed view props

* fix: changed the keywords
2023-09-13 12:12:21 +05:30
Bavisetti NarayanandGitHub 5a91031243 feat: issue drafts (#2161) 2023-09-13 12:10:22 +05:30
Bavisetti NarayanandGitHub 47bec7704b chore: priority migration (#2162) 2023-09-13 12:06:38 +05:30
sriram veeraghantaandGitHub b9c935092a chore: eslint config package fixes (#2165)
* eslint fixes

* lint rules added
2023-09-13 12:06:17 +05:30
Aaryan KhandelwalandGitHub 3a2a329000 fix: view props undefined (#2160) 2023-09-12 22:51:13 +05:30
Aaryan KhandelwalandGitHub 8e9a4dca78 refactor: view props structure (#2159)
* chore: update view_props types

* refactor: view props structure
2023-09-12 22:27:15 +05:30
sriram veeraghantaandGitHub cdb888c23e fix: selfhosted fixes (#2154)
* fix: selfhosted fixes

* fix: updated env example
2023-09-12 20:32:26 +05:30
Dakshesh JainandGitHub 2186db8bba feat: users can select timezone during onboarding (#2148)
feat: using Intl timezone will be automatically selected but they have the option to change it
2023-09-12 13:35:15 +05:30
Bavisetti NarayanandGitHub 9bff10de6d chore: changed issue priority from NULL to none (#2142)
* chore: changed issue priority from NULL to none

* fix: deleted the migration file
2023-09-12 13:06:49 +05:30
Henit ChobisaandGitHub 6867154963 chore: added pre-release tag for workflow publications (#2133)
* chore: added pre-release tag for workflow publications

* chore: added backend services under a single image

* chore: exposed backend port for compose hub

* chore: removed backend exposed ports
2023-09-11 18:02:56 +05:30
Aaryan KhandelwalandGitHub 7bb73b74ba refactor: priority icon component (#2132) 2023-09-11 14:35:58 +05:30
Dakshesh JainandGitHub 991258084e fix: query checking (#2137)
fix: the logic should be to check if object exist not if it's true or false
2023-09-11 13:21:50 +05:30
ThomasandGitHub 1a37668f0b fix: husky was removed in commit #2086, but prepare still uses it (#2128) 2023-09-11 13:05:09 +05:30
4447a4b519 fix: Tiptap comment card fix for space (#2129)
* fix:(space) fixed comment card's editor integration

* regression: removed content being set twice

* chore: added controller to manage tiptap editor

* chore: remove unused functions

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-09-11 12:54:19 +05:30
Dakshesh JainandGitHub 7842c4b2ea fix: authorize editor (#2122) 2023-09-11 12:24:46 +05:30
Aaryan KhandelwalandGitHub 8de93d0081 chore: remove getServerSideProps (#2130) 2023-09-11 12:13:00 +05:30
Aaryan KhandelwalandGitHub 5b228bd1eb chore: update state icons and colors (#2126)
* chore: update state icons and colors

* chore: update icons
2023-09-11 11:45:28 +05:30
Dakshesh JainandGitHub ad8a011bb9 fix: issue activity (#2127) 2023-09-11 11:44:16 +05:30
Aaryan KhandelwalandGitHub 49d0b3f4a1 fix: handleClose function of the export modal (#2124) 2023-09-08 13:29:06 +05:30
Aaryan KhandelwalandGitHub 1872dff00d fix: custom date filter not working on my issues and profile issues (#2123) 2023-09-08 13:28:32 +05:30
Dakshesh JainandGitHub faa6a2bcbc feat: select blocker, blocking, and parent (#2121)
* feat: update, delete link

refactor: using old fetch-key

* feat: issue activity with ability to view & add comment

feat: click on view more to view more options in the issue detail

* fix: upload image not working on mobile

* feat: select blocker, blocking, and parent

dev: auth layout for web-view, console.log callback for web-view

* style: made design consistant

* fix: displaying page only on web-view

* style: removed overflow hidden
2023-09-07 18:42:24 +05:30
sriram veeraghantaandGitHub 6d52707ff5 editor fixes for space (#2119) 2023-09-07 15:09:16 +05:30
NikhilandGitHub 8ba482bc9c chore: response status for project views update (#2111)
* chore: response status for project views update

* dev: remove 200 OK response from empty contents
2023-09-07 14:49:45 +05:30
Aaryan KhandelwalandGitHub 5989f2476a fix: update plane logo (#2118) 2023-09-07 13:41:29 +05:30
Aaryan KhandelwalandGitHub 8ea6dd4e84 fix: onboarding role select dropdown text color (#2117) 2023-09-07 13:40:50 +05:30
NikhilandGitHub 39bc975994 fix: remove triage issue status from public boards (#2110) 2023-09-07 13:21:58 +05:30
NikhilandGitHub 866eead35f fix: issue comment ordering for public boards (#2108) 2023-09-07 13:21:05 +05:30
NikhilandGitHub 9c3510851d dev: update python packages (#2095) 2023-09-07 13:20:32 +05:30
Aaryan KhandelwalandGitHub 81436902a3 chore: option to switch access of a comment (#2116) 2023-09-07 12:54:30 +05:30
Aaryan KhandelwalandGitHub d26aa1b2da chore: render proper icons for due dates (#2114) 2023-09-07 12:53:49 +05:30
M. PalanikannanandGitHub b47c7d363f fix: Improved Image Deletion Logic, Image ID Issue in Modals and Performance Optimization in Editor (#2092)
* added improved delete logic in modals

* added better ts support

* impoved complexity to O(1) from O(n) for large docs

* regression: removed ts nocheck
2023-09-07 12:22:02 +05:30
Aaryan KhandelwalandGitHub 85f797058d fix: edit issue comment mutation (#2109) 2023-09-06 19:02:59 +05:30
Dakshesh JainandGitHub 1655d0cb1c feat: view, create, update and delete comment (#2106)
* feat: update, delete link

refactor: using old fetch-key

* feat: issue activity with ability to view & add comment

feat: click on view more to view more options in the issue detail

* fix: upload image not working on mobile
2023-09-06 17:08:19 +05:30
Anmol Singh BhatiaandGitHub 58562dc4b7 fix: ui improvement and bug fixes (#2105)
* chore: workspace level typo fix

* fix: setting opacity fix
2023-09-06 16:15:21 +05:30
NikhilandGitHub 2ad46d7bfa fix: public issue list endpoint n+1 (#2099) 2023-09-06 16:04:12 +05:30
NikhilandGitHub 4f0cac37db fix: issue object for filtering (#2102) 2023-09-06 16:03:41 +05:30
sriram veeraghantaandGitHub b46a7481ae Merge pull request #2101 from makeplane/fix/gantt_issues
fix: don't render invalid dated blocks on the gantt chart
2023-09-06 15:00:03 +05:30
sriram veeraghantaandGitHub f11ae00201 Merge pull request #2100 from makeplane/feat/comment_reactions
feat: plane space comment reactions
2023-09-06 14:52:55 +05:30
Aaryan Khandelwal c5612ee7a3 fix: don't render invalid dated cycles and modules 2023-09-06 12:26:51 +05:30
Aaryan Khandelwal 0dd336aec8 fix: don't render invalid dated issues 2023-09-06 12:25:34 +05:30
sriram veeraghantaandGitHub 4b364f72b5 Merge pull request #2096 from makeplane/fix/login_redirection
fix: redirection after signing in on space
2023-09-06 12:21:08 +05:30
Aaryan Khandelwal 6d13332818 style: add shadow to reaction selector 2023-09-06 12:17:47 +05:30
Aaryan Khandelwal ac4127c93d chore: add tooltip for user info 2023-09-06 12:04:18 +05:30
Aaryan Khandelwal 60c3d1a6e9 feat: comment reactions 2023-09-06 11:59:57 +05:30
sriram veeraghantaandGitHub 70ed3c1fdf Merge pull request #2097 from makeplane/feat/issue_detail_for_webview
feat: issue detail for web-view
2023-09-05 17:56:33 +05:30
dakshesh14 b40059ea21 feat: add links and permission to perform actions
refactor: divided file into components
2023-09-05 17:06:17 +05:30
Bavisetti NarayanandGitHub 90276073cd fix: validation in automation task (#2094) 2023-09-05 16:53:53 +05:30
Aaryan Khandelwal 8d5ff1a628 fix: redirection after signing in on space 2023-09-05 16:12:17 +05:30
dakshesh14 065a4a3cf7 feat: issue detail for web view 2023-09-05 14:42:34 +05:30
MengYXandGitHub 928ae775f4 fix: replace Completion with ChatCompletion to use gpt-3.5-turbo model (#2066)
According to https://platform.openai.com/docs/guides/gpt/chat-completions-vs-completions , GPT-3.5 Turbo & GPT-4 is not working on **Legacy** Completions API
2023-09-05 13:11:31 +05:30
Aaryan KhandelwalandGitHub 900a4fcb0e fix: profile time according to the user timezone (#2089) 2023-09-04 18:48:33 +05:30
19c65b26d6 dev: docker environment deploy fixes (#2087)
* chore: updated space and web dockerfiles

* chore: updated compose file params

* updated nextjs config for basepath

* chore: updated package.json with new packages

* chore: modified space and web dockerfiles

* dev: update deploy configuration for deploy images

* dev: update docker folder for web

* dev: add semi colon for module exports

---------

Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
2023-09-04 18:46:10 +05:30
Anmol Singh BhatiaandGitHub 71394d3316 chore: add issue option removed from subscribed issue page (#2088)
* chore: condition for subscribed page add issue option

* chore: condition for subscribed page add issue option
2023-09-04 18:42:31 +05:30
sriram veeraghantaandGitHub 9423472838 Env Fixes (#2086)
* fixing env issues

* removing husky
2023-09-04 18:03:31 +05:30
sriram veeraghantaandGitHub 729eabdd3f next config fixes in space app (#2084) 2023-09-04 17:55:40 +05:30
Aaryan KhandelwalandGitHub 03f204a71c chore: invalid url content (#2082) 2023-09-04 17:27:29 +05:30
guru_sainathandGitHub faf5a274cb fix: mutation latency in sidebar projects when user leaves the project (#2083)
* fix: mutation latency in sidebar projects when user leaves the project

* chore: remove console
2023-09-04 17:24:52 +05:30
Aaryan KhandelwalandGitHub 2c9c8d5a89 feat: landing page after logging in (#2081) 2023-09-04 16:55:43 +05:30
Aaryan KhandelwalandGitHub 5e02ad8104 fix: project invite modal members filter function (#2080) 2023-09-04 16:35:28 +05:30
f554ad95e9 fix: favicon path on Plane space (#2077)
* fix: favicon path

* chore: add webmanifest file

* favicon fixes with nginx

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-09-04 16:34:53 +05:30
Aaryan KhandelwalandGitHub 59b69d3072 chore: add the env example files (#2078) 2023-09-04 15:59:51 +05:30
ccbb54bb87 feat: Leaving from project for viewer and guest roles has implemented (#2079)
* feat: leave project services and components

* feat: Leaving from project for viewer and guest roles has implemented

---------

Co-authored-by: dakshesh14 <dakshesh.jain14@gmail.com>
2023-09-04 15:53:46 +05:30
Aaryan KhandelwalandGitHub 8f46492c42 fix: copy link button not working on the peek overview (#2075)
* fix: copy issue link from the peek overview

* refactor: peek overview layout
2023-09-04 14:47:28 +05:30
NikhilandGitHub 58e23304a7 fix: state ordering for projects (#2073) 2023-09-04 14:38:39 +05:30
NikhilandGitHub dc26e1ea50 chore: cycle update errors (#2070) 2023-09-04 14:37:29 +05:30
Aaryan KhandelwalandGitHub f583789584 chore: add authorization to the gantt chart (#2074) 2023-09-04 13:08:49 +05:30
Aaryan KhandelwalandGitHub 9d9c1a86bf fix: state group icon (#2072) 2023-09-04 13:02:47 +05:30
Aaryan KhandelwalandGitHub 4559a1bd5d refactor: publish project store (#2068) 2023-09-04 12:34:12 +05:30
sriram veeraghantaandGitHub 0de62b3b0c removing gitpod config (#2071) 2023-09-04 12:08:58 +05:30
sriram veeraghantaandGitHub d3a9a764dc fix: space redirections (#2069) 2023-09-04 01:50:49 +05:30
sriram veeraghantaandGitHub 4ea52302ba fixing vercel deployments by switching next config using env (#2067) 2023-09-03 20:55:37 +05:30
1e152c666c New Directory Setup (#2065)
* chore: moved app & space from apps to root

* chore: modified workspace configuration

* chore: modified dockerfiles for space and web

* chore: modified icons for space

* feat: updated files for new svg icons supported by next-images

* chore: added /spaces base path for next

* chore: added compose config for space

* chore: updated husky configuration

* chore: updated workflows for new configuration

* chore: changed app name to web

* fix: resolved build errors with web

* chore: reset file tracing root for both projects

* chore: added nginx config for deploy

* fix: eslint and tsconfig settings for space app

* husky setup fixes based on new dir

* eslint fixes

* prettier formatting

---------

Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
2023-09-03 18:50:30 +05:30
Aaryan KhandelwalandGitHub 20e36194b4 fix: peek overview layout switch (#2064) 2023-09-03 11:25:37 +05:30
NikhilandGitHub 874d6e951b dev: updated error handling for project deploy board attributes (#2062)
* dev: updated error handling for project deploy board attributes

* dev: reaction integrity handling
2023-09-02 19:43:17 +05:30
Henit ChobisaandGitHub 63d799310b [chore] Added Husky for Automating Building and Linting Projects Before Push (#2032)
* chore: Added Husky as Root Dependency

* chore: Added Husky Prepush Script

* chore: Modified Husky Pre-Push Script to Conditionally Build Projects

* chore: added husky as dev dependency
2023-09-02 13:47:21 +05:30
M. PalanikannanandGitHub abe8df4eca added table-icons for left,right columns and top,bottom rows (#2061) 2023-09-02 00:45:34 +05:30
Aaryan KhandelwalandGitHub 0196fee7e3 fix: sidebar cycle and module select (#2056) 2023-09-01 20:54:14 +05:30
sriram veeraghantaandGitHub a6cd0809fa Merge pull request #2058 from makeplane/dev-deploy-fixes
fix: speeding up reactions and votes 🚀
2023-09-01 20:52:42 +05:30
sriram veeraghanta 2155a336ed peekover mutation fixes 2023-09-01 20:52:12 +05:30
sriram veeraghanta 1732945ec6 fix: speeding up reactions and votes 🚀 2023-09-01 20:38:53 +05:30
NikhilandGitHub 71c8f79276 fix: issue vote constraints (#2057) 2023-09-01 18:46:38 +05:30
Aaryan KhandelwalandGitHub f71a62f142 style: sign in page bg color (#2055) 2023-09-01 17:21:52 +05:30
Aaryan KhandelwalandGitHub 54d781ef91 fix: auth screens (#2054) 2023-09-01 17:10:06 +05:30
Anmol Singh BhatiaandGitHub 441e83eba6 fix: notification count mutation fix (#2053) 2023-09-01 17:08:02 +05:30
Anmol Singh BhatiaandGitHub 74bf9062b4 chore: bug fixes and ui/ux enhancements (#2036) 2023-09-01 16:52:44 +05:30
8a95a41100 feat: Converting space app to pages dir (#2052)
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Bavisetti Narayan <narayan@Bavisettis-MacBook-Pro.local>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: M. Palanikannan <73993394+Palanikannan1437@users.noreply.github.com>
Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
2023-09-01 16:42:30 +05:30
NikhilandGitHub c03550656a chore: vote actor details (#2047)
* chore: vote actor details

* dev: add field in serializer

* dev: remove _id in workspace and project
2023-09-01 15:58:25 +05:30
NikhilandGitHub 82a48d4805 chore: reaction serializers (#2046)
* dev: chore reaction serializers

* fix: remove issue reaction lite serializer
2023-09-01 15:55:21 +05:30
NikhilandGitHub f4fa2e011a feat: leave project and workspace endpoint (#2042)
* feat: leave project and workspace endpoint

* fix: argument error

* dev: update endpoint status
2023-09-01 15:55:06 +05:30
Kritika UpadhyayandGitHub 42ece0d784 chore: updates project invite placeholder (#2049) 2023-09-01 15:37:27 +05:30
Bavisetti NarayanandGitHub 1e9f0823f8 fix: imported uuid (#2048) 2023-09-01 14:41:20 +05:30
Aaryan KhandelwalandGitHub 4ba3ef5c24 fix: peek overview bugs (#2043)
* fix: side peek modal shaking

* refactor: peek overview layout

* fix: date selector, activity mutation

* fix: delete issue handler

* fix: assignees mutation
2023-09-01 13:52:55 +05:30
NikhilandGitHub c6d9ace6a2 dev: migrations for v0.12 release (#2044) 2023-09-01 13:20:52 +05:30
Aaryan KhandelwalandGitHub 0d4bcd2758 fix: Gantt chart bugs (#2024)
* fix: only left mouse button should trigger all the events

* fix: extra block shadow
2023-09-01 11:23:43 +05:30
NikhilandGitHub 3a0d96a48d chore: cycle endpoint to return display name as well in the assignee distribution (#2041)
* chore: cycle endpoint to return display name as well in the assignee distribution

* fix: value error
2023-09-01 11:21:34 +05:30
Lakhan BahetiandGitHub eab1d9329b feat: editor for issue description (#2038) 2023-09-01 10:59:17 +05:30
NikhilandGitHub 099bce87b5 chore: public board endpoints (#2030) 2023-09-01 00:08:40 +05:30
NikhilandGitHub b496a62540 fix: subscribed issues are filtering (#2037) 2023-08-31 19:07:56 +05:30
Aaryan KhandelwalandGitHub af929ab741 style: tiptap table (#2033) 2023-08-31 16:30:28 +05:30
M. PalanikannanandGitHub 38b7f4382f [feat]: Tiptap table integration (#2008)
* added basic table support

* fixed table position at bottom

* fixed image node deletion logic's regression issue

* added compatible styles

* enabled slash commands

* disabled slash command and bubble menu's node selector for table cells

* added dropcursor support to type below the table/image

* blocked image uploads for handledrop and paste actions
2023-08-31 13:41:41 +05:30
NikhilandGitHub 320608ea73 chore: return issue votes in public issue list endpoint (#2026) 2023-08-31 11:32:58 +05:30
Aaryan KhandelwalandGitHub 5e00ffee05 fix: bugs on the user profile page (#2018) 2023-08-30 17:28:17 +05:30
Aaryan KhandelwalandGitHub 54527cc2bb dev: revamp publish project modal (#2022)
* dev: revamp publish project modal

* chore: sidebar dropdown text
2023-08-30 17:27:49 +05:30
Bavisetti NarayanandGitHub 6c6b81bea7 chore: tracking the history of issue reactions and votes. (#2020)
* chore: tracking the issues reaction and vote history

* fix: changed the keywords for vote and reaction

* chore: added validation
2023-08-30 16:38:04 +05:30
Aaryan KhandelwalandGitHub f5a076e9a9 dev: revamp peek overview (#2021)
* dev: mobx for issues store

* refactor: peek overview component

* chore: update open issue button

* fix: issue mutation after any crud action

* chore: remove peek overview from gantt

* chore: refactor code
2023-08-30 13:26:28 +05:30
17aff1f369 fix: asset key validation (#1938)
* fix: asset key validation

* chore: asset key validation in user assets

---------

Co-authored-by: Bavisetti Narayan <narayan@Bavisettis-MacBook-Pro.local>
2023-08-30 12:20:13 +05:30
NikhilandGitHub 761a1eb41a fix: user created by stats (#2016) 2023-08-30 12:18:56 +05:30
NikhilandGitHub 426f65898b feat: user timezones (#2009)
* dev: user timezones

* feat: user timezones
2023-08-30 12:18:18 +05:30
NikhilandGitHub 23f5d5d172 chore: track public board comments and reaction users for public deploy boards (#1972)
* chore: track project deploy board comment and reaction users for public deploy boards

* dev: remove tracking from project viewsets
2023-08-30 12:15:08 +05:30
Aaryan KhandelwalandGitHub 2e5ade05fe chore: update module status icons and colors (#2011)
* chore: update module status icons and colors

* refactor: import statements

* fix: add default alue to module status
2023-08-30 11:43:47 +05:30
Aaryan KhandelwalandGitHub 168e79d6df style: revamp of the issue details sidebar (#2014) 2023-08-29 20:15:12 +05:30
Aaryan KhandelwalandGitHub d8bbdc14ac feat: access selector for comment (#2012)
* dev: access specifier for comment

* chore: change access order
2023-08-29 20:14:13 +05:30
Aaryan KhandelwalandGitHub fd0efb0242 fix: start date filter not working on the platform (#2007) 2023-08-29 20:11:38 +05:30
Aaryan KhandelwalandGitHub 38a5623c43 dev: user timezone select option (#2002) 2023-08-29 20:11:06 +05:30
NikhilandGitHub 90cf39cf59 fix: access creation in comments (#2013) 2023-08-29 16:40:28 +05:30
Bavisetti NarayanandGitHub b2a41d3bf6 fix: issue votes (#2006)
* fix: issue votes

* fix: added default as 1 in vote

* fix: issue vote migration file
2023-08-29 15:02:29 +05:30
Bavisetti NarayanandGitHub 1cf5e8d80a fix: only external comments will show in deploy boards (#2010) 2023-08-29 15:01:18 +05:30
NikhilandGitHub 1d30a9a0a8 chore: project public board issue retrieve (#2003)
* chore: project public board issue retrieve

* dev: project issues list endpoint

* fix: issue public retrieve endpoint
2023-08-29 15:00:26 +05:30
Bavisetti NarayanandGitHub 91c10930a4 feat: mark all read notifications (#1963)
* feat: mark all read notifications

* fix: changed string to boolean

* fix: changed snoozed condition
2023-08-29 14:57:27 +05:30
NikhilandGitHub 5ad5da4fd7 dev: remove gunicorn config (#1999) 2023-08-29 13:45:25 +05:30
e1ad385688 fix: issue exports in self hosted instances (#1996)
* fix: issue exports in self hosted instances

* dev: remove print logs

* dev: update url creation function

* fix: changed the presigned url for self hosted exports

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-08-29 13:45:04 +05:30
NikhilandGitHub abcdebef85 fix: n+1 in issue history and issue automation tasks (#1994) 2023-08-29 13:35:36 +05:30
NikhilandGitHub 3a41ec7442 chore: update user activity endpoint to return only workspace activities (#1980) 2023-08-29 13:35:13 +05:30
8581226e60 chore: improve access field for comments for public boards (#1956)
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
2023-08-29 13:34:38 +05:30
sriram veeraghantaandGitHub c65bbf865d fix: tiptap editor export fixes (#2001) 2023-08-28 15:54:49 +05:30
srinivas pendemGitHubsrinivaspendem <you@example.comsrinivaspendem2612@gmail.com>
b2e5760391 bugfix: Export download next link changed to anchor (#2000)
* bugfix: Export download next link changd to anchot

* bugfix: user workspace service name update

---------

Co-authored-by: srinivaspendem <you@example.comsrinivaspendem2612@gmail.com>
2023-08-28 15:54:20 +05:30
Henit ChobisaandGitHub 8a3b65a740 [chore] Update development workflows with every PR build and removed image update on every merge (#1985)
* chore: Combined Github Workflows for Release

* chore: Added Workflows for Building and Testing Changes
2023-08-28 13:36:08 +05:30
sriram veeraghantaandGitHub 293d90ddda fix: Cycles and Modules Cards view responsiveness (#1997)
* fix: modules cards fixes

* fix:cycles responsive cards
2023-08-28 13:34:05 +05:30
Aaryan KhandelwalandGitHub 485e56bcdf fix: my profile activity endpoint (#1983)
* fix: my profile activity endpoint

* chore: update service name
2023-08-28 13:29:48 +05:30
Aaryan KhandelwalandGitHub 6e7701d854 chore: don't show completion percentage if user has no assigned issues (#1984) 2023-08-28 13:29:07 +05:30
Aaryan KhandelwalandGitHub a1acd2772e feat: mark all as read (#1982) 2023-08-28 13:26:38 +05:30
47abe9db5e dev: gantt chart revamp (#1900)
* style: gantt chart polishing

* chore: sidebar y-axis drag and drop

* chore: remove y-axis drag and drop from the main content

* refactor: drop end function

* refactor: resizing logic

* chore: x-axis block move

* chore: x-axis block move flag

* chore: update scroll end logic

* style: modules gantt chart

* style: block background tint

* refactor: context dispatcher types

* refactor: draggable component

* chore: filters added to gantt chart

* refactor: folder structure

* style: cycle blocks

* chore: move to block arrow

* chore: move to block on the right side arrow

* chore: added proper comments for functions

* refactor: blocks render logic

* fix: x-axis drag and drop

* chore: minor ui fixes

* chore: remove link tag from blocks

---------

Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-28 13:25:47 +05:30
sriram veeraghantaandGitHub a61e8370b5 fix: workspace accepted invitation redirects to the workspace (#1971)
* fix: workspace accepted invitation redirects to the workspace

* chore: removing logs

* fix: updating user last workspace id with newly joined one

* adding error toast
2023-08-28 13:25:09 +05:30
sriram veeraghantaandGitHub 9f420a00d7 fix: create new project as fav (#1993) 2023-08-28 13:09:27 +05:30
NikhilandGitHub a9ff4b8c93 fix: project members n+1 (#1975) 2023-08-27 20:31:32 +05:30
2b168edd99 feat: peek overview for spreadsheet issues (#1979)
* feat: peak overview for issues

* fix: peek spelling

* chore: truncate issue property labels

* style: full screen view designed

* chore: add comment section

* chore: copy link and delete options added

* chore: update icons

---------

Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-25 17:41:23 +05:30
NikhilandGitHub 93fa093a79 dev: update python runtime (#1981) 2023-08-25 17:23:07 +05:30
NikhilandGitHub fd8c368c97 fix: add member role and member status in project create response (#1962) 2023-08-25 15:16:15 +05:30
sriram veeraghantaandGitHub 0525e7d6b3 fix: workspace members reordering (#1978) 2023-08-25 13:38:50 +05:30
1530993b84 fix: tiptap editor max width (#1968)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-25 12:42:12 +05:30
d8b8c903f2 fix: issue activity redirection to cycle and module (#1973)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-25 12:21:11 +05:30
bf0d0503b2 fix: redirection after deleting a project (#1970)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-25 12:17:17 +05:30
Henit ChobisaandGitHub fe1b0c1d73 [chore] Fixed Github Workflows for Building and Pushing, frontend, backend, proxy & plane-deploy (#1959)
* chore: modified frontend github workflow

* chore: modified backend github workflows

* chore: added github workflow for build and push for plane-deploy

* chore: added github workflow for plane-proxy
2023-08-25 12:12:07 +05:30
ab4a17c178 chore: custom CSS shadow variables added (#1969)
* chore: custom shadow variables added

* fix: 2xs shade

---------

Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-24 23:07:20 +05:30
38934e8b99 chore: group by assignees option for project issues (#1957)
* dev: group by assignees option for project issues

* fix: no assignee title

---------

Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-24 19:46:12 +05:30
d18ac83909 feat: start date filter added across the platform (#1955)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-24 19:45:23 +05:30
sriram veeraghantaandGitHub 802e6b3e8e fix: project member mutate issue (#1967) 2023-08-24 19:43:50 +05:30
sriram veeraghantaandGitHub 489ef6a3cc Merge pull request #1966 from makeplane/fix/workspace-members-mutate
fix: workspace members mutate issue
2023-08-24 18:26:11 +05:30
sriram veeraghanta bce8cae0da fix: mutate fixes 2023-08-24 18:21:57 +05:30
sriram veeraghanta f97597958a fix: workspace memebers mutate issue 2023-08-24 17:44:20 +05:30
NikhilandGitHub 7fca01d8c9 feat: project deploy board endpoint (#1943) 2023-08-23 22:13:37 +05:30
Bavisetti NarayanandGitHub 529ab19747 chore: removed extra exporter function (#1953) 2023-08-23 22:13:04 +05:30
2d1406953e fix: mutate projects list after joining (#1944)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-23 15:44:27 +05:30
a8fdd42cb9 fix: label color select popover overflow (#1949)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-23 15:43:09 +05:30
561fb9815b chore: update export services icons (#1946)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-23 12:29:59 +05:30
Bavisetti NarayanandGitHub 2cc67f6498 fix: date validation in cycle and module (#1945) 2023-08-23 12:17:20 +05:30
NikhilandGitHub eee6658cc2 dev: deploy docker containers (#1939) 2023-08-22 19:33:29 +05:30
68b438ab1a fix: aws region changed for exporter (#1933)
Co-authored-by: Bavisetti Narayan <narayan@Bavisettis-MacBook-Pro.local>
2023-08-22 13:18:15 +05:30
b406a70e72 fix: access environment variables is changed in services (#1930)
Co-authored-by: Sainath <sainath@Sainaths-MacBook-Pro.local>
2023-08-22 01:13:51 +05:30
Aaryan KhandelwalandGitHub b02417120b chore: hide new issue button from my subscribed issues page (#1927) 2023-08-21 20:50:17 +05:30
Aaryan KhandelwalandGitHub d040394826 fix: create project button not appearing on the sidebar (#1926) 2023-08-21 20:44:51 +05:30
NikhilandGitHub f7682c57ba fix: plane space start up command (#1925) 2023-08-21 20:25:12 +05:30
guru_sainathandGitHub 9bb6254515 chore: updated default api base_url (#1922) 2023-08-21 18:17:32 +05:30
Aaryan KhandelwalandGitHub ae052f1890 chore: update restricted workspace slugs (#1920) 2023-08-21 18:13:08 +05:30
cfc7049343 Dockerrizing space project (#1921)
* chore: Added Dockerfile for Space Project

* fix: next js config to standalone mode

* fix: workedaround build error with rename 404 page

* chore: modified dockerfile with new conventions

* chore: modified dockercompose file for new plane-deploy

* fix: handled ts errors with possibly undefined states

* chore: updated main dockerfile with plane-deploy

* feat: included space project to start.sh

* chore: modified space project port while running in production

* chore: restored changes inside space project

* chore: added ngnix config for space project running :4000

* fix: Updated docker-compose files

* chore: added space url for ngnix config

* chore: Updated ngnix template

* chore: updated space url in compose hub file

* dev: updated dockerfile.space and start and replace script

* dev: equate hub and build docker files

* dev: revert workspace space page

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-08-21 18:12:41 +05:30
guru_sainathandGitHub 41e55dff85 fix: build error for 404 and search params null check (#1919) 2023-08-21 12:43:41 +05:30
Aaryan KhandelwalandGitHub 0bccb63a9f fix: module start and target date validations (#1914) 2023-08-21 11:46:02 +05:30
Aaryan KhandelwalandGitHub 2eb956e97e refactor: project and workspace delete modals (#1915) 2023-08-21 11:44:59 +05:30
M. PalanikannanandGitHub d470adf262 fix: Image resize, Link selector in Modals, Delete/ sync images and so much more (#1896)
* added image-resizing support

* link form removed

* updated image upload logic and 35% default width on upload

* removed shadow, added alert if not saved and ts errors

* prevent enter key on Link selector to trigger modal submit

* added workspace slug to all tiptap instances

* added delete plugin with loading indicator

* added better syncing of "Saved" state of editor and Image uploads

* removed redundant description_html check
2023-08-19 18:58:54 +05:30
Aaryan KhandelwalandGitHub cebc8bdc8d fix: context menu dynamic positioning, multiple context menus opening issue (#1913)
* fix: context menu positioning

* fix: close already opened context menu on outside click
2023-08-19 18:50:12 +05:30
Anmol Singh BhatiaandGitHub 64b5ba196f style: responsive empty state for profile stats (#1911) 2023-08-18 20:18:03 +05:30
Anmol Singh BhatiaandGitHub 8d5018318d chore: add favorite project from sidebar (#1909) 2023-08-18 18:42:50 +05:30
Anmol Singh BhatiaandGitHub 0fbdc0b157 style: consistent ui for create update issue modal (#1907) 2023-08-18 18:42:04 +05:30
Bavisetti NarayanandGitHub 2f39181eb7 fix: priority ordering (#1908) 2023-08-18 18:27:29 +05:30
Dakshesh JainandGitHub d825dc5579 style: showing first name for bot profile (#1894) 2023-08-18 17:15:20 +05:30
Aaryan KhandelwalandGitHub 1f8117c987 fix: dashboard upcoming issues list (#1904) 2023-08-18 17:10:12 +05:30
Bavisetti NarayanandGitHub 125e9090ea chore: module link model (#1905)
* chore: module link model

* chore: added migration
2023-08-18 15:50:48 +05:30
Bavisetti NarayanandGitHub 02ac4cee22 chore: renamed target date to start date (#1902) 2023-08-18 15:25:42 +05:30
Anmol Singh BhatiaandGitHub 93164755e2 style: analytics stats empty state (#1903) 2023-08-18 15:15:33 +05:30
Aaryan KhandelwalandGitHub 6344f6f562 chore: set order by to manual on gantt chart (#1886) 2023-08-18 15:12:12 +05:30
Aaryan KhandelwalandGitHub b67e30fd9c chore: analytics start date property for x-axis and group (#1888) 2023-08-18 15:11:25 +05:30
Anmol Singh BhatiaandGitHub 93fec2c678 fix: completed cycle validation , style: assignee count alignment fix (#1901)
* style: assignee count alignment fix

* fix: completed cycle validation
2023-08-18 14:23:13 +05:30
Anmol Singh BhatiaandGitHub c3c6ba9e34 chore: link edit functionality (#1895) 2023-08-18 12:03:31 +05:30
Anmol Singh BhatiaandGitHub d74ec7bda9 fix: ui improvement and bug fixes (#1883) 2023-08-18 12:01:51 +05:30
Henit ChobisaandGitHub e593a8d4bd chore: Edited Setup Script to take TipTap Auth Token and Generate .npmrc (#1897) 2023-08-18 11:47:58 +05:30
guru_sainathandGitHub abb8782c44 fix: handled default view on plane deploy (#1893)
* fix: handled default view on plane deploy

* fix: handled default view on refresh
2023-08-17 14:24:33 +05:30
guru_sainathandGitHub 0afd72db95 fix: updated theming in workspace preferences (#1890) 2023-08-16 20:35:17 +05:30
guru_sainathandGitHub 65295f6c6f chore: updating the theme using MobX from command k (#1879)
* chore: updating the theme using mobx from command k

* feat: Showing the project published status in the app header

* dev: updated validation and redirection the project publish modal and added redirection on the app header
2023-08-16 18:26:36 +05:30
Aaryan KhandelwalandGitHub 5b6b43fb83 fix: quick action buttons: (#1884) 2023-08-16 18:25:11 +05:30
Dakshesh JainandGitHub f8497125db style: fixed display name coming twice on profile page (#1889) 2023-08-16 18:18:57 +05:30
Bavisetti NarayanandGitHub b24622e5ef fix: validation for issue activity description (#1887) 2023-08-16 17:12:09 +05:30
guru_sainathandGitHub 10dface85d chore: updated error pages 404 and project-not-found in plane deploy (#1885)
* dev: custom error messages.

* dev: updated next version in yarn.lock

* dev: updated project-not-published icon
2023-08-16 17:05:40 +05:30
Aditi PatelandGitHub 2b6debaa3e Updated setup document to include tiptap pro install (#1871) 2023-08-16 14:42:37 +05:30
1750ba344b fix: my issue duplication (#1882)
Co-authored-by: Plane Team <planeteam@srirams-Mac-mini.local>
2023-08-16 14:40:10 +05:30
NikhilandGitHub 550473bb02 Merge pull request #1876 from makeplane/stage/merge-fixes
Promote: Develop to Stage Release
2023-08-16 13:18:26 +05:30
sriramveeraghanta fde978861c Merge branch 'develop' of github.com:makeplane/plane into stage/merge-fixes 2023-08-16 13:17:21 +05:30
guru_sainathandGitHub f44d142f2c chore: tip-tap editor update in workspace user activity (#1877) 2023-08-16 13:16:20 +05:30
guru_sainathandGitHub 1ded8f486f chore: updated meta tags for project issues (#1875)
* dev: updated meta tags for project issues

* dev: updated project description in meta tags in plane deploy.
2023-08-16 13:15:57 +05:30
2c43a15515 chore: added new filed in serializer (#1874)
Co-authored-by: NarayanBavisetti <narayan311@gmail.com>
2023-08-16 13:10:03 +05:30
Dakshesh JainandGitHub 0979acc1a4 fix: notification card not redirecting to archive issue detail for archived issue (#1861) 2023-08-16 13:09:56 +05:30
sriramveeraghanta 9003c58d89 merge conflicts resolved 2023-08-16 13:00:58 +05:30
NikhilandGitHub 55e2f00ffe fix: members list filtering for workspace and projects (#1872) 2023-08-16 12:21:56 +05:30
NikhilandGitHub 08382f88b4 chore: updated migration files for 0.11 (#1851) 2023-08-16 10:37:22 +05:30
72419447ec fix: added slug in filename for export issues (#1870)
* chore: file name changed for exported issues

* fix: added slug in filename for export issues

---------

Co-authored-by: NarayanBavisetti <narayan311@gmail.com>
2023-08-16 09:39:17 +05:30
NikhilandGitHub b554087b1f chore: deploy board status for project (#1866) 2023-08-16 01:00:22 +05:30
07717e9a93 chore: file name changed for exported issues (#1865)
Co-authored-by: NarayanBavisetti <narayan311@gmail.com>
2023-08-15 19:49:17 +05:30
NikhilandGitHub df46a45afc fix: analytics export (#1862)
* fix: analytics export

* dev: export analytics assignee indexing

* dev: total counts
2023-08-15 15:48:22 +05:30
e1ae0d3b56 feat : Tiptap integration (#1832)
* remirror instances commented out to avoid prosemirror conflicts

* styles migrated for remirror to tiptap transition

* added bubblemenu support with extensions

* fixed css for task lists and code with syntax highlighting

* added support for slash command

* fixed bubble menu to match styles and added better seperation in UI

* saving with debounce logic added and it's stored in backend

* added migration support by updating to html

* Image uploads done

* improved file structure and delete image function implemented

* Integrated tiptap with Issue Modal

* added additional props and Tiptap Integration with Comments

* added tiptap integration with user activity feeds

* added ref control support and bubble menu support for readonly editor

* added tiptap support for plane pages

* added tiptap support to gpt assistant modal (yet to be tested)

* removed remirror instances and cleaned up code

* improved code structure for extracting props in Tiptap

* fixing ts errors for next build

* fixing node ts error for Horizontal Rule

* added ts fix for node types

* temp fix

* temp fix

* added min height for issue description in modal

* added resolutions to prosemirror-model version

* trying pnpm overrides

* explicitly added prosemirror deps

* bugfixes

* removed extra gap at the top and moved saved indicator to the bottom

* fix: slash command scroll position

* chore: update custom css variables

* matched theme colours

* fixed gpt-assistant modal

* updated yarn lock

* added debounced updates for the title and removed saved state after timeout

* added css animations for saved state

* build fixes and remove remirror instances

* minor commenting fixes

---------

Co-authored-by: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-08-15 15:04:46 +05:30
guru_sainathandGitHub daa8f7d79b feat / public deploy settings workflow (#1863)
* Feat: Implemented project publish settings

* dev: updated the env dependancy in turbo and enabling the publish access to admin
2023-08-14 19:18:38 +05:30
NikhilandGitHub d2cdaaccb9 fix: slack queryset (#1860) 2023-08-14 18:53:09 +05:30
Anmol Singh BhatiaandGitHub 6774eddb66 style: sidebar select date width (#1859) 2023-08-14 18:10:21 +05:30
Dakshesh JainandGitHub 8ccc1b3fcc fix: gray background on png image (#1856) 2023-08-14 16:23:10 +05:30
Anmol Singh BhatiaandGitHub 5e76e03a55 style: profile activity loader (#1858) 2023-08-14 16:20:53 +05:30
srinivas pendemandGitHub 77fb50faa4 fix: route fix in imports and exports (#1857) 2023-08-14 16:19:13 +05:30
5ddfee12bc fix: changed the display of date format (#1855)
Co-authored-by: NarayanBavisetti <narayan311@gmail.com>
2023-08-14 16:10:57 +05:30
Aaryan KhandelwalandGitHub f7a596c113 fix: gantt chart block left drag flicker (#1854)
* fix: left drag flicker

* fix: opposite side manual scroll
2023-08-14 14:27:02 +05:30
Dakshesh JainandGitHub f73239be92 refactor: using webp image format instead of svg (#1852) 2023-08-14 13:26:16 +05:30
Anmol Singh BhatiaandGitHub dc2438b2d3 style: image upload modal aspect ratio (#1853) 2023-08-14 13:06:00 +05:30
srinivas pendemGitHubsrinivaspendem <you@example.comsrinivaspendem2612@gmail.com>
ddd3301d17 feat: csv, json and, xlsx exporter (#1840)
* feat : csv, jason and, xlxs exporter

* handeling the export fail

* adding expired state to exports

* typo update

* header change

* improvement: added validation for the expired date

---------

Co-authored-by: srinivaspendem <you@example.comsrinivaspendem2612@gmail.com>
2023-08-14 11:44:17 +05:30
Vamsi KuramaandGitHub 816f00d956 Merge pull request #1848 from ryota-murakami/fix/license 2023-08-12 17:12:14 +05:30
ryota-murakami 70e2509d52 package.json; fix license 2023-08-12 20:30:01 +09:00
1a9faa025a fix: export issues in CSV, JSON and XLSX (#1794)
* fix: file name change

* feat: added xml json and csv export

* chore: added openpyxl package

* fix: added initiated_by field

* fix: added initiated by details

* dev: refactoring

* fix: rendering assignee name and labels in sheet

* fix: handeled exception in label

* feat: implemented link expiration scheduler(8 days)

* fix: removed the expired field

---------

Co-authored-by: NarayanBavisetti <narayan311@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-08-11 20:09:52 +05:30
NikhilandGitHub feba1cc4d0 feat: project public boards (#1772)
* feat: project public boards

* dev: public issue and comment reactions

* dev: public issue comments

* dev: public comments

* dev: inbox for public boards

* dev: inbox issues for public board

* dev: public inbox issue

* dev: migrations

* dev: update api endpoints

* dev: project boards and views

* dev: state and label details

* dev: public issue voting

* dev: issue voting

* dev: workspace details

* dev: project icon and emoji
2023-08-11 19:27:44 +05:30
Anmol Singh BhatiaandGitHub 079a5b28d8 style: custom theming color picker position (#1846) 2023-08-11 19:17:59 +05:30
Aaryan KhandelwalandGitHub c6eea9c7a9 fix: empty groups not appearing in the kanban view (#1843) 2023-08-11 19:17:02 +05:30
Anmol Singh BhatiaandGitHub 5f5790ebf9 chore: start date option added in spreadsheet view (#1824) 2023-08-11 19:14:54 +05:30
Anmol Singh BhatiaandGitHub a3d99100ee fix: sub issue progress indicator fix (#1847) 2023-08-11 19:05:13 +05:30
Anmol Singh BhatiaandGitHub ac6d2b0139 chore: updated user permission for project and workspace settings (#1807)
* fix: updated user permission for project and workspace settings

* chore: workspace delete modal fix
2023-08-11 18:29:24 +05:30
Dakshesh JainandGitHub 7becec4ee9 feat: randomize color on label create (#1839)
fix: create label state being persisted after edit label
2023-08-11 17:42:47 +05:30
guru_sainathandGitHub cd5e5b96da feat: Mobx integration, List and Kanban boards implementation in plane space (#1844)
* feat: init mobx and issue filter

* feat: Implemented list and kanban views in plane space and integrated mobx.

* feat: updated store type check
2023-08-11 17:18:33 +05:30
Dakshesh JainandGitHub ad4cdcc512 fix: cmdk modal not closing when choosing an option (#1833) 2023-08-11 17:16:37 +05:30
NikhilandGitHub 6617049983 fix: sub issue endpoint for state distribution (#1845) 2023-08-11 16:38:03 +05:30
Dakshesh JainandGitHub abec46a725 fix: cmdk not changing theme if active theme is custom-theme (#1842) 2023-08-11 16:16:20 +05:30
NikhilandGitHub 88e987b902 feat: cycle and module sort order (#1841)
* dev: cycle and module ordering

* dev: sort order for smallest
2023-08-11 16:03:15 +05:30
Aaryan KhandelwalandGitHub 785a6e8871 chore: gantt chart resizable blocks, y-axis drag and drop (#1810)
* chore: gantt chart resizable blocks

* chore: right scroll added

* chore: left scroll added

* fix: build errors

* chore: remove unnecessary console logs

* chore: add block type and remove info toggle

* feat: gantt chart blocks y-axis drag and drop

* chore: disable drag flag

* fix: y-axis drag mutation

* fix: scroll container undefined error

* fix: negative scroll

* fix: negative scroll

* style: blocks tooltip consistency
2023-08-11 15:59:13 +05:30
Anmol Singh BhatiaandGitHub 762ef422ca chore: project search added in project list page (#1837) 2023-08-11 15:57:11 +05:30
Anmol Singh BhatiaandGitHub e2b5657c3e chore: profile cover photo (#1836) 2023-08-11 15:56:26 +05:30
Anmol Singh BhatiaandGitHub dbb53a663e style: sidebar ui improvement (#1816)
* style: sidebar ui improvement

* style: sidebar menu spacing

* style: sidebar consistent spacing

* style: notification improvement

* style: remove from favorite filled icon added

* chore: settings option added in sidebar context menu

* chore: update delete project option visibility for sidebar

* style: sidebar project list display border top on scroll
2023-08-11 15:53:10 +05:30
Anmol Singh BhatiaandGitHub 5c964d144a style: ui improvement (#1806)
* style: kanban board theming

* style: assignee ui revamp

* style: kanban board header, label , priority , state and avatar ui revamp

* style: kanban card state dropdown

* style: sidebar profile dropdown

* style: sidebar dropdown icon

* style: sidebar workspace dropdown

* style: assignee select component

* fix: state select

* style: consistent app header button
2023-08-11 15:50:05 +05:30
Anmol Singh BhatiaandGitHub 289e81d6eb chore: issue activity user redirection added (#1805)
* feat: issue activity user redirection added

* feat: analytics page user redirection
2023-08-11 15:48:52 +05:30
NikhilandGitHub def10af1e2 fix: issue date filtering (#1834) 2023-08-11 12:48:30 +05:30
NikhilandGitHub edaeae1b69 chore: sort order for cycle and modules (#1835) 2023-08-11 12:48:02 +05:30
NikhilandGitHub e06ee25800 fix: project states create (#1830) 2023-08-11 12:46:50 +05:30
NikhilandGitHub be86a7d38e feat: project member role (#1828) 2023-08-11 12:46:29 +05:30
NikhilandGitHub 0a1483c482 chore: issue list date filters and properties (#1820)
* dev: start date and target date validation and filter for null dated issues

* dev: remove print logs

* dev: issue property dates
2023-08-11 12:45:51 +05:30
NikhilandGitHub 11abd3cadf fix: user id for default analytics (#1808) 2023-08-11 12:45:04 +05:30
NikhilandGitHub 085cd1960e fix: project members endpoint email (#1804)
* fix: project members endpoint email

* dev: cycle and module assignee display name
2023-08-11 12:44:20 +05:30
NikhilandGitHub 2769a73898 remove: auto start date configuration (#1799) 2023-08-11 12:43:43 +05:30
NikhilandGitHub 8373f20944 fix: issues n plus 1 (#1785) 2023-08-11 12:43:00 +05:30
Dakshesh JainandGitHub f562fcd466 refactor: both delete or backspace can be used to open modal (#1829)
* refactor: both delete or backspace can be used to open modal

* fix: 'Bulk Issue Delete' modal will open on single click
2023-08-10 16:02:10 +05:30
Dakshesh JainandGitHub 8c8668a3e6 refactor: not making calls to member's endpoint to get membership status (#1831)
* refactor: not making call to members endpoint to get membership status

* refactor: type for IProject
2023-08-10 15:39:32 +05:30
Dakshesh JainandGitHub 9ce85cdf21 refactor: if/else conditions (#1822)
* refactor: shortcuts if/else

* fix: merge conflict
2023-08-10 13:12:17 +05:30
Dakshesh JainandGitHub 1c6cdb8328 refactor: added validation for display_name (#1823) 2023-08-10 13:05:03 +05:30
guru_sainathandGitHub 005b42cb8d dev: Updating themening worfkflow (#1827) 2023-08-10 13:03:42 +05:30
Dakshesh JainandGitHub be062ccd34 fix: import user using endpoint with email (#1819) 2023-08-09 17:49:25 +05:30
Dakshesh JainandGitHub c9d0c5353d style: displaying email in members page depending on user role (#1817)
* style: displaying email in members page depending on user role

* refactor: added null safety
2023-08-09 17:23:57 +05:30
Dakshesh JainandGitHub 2a6eb5fe23 style: option to edit display name in profile page (#1818) 2023-08-09 16:44:35 +05:30
Aaryan KhandelwalandGitHub d9ccce41bc chore: profile and my issues page start date option (#1815)
* chore: profile and my issues page start date option

* fix: properties order in my profile view options
2023-08-09 15:46:47 +05:30
Anmol Singh BhatiaandGitHub 3db69a3a71 style: profile empty state (#1811)
* style: profile empty state

* style: priority empty state padding
2023-08-09 15:45:20 +05:30
Dakshesh JainandGitHub faa50b0bbb fix: analytics showing user id instead of display name (#1812)
* fix: replacing first, last name and email with display name

* fix: different endpoint for workspace & project member

* fix: falling back to email if display_name doesn't exist

* fix: analytics showing user id instead of display name
2023-08-09 15:20:29 +05:30
Dakshesh JainandGitHub 1991e09035 refactor: removed escape keydown listener (#1814) 2023-08-09 15:18:26 +05:30
Aaryan KhandelwalandGitHub 4fcd081d27 chore: select start date option for issue (#1813) 2023-08-09 15:17:32 +05:30
5f1209f1db chore: empty state for multi-level dropdown (#1802)
* fix :label filter should show something if there is no label #1779 (#1795)

* style: children empty state

---------

Co-authored-by: Pankaj Chotaliya <34762752+pankajvc@users.noreply.github.com>
2023-08-08 14:13:26 +05:30
Aaryan KhandelwalandGitHub 88e5a05253 chore: subscribed by me tab on my issues page (#1800)
* chore: add subscribed by me tab in my issues

* chore: update tab titles

* fix: build error
2023-08-08 13:15:25 +05:30
Dakshesh JainandGitHub 981acc81c1 fix: replaced first name, last name or email to display name (#1796)
* fix: replacing first, last name and email with display name

* fix: different endpoint for workspace & project member

* fix: falling back to email if display_name doesn't exist
2023-08-08 13:01:43 +05:30
NikhilandGitHub cf306ee605 feat: user display name (#1179)
* feat: user display name for the entire system

* feat: update issue activity to remove emails

* dev: update to display name wherever assignees__email and member__email

* dev: update display names on issue activity and the user script

* dev: update display_name function to generate display_name from email

* dev: add email for test purpose

* dev: set default display name for the user

* dev: add migration script and default value

* dev: annotate with assignees_id

* dev: return assignees id

* dev: display name for the profile

* dev: project members endpoint

* dev: url update

* dev: trailing /

* dev: update workspace member serializer

* fix: activity for assignees
2023-08-08 12:59:04 +05:30
guru_sainathandGitHub 9df0ba6e3a feat: initiated plane space (#1801) 2023-08-08 12:55:42 +05:30
guru_sainathandGitHub b6744dcd29 Chore: mobx setup and app sidebar and theme management (#1798)
* dev: Mobx integration for app sidebar and custom theme

* dev: Handled edge case and conditional rendering for mobx store
2023-08-08 12:50:27 +05:30
DevMinerandGitHub a164dfd532 chore(frontend): add sharp (#1451) 2023-08-07 15:52:08 +05:30
2b46e5f977 feat: issue export csv (#1781)
* feat: created issue export csv

* fix: optimized the queries

---------

Co-authored-by: NarayanBavisetti <narayan311@gmail.com>
2023-08-07 11:59:04 +05:30
9b4aebc385 promote: develop to stage-release v0.10-patch (#1783)
* chore: show message if dragging unjoined project (#1763)

* fix: invalid project selection in create issue modal (#1766)

* style: sidebar project list improvement (#1767)

* fix: comment reaction mutation (#1768)

* fix: user profiles n plus 1 (#1765)

* fix: bulk issue import (#1773)

* style: profile activity (#1771)

* style: profile activity comment log styling

* chore: profile feed activity refactor

* style: sidebar project list

* chore: add non existing states for project entities (#1770)

* fix: notification read status being toggled when click on link (#1769)

* fix: custom theme persisting after signing out (#1780)

* fix: custom theme persistence

* chore: remove console logs

* fix: build error

* fix: change theme from command k

---------

Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
2023-08-03 15:28:22 +05:30
Aaryan KhandelwalandGitHub 97c3fb40e7 fix: custom theme persisting after signing out (#1780)
* fix: custom theme persistence

* chore: remove console logs

* fix: build error

* fix: change theme from command k
2023-08-03 15:01:31 +05:30
Dakshesh JainandGitHub 5aad6c71da fix: notification read status being toggled when click on link (#1769) 2023-08-02 17:23:55 +05:30
Aaryan KhandelwalandGitHub a1ae338c37 chore: add non existing states for project entities (#1770) 2023-08-02 16:47:36 +05:30
Anmol Singh BhatiaandGitHub c16b0daa22 style: profile activity (#1771)
* style: profile activity comment log styling

* chore: profile feed activity refactor

* style: sidebar project list
2023-08-02 16:45:34 +05:30
NikhilandGitHub 9a29896291 fix: bulk issue import (#1773) 2023-08-02 16:42:47 +05:30
NikhilandGitHub a66dcb9419 fix: user profiles n plus 1 (#1765) 2023-08-02 16:42:24 +05:30
Dakshesh JainandGitHub 87a920174e fix: comment reaction mutation (#1768) 2023-08-02 14:21:48 +05:30
Anmol Singh BhatiaandGitHub 584192faba style: sidebar project list improvement (#1767) 2023-08-02 14:21:26 +05:30
Dakshesh JainandGitHub b61adbed4b fix: invalid project selection in create issue modal (#1766) 2023-08-02 13:38:45 +05:30
Aaryan KhandelwalandGitHub 7434800999 chore: show message if dragging unjoined project (#1763) 2023-08-02 12:09:53 +05:30
NikhilandGitHub 9828d2332a Merge pull request #1761 from makeplane/develop
promote: develop to stage-release
2023-08-01 22:07:03 +05:30
NikhilandGitHub 78095e3823 Merge pull request #1760 from makeplane/fix/project_member_invite
fix: project invite
2023-08-01 22:05:48 +05:30
pablohashescobar f41086fd26 fix: typeerror 2023-08-01 22:00:56 +05:30
pablohashescobar 0866dc3494 fix: project invite 2023-08-01 21:42:48 +05:30
NikhilandGitHub 9f69fe6060 Merge pull request #1750 from makeplane/develop
promote: develop to stage-release
2023-08-01 19:35:54 +05:30
NikhilandGitHub 6ea15ced02 fix: project identifier length (#1757) 2023-08-01 19:25:04 +05:30
NikhilandGitHub 11525f26d0 fix: project identifier migration (#1755) 2023-08-01 19:11:32 +05:30
Anmol Singh BhatiaandGitHub f3bd1691ce style: sidebar project list styling (#1756) 2023-08-01 19:11:09 +05:30
Anmol Singh BhatiaandGitHub d83a76a3aa style: my issue and profile page view dropdown (#1754) 2023-08-01 19:04:53 +05:30
Aaryan KhandelwalandGitHub 2cd431b4a4 fix: my issues mutation (#1753)
* fix: my issues mutation

* fix: activity message and icon return value
2023-08-01 18:40:04 +05:30
Aaryan KhandelwalandGitHub d22e4b8212 fix: profile activity workspace slug (#1752) 2023-08-01 18:38:33 +05:30
Anmol Singh BhatiaandGitHub 0e0e09c4fd style: profile dropdown updated (#1751) 2023-08-01 18:25:13 +05:30
Aaryan KhandelwalandGitHub a8816ef473 refactor: issue activity component (#1749) 2023-08-01 17:07:11 +05:30
Anmol Singh BhatiaandGitHub d315a24c1c style: primary color variable added in global (#1748) 2023-08-01 17:04:23 +05:30
NikhilandGitHub e73a4bef4e chore: issue and project details in activity (#1747)
* chore: issue and project details in activity

* dev: update capture log
2023-08-01 17:03:19 +05:30
Aaryan KhandelwalandGitHub d9339b8f8e Merge pull request #1729 from makeplane/develop
promote: develop to stage-release
2023-08-01 15:46:56 +05:30
Dakshesh JainandGitHub a66a0680df fix: showing alert on error while deleting workspace member or invited member (#1746)
style: showing 'Leave' for current user
2023-08-01 15:32:42 +05:30
Anmol Singh BhatiaandGitHub 98c7453741 style: view dropdown (#1742) 2023-08-01 14:16:43 +05:30
NikhilandGitHub 1a5faca77c chore: show created by empty for viewers and guests (#1740)
* chore: show created by empty for viewers and guests

* dev: return empty queryset
2023-08-01 14:16:21 +05:30
NikhilandGitHub 6e7fa1a39c chore: project create to return sort order (#1738)
* chore: project create retun sort order

* chore: project create return sort order
2023-08-01 14:15:40 +05:30
NikhilandGitHub 7a6e742362 dev: fix migrations (#1735)
* dev: fix migrations

* dev: migrations for issue comment reactions and preference and cover image fields
2023-08-01 14:15:09 +05:30
Anmol Singh BhatiaandGitHub 8a9ff31009 style: sidebar project disclosure list open by default (#1744) 2023-08-01 14:13:42 +05:30
Dakshesh JainandGitHub d310b8f86f style: style if user doesn't have profile pic (#1745) 2023-08-01 14:12:57 +05:30
4e297d92f3 chore: update empty states, fix: delete issue modal (#1743)
* chore: update empty states

* fix: delete issue modal

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-08-01 13:58:58 +05:30
Anmol Singh BhatiaandGitHub e48147f87e style: create project modal (#1741) 2023-08-01 13:34:54 +05:30
Aaryan KhandelwalandGitHub 85a7a7df2b chore: profile dropdown in the sidebar (#1737)
* chore: profile dropdown in the sidebar

* style: update spacing and font sizes
2023-08-01 13:31:16 +05:30
Dakshesh JainandGitHub 92b22dc99e style: showing 'Created by me' tab to all user (#1739)
* style: showing 'Created by me' tab to all user

* refactor: removed unnecessary imports
2023-08-01 13:30:51 +05:30
Aaryan KhandelwalandGitHub cb4d294608 style: sidebar projects design (#1736)
* chore: disclosure menu for sidebar projects

* fix: projects list spacing

* style: new design
2023-08-01 12:24:34 +05:30
Aaryan KhandelwalandGitHub df8504e6f7 fix: issue redirection (#1733) 2023-08-01 11:22:47 +05:30
Dakshesh JainandGitHub 7287c27b73 refactor: changed per_page to 30 (#1734) 2023-08-01 11:19:06 +05:30
NikhilandGitHub cc2e6182b6 feat: user project sorting (#1719)
* feat: user project sorting

* dev: migration typo fix and query member fix

* feat: project member sort order update

* fix: project sorting per members
2023-07-31 18:12:02 +05:30
NikhilandGitHub 40fd7790eb fix: my issues duplication (#1726) 2023-07-31 18:11:46 +05:30
Aaryan KhandelwalandGitHub d733fb92cd fix: show project drag handle only when uncollapsed (#1727)
* fix: show project drag handle only when uncollapsed

* chore: fetch profile issues auth

* chore: update theme colors

* chore: update profile page redirection
2023-07-31 18:11:25 +05:30
NikhilandGitHub 1ae78e55c9 chore: profile page permission (#1728)
* chore: profile page permission

* dev: change the default type
2023-07-31 18:04:01 +05:30
NikhilandGitHub ff3f1897bc feat: user cycle stats (#1723)
* feat: user cycle stats

* dev: revert capture exception
2023-07-31 17:49:07 +05:30
Sai PhanindraandGitHub f42f2465a9 Changed alt text for badges in Readme (#1686) 2023-07-31 17:25:37 +05:30
Aaryan KhandelwalandGitHub 7ad0466d65 refactor: new onboarding workflow (#1724)
* refactor: new onboarding workflow

* refactor: new onboarding workflow
2023-07-31 17:23:49 +05:30
Anmol Singh BhatiaandGitHub e8f748a67d style: responsive title (#1683)
* style: responsive issue title added

* style: responsive breadcrumbs and app-header layout

* style: breadcrumbs styling

* fix: app header dropdown issue and limit app header title to 32 characters
2023-07-31 17:22:48 +05:30
Dakshesh JainandGitHub 81b1405448 refactor: moved mutate to 'finally' block (#1722)
* refactor: moved mutate to 'finally' block

fix: content for success

* fix: invited -> added
2023-07-31 17:21:37 +05:30
Anmol Singh BhatiaandGitHub 98d9763f8e feat: sidebar project ordering functionality added (#1725) 2023-07-31 17:10:23 +05:30
Dakshesh JainandGitHub c9498fa54d fix: workspace member invitation mutate (#1721)
* fix: workspace member invitation mutate

* fix: mutate on finally
2023-07-31 16:58:45 +05:30
Aaryan KhandelwalandGitHub 0586d30a33 style: profile page responsiveness added (#1710)
* refactor: folder structure

* style: mobile responsiveness added

* chore: add user profile redirection

* chore: profile page authorization
2023-07-31 16:57:28 +05:30
Dakshesh JainandGitHub 406b323e8e fix: image upload accepting non-image format (#1720)
* fix: image upload accepting non-image format

* style: showing all the valid for image upload
2023-07-31 14:17:51 +05:30
Anmol Singh BhatiaandGitHub 47838a506a fix: workspace name validation added (#1702) 2023-07-31 12:03:16 +05:30
d9ce042dff handle email invitation urls in outlook clients properly (#1671)
Co-authored-by: Ankur Singh <ankur.singh@epfl.ch>
2023-07-31 11:48:29 +05:30
Dakshesh JainandGitHub 4fb11cb388 fix: project cover image upload (#1704)
* fix: popover close on image upload

* fix: removed comments
2023-07-31 11:47:45 +05:30
Anmol Singh BhatiaandGitHub 6769d1139e feat: project user preference for pages (#1673)
* feat: project user preference for pages

* feat: page block description improvement

* fix: create block input box
2023-07-31 11:47:22 +05:30
Anmol Singh BhatiaandGitHub 89e7975821 style: ui improvements (#1699)
* style: sibling issues ui

* style: view dropdown
2023-07-31 11:45:10 +05:30
NikhilandGitHub 89bf24bd64 Merge pull request #1718 from makeplane/stage-release
dev: equate stage-release and develop branch
2023-07-31 11:42:18 +05:30
NikhilandGitHub 922735e5f2 feat: issue and comments reaction (#1674)
* dev: initialize issue reactions

* dev: issue reactions

* dev: comment reactions and update in urls

* dev: reactions in issue and comment list

* dev: reaction filtering

* dev: comment reaction lite serializer

* fix: reaction delete endpoint query
2023-07-31 10:42:17 +05:30
NikhilandGitHub ed75163ec4 chore: project preferences (#1669) 2023-07-31 10:42:00 +05:30
NikhilandGitHub 8e0124be91 feat: project ordering (#1701)
* dev: project ordering

* dev: add ordering for projects list
2023-07-31 10:41:25 +05:30
Anmol Singh BhatiaandGitHub c98edd4a91 style: reactions ui improvement (#1705) 2023-07-30 02:08:52 +05:30
Dakshesh JainandGitHub 35bb71303e refactor: fetching notification only when popover is open (#1706) 2023-07-30 02:05:30 +05:30
Aaryan KhandelwalandGitHub 30054f71a8 Merge pull request #1708 from makeplane/fix/google_authentication
fix: google not accepting width as string
2023-07-28 19:35:10 +05:30
Dakshesh Jain f40eb1add1 fix: google not accepting width as string 2023-07-28 19:21:55 +05:30
NikhilandGitHub e0affa21c4 feat: profile page endpoints (#1682)
* dev: profile page endpoints

* dev: workspace projects endpoint

* dev: user profile page endpoints

* dev: profile page endpoints

* dev: project filters

* dev: fix priority distribution

* dev: issue subscriptions

* dev: issue priority distribution and issue activity api optimization

* dev: user data in profile endpoints

* dev: profile page data

* dev: project list endpoint

* dev: project emojis

* dev: capture exception

* dev: update workspace user profile urls

* dev: user profile endpoints rename and activity filter

* dev: fix subscriber issues filtering
2023-07-28 14:35:45 +05:30
NikhilandGitHub b14c70df71 fix: workspace member only admin delete (#1700) 2023-07-28 14:29:40 +05:30
NikhilandGitHub 4c54ca5494 dev: revert issue filters query parameters (#1703) 2023-07-28 14:29:05 +05:30
Aaryan KhandelwalandGitHub 10f145f85c feat: user profile analytics, views and filters (#1698)
* feat: user profile overview

* chore: profile sidebar designed

* feat: user issues filters and view options

* refactor: filters

* refactor: mutation logic

* fix: percentage calculation logic and sidebar shadow
2023-07-28 13:39:42 +05:30
Dakshesh JainandGitHub 8930840a76 style: text color & position (#1692)
* feat: developed reaction selector component

* feat add reaction on issue & issue comment

refactor: reaction selector component, made hooks to abstracted reaction logic & state by making custom hook

* fix: emoji.helper.tsx function

* refactor: reaction not working on inbox issue

* fix: user not been passed to create/delete function

* style: text position & color
2023-07-28 10:46:35 +05:30
Vihar KuramaandGitHub 865698bcb4 Merge pull request #1696 from makeplane/remove-events
chore: remove events
2023-07-28 00:33:12 +05:30
Aaryan Khandelwal a3678b490a chore: update .env.example 2023-07-28 00:32:34 +05:30
Aaryan Khandelwal 5117859142 chore: update .env.example 2023-07-28 00:29:47 +05:30
Aaryan Khandelwal 05c923a97f chore: remove events 2023-07-28 00:26:35 +05:30
Aaryan KhandelwalandGitHub bedc3ab5a1 fix: create view form (#1691) 2023-07-27 21:38:16 +05:30
Dakshesh JainandGitHub 0cc4468091 feat: issue & comment reaction (#1690)
* feat: developed reaction selector component

* feat add reaction on issue & issue comment

refactor: reaction selector component, made hooks to abstracted reaction logic & state by making custom hook

* fix: emoji.helper.tsx function

* refactor: reaction not working on inbox issue
2023-07-27 18:55:03 +05:30
NikhilandGitHub 5cfea3948f fix: issue filters (#1688) 2023-07-27 17:30:39 +05:30
NikhilandGitHub c947a6dd64 fix: cycles n+1 (#1689) 2023-07-27 17:30:00 +05:30
Aaryan KhandelwalandGitHub c54b8b9a15 refactor: filters list component (#1687)
* refactor: filters list component

* fix: build error
2023-07-27 00:57:12 +05:30
Dakshesh JainandGitHub 0b86080166 fix: displaying API error for AI response (#1677) 2023-07-26 23:59:34 +05:30
Dakshesh JainandGitHub 0e352b7bcb style: padding bottom no load more (#1678) 2023-07-26 23:59:05 +05:30
Dakshesh JainandGitHub bc8be73d6c fix: invalid isOpen flag (#1676) 2023-07-26 23:58:40 +05:30
Aaryan KhandelwalandGitHub d6f3c2515a fix: fetch states only when the dropdown is opened (#1684) 2023-07-26 23:20:44 +05:30
NikhilandGitHub fd9dcfa2ec feat: my issues filtering (#1666)
* feat: my issues filtering

* dev: migrations

* dev: remove state list endpoint

* dev: state group filtering
2023-07-26 17:52:35 +05:30
Dakshesh JainandGitHub 39274fd5fa fix: show-sub issue filter is true now by default (#1679) 2023-07-26 17:52:03 +05:30
Aaryan KhandelwalandGitHub 3d7fe40035 feat: my issues view layouts and filters, refactor: issue views (#1681)
* refactor: issue views and my issues

* chore: update view dropdown options

* refactor: render emoji function

* refactor: api calss

* fix: build errors

* fix: fetch states only when dropdown is opened

* chore: my issues dnd

* fix: build errors

* refactor: folder structure
2023-07-26 17:51:26 +05:30
Aaryan KhandelwalandGitHub ec62308195 refactor: cmdk (#1680)
* chore: add project identifier to each result item

* refactor: cmdk code structure

* refactor: folder structure
2023-07-26 15:12:00 +05:30
Dakshesh JainandGitHub 9c28011ea5 refactor: notification pagination & made context for handling functions related to notification (#1654)
* refactor: add pagination and notification context

* refactor: moved header & snooze option to their own files

added check before toUpperCase function
2023-07-26 12:02:14 +05:30
Dakshesh JainandGitHub 10059b2150 feat: upload cover image for project (#1668) 2023-07-26 12:01:33 +05:30
Anmol Singh BhatiaandGitHub 6fe99c7f3e fix: custom search select query fix (#1610) 2023-07-25 16:08:11 +05:30
Anmol Singh BhatiaandGitHub 2229d8d828 fix: link validation fix (#1645)
* fix: link validation fix

* fix: build fix and code refactor

* fix: build fix

* chore: code refactor

* fix: link url input fix

* chore: link type updated

* fix: build fix
2023-07-25 15:43:47 +05:30
ad410d134f fix: project identifier validation (#1643)
* fix: project identifier validation

* feat: alphanumric identifier validation

* chore: code refactor

* refactor: project identifier change function

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-07-25 15:43:28 +05:30
Anmol Singh BhatiaandGitHub 9b531aca47 feat: workspace member bulk invite (#1530)
* feat: workspace member bulk invite

* style: invite dropdown styling
2023-07-25 15:42:52 +05:30
NikhilandGitHub 2bb842367f fix: pages n plus 1 (#1651) 2023-07-25 14:23:58 +05:30
NikhilandGitHub 916fca53ac chore: project identifier for all the search items and take project id as query params (#1663) 2023-07-25 14:23:41 +05:30
NikhilandGitHub 7763cca9a2 fix: email magic sign in integrity error (#1662) 2023-07-25 14:23:26 +05:30
NikhilandGitHub 3ad3cc77f9 fix: handle model save error in issue activity (#1661) 2023-07-25 14:23:10 +05:30
NikhilandGitHub 998fab80b5 feat: project preferences (#1652) 2023-07-25 14:22:54 +05:30
NikhilandGitHub 679c97bbe3 fix: make title as optional for issue links (#1641) 2023-07-25 14:22:38 +05:30
Aaryan KhandelwalandGitHub c87d70195d refactor: command k and enable workspace level search (#1664)
* refactor: command k and enable workspace level search

* fix: global level search
2023-07-25 13:52:21 +05:30
Dakshesh JainandGitHub c2327fa538 feat: show sub-issue on list-view (#1665) 2023-07-25 13:09:38 +05:30
Anmol Singh BhatiaandGitHub bc076e69f7 style: issue label ui updated (#1653)
* style: issue label ui updated

* chore: code refactor
2023-07-25 12:07:16 +05:30
Rhea JainandGitHub 1db9030f20 Update README.md (#1659) 2023-07-24 21:19:31 +05:30
NikhilandGitHub 737ac33d5d chore: project issue link activity (#1639)
* chore: project issue link activity

* dev: fix delete activity
2023-07-24 17:41:04 +05:30
NikhilandGitHub fd17c249fd chore: ability to comment for guests and viewers (#1650) 2023-07-24 17:40:26 +05:30
NikhilandGitHub afce027bf3 chore: global search endpoint for workspace (#1649)
* chore: global search endpoint

* chore: global search endpoitnt for workspace search
2023-07-24 17:39:29 +05:30
Aaryan KhandelwalandGitHub 6db1db55e9 fix: create issue modal restting (#1647) 2023-07-24 14:39:47 +05:30
NikhilandGitHub 08a025f67c fix: notification select related fields (#1646) 2023-07-24 14:39:25 +05:30
Aaryan KhandelwalandGitHub 8a2cc6f919 fix: remove issue link activity (#1644) 2023-07-24 13:57:23 +05:30
Aaryan KhandelwalandGitHub 29b04bb3ef chore: increase project identifier max length (#1638) 2023-07-24 12:57:05 +05:30
Aaryan KhandelwalandGitHub f3b09a13b8 chore: set all issue properties as true by default (#1640) 2023-07-24 12:56:27 +05:30
NikhilandGitHub e83ef7332d fix: issue create update n+1 and issue activity get n+1 (#1606)
* fix: issue create update n+1 and issue activity get n+1

* dev: notifications n+1
2023-07-24 12:23:34 +05:30
NikhilandGitHub 8ff834c328 chore: return total members, cycle and modules (#1637) 2023-07-24 12:23:04 +05:30
NikhilandGitHub 4ee161bae2 chore: project identifiers (#1636) 2023-07-24 12:19:11 +05:30
NikhilandGitHub 27402b52b6 fix: issue activity for attachments (#1626)
* fix: issue activity for attachments

* fix: issue activity assignees
2023-07-24 12:17:41 +05:30
NikhilandGitHub 479dfc17f5 feat: notification pagination (#1580) 2023-07-24 12:10:21 +05:30
NikhilandGitHub 8e70a036b7 chore: add project lead as project member (#1627)
* chore: add project lead as project member

* fix: project member lead create
2023-07-24 12:09:32 +05:30
NikhilandGitHub 73b38f4db9 feat: cross project issue linking (#1609)
* dev: sub issue listing

* feat: cross project issue linking

* dev: project search

* dev: workspace search logic

* dev: return state and project details for parent issues

* dev: issue state flat serializer

* dev: id for lite serializer

* dev: project name in for the response issues

* dev: issue cross project

* dev: issue project identifiers

* dev: blocked and blocked by activity
2023-07-24 12:08:47 +05:30
Ikko Eltociear AshimineandGitHub e357283789 chore: fix typo in replace-env-vars.sh (#1632)
peform -> perform
2023-07-24 12:08:29 +05:30
Aaryan KhandelwalandGitHub 2ce7914b7a style: new cycles list page design with empty states (#1633) 2023-07-24 11:32:59 +05:30
Aaryan KhandelwalandGitHub fe60771943 fix: create cycle modal (#1631)
* fix: cycle date select in the modal

* chore: update remove assignee issue activity
2023-07-23 22:54:17 +05:30
Aaryan KhandelwalandGitHub 464c13fcd0 fix: issue parent activity (#1629) 2023-07-23 22:14:41 +05:30
Aaryan KhandelwalandGitHub a7b5ad55ab style: create project modal (#1628)
* style: create project modal

* fix: build error

* fix: modal width
2023-07-23 22:14:26 +05:30
Aaryan KhandelwalandGitHub ccbcfecc6d fix: form not submitting on enter (#1613) 2023-07-23 22:13:09 +05:30
Aaryan KhandelwalandGitHub 7669ee8755 fix: project select not working on the create issue modal (#1608) 2023-07-23 22:12:13 +05:30
Aaryan KhandelwalandGitHub fdb7da4d45 chore: show proper error messages on profile form submit (#1611) 2023-07-23 22:10:40 +05:30
Aaryan KhandelwalandGitHub ff6690afd2 chore: workspace level toggle (#1625) 2023-07-23 15:11:28 +05:30
Aaryan KhandelwalandGitHub f9c3f02d15 refactor: issue activity message logic (#1614) 2023-07-23 14:38:07 +05:30
Aaryan KhandelwalandGitHub 6c2600efa7 feat: cross-project issue linking (#1612)
* feat: cross project issue linking

* fix: remove parent issue mutation

* fix: build error
2023-07-22 14:53:48 +05:30
Aaryan KhandelwalandGitHub 0e5c0fe31e refactor: issue search (#1607) 2023-07-21 14:29:06 +05:30
Anmol Singh BhatiaandGitHub 4424d67073 style: ui improvements (#1605)
* style: automation setting border

* style: sidebar ui improvement
2023-07-21 12:37:48 +05:30
guru_sainathandGitHub 26eb3b59ce Merge pull request #1604 from makeplane/stage-release
promote: stage-release to master
2023-07-20 22:45:17 +05:30
guru_sainathandGitHub 4d909fbef3 Merge pull request #1603 from makeplane/develop
promote: develop to stage-release
2023-07-20 22:42:28 +05:30
guru_sainathandGitHub 89210accae Merge pull request #1602 from makeplane/dev/monitoring_settings
dev: add server monitoring configuration
2023-07-20 22:40:08 +05:30
pablohashescobar 7c5c02bba6 dev: add server monitoring configuration 2023-07-20 22:34:47 +05:30
Vamsi KuramaandGitHub 11faf3f810 Merge pull request #1600 from makeplane/stage-release
promote: stage-release to master
2023-07-20 19:36:43 +05:30
Aaryan KhandelwalandGitHub 30ea1adf61 Merge pull request #1597 from makeplane/develop
promote: develop to stage-release
2023-07-20 19:32:04 +05:30
Dakshesh JainandGitHub 546aa40aa3 fix: un-read count not mutating (#1598)
* refactor: height of popover & api fetch call

* fix: notification subscribe endpoint

* fix: un-read count not mutating
2023-07-20 19:15:13 +05:30
NikhilandGitHub 78669363b1 fix: restrict notifications for created by when the actor is same (#1596) 2023-07-20 18:53:02 +05:30
guru_sainathandGitHub bca749986a Merge pull request #1595 from makeplane/develop
promote: develop to stage-release
2023-07-20 18:11:18 +05:30
gurusainath 229938114d Merge branch 'develop' of gurusainath:makeplane/plane into develop 2023-07-20 17:25:31 +05:30
gurusainath c5e418ab47 Merge branch 'stage-release' of gurusainath:makeplane/plane into develop 2023-07-20 17:25:00 +05:30
ecdd1f1d03 promote: develop to stage-release (#1594)
* fix: onboarding invitations overflow (#1575)

* fix: onboarding invitations overflow

* fix: user avatar in the notification card

* style: update graph grid color

* fix: no 'Create by me' label coming up (#1573)

* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

* fix: 'read' -> 'unread' heading, my issue wrong filter

* feat: made snooze dropdown & modal

feat: switched to calendar

* fix: minor ui fixes

* feat: snooze modal date/time select

* fix: params for read/un-read notification

* style: snooze notification modal

* fix: no label for 'Create by me'

* fix: no label for 'Create by me'

* fix: removed console log

* fix: tooltip going behind popover

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* style: tooltip on notification header actions (#1577)

* style: tooltip on notification header

* chore: update tooltip content

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* fix: user migrations for back population (#1578)

* fix: total notifications count (#1579)

* fix: notification card (#1583)

* feat: add new icons package (#1586)

* feat: add material icons package

* chore: replace issue view icons

* chore: notification ordering (#1584)

* fix: uuid error when cycle and module updates (#1585)

* refactor: height of popover & api fetch call (#1587)

* fix: snooze dropdown overflow (#1588)

* fix: notification subscribe endpoint (#1593)

* refactor: height of popover & api fetch call

* fix: notification subscribe endpoint

* chore: notification empty state overflow (#1592)

* chore: notification empty state overflow

* fix: white logo

* fix: custom theme default values

* fix: custom theme default values

* fix: issues count to remove archived issues (#1591)

* dev: background migration for user custom themes (#1590)

---------

Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-07-20 17:24:17 +05:30
NikhilandGitHub e687cd6f6a dev: background migration for user custom themes (#1590) 2023-07-20 16:59:09 +05:30
NikhilandGitHub 9b6721790f fix: issues count to remove archived issues (#1591) 2023-07-20 16:58:34 +05:30
gurusainath 05df65577a Merge branch 'stage-release' of gurusainath:makeplane/plane into develop 2023-07-20 16:54:40 +05:30
Aaryan KhandelwalandGitHub 52d21b9dda chore: notification empty state overflow (#1592)
* chore: notification empty state overflow

* fix: white logo

* fix: custom theme default values

* fix: custom theme default values
2023-07-20 16:51:00 +05:30
Dakshesh JainandGitHub 51f10d5f36 fix: notification subscribe endpoint (#1593)
* refactor: height of popover & api fetch call

* fix: notification subscribe endpoint
2023-07-20 16:35:18 +05:30
9275e6f373 promote: develop to stage-release (#1589)
* fix: onboarding invitations overflow (#1575)

* fix: onboarding invitations overflow

* fix: user avatar in the notification card

* style: update graph grid color

* fix: no 'Create by me' label coming up (#1573)

* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

* fix: 'read' -> 'unread' heading, my issue wrong filter

* feat: made snooze dropdown & modal

feat: switched to calendar

* fix: minor ui fixes

* feat: snooze modal date/time select

* fix: params for read/un-read notification

* style: snooze notification modal

* fix: no label for 'Create by me'

* fix: no label for 'Create by me'

* fix: removed console log

* fix: tooltip going behind popover

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* style: tooltip on notification header actions (#1577)

* style: tooltip on notification header

* chore: update tooltip content

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* fix: user migrations for back population (#1578)

* fix: total notifications count (#1579)

* fix: notification card (#1583)

* feat: add new icons package (#1586)

* feat: add material icons package

* chore: replace issue view icons

* chore: notification ordering (#1584)

* fix: uuid error when cycle and module updates (#1585)

* refactor: height of popover & api fetch call (#1587)

* fix: snooze dropdown overflow (#1588)

---------

Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
2023-07-20 15:14:57 +05:30
Aaryan KhandelwalandGitHub 4aef8c2242 fix: snooze dropdown overflow (#1588) 2023-07-20 14:49:30 +05:30
Dakshesh JainandGitHub 780573dadd refactor: height of popover & api fetch call (#1587) 2023-07-20 14:33:24 +05:30
NikhilandGitHub 5e625ab132 fix: uuid error when cycle and module updates (#1585) 2023-07-20 14:18:28 +05:30
NikhilandGitHub 34123681cf chore: notification ordering (#1584) 2023-07-20 14:18:06 +05:30
Aaryan KhandelwalandGitHub c72ff782ac feat: add new icons package (#1586)
* feat: add material icons package

* chore: replace issue view icons
2023-07-20 14:17:21 +05:30
Aaryan KhandelwalandGitHub 6eb72507a5 fix: notification card (#1583) 2023-07-20 10:52:52 +05:30
Aaryan KhandelwalandGitHub 26b18b431b fix: total notifications count (#1579) 2023-07-19 20:37:40 +05:30
NikhilandGitHub 1bae9289f5 fix: user migrations for back population (#1578) 2023-07-19 20:06:12 +05:30
5c5bcb33e3 style: tooltip on notification header actions (#1577)
* style: tooltip on notification header

* chore: update tooltip content

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-07-19 18:42:32 +05:30
bed5f76082 fix: no 'Create by me' label coming up (#1573)
* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

* fix: 'read' -> 'unread' heading, my issue wrong filter

* feat: made snooze dropdown & modal

feat: switched to calendar

* fix: minor ui fixes

* feat: snooze modal date/time select

* fix: params for read/un-read notification

* style: snooze notification modal

* fix: no label for 'Create by me'

* fix: no label for 'Create by me'

* fix: removed console log

* fix: tooltip going behind popover

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-07-19 17:40:40 +05:30
Aaryan KhandelwalandGitHub 124c2f772e fix: onboarding invitations overflow (#1575)
* fix: onboarding invitations overflow

* fix: user avatar in the notification card

* style: update graph grid color
2023-07-19 17:09:53 +05:30
Vihar KuramaandGitHub b38898753f Merge pull request #1570 from makeplane/develop
promote: develop to stage-release
2023-07-19 16:07:43 +05:30
guru_sainathandGitHub 86a120f11e Merge branch 'stage-release' into develop 2023-07-19 16:05:13 +05:30
Rhea JainandGitHub da603dc3f8 Update README.md (#1543) 2023-07-19 15:28:01 +05:30
NikhilandGitHub 2f3970f641 feat: beat worker for configuration (#1571) 2023-07-19 15:25:33 +05:30
NikhilandGitHub d759438ebd fix: issue automation (#1569) 2023-07-19 15:08:41 +05:30
NikhilandGitHub 0102f1d693 fix: psycopg errors (#1568) 2023-07-19 15:07:54 +05:30
53e443d816 feat: notifications (#1566)
* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

* fix: 'read' -> 'unread' heading, my issue wrong filter

* feat: made snooze dropdown & modal

feat: switched to calendar

* fix: minor ui fixes

* feat: snooze modal date/time select

* fix: params for read/un-read notification

* style: snooze notification modal

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-07-19 14:44:04 +05:30
NikhilandGitHub 98b9957753 fix: issue created notification (#1553)
* fix: issue created notification

* chore: my issues assigned notifications

* dev: fix read at notification
2023-07-19 14:31:45 +05:30
NikhilandGitHub 509af4662d dev: migrations (#1561) 2023-07-19 14:31:07 +05:30
Aaryan KhandelwalandGitHub d1f2a819f5 fix: update border colors on theme switch (#1565) 2023-07-19 13:53:22 +05:30
Aaryan KhandelwalandGitHub a42bff675b fix: archived issue details page overflow, restore button loading (#1564) 2023-07-19 13:29:08 +05:30
Aaryan KhandelwalandGitHub c71a2137e6 fix: scope and demand line graph y axis values (#1563) 2023-07-19 13:13:23 +05:30
Rhea JainandGitHub 5e1f0a2604 fixes as per consistency (#1562) 2023-07-19 13:00:11 +05:30
Aaryan KhandelwalandGitHub ccab382d7d chore: add dashboard navbar (#1560) 2023-07-19 12:16:39 +05:30
Aaryan KhandelwalandGitHub a7aa78a349 chore: high contrast theming (#1559)
* fix: archived issues empty state

* chore: update high contrast theme colors
2023-07-19 11:47:41 +05:30
Aaryan KhandelwalandGitHub 5ae963c451 fix: tour sidebar icons, auth pages scroll (#1555)
* fix: tour sidebar icons, menu button word wrap

* chore: change theme on sign out

* fix: auth pages scroll y-padding
2023-07-18 19:02:33 +05:30
Dakshesh JainandGitHub 07c097c9ad style: different style for archive & project issues (#1552)
* fix: same empty state coming for all issue list related UI

* style: different style for archive & project  issues
2023-07-18 18:58:36 +05:30
Aaryan KhandelwalandGitHub fc92d7d1a0 chore: hide issue properties if there are none (#1554) 2023-07-18 18:47:28 +05:30
NikhilandGitHub 9ba8f5c21f fix: cycle module notifications (#1536)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* dev: improve issue fetch
2023-07-18 15:59:58 +05:30
Aaryan KhandelwalandGitHub 059b8c793a refactor: parent issue select (#1546)
* refactor: parent issue select

* fix: sibling issues list
2023-07-18 15:36:03 +05:30
Dakshesh JainandGitHub 93da220c4a fix: same empty state coming for all issue list related UI (#1547) 2023-07-18 15:28:12 +05:30
Aaryan KhandelwalandGitHub 0feab162ff style: onboarding screens (#1539)
* fix: onboarding screen styling

* chore: minor styling fixes

* chore: disable buttons if form is invalid
2023-07-18 15:20:05 +05:30
NikhilandGitHub 55a1291b1d fix: inbox issue archival (#1538)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* dev: fix inboc issue archival

* dev: update the filter for closing issues
2023-07-18 15:10:20 +05:30
NikhilandGitHub b12a00cf4a chore: default project state colors (#1540)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* chore: project create default state colors
2023-07-18 15:10:01 +05:30
NikhilandGitHub 52ee8c5615 chore: remove logger from gpt assistant (#1542)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* chore: remove logger from gpt assistant
2023-07-18 15:09:44 +05:30
NikhilandGitHub 68108c9fe9 fix: favorite permissions (#1549)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* dev: fix permissions for cycle and views
2023-07-18 15:09:18 +05:30
Aaryan KhandelwalandGitHub 88fbe85087 fix: button and input heights (#1548) 2023-07-18 15:00:06 +05:30
Dakshesh JainandGitHub 9b423cea4b fix: create workspace valid (#1535)
show alert on-change rather then on-submit
2023-07-18 12:08:22 +05:30
Dakshesh JainandGitHub 9d891ecce1 fix: selecting label once create using modal (#1537) 2023-07-18 12:08:09 +05:30
16a7bd3bda feat: user issue notifications (#1523)
* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-07-18 12:07:55 +05:30
Dakshesh JainandGitHub 6e9f3971a5 style: empty state for archive issue (#1541) 2023-07-18 12:07:35 +05:30
Aaryan KhandelwalandGitHub dddfeb17b7 refactor: dropdowns (#1532) 2023-07-17 17:35:47 +05:30
Aaryan KhandelwalandGitHub 538d67dbd9 fix: theming colors (#1533)
* chore: update border colors

* chore: loading screens bg color, custom theming default values

* chore: remove unnecessary images

* chore: update static colors

* chore: update old variable names

* chore: update issue activity icon colors

* chore: update user activity icon colors
2023-07-17 16:28:23 +05:30
NikhilandGitHub 090870b03e fix: created_by notifications (#1534)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* dev: remove unused imports and improve the filtering

* dev: fix unread filter

* dev: fix created_by none when updating the field
2023-07-17 16:09:30 +05:30
Aaryan KhandelwalandGitHub 0a56a30ab2 fix: issue sidebar create label form (#1527)
* fix: issue sidebar create label form

* fix: issue details page overflow
2023-07-17 15:37:50 +05:30
Aaryan KhandelwalandGitHub 8df1648329 fix: dashboard pie chart legends overflow (#1528) 2023-07-17 14:48:43 +05:30
NikhilandGitHub b69c4b6b30 chore: notifications (#1515)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* dev: triggered_by details

* dev: add bot filtering

* dev: unread notification count endpoint

* dev: update endpoint to send count for all notification types
2023-07-17 13:17:48 +05:30
NikhilandGitHub e0181342c0 fix: n+1s (#1514)
* dev: fix workspace n+1

* dev: trim down label details for issue state serializer
2023-07-17 13:17:34 +05:30
NikhilandGitHub f9f8b5c3d9 chore: update bridge id to cycle and module id (#1513)
* chore: update bridge id to cycle and module id

* fix: typo
2023-07-17 13:17:11 +05:30
Aaryan KhandelwalandGitHub 5fadf53580 style: auth screens (#1531)
* chore: invitations empty state

* style: new auth screens

* chore: update typography

* chore: update border colors

* chore: resend code text
2023-07-17 13:00:44 +05:30
Aaryan KhandelwalandGitHub da6ecd439c refactor: favorite projects fetch function, chore: update tooltip design (#1526)
* chore: update tooltip design

* fix: due date popup overflow

* fix: build error

* fix: build error

* chore: add key to map return value

* refactor: favorite projects sidebar list
2023-07-14 15:08:07 +05:30
pablohashescobarandGitHub 7914bcf486 refactor: serializers to avoid getting child items (#1504)
* refactor: serializers to avoid getting child items

* dev: update issue state serializer to remove n+1 items
2023-07-14 12:01:38 +05:30
Aaryan KhandelwalandGitHub c9a5893c3f chore: update favorite projects endpoint (#1522)
* chore: upate favorite projects list endpoint

* chore: add project button on the sidebar

* refactor: sidebar favorite projects workflow
2023-07-13 19:44:53 +05:30
Anmol Singh BhatiaandGitHub 7361657660 fix: issue delete redirection, chore: code refactor (#1521) 2023-07-13 19:00:43 +05:30
60e96bcb72 style: app sidebar revamp (#1120)
* style: app sidebar, sidebar workspace dropdown and help section styling

* style: consistent  padding and spacing

* feat: material icon global component

* style: icons updated and tooltip added

* style: project list spacing and project name truncate

* style: sidebar padding and theming

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-07-13 18:35:43 +05:30
Anmol Singh BhatiaandGitHub 3f3fb373cc fix: issue archive improvement, chore: code refactor (#1520)
* style: select month modal

* chore: automation setting dropdown updated ,style: month dropdown styling

* chore: restore issue alert message updated

* chore: archive issue fetching updated and code refactor

* fix: build fix
2023-07-13 17:04:24 +05:30
a829e6fc40 chore: added is_favorite filter (#1518)
* fix: notification filtering

* dev: reverse logic for archive filtering

* dev: fix watching notification

* dev: read filter

* dev: update automatic issue archival and close to send notifications

* dev: update archival structure for auto close issues

* chore: added is_favorite filter

* fix: removed the unwanted filter

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-07-13 16:51:42 +05:30
Aaryan KhandelwalandGitHub 864e592bc5 fix: onboarding screen design issues (#1517)
* fix: projects empty state flicker

* fix: tour images padding

* chore: add greeting icon on the dashboard

* chore: update email id font weight

* fix: placeholder color
2023-07-13 16:51:08 +05:30
pablohashescobarandGitHub 411a661abd fix: magic code casing (#1519) 2023-07-13 16:49:37 +05:30
pablohashescobarandGitHub 61ad6b9e0e chore: upgrade psycopg to 3 (#1507) 2023-07-13 13:51:02 +05:30
2ff49c93bd fix: notifications (#1498)
* fix: notification filtering

* dev: reverse logic for archive filtering

* dev: fix watching notification

* dev: read filter

* dev: update automatic issue archival and close to send notifications

* dev: update archival structure for auto close issues

* Closing of dialog when we click around the dialog (#1364)

* style: new empty states (#1497)

* fix: custom colors opacity

* chore: update text colors for dark mode

* fix: dropdown text colors, datepicker bg color

* chore: update text colors

* chore: updated primary bg color

* style: new empty states added

* refactor: empty state for issues

* style: empty state for estimates

* chore: update labels, estimates and integrations empty states

* fix: custom analytics sidebar

* fix: archival spelling mistake

* fix: updated the default state logic

* dev: fix key

---------

Co-authored-by: Khrystyna Derenivska <56432889+kblueberry@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-07-13 13:47:24 +05:30
Aaryan KhandelwalandGitHub 120d06983d style: inbox (#1508) 2023-07-13 11:38:45 +05:30
Anmol Singh BhatiaandGitHub c9cbca5ec8 feat: auto-archive and auto-close (#1502)
* chore: issue archive services and types added

* chore: project type and constant updated

* feat: auto-close and auto-archive feature added

* feat: implement rendering of archived issues

* feat: implemented rendering of only list view for archived issues , chore: update types and services

* feat: implemented archive issue detail page and unarchive issue functionality , chore: refactor code

* feat: activity for issue archive and issue restore added

* fix: redirection and delete fix

* fix: merge conflict

* fix: restore issue redirection fix

* fix: disable modification of issue properties for archived issues, style: disable properties styling

* fix: hide empty group, switch to list view on redirct to archived issues

* fix: remove unnecessary header buttons for archived issue

* fix: auto-close dropdown fix
2023-07-13 11:34:37 +05:30
pablohashescobarandGitHub 275942a246 feat: flag for onboarding tour completion (#1499)
* feat: flag for onboarding tour completion

* dev: boolean field

* dev: user tour completed endpoint

* dev: onboarding step json
2023-07-12 22:10:07 +05:30
Aaryan KhandelwalandGitHub a1b09fcbc6 style: onboarding screens (#1412)
* style: new onboarding screens

* chore: onboarding tour screens

* fix: build error

* fix: build errors

* style: default layout background

* chor: update user auth hook logic, style: new onboarding screens

* fix: component structure

* chore: tab responsiveness added

* fix: redirection logic

* style: welcome screens responsiveness

* chore: update workspace url input field

* style: mobile responsiveness added

* chore: complete onboarding workflow

* style: create workspace page design update

* style: workspace invitations page design update

* chore: update steps logic

* fix: step change logic

* style: tour steps
2023-07-12 19:55:08 +05:30
Aaryan KhandelwalandGitHub 26f0e9da00 fix: add missing argument (#1506) 2023-07-12 17:55:58 +05:30
Aaryan KhandelwalandGitHub 4a2057c0b3 style: new empty states (#1497)
* fix: custom colors opacity

* chore: update text colors for dark mode

* fix: dropdown text colors, datepicker bg color

* chore: update text colors

* chore: updated primary bg color

* style: new empty states added

* refactor: empty state for issues

* style: empty state for estimates

* chore: update labels, estimates and integrations empty states

* fix: custom analytics sidebar
2023-07-12 11:45:45 +05:30
Khrystyna DerenivskaandGitHub 82ff786666 Closing of dialog when we click around the dialog (#1364) 2023-07-12 11:39:40 +05:30
Bavisetti NarayanandGitHub 5773ff2afd chore: updated old and new value in issue archival (#1500) 2023-07-11 19:15:39 +05:30
+6 1403a536c1 feat: openai host (#1447)
* support custom openai api endpoints (for azure and local deployments)

* update openai python package and use new api format

* fix: project member list endpoint n+1 (#1458)

* chore: workspace char name and slug maximum length (#1453)

* fix: user invitation workflow for self hosted version (#1441)

* chore: due date filter (#1460)

* refactor: standardized date format throughout the platform (#1461)

* chore: due date filter (#965)

* chore: due date filter

* fix: deployment error

* chore: optimized code

* chore: created constants for due date

* chore: create seperated css file for react datepicker styling

* fix: due date filter

* chore: highlight selected option

* fix: merge conflicts

* fix: build error

* chore: date range selector validation

* fix: issue views overflow

* refactor: due date filter modal code

* refactor: multi level dropdown

* chore: due date filter select default value

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* fix: layout of tabs on Pages is not adaptable to mobile screens #1380 (#1400)

* fix: layout of tabs on Pages is not adaptable to mobile screens #1380

* fix: scrolling experience on page

* chore: update project members type (#1459)

* fix: state icon color on group titles (#1435)

* fix: workspace invitation delete for self hosted (#1475)

* chore: upgrade backend dependencies (#1479)

* chore: upgrade backend dependencies

* dev: update storage settings for self hosted version

* chore: project members endpoint to support bulk operations (#1464)

* chore: rename workspace company size to organization size (#1463)

* chore: rename workspace company size to organization size

* chore: make workspace organization size as required

* fix: static and media files storages (#1482)

* fix: emoji render function (#1484)

* fix: emoji render function

* fix: emoji render function

* feat: bulk invite for project (#1466)

* feat: bulk invite for project

* feat: members dropdown updated

* fix: error message added ,style: ui improvement

* feat: added add members button for scenarios with multiple members

* chore: updated watch to fields

* feat: created on and updated on column added in spreadsheet view (#1454)

* feat: created on and updated on column added in spreadsheet view

* fix: build fix

* refactor: simplify logic

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* fix: resolved overflow issue with longer state names (#1444)

* chore: update theming structure (#1422)

* chore: store various shades of accent color

* refactor: custom theme selector

* refactor: custom theme selector

* chore: update custom theme input labels

* fix: color generator function logic

* fix: accent color preloaded data

* chore: new theming structure

* chore: update shades calculation logic

* refactor: variable names

* chore: update color scheming

* chore: new color scheming

* refactor: themes folder structure

* chore: update classnames according to new variables

* Revert "chore: update classnames according to new variables"

This reverts commit 60a87453b2.

* chore: remove temp file

* chore: update classnames according to the new theming structure (#1494)

* chore: store various shades of accent color

* refactor: custom theme selector

* refactor: custom theme selector

* chore: update custom theme input labels

* fix: color generator function logic

* fix: accent color preloaded data

* chore: new theming structure

* chore: update shades calculation logic

* refactor: variable names

* chore: update color scheming

* chore: new color scheming

* refactor: themes folder structure

* chore: update classnames to the new ones

* chore: update static colors

* chore: sidebar link colors

* chore: placeholder color

* chore: update border classnames

* chore:  environment variables for worker and api (#1492)

* feat: notifications (#1363)

* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>

* fix: docker inconsistencies (#1493)

Co-authored-by: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>

* feat: issue archival and close (#1474)

* chore: added issue archive using celery beat

* chore: changed the file name

* fix: created API and updated logic for achived-issues

* chore: added issue activity message

* chore: added the beat scheduler command

* feat: added unarchive issue functionality

* feat: auto issue close

* dev: refactor endpoints and issue archive activity

* dev: update manager for global filtering

* fix: added id in issue unarchive url

* dev: update auto close to include default close state

* fix: updated the list and retrive function

* fix: added the prefetch fields

* dev: update unarchive

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>

* fix: updated text and background colors (#1496)

* fix: custom colors opacity

* chore: update text colors for dark mode

* fix: dropdown text colors, datepicker bg color

* chore: update text colors

* chore: updated primary bg color

* feat: web waitlist modal integration (#1487)

* dev : Updating the limit of the issues in the sidebar and a weight list modal

* dev: integrated supabase and implemented web waitlist api endpoint

* dev : updated web pro weightlist request

* dev: rename typo

* dev: web waitlist endpoint update

* update: ui fixes

* fix: removed supabase from env.example

* chore: replaced supabase npm package to cdn

* chore: updated supabase req

* fix: Handled error status and error message.

---------

Co-authored-by: srinivaspendem <you@example.comsrinivaspendem2612@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>

* add openai host env in all places

---------

Co-authored-by: Ankur Singh <ankur.singh@epfl.ch>
Co-authored-by: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Kunal Vishwakarma <116634168+kunalv17@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Chandan Jal <97095857+ChandanJal@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Quadrubo <71718414+Quadrubo@users.noreply.github.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: srinivas pendem <65014795+srinivaspendem@users.noreply.github.com>
Co-authored-by: srinivaspendem <you@example.comsrinivaspendem2612@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-07-11 18:51:25 +05:30
srinivas pendemGitHubsrinivaspendem <you@example.comsrinivaspendem2612@gmail.com>gurusainath
eba2f3820a feat: web waitlist modal integration (#1487)
* dev : Updating the limit of the issues in the sidebar and a weight list modal

* dev: integrated supabase and implemented web waitlist api endpoint

* dev : updated web pro weightlist request

* dev: rename typo

* dev: web waitlist endpoint update

* update: ui fixes

* fix: removed supabase from env.example

* chore: replaced supabase npm package to cdn

* chore: updated supabase req

* fix: Handled error status and error message.

---------

Co-authored-by: srinivaspendem <you@example.comsrinivaspendem2612@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-07-11 16:27:29 +05:30
Aaryan KhandelwalandGitHub 253edebb93 fix: updated text and background colors (#1496)
* fix: custom colors opacity

* chore: update text colors for dark mode

* fix: dropdown text colors, datepicker bg color

* chore: update text colors

* chore: updated primary bg color
2023-07-11 15:18:47 +05:30
7554988164 feat: issue archival and close (#1474)
* chore: added issue archive using celery beat

* chore: changed the file name

* fix: created API and updated logic for achived-issues

* chore: added issue activity message

* chore: added the beat scheduler command

* feat: added unarchive issue functionality

* feat: auto issue close

* dev: refactor endpoints and issue archive activity

* dev: update manager for global filtering

* fix: added id in issue unarchive url

* dev: update auto close to include default close state

* fix: updated the list and retrive function

* fix: added the prefetch fields

* dev: update unarchive

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-07-11 14:35:20 +05:30
7087b1b5f2 fix: docker inconsistencies (#1493)
Co-authored-by: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
2023-07-11 14:10:57 +05:30
abdb4a4778 feat: notifications (#1363)
* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-07-11 13:36:31 +05:30
pablohashescobarandGitHub ad1a074292 chore: environment variables for worker and api (#1492) 2023-07-11 13:30:55 +05:30
Aaryan KhandelwalandGitHub 4c2cb2368a chore: update classnames according to the new theming structure (#1494)
* chore: store various shades of accent color

* refactor: custom theme selector

* refactor: custom theme selector

* chore: update custom theme input labels

* fix: color generator function logic

* fix: accent color preloaded data

* chore: new theming structure

* chore: update shades calculation logic

* refactor: variable names

* chore: update color scheming

* chore: new color scheming

* refactor: themes folder structure

* chore: update classnames to the new ones

* chore: update static colors

* chore: sidebar link colors

* chore: placeholder color

* chore: update border classnames
2023-07-10 12:47:00 +05:30
Aaryan KhandelwalandGitHub a14f8c281b chore: update theming structure (#1422)
* chore: store various shades of accent color

* refactor: custom theme selector

* refactor: custom theme selector

* chore: update custom theme input labels

* fix: color generator function logic

* fix: accent color preloaded data

* chore: new theming structure

* chore: update shades calculation logic

* refactor: variable names

* chore: update color scheming

* chore: new color scheming

* refactor: themes folder structure

* chore: update classnames according to new variables

* Revert "chore: update classnames according to new variables"

This reverts commit 60a87453b2.

* chore: remove temp file
2023-07-10 12:25:32 +05:30
Anmol Singh BhatiaandGitHub d564ea8898 fix: resolved overflow issue with longer state names (#1444) 2023-07-07 14:11:33 +05:30
7868bfa2b1 feat: created on and updated on column added in spreadsheet view (#1454)
* feat: created on and updated on column added in spreadsheet view

* fix: build fix

* refactor: simplify logic

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-07-07 14:11:07 +05:30
Anmol Singh BhatiaandGitHub 353c85120f feat: bulk invite for project (#1466)
* feat: bulk invite for project

* feat: members dropdown updated

* fix: error message added ,style: ui improvement

* feat: added add members button for scenarios with multiple members

* chore: updated watch to fields
2023-07-07 14:10:36 +05:30
Aaryan KhandelwalandGitHub 49f37e0346 fix: emoji render function (#1484)
* fix: emoji render function

* fix: emoji render function
2023-07-06 16:08:49 +05:30
pablohashescobarandGitHub 3a2f4d55d7 fix: static and media files storages (#1482) 2023-07-06 15:30:34 +05:30
pablohashescobarandGitHub 8443dfc9dd chore: rename workspace company size to organization size (#1463)
* chore: rename workspace company size to organization size

* chore: make workspace organization size as required
2023-07-06 11:58:47 +05:30
pablohashescobarandGitHub 2f8a1dbe20 chore: project members endpoint to support bulk operations (#1464) 2023-07-06 11:58:35 +05:30
pablohashescobarandGitHub 6bfeb6af34 chore: upgrade backend dependencies (#1479)
* chore: upgrade backend dependencies

* dev: update storage settings for self hosted version
2023-07-06 11:58:24 +05:30
pablohashescobarandGitHub fd3f6ab7a4 Merge pull request #1478 from makeplane/stage-release
promote: stage-release to master
2023-07-05 21:24:39 +05:30
pablohashescobarandGitHub d05d814efe chore: workspace invitation for self-hosted versions (#1477) 2023-07-05 21:21:31 +05:30
pablohashescobarandGitHub cc0701a823 fix: workspace invitation delete for self hosted (#1475) 2023-07-05 21:00:30 +05:30
Aaryan KhandelwalandGitHub 3906503c1b fix: state icon color on group titles (#1435) 2023-07-05 00:56:39 +05:30
Aaryan KhandelwalandGitHub c3fe221e7a chore: update project members type (#1459) 2023-07-05 00:56:15 +05:30
Chandan JalandGitHub e530533f9e fix: layout of tabs on Pages is not adaptable to mobile screens #1380 (#1400)
* fix: layout of tabs on Pages is not adaptable to mobile screens #1380

* fix: scrolling experience on page
2023-07-04 23:13:31 +05:30
4c3e2c252a chore: due date filter (#965)
* chore: due date filter

* fix: deployment error

* chore: optimized code

* chore: created constants for due date

* chore: create seperated css file for react datepicker styling

* fix: due date filter

* chore: highlight selected option

* fix: merge conflicts

* fix: build error

* chore: date range selector validation

* fix: issue views overflow

* refactor: due date filter modal code

* refactor: multi level dropdown

* chore: due date filter select default value

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-07-04 23:13:07 +05:30
Anmol Singh BhatiaandGitHub 4ede04d72f refactor: standardized date format throughout the platform (#1461) 2023-07-04 18:19:19 +05:30
pablohashescobarandGitHub 5a6fd0efdb chore: due date filter (#1460) 2023-07-04 16:48:59 +05:30
pablohashescobarandGitHub 1a72a0dff4 fix: user invitation workflow for self hosted version (#1441) 2023-07-04 13:56:51 +05:30
pablohashescobarandGitHub e4ee6a5bfb chore: workspace char name and slug maximum length (#1453) 2023-07-04 13:54:48 +05:30
pablohashescobarandGitHub e23e9ccdbb fix: project member list endpoint n+1 (#1458) 2023-07-04 13:54:37 +05:30
guru_sainathandGitHub 54a536e268 Merge pull request #1436 from makeplane/fix/bug_fix_and_ui_improvement 2023-07-03 13:57:15 +05:30
pablohashescobarandGitHub 110eb39a51 dev: update packages to latest version (#1431) 2023-06-30 19:37:26 +05:30
anmolsinghbhatia fc15da826b style: label dropdown empty state 2023-06-30 18:28:12 +05:30
Mark PercivalandGitHub 7e2caf65bb chore: Dry up docker compose environment variables (#1438)
* chore: Use docker compose extensions to dry up duplicate environment
variables between worker and api services.
See: https://docs.docker.com/compose/compose-file/11-extension/
2023-06-30 10:00:45 +05:30
tajkirkpatrickandGitHub d62dc25aa0 Add network timeout option to yarn install (#1382)
Inside of the Dockerfile.web, the yarn install command is susceptible to encountering a ECONNECT timeout error on aarch64 and ARM devices (such as a raspberry pi). This explicit overwrite of the default network timeout extends the window for low power CPU devices.
2023-06-30 09:53:44 +05:30
anmolsinghbhatia 32bf2ed56a fix: display properties fix 2023-06-29 18:37:19 +05:30
anmolsinghbhatia ebf877b7fa fix: display properties fix 2023-06-29 18:33:44 +05:30
anmolsinghbhatia d8eeb9e17a fix: cycle list item fix 2023-06-29 18:20:22 +05:30
anmolsinghbhatia f2a4e026a5 fix: cycle and module sidebar overflow fix 2023-06-29 18:19:38 +05:30
anmolsinghbhatia 8229b12c9c style: recent page overflow fix 2023-06-29 18:17:52 +05:30
anmolsinghbhatia 3c07da433a fix: overflow fix 2023-06-29 18:16:01 +05:30
pablohashescobarandGitHub 1ed7935bf0 docs: update readme.md for self hosting setup and remove pnpm from contribution guidelines (#1423) 2023-06-29 11:52:44 +05:30
Aaryan KhandelwalandGitHub c743d43ce2 fix: charts design and mutation (#1426)
* fix: pie chart overlap issue

* fix: burndown chart mutation

* fix: burndown chart mutation
2023-06-28 19:08:33 +05:30
1310 changed files with 70243 additions and 27716 deletions
+4 -51
View File
@@ -1,32 +1,3 @@
# Frontend
# Extra image domains that need to be added for Next Image
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
# Google Client ID for Google OAuth
NEXT_PUBLIC_GOOGLE_CLIENTID=""
# Github ID for Github OAuth
NEXT_PUBLIC_GITHUB_ID=""
# Github App Name for GitHub Integration
NEXT_PUBLIC_GITHUB_APP_NAME=""
# Sentry DSN for error monitoring
NEXT_PUBLIC_SENTRY_DSN=""
# Enable/Disable OAUTH - default 0 for selfhosted instance
NEXT_PUBLIC_ENABLE_OAUTH=0
# Enable/Disable sentry
NEXT_PUBLIC_ENABLE_SENTRY=0
# Enable/Disable session recording
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
# Enable/Disable event tracking
NEXT_PUBLIC_TRACK_EVENTS=0
# Slack for Slack Integration
NEXT_PUBLIC_SLACK_CLIENT_ID=""
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
# Error logs
SENTRY_DSN=""
# Database Settings
PGUSER="plane"
PGPASSWORD="plane"
@@ -39,15 +10,6 @@ REDIS_HOST="plane-redis"
REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
# Email Settings
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
EMAIL_USE_TLS="1"
EMAIL_USE_SSL="0"
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
@@ -59,24 +21,15 @@ AWS_S3_BUCKET_NAME="uploads"
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_KEY=""
GPT_ENGINE=""
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
# Settings related to Docker
DOCKERIZED=1
# set to 1 If using the pre-configured minio setup
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Default Creds
DEFAULT_EMAIL="captain@plane.so"
DEFAULT_PASSWORD="password123"
# SignUps
ENABLE_SIGNUP="1"
# Auto generated and Required that will be generated from setup.sh
+1 -1
View File
@@ -4,7 +4,7 @@ module.exports = {
extends: ["custom"],
settings: {
next: {
rootDir: ["apps/*"],
rootDir: ["web/", "space/"],
},
},
};
@@ -0,0 +1,55 @@
name: Build Pull Request Contents
on:
pull_request:
types: ["opened", "synchronize"]
jobs:
build-pull-request-contents:
name: Build Pull Request Contents
runs-on: ubuntu-20.04
permissions:
pull-requests: read
steps:
- name: Checkout Repository to Actions
uses: actions/checkout@v3.3.0
- name: Setup Node.js 18.x
uses: actions/setup-node@v2
with:
node-version: 18.x
cache: 'yarn'
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v38
with:
files_yaml: |
apiserver:
- apiserver/**
web:
- web/**
deploy:
- space/**
- name: Setup .npmrc for repository
run: |
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
- name: Build Plane's Main App
if: steps.changed-files.outputs.web_any_changed == 'true'
run: |
mv ./.npmrc ./web
cd web
yarn
yarn build
- name: Build Plane's Deploy App
if: steps.changed-files.outputs.deploy_any_changed == 'true'
run: |
cd space
yarn
yarn build
+111
View File
@@ -0,0 +1,111 @@
name: Update Docker Images for Plane on Release
on:
release:
types: [released, prereleased]
jobs:
build_push_backend:
name: Build and Push Api Server Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup .npmrc for repository
run: |
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaFrontend
uses: docker/metadata-action@v4.3.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend
tags: |
type=ref,event=tag
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaBackend
uses: docker/metadata-action@v4.3.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend
tags: |
type=ref,event=tag
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaDeploy
uses: docker/metadata-action@v4.3.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
tags: |
type=ref,event=tag
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaProxy
uses: docker/metadata-action@v4.3.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy
tags: |
type=ref,event=tag
- name: Build and Push Frontend to Docker Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./web/Dockerfile.web
platforms: linux/amd64
tags: ${{ steps.metaFrontend.outputs.tags }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Backend to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: ./apiserver
file: ./apiserver/Dockerfile.api
platforms: linux/amd64
push: true
tags: ${{ steps.metaBackend.outputs.tags }}
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Plane-Deploy to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./space/Dockerfile.space
platforms: linux/amd64
push: true
tags: ${{ steps.metaDeploy.outputs.tags }}
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Plane-Proxy to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: ./nginx
file: ./nginx/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.metaProxy.outputs.tags }}
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
-77
View File
@@ -1,77 +0,0 @@
name: Build and Push Backend Docker Image
on:
push:
branches:
- 'develop'
- 'master'
tags:
- '*'
jobs:
build_push_backend:
name: Build and Push Api Server Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
with:
platforms: linux/arm64,linux/amd64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.1.0
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
registry: "registry.hub.docker.com"
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
id: ghmeta
uses: docker/metadata-action@v4.3.0
with:
images: makeplane/plane-backend
- name: Extract metadata (tags, labels) for Docker (Github)
id: dkrmeta
uses: docker/metadata-action@v4.3.0
with:
images: ghcr.io/${{ github.repository }}-backend
- name: Build and Push to GitHub Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: ./apiserver
file: ./apiserver/Dockerfile.api
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.ghmeta.outputs.tags }}
labels: ${{ steps.ghmeta.outputs.labels }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: ./apiserver
file: ./apiserver/Dockerfile.api
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.dkrmeta.outputs.tags }}
labels: ${{ steps.dkrmeta.outputs.labels }}
-77
View File
@@ -1,77 +0,0 @@
name: Build and Push Frontend Docker Image
on:
push:
branches:
- 'develop'
- 'master'
tags:
- '*'
jobs:
build_push_frontend:
name: Build Frontend Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
with:
platforms: linux/arm64,linux/amd64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Github Container Registry
uses: docker/login-action@v2.1.0
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
registry: "registry.hub.docker.com"
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
id: ghmeta
uses: docker/metadata-action@v4.3.0
with:
images: makeplane/plane-frontend
- name: Extract metadata (tags, labels) for Docker (Github)
id: meta
uses: docker/metadata-action@v4.3.0
with:
images: ghcr.io/${{ github.repository }}-frontend
- name: Build and Push to GitHub Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./apps/app/Dockerfile.web
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.ghmeta.outputs.tags }}
labels: ${{ steps.ghmeta.outputs.labels }}
- name: Build and Push to Docker Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./apps/app/Dockerfile.web
platforms: linux/arm64,linux/amd64
push: true
cache-from: type=gha
cache-to: type=gha
tags: ${{ steps.dkrmeta.outputs.tags }}
labels: ${{ steps.dkrmeta.outputs.labels }}
+3 -1
View File
@@ -70,4 +70,6 @@ package-lock.json
# lock files
package-lock.json
pnpm-lock.yaml
pnpm-workspace.yaml
pnpm-workspace.yaml
.npmrc
+12 -12
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
@@ -125,4 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
https://www.contributor-covenant.org/translations.
-1
View File
@@ -23,7 +23,6 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
- Python version 3.8+
- Postgres version v14
- Redis version v6.2.7
- pnpm version 7.22.0
### Setup the project
+10 -3
View File
@@ -5,9 +5,11 @@ WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
RUN apk add tree
COPY . .
RUN turbo prune --scope=app --docker
RUN turbo prune --scope=app --scope=plane-deploy --docker
CMD tree -I node_modules/
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
@@ -21,14 +23,14 @@ COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# Build the project
# # Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build --filter=app
RUN yarn turbo run build
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
@@ -96,11 +98,16 @@ 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 --chown=captain:plane /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
ENV NEXT_TELEMETRY_DISABLED 1
# RUN rm /etc/nginx/conf.d/default.conf
+30 -30
View File
@@ -2,7 +2,7 @@
<p align="center">
<a href="https://plane.so">
<img src="https://res.cloudinary.com/toolspacedev/image/upload/v1680596414/Plane/Plane_Icon_Blue_on_White_150x150_muysa3.jpg" alt="Plane Logo" width="70">
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_logo_.webp" alt="Plane Logo" width="70">
</a>
</p>
@@ -11,22 +11,22 @@
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
<img alt="Discord" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" />
<img alt="Discord online members" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" />
</a>
<img alt="Discord" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
<img alt="Commit activity per month" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
</p>
<p>
<a href="https://app.plane.so/#gh-light-mode-only" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Screen.png?updatedAt=1684942001069"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_screen.webp"
alt="Plane Screens"
width="100%"
/>
</a>
<a href="https://app.plane.so/#gh-dark-mode-only" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Screens_Dark_Mode.png?updatedAt=1684942388044"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_screens_dark_mode.webp"
alt="Plane Screens"
width="100%"
/>
@@ -35,12 +35,10 @@
Meet [Plane](https://plane.so). 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.
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
## ⚡️ Quick start with Docker Compose
### Docker Compose Setup
@@ -56,17 +54,20 @@ chmod +x setup.sh
- Run setup.sh
```bash
./setup.sh http://localhost
./setup.sh http://localhost
```
> If running in a cloud env replace localhost with public facing IP address of the VM
- Export Environment Variables
- Setup Tiptap Pro
```bash
set -a
source .env
set +a
Visit [Tiptap Pro](https://collab.tiptap.dev/pro-extensions) and signup (it is free).
Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro.
```
@tiptap-pro:registry=https://registry.tiptap.dev/
//registry.tiptap.dev/:_authToken=YOUR_REGISTRY_TOKEN
```
- Run Docker compose up
@@ -75,26 +76,26 @@ set +a
docker compose up -d
```
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
## 🚀 Features
* **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
* **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
* **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
* **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
* **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
* **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
* **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
* **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
* **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
- **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
- **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
- **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
- **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
- **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
- **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
- **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
## 📸 Screenshots
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Views_Dark_Mode.png?updatedAt=1684943050275"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_views_dark_mode.webp"
alt="Plane Views"
width="100%"
/>
@@ -103,7 +104,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Issue_Detail_Dark_Mode.png?updatedAt=1684943050202"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_issue_detail_dark_mode.webp"
alt="Plane Issue Details"
width="100%"
/>
@@ -112,7 +113,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Cycles___Modules_Dark_Mode.png?updatedAt=1684943050281"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_cycles_modules_dark_mode.webp"
alt="Plane Cycles and Modules"
width="100%"
/>
@@ -121,7 +122,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Analytics_Dark_Mode.png?updatedAt=1684944596824"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_analytics_dark_mode.webp"
alt="Plane Analytics"
width="100%"
/>
@@ -130,7 +131,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Pages_Dark_Mode.png?updatedAt=1684943050202"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_pages_dark_mode.webp"
alt="Plane Pages"
width="100%"
/>
@@ -140,7 +141,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Commad_K_Dark_Mode.png?updatedAt=1684943050312"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_commad_k_dark_mode.webp"
alt="Plane Command Menu"
width="100%"
/>
@@ -148,7 +149,6 @@ docker compose up -d
</p>
</p>
## 📚Documentation
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
+60
View File
@@ -0,0 +1,60 @@
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
# Error logs
SENTRY_DSN=""
# Database Settings
PGUSER="plane"
PGPASSWORD="plane"
PGHOST="plane-db"
PGDATABASE="plane"
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
# Redis Settings
REDIS_HOST="plane-redis"
REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
# Email Settings
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
EMAIL_USE_TLS="1"
EMAIL_USE_SSL="0"
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
AWS_SECRET_ACCESS_KEY="secret-key"
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
# Changing this requires change in the nginx.conf for uploads if using minio setup
AWS_S3_BUCKET_NAME="uploads"
# Maximum file upload limit
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
# Settings related to Docker
DOCKERIZED=1
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Default Creds
DEFAULT_EMAIL="captain@plane.so"
DEFAULT_PASSWORD="password123"
# SignUps
ENABLE_SIGNUP="1"
+1 -1
View File
@@ -49,7 +49,7 @@ USER root
RUN apk --no-cache add "bash~=5.2"
COPY ./bin ./bin/
RUN chmod +x ./bin/takeoff ./bin/worker
RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
RUN chmod -R 777 /code
USER captain
+3 -2
View File
@@ -1,2 +1,3 @@
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: celery -A plane worker -l info
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: celery -A plane worker -l info
beat: celery -A plane beat -l INFO
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -e
python manage.py wait_for_db
celery -A plane beat -l info
+1 -1
View File
@@ -6,4 +6,4 @@ python manage.py migrate
# Create a Default User
python bin/user_script.py
exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --config gunicorn.config.py --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
+4 -4
View File
@@ -1,4 +1,4 @@
import os, sys
import os, sys, random, string
import uuid
sys.path.append("/code")
@@ -19,9 +19,9 @@ def populate():
user = User.objects.create(email=default_email, username=uuid.uuid4().hex)
user.set_password(default_password)
user.save()
print("User created")
print("Success")
print(f"User created with an email: {default_email}")
else:
print(f"User already exists with the default email: {default_email}")
if __name__ == "__main__":
+1 -1
View File
@@ -1,2 +1,2 @@
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, WorkspaceViewerPermission
from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission
@@ -61,3 +61,13 @@ class WorkspaceEntityPermission(BasePermission):
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug
).exists()
class WorkspaceViewerPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug, role__gte=10
).exists()
+19 -9
View File
@@ -1,10 +1,5 @@
from .base import BaseSerializer
from .people import (
ChangePasswordSerializer,
ResetPasswordSerializer,
TokenSerializer,
)
from .user import UserSerializer, UserLiteSerializer
from .user import UserSerializer, UserLiteSerializer, ChangePasswordSerializer, ResetPasswordSerializer, UserAdminLiteSerializer
from .workspace import (
WorkSpaceSerializer,
WorkSpaceMemberSerializer,
@@ -12,6 +7,7 @@ from .workspace import (
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
WorkspaceMemberAdminSerializer,
)
from .project import (
ProjectSerializer,
@@ -21,18 +17,20 @@ from .project import (
ProjectIdentifierSerializer,
ProjectFavoriteSerializer,
ProjectLiteSerializer,
ProjectMemberLiteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer
)
from .state import StateSerializer, StateLiteSerializer
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
from .asset import FileAssetSerializer
from .issue import (
IssueCreateSerializer,
IssueActivitySerializer,
IssueCommentSerializer,
IssuePropertySerializer,
BlockerIssueSerializer,
BlockedIssueSerializer,
IssueAssigneeSerializer,
LabelSerializer,
IssueSerializer,
@@ -41,6 +39,13 @@ from .issue import (
IssueLinkSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
IssueSubscriberSerializer,
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
)
from .module import (
@@ -74,4 +79,9 @@ from .estimate import (
)
from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
from .analytic import AnalyticViewSerializer
from .notification import NotificationSerializer
from .exporter import ExporterHistorySerializer
+17
View File
@@ -12,6 +12,17 @@ from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Cycle, CycleIssue, CycleFavorite
class CycleWriteSerializer(BaseSerializer):
def validate(self, data):
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
raise serializers.ValidationError("Start date cannot exceed end date")
return data
class Meta:
model = Cycle
fields = "__all__"
class CycleSerializer(BaseSerializer):
owned_by = UserLiteSerializer(read_only=True)
@@ -29,12 +40,18 @@ class CycleSerializer(BaseSerializer):
started_estimates = serializers.IntegerField(read_only=True)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
def validate(self, data):
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
raise serializers.ValidationError("Start date cannot exceed end date")
return data
def get_assignees(self, obj):
members = [
{
"avatar": assignee.avatar,
"first_name": assignee.first_name,
"display_name": assignee.display_name,
"id": assignee.id,
}
for issue_cycle in obj.issue_cycle.all()
@@ -0,0 +1,26 @@
# Module imports
from .base import BaseSerializer
from plane.db.models import ExporterHistory
from .user import UserLiteSerializer
class ExporterHistorySerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
class Meta:
model = ExporterHistory
fields = [
"id",
"created_at",
"updated_at",
"project",
"provider",
"status",
"url",
"initiated_by",
"initiated_by_detail",
"token",
"created_by",
"updated_by",
]
read_only_fields = fields
+224 -126
View File
@@ -17,17 +17,20 @@ from plane.db.models import (
IssueActivity,
IssueComment,
IssueProperty,
IssueBlocker,
IssueAssignee,
IssueSubscriber,
IssueLabel,
Label,
IssueBlocker,
CycleIssue,
Cycle,
Module,
ModuleIssue,
IssueLink,
IssueAttachment,
IssueReaction,
CommentReaction,
IssueVote,
IssueRelation,
)
@@ -46,9 +49,24 @@ class IssueFlatSerializer(BaseSerializer):
"target_date",
"sequence_id",
"sort_order",
"is_draft",
]
class IssueProjectLiteSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = Issue
fields = [
"id",
"project_detail",
"name",
"sequence_id",
]
read_only_fields = fields
##TODO: Find a better way to write this serializer
## Find a better approach to save manytomany?
class IssueCreateSerializer(BaseSerializer):
@@ -63,25 +81,12 @@ class IssueCreateSerializer(BaseSerializer):
required=False,
)
# List of issues that are blocking this issue
blockers_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
write_only=True,
required=False,
)
labels_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
# List of issues that are blocked by this issue
blocks_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = Issue
fields = "__all__"
@@ -94,30 +99,28 @@ class IssueCreateSerializer(BaseSerializer):
"updated_at",
]
def validate(self, data):
if (
data.get("start_date", None) is not None
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def create(self, validated_data):
blockers = validated_data.pop("blockers_list", None)
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
project = self.context["project"]
issue = Issue.objects.create(**validated_data, project=project)
project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"]
default_assignee_id = self.context["default_assignee_id"]
if blockers is not None and len(blockers):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=issue,
blocked_by=blocker,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
)
for blocker in blockers
],
batch_size=10,
)
issue = Issue.objects.create(**validated_data, project_id=project_id)
# Issue Audit Users
created_by_id = issue.created_by_id
updated_by_id = issue.updated_by_id
if assignees is not None and len(assignees):
IssueAssignee.objects.bulk_create(
@@ -125,10 +128,10 @@ class IssueCreateSerializer(BaseSerializer):
IssueAssignee(
assignee=user,
issue=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user in assignees
],
@@ -136,14 +139,14 @@ class IssueCreateSerializer(BaseSerializer):
)
else:
# Then assign it to default assignee
if project.default_assignee is not None:
if default_assignee_id is not None:
IssueAssignee.objects.create(
assignee=project.default_assignee,
assignee_id=default_assignee_id,
issue=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
if labels is not None and len(labels):
@@ -152,56 +155,27 @@ class IssueCreateSerializer(BaseSerializer):
IssueLabel(
label=label,
issue=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
)
if blocks is not None and len(blocks):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=block,
blocked_by=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
)
for block in blocks
],
batch_size=10,
)
return issue
def update(self, instance, validated_data):
blockers = validated_data.pop("blockers_list", None)
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
if blockers is not None:
IssueBlocker.objects.filter(block=instance).delete()
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=instance,
blocked_by=blocker,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
)
for blocker in blockers
],
batch_size=10,
)
# Related models
project_id = instance.project_id
workspace_id = instance.workspace_id
created_by_id = instance.created_by_id
updated_by_id = instance.updated_by_id
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete()
@@ -210,10 +184,10 @@ class IssueCreateSerializer(BaseSerializer):
IssueAssignee(
assignee=user,
issue=instance,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user in assignees
],
@@ -227,40 +201,25 @@ class IssueCreateSerializer(BaseSerializer):
IssueLabel(
label=label,
issue=instance,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
)
if blocks is not None:
IssueBlocker.objects.filter(blocked_by=instance).delete()
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=block,
blocked_by=instance,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
)
for block in blocks
],
batch_size=10,
)
# Time updation occues even when other related models are updated
instance.updated_at = timezone.now()
return super().update(instance, validated_data)
class IssueActivitySerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = IssueActivity
@@ -333,20 +292,39 @@ class IssueLabelSerializer(BaseSerializer):
]
class BlockedIssueSerializer(BaseSerializer):
blocked_issue_detail = IssueFlatSerializer(source="block", read_only=True)
class IssueRelationSerializer(BaseSerializer):
related_issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
class Meta:
model = IssueBlocker
fields = "__all__"
model = IssueRelation
fields = [
"related_issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
]
read_only_fields = [
"workspace",
"project",
]
class BlockerIssueSerializer(BaseSerializer):
blocker_issue_detail = IssueFlatSerializer(source="blocked_by", read_only=True)
class RelatedIssueSerializer(BaseSerializer):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
class Meta:
model = IssueBlocker
fields = "__all__"
model = IssueRelation
fields = [
"issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
]
read_only_fields = [
"workspace",
"project",
]
class IssueAssigneeSerializer(BaseSerializer):
@@ -459,11 +437,93 @@ class IssueAttachmentSerializer(BaseSerializer):
]
class IssueReactionSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
class Meta:
model = IssueReaction
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"actor",
]
class CommentReactionLiteSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
class Meta:
model = CommentReaction
fields = [
"id",
"reaction",
"comment",
"actor_detail",
]
class CommentReactionSerializer(BaseSerializer):
class Meta:
model = CommentReaction
fields = "__all__"
read_only_fields = ["workspace", "project", "comment", "actor"]
class IssueVoteSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
class Meta:
model = IssueVote
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
read_only_fields = fields
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
is_member = serializers.BooleanField(read_only=True)
class Meta:
model = IssueComment
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssueStateFlatSerializer(BaseSerializer):
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = Issue
fields = [
"id",
"sequence_id",
"name",
"state_detail",
"project_detail",
]
# Issue Serializer with state details
class IssueStateSerializer(BaseSerializer):
state_detail = StateSerializer(read_only=True, source="state")
project_detail = ProjectSerializer(read_only=True, source="project")
label_details = LabelSerializer(read_only=True, source="labels", many=True)
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
@@ -476,20 +536,19 @@ class IssueStateSerializer(BaseSerializer):
class IssueSerializer(BaseSerializer):
project_detail = ProjectSerializer(read_only=True, source="project")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateSerializer(read_only=True, source="state")
parent_detail = IssueFlatSerializer(read_only=True, source="parent")
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
label_details = LabelSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
# List of issues blocked by this issue
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
# List of issues that block this issue
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
issue_cycle = IssueCycleDetailSerializer(read_only=True)
issue_module = IssueModuleDetailSerializer(read_only=True)
issue_link = IssueLinkSerializer(read_only=True, many=True)
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
class Meta:
model = Issue
@@ -515,6 +574,7 @@ class IssueLiteSerializer(BaseSerializer):
module_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
class Meta:
model = Issue
@@ -530,3 +590,41 @@ class IssueLiteSerializer(BaseSerializer):
"created_at",
"updated_at",
]
class IssuePublicSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state")
reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
votes = IssueVoteSerializer(read_only=True, many=True)
class Meta:
model = Issue
fields = [
"id",
"name",
"description_html",
"sequence_id",
"state",
"state_detail",
"project",
"project_detail",
"workspace",
"priority",
"target_date",
"reactions",
"votes",
]
read_only_fields = fields
class IssueSubscriberSerializer(BaseSerializer):
class Meta:
model = IssueSubscriber
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
]
+7 -2
View File
@@ -40,6 +40,11 @@ class ModuleWriteSerializer(BaseSerializer):
"updated_at",
]
def validate(self, data):
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def create(self, validated_data):
members = validated_data.pop("members_list", None)
@@ -106,7 +111,7 @@ class ModuleFlatSerializer(BaseSerializer):
class ModuleIssueSerializer(BaseSerializer):
module_detail = ModuleFlatSerializer(read_only=True, source="module")
issue_detail = IssueStateSerializer(read_only=True, source="issue")
issue_detail = ProjectLiteSerializer(read_only=True, source="issue")
sub_issues_count = serializers.IntegerField(read_only=True)
class Meta:
@@ -151,7 +156,7 @@ class ModuleLinkSerializer(BaseSerializer):
class ModuleSerializer(BaseSerializer):
project_detail = ProjectSerializer(read_only=True, source="project")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
lead_detail = UserLiteSerializer(read_only=True, source="lead")
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
link_module = ModuleLinkSerializer(read_only=True, many=True)
@@ -0,0 +1,12 @@
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from plane.db.models import Notification
class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
class Meta:
model = Notification
fields = "__all__"
+9 -3
View File
@@ -3,7 +3,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Page, PageBlock, PageFavorite, PageLabel, Label
@@ -23,16 +23,22 @@ class PageBlockSerializer(BaseSerializer):
"page",
]
class PageBlockLiteSerializer(BaseSerializer):
class Meta:
model = PageBlock
fields = "__all__"
class PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelSerializer(read_only=True, source="labels", many=True)
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
labels_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
blocks = PageBlockSerializer(read_only=True, many=True)
blocks = PageBlockLiteSerializer(read_only=True, many=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
-57
View File
@@ -1,57 +0,0 @@
from rest_framework.serializers import (
ModelSerializer,
Serializer,
CharField,
SerializerMethodField,
)
from rest_framework.authtoken.models import Token
from rest_framework_simplejwt.tokens import RefreshToken
from plane.db.models import User
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = "__all__"
extra_kwargs = {"password": {"write_only": True}}
class ChangePasswordSerializer(Serializer):
model = User
"""
Serializer for password change endpoint.
"""
old_password = CharField(required=True)
new_password = CharField(required=True)
class ResetPasswordSerializer(Serializer):
model = User
"""
Serializer for password change endpoint.
"""
new_password = CharField(required=True)
confirm_password = CharField(required=True)
class TokenSerializer(ModelSerializer):
user = UserSerializer()
access_token = SerializerMethodField()
refresh_token = SerializerMethodField()
def get_access_token(self, obj):
refresh_token = RefreshToken.for_user(obj.user)
return str(refresh_token.access_token)
def get_refresh_token(self, obj):
refresh_token = RefreshToken.for_user(obj.user)
return str(refresh_token)
class Meta:
model = Token
fields = "__all__"
+67 -9
View File
@@ -7,13 +7,15 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
from plane.api.serializers.user import UserLiteSerializer
from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
Project,
ProjectMember,
ProjectMemberInvite,
ProjectIdentifier,
ProjectFavorite,
ProjectDeployBoard,
ProjectPublicMember,
)
@@ -77,6 +79,21 @@ class ProjectSerializer(BaseSerializer):
raise serializers.ValidationError(detail="Project Identifier is already taken")
class ProjectLiteSerializer(BaseSerializer):
class Meta:
model = Project
fields = [
"id",
"identifier",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
read_only_fields = fields
class ProjectDetailSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
default_assignee = UserLiteSerializer(read_only=True)
@@ -86,6 +103,9 @@ class ProjectDetailSerializer(BaseSerializer):
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
member_role = serializers.IntegerField(read_only=True)
is_deployed = serializers.BooleanField(read_only=True)
class Meta:
model = Project
@@ -93,8 +113,8 @@ class ProjectDetailSerializer(BaseSerializer):
class ProjectMemberSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
project = ProjectSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
project = ProjectLiteSerializer(read_only=True)
member = UserLiteSerializer(read_only=True)
class Meta:
@@ -102,9 +122,19 @@ class ProjectMemberSerializer(BaseSerializer):
fields = "__all__"
class ProjectMemberAdminSerializer(BaseSerializer):
workspace = WorkspaceLiteSerializer(read_only=True)
project = ProjectLiteSerializer(read_only=True)
member = UserAdminLiteSerializer(read_only=True)
class Meta:
model = ProjectMember
fields = "__all__"
class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectSerializer(read_only=True)
workspace = WorkSpaceSerializer(read_only=True)
project = ProjectLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
class Meta:
model = ProjectMemberInvite
@@ -118,7 +148,7 @@ class ProjectIdentifierSerializer(BaseSerializer):
class ProjectFavoriteSerializer(BaseSerializer):
project_detail = ProjectSerializer(source="project", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = ProjectFavorite
@@ -129,8 +159,36 @@ class ProjectFavoriteSerializer(BaseSerializer):
]
class ProjectLiteSerializer(BaseSerializer):
class ProjectMemberLiteSerializer(BaseSerializer):
member = UserLiteSerializer(read_only=True)
is_subscribed = serializers.BooleanField(read_only=True)
class Meta:
model = Project
fields = ["id", "identifier", "name"]
model = ProjectMember
fields = ["member", "id", "is_subscribed"]
read_only_fields = fields
class ProjectDeployBoardSerializer(BaseSerializer):
project_details = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = ProjectDeployBoard
fields = "__all__"
read_only_fields = [
"workspace",
"project", "anchor",
]
class ProjectPublicMemberSerializer(BaseSerializer):
class Meta:
model = ProjectPublicMember
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"member",
]
+43 -1
View File
@@ -1,3 +1,6 @@
# Third party imports
from rest_framework import serializers
# Module import
from .base import BaseSerializer
from plane.db.models import User
@@ -37,11 +40,50 @@ class UserLiteSerializer(BaseSerializer):
"id",
"first_name",
"last_name",
"email",
"avatar",
"is_bot",
"display_name",
]
read_only_fields = [
"id",
"is_bot",
]
class UserAdminLiteSerializer(BaseSerializer):
class Meta:
model = User
fields = [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
"email",
]
read_only_fields = [
"id",
"is_bot",
]
class ChangePasswordSerializer(serializers.Serializer):
model = User
"""
Serializer for password change endpoint.
"""
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)
class ResetPasswordSerializer(serializers.Serializer):
model = User
"""
Serializer for password change endpoint.
"""
new_password = serializers.CharField(required=True)
confirm_password = serializers.CharField(required=True)
+22 -13
View File
@@ -3,7 +3,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
User,
@@ -33,10 +33,30 @@ class WorkSpaceSerializer(BaseSerializer):
"owner",
]
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
class WorkSpaceMemberSerializer(BaseSerializer):
member = UserLiteSerializer(read_only=True)
workspace = WorkSpaceSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
class Meta:
model = WorkspaceMember
fields = "__all__"
class WorkspaceMemberAdminSerializer(BaseSerializer):
member = UserAdminLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
class Meta:
model = WorkspaceMember
@@ -101,17 +121,6 @@ class TeamSerializer(BaseSerializer):
return super().update(instance, validated_data)
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
+428 -5
View File
@@ -22,6 +22,7 @@ from plane.api.views import (
# User
UserEndpoint,
UpdateUserOnBoardedEndpoint,
UpdateUserTourCompletedEndpoint,
UserActivityEndpoint,
## End User
# Workspaces
@@ -31,6 +32,7 @@ from plane.api.views import (
InviteWorkspaceEndpoint,
JoinWorkspaceEndpoint,
WorkSpaceMemberViewSet,
WorkspaceMembersEndpoint,
WorkspaceInvitationsViewset,
UserWorkspaceInvitationsEndpoint,
WorkspaceMemberUserEndpoint,
@@ -44,6 +46,12 @@ from plane.api.views import (
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,
WorkspaceUserProfileStatsEndpoint,
WorkspaceUserActivityEndpoint,
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
LeaveWorkspaceEndpoint,
## End Workspaces
# File Assets
FileAssetEndpoint,
@@ -53,6 +61,7 @@ from plane.api.views import (
ProjectViewSet,
InviteProjectEndpoint,
ProjectMemberViewSet,
ProjectMemberEndpoint,
ProjectMemberInvitationsViewset,
ProjectMemberUserEndpoint,
AddMemberToProjectEndpoint,
@@ -60,6 +69,7 @@ from plane.api.views import (
UserProjectInvitationsViewset,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
LeaveProjectEndpoint,
## End Projects
# Issues
IssueViewSet,
@@ -76,6 +86,13 @@ from plane.api.views import (
IssueLinkViewSet,
BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint,
IssueArchiveViewSet,
IssueSubscriberViewSet,
IssueCommentPublicViewSet,
IssueReactionViewSet,
IssueRelationViewSet,
CommentReactionViewSet,
IssueDraftViewSet,
## End Issues
# States
StateViewSet,
@@ -148,6 +165,26 @@ from plane.api.views import (
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
## End Analytics
# Notification
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
## End Notification
# Public Boards
ProjectDeployBoardViewSet,
ProjectIssuesPublicEndpoint,
ProjectDeployBoardPublicSettingsEndpoint,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
InboxIssuePublicViewSet,
IssueVotePublicViewSet,
WorkspaceProjectDeployBoardEndpoint,
IssueRetrievePublicEndpoint,
## End Public Boards
## Exporter
ExportIssuesEndpoint,
## End Exporter
)
@@ -197,9 +234,14 @@ urlpatterns = [
path(
"users/me/onboard/",
UpdateUserOnBoardedEndpoint.as_view(),
name="change-password",
name="user-onboard",
),
path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
path(
"users/me/tour-completed/",
UpdateUserTourCompletedEndpoint.as_view(),
name="user-tour",
),
path("users/workspaces/<str:slug>/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
# user workspaces
path(
"users/me/workspaces/",
@@ -295,7 +337,6 @@ urlpatterns = [
{
"delete": "destroy",
"get": "retrieve",
"get": "retrieve",
}
),
name="workspace",
@@ -316,6 +357,11 @@ urlpatterns = [
),
name="workspace",
),
path(
"workspaces/<str:slug>/workspace-members/",
WorkspaceMembersEndpoint.as_view(),
name="workspace-members",
),
path(
"workspaces/<str:slug>/teams/",
TeamMemberViewSet.as_view(
@@ -374,6 +420,36 @@ urlpatterns = [
),
name="workspace-themes",
),
path(
"workspaces/<str:slug>/user-stats/<uuid:user_id>/",
WorkspaceUserProfileStatsEndpoint.as_view(),
name="workspace-user-stats",
),
path(
"workspaces/<str:slug>/user-activity/<uuid:user_id>/",
WorkspaceUserActivityEndpoint.as_view(),
name="workspace-user-activity",
),
path(
"workspaces/<str:slug>/user-profile/<uuid:user_id>/",
WorkspaceUserProfileEndpoint.as_view(),
name="workspace-user-profile-page",
),
path(
"workspaces/<str:slug>/user-issues/<uuid:user_id>/",
WorkspaceUserProfileIssuesEndpoint.as_view(),
name="workspace-user-profile-issues",
),
path(
"workspaces/<str:slug>/labels/",
WorkspaceLabelsEndpoint.as_view(),
name="workspace-labels",
),
path(
"workspaces/<str:slug>/members/leave/",
LeaveWorkspaceEndpoint.as_view(),
name="workspace-labels",
),
## End Workspaces ##
# Projects
path(
@@ -424,6 +500,11 @@ urlpatterns = [
),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/",
ProjectMemberEndpoint.as_view(),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/add/",
AddMemberToProjectEndpoint.as_view(),
@@ -468,7 +549,6 @@ urlpatterns = [
"workspaces/<str:slug>/user-favorite-projects/",
ProjectFavoritesViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
@@ -483,6 +563,11 @@ urlpatterns = [
),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
LeaveProjectEndpoint.as_view(),
name="project",
),
# End Projects
# States
path(
@@ -766,6 +851,11 @@ urlpatterns = [
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/export-issues/",
ExportIssuesEndpoint.as_view(),
name="export-issues",
),
## End Issues
## Issue Activity
path(
@@ -798,6 +888,76 @@ urlpatterns = [
name="project-issue-comment",
),
## End IssueComments
# Issue Subscribers
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
IssueSubscriberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/<uuid:subscriber_id>/",
IssueSubscriberViewSet.as_view({"delete": "destroy"}),
name="project-issue-subscribers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view(
{
"get": "subscription_status",
"post": "subscribe",
"delete": "unsubscribe",
}
),
name="project-issue-subscribers",
),
## End Issue Subscribers
# Issue Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-issue-reactions",
),
## End Issue Reactions
# Comment Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-comment-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-issue-comment-reactions",
),
## End Comment Reactions
## IssueProperty
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
@@ -822,6 +982,79 @@ urlpatterns = [
name="project-issue-roadmap",
),
## IssueProperty Ebd
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
IssueArchiveViewSet.as_view(
{
"get": "list",
}
),
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/<uuid:pk>/",
IssueArchiveViewSet.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/unarchive/<uuid:pk>/",
IssueArchiveViewSet.as_view(
{
"post": "unarchive",
}
),
name="project-issue-archive",
),
## End Issue Archives
## Issue Relation
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view(
{
"post": "create",
}
),
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
IssueRelationViewSet.as_view(
{
"delete": "destroy",
}
),
name="issue-relation",
),
## End Issue Relation
## Issue Drafts
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
IssueDraftViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
IssueDraftViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-draft",
),
## End Issue Drafts
## File Assets
path(
"workspaces/<str:slug>/file-assets/",
@@ -1172,7 +1405,7 @@ urlpatterns = [
## End Importer
# Search
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/search/",
"workspaces/<str:slug>/search/",
GlobalSearchEndpoint.as_view(),
name="global-search",
),
@@ -1274,4 +1507,194 @@ urlpatterns = [
name="default-analytics",
),
## End Analytics
# Notification
path(
"workspaces/<str:slug>/users/notifications/",
NotificationViewSet.as_view(
{
"get": "list",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
NotificationViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/read/",
NotificationViewSet.as_view(
{
"post": "mark_read",
"delete": "mark_unread",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/archive/",
NotificationViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/unread/",
UnreadNotificationEndpoint.as_view(),
name="unread-notifications",
),
path(
"workspaces/<str:slug>/users/notifications/mark-all-read/",
MarkAllReadNotificationViewSet.as_view(
{
"post": "create",
}
),
name="mark-all-read-notifications",
),
## End Notification
# Public Boards
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
ProjectDeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
ProjectDeployBoardViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/settings/",
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
name="project-deploy-board-settings",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
ProjectIssuesPublicEndpoint.as_view(),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/",
IssueRetrievePublicEndpoint.as_view(),
name="workspace-project-boards",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-comments-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
IssueCommentPublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="issue-comments-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionPublicViewSet.as_view(
{
"delete": "destroy",
}
),
name="issue-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="comment-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionPublicViewSet.as_view(
{
"delete": "destroy",
}
),
name="comment-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
InboxIssuePublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
InboxIssuePublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/votes/",
IssueVotePublicViewSet.as_view(
{
"get": "list",
"post": "create",
"delete": "destroy",
}
),
name="issue-vote-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/",
WorkspaceProjectDeployBoardEndpoint.as_view(),
name="workspace-project-boards",
),
## End Public Boards
]
+32 -2
View File
@@ -12,10 +12,16 @@ from .project import (
ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint,
ProjectFavoritesViewSet,
ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint,
ProjectMemberEndpoint,
WorkspaceProjectDeployBoardEndpoint,
LeaveProjectEndpoint,
)
from .people import (
from .user import (
UserEndpoint,
UpdateUserOnBoardedEndpoint,
UpdateUserTourCompletedEndpoint,
UserActivityEndpoint,
)
@@ -41,6 +47,13 @@ from .workspace import (
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,
WorkspaceUserProfileStatsEndpoint,
WorkspaceUserActivityEndpoint,
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceMembersEndpoint,
LeaveWorkspaceEndpoint,
)
from .state import StateViewSet
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
@@ -65,6 +78,18 @@ from .issue import (
IssueLinkViewSet,
BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint,
IssueArchiveViewSet,
IssueSubscriberViewSet,
IssueCommentPublicViewSet,
CommentReactionViewSet,
IssueReactionViewSet,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
IssueVotePublicViewSet,
IssueRelationViewSet,
IssueRetrievePublicEndpoint,
ProjectIssuesPublicEndpoint,
IssueDraftViewSet,
)
from .auth_extended import (
@@ -132,7 +157,8 @@ from .estimate import (
from .release import ReleaseNotesEndpoint
from .inbox import InboxViewSet, InboxIssueViewSet
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
from .analytic import (
AnalyticsEndpoint,
AnalyticViewViewset,
@@ -140,3 +166,7 @@ from .analytic import (
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
)
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
from .exporter import ExportIssuesEndpoint
+5 -5
View File
@@ -79,12 +79,12 @@ class AnalyticsEndpoint(BaseAPIView):
)
assignee_details = {}
if x_axis in ["assignees__email"] or segment in ["assignees__email"]:
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
assignee_details = (
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
.order_by("assignees__id")
.distinct("assignees__id")
.values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name")
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id")
)
@@ -243,21 +243,21 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
)
most_issue_created_user = (
queryset.exclude(created_by=None)
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__email")
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name", "created_by__id")
.annotate(count=Count("id"))
.order_by("-count")
)[:5]
most_issue_closed_user = (
queryset.filter(completed_at__isnull=False, assignees__isnull=False)
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__email")
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id")
.annotate(count=Count("id"))
.order_by("-count")
)[:5]
pending_issue_user = (
queryset.filter(completed_at__isnull=True)
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__email")
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id")
.annotate(count=Count("id"))
.order_by("-count")
)
+24 -8
View File
@@ -18,10 +18,21 @@ class FileAssetEndpoint(BaseAPIView):
"""
def get(self, request, workspace_id, asset_key):
asset_key = str(workspace_id) + "/" + asset_key
files = FileAsset.objects.filter(asset=asset_key)
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
return Response(serializer.data)
try:
asset_key = str(workspace_id) + "/" + asset_key
files = FileAsset.objects.filter(asset=asset_key)
if files.exists():
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
else:
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def post(self, request, slug):
try:
@@ -68,11 +79,16 @@ class UserAssetsEndpoint(BaseAPIView):
def get(self, request, asset_key):
try:
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
serializer = FileAssetSerializer(files, context={"request": request})
return Response(serializer.data)
except FileAsset.DoesNotExist:
if files.exists():
serializer = FileAssetSerializer(files, context={"request": request})
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
else:
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "File Asset does not exist"}, status=status.HTTP_404_NOT_FOUND
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def post(self, request):
+1 -1
View File
@@ -22,7 +22,7 @@ from sentry_sdk import capture_exception
## Module imports
from . import BaseAPIView
from plane.api.serializers.people import (
from plane.api.serializers import (
ChangePasswordSerializer,
ResetPasswordSerializer,
)
+4 -2
View File
@@ -279,6 +279,8 @@ class MagicSignInGenerateEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
# Clean up
email = email.strip().lower()
validate_email(email)
## Generate a random token
@@ -345,8 +347,8 @@ class MagicSignInEndpoint(BaseAPIView):
def post(self, request):
try:
user_token = request.data.get("token", "").strip().lower()
key = request.data.get("key", False)
user_token = request.data.get("token", "").strip()
key = request.data.get("key", False).strip().lower()
if not key or user_token == "":
return Response(
+22 -5
View File
@@ -1,24 +1,41 @@
# Python imports
import zoneinfo
# Django imports
from django.urls import resolve
from django.conf import settings
from django.utils import timezone
# Third part imports
from rest_framework import status
from rest_framework.viewsets import ModelViewSet
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 rest_framework.exceptions import NotFound
from sentry_sdk import capture_exception
from django_filters.rest_framework import DjangoFilterBackend
# Module imports
from plane.db.models import Workspace, Project
from plane.utils.paginator import BasePaginator
class BaseViewSet(ModelViewSet, BasePaginator):
class TimezoneMixin:
"""
This enables timezone conversion according
to the user set timezone
"""
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
if request.user.is_authenticated:
timezone.activate(zoneinfo.ZoneInfo(request.user.user_timezone))
else:
timezone.deactivate()
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
model = None
@@ -67,7 +84,7 @@ class BaseViewSet(ModelViewSet, BasePaginator):
return self.kwargs.get("pk", None)
class BaseAPIView(APIView, BasePaginator):
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
permission_classes = [
IsAuthenticated,
+35 -19
View File
@@ -31,6 +31,7 @@ from plane.api.serializers import (
CycleIssueSerializer,
CycleFavoriteSerializer,
IssueStateSerializer,
CycleWriteSerializer,
)
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
@@ -164,6 +165,9 @@ class CycleViewSet(BaseViewSet):
try:
queryset = self.get_queryset()
cycle_view = request.GET.get("cycle_view", "all")
order_by = request.GET.get("order_by", "sort_order")
queryset = queryset.order_by(order_by)
# All Cycles
if cycle_view == "all":
@@ -187,11 +191,10 @@ class CycleViewSet(BaseViewSet):
workspace__slug=slug,
project_id=project_id,
)
.annotate(first_name=F("assignees__first_name"))
.annotate(last_name=F("assignees__last_name"))
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("first_name", "last_name", "assignee_id", "avatar")
.values("display_name", "assignee_id", "avatar")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
@@ -205,7 +208,7 @@ class CycleViewSet(BaseViewSet):
filter=Q(completed_at__isnull=True),
)
)
.order_by("first_name", "last_name")
.order_by("display_name")
)
label_distribution = (
@@ -330,15 +333,23 @@ class CycleViewSet(BaseViewSet):
workspace__slug=slug, project_id=project_id, pk=pk
)
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
return Response(
{
"error": "The Cycle has already been completed so it cannot be edited"
},
status=status.HTTP_400_BAD_REQUEST,
)
request_data = request.data
serializer = CycleSerializer(cycle, data=request.data, partial=True)
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
if "sort_order" in request_data:
# Can only change sort order
request_data = {
"sort_order": request_data.get("sort_order", cycle.sort_order)
}
else:
return Response(
{
"error": "The Cycle has already been completed so it cannot be edited"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -369,7 +380,10 @@ class CycleViewSet(BaseViewSet):
.annotate(last_name=F("assignees__last_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("first_name", "last_name", "assignee_id", "avatar")
.annotate(display_name=F("assignees__display_name"))
.values(
"first_name", "last_name", "assignee_id", "avatar", "display_name"
)
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
@@ -503,6 +517,7 @@ class CycleIssueViewSet(BaseViewSet):
try:
order_by = request.GET.get("order_by", "created_at")
group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
@@ -541,9 +556,15 @@ class CycleIssueViewSet(BaseViewSet):
issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by:
return Response(
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
return Response(
group_results(issues_data, group_by),
group_results(issues_data, group_by, sub_group_by),
status=status.HTTP_200_OK,
)
@@ -691,7 +712,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
return Response(
{
"error": "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
"cycles": CycleSerializer(cycles, many=True).data,
"status": False,
}
)
@@ -706,10 +726,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
class CycleFavoriteViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = CycleFavoriteSerializer
model = CycleFavorite
+100
View File
@@ -0,0 +1,100 @@
# Third Party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from . import BaseAPIView
from plane.api.permissions import WorkSpaceAdminPermission
from plane.bgtasks.export_task import issue_export_task
from plane.db.models import Project, ExporterHistory, Workspace
from plane.api.serializers import ExporterHistorySerializer
class ExportIssuesEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
model = ExporterHistory
serializer_class = ExporterHistorySerializer
def post(self, request, slug):
try:
# Get the workspace
workspace = Workspace.objects.get(slug=slug)
provider = request.data.get("provider", False)
multiple = request.data.get("multiple", False)
project_ids = request.data.get("project", [])
if provider in ["csv", "xlsx", "json"]:
if not project_ids:
project_ids = Project.objects.filter(
workspace__slug=slug
).values_list("id", flat=True)
project_ids = [str(project_id) for project_id in project_ids]
exporter = ExporterHistory.objects.create(
workspace=workspace,
project=project_ids,
initiated_by=request.user,
provider=provider,
)
issue_export_task.delay(
provider=exporter.provider,
workspace_id=workspace.id,
project_ids=project_ids,
token_id=exporter.token,
multiple=multiple,
slug=slug,
)
return Response(
{
"message": f"Once the export is ready you will be able to download it"
},
status=status.HTTP_200_OK,
)
else:
return Response(
{"error": f"Provider '{provider}' not found."},
status=status.HTTP_400_BAD_REQUEST,
)
except Workspace.DoesNotExist:
return Response(
{"error": "Workspace does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def get(self, request, slug):
try:
exporter_history = ExporterHistory.objects.filter(
workspace__slug=slug
).select_related("workspace","initiated_by")
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
return self.paginate(
request=request,
queryset=exporter_history,
on_results=lambda exporter_history: ExporterHistorySerializer(
exporter_history, many=True
).data,
)
else:
return Response(
{"error": "per_page and cursor are required"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+4 -30
View File
@@ -30,31 +30,6 @@ class GPTIntegrationEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
count = 0
# If logger is enabled check for request limit
if settings.LOGGER_BASE_URL:
try:
headers = {
"Content-Type": "application/json",
}
response = requests.post(
settings.LOGGER_BASE_URL,
json={"user_id": str(request.user.id)},
headers=headers,
)
count = response.json().get("count", 0)
if not response.json().get("success", False):
return Response(
{
"error": "You have surpassed the monthly limit for AI assistance"
},
status=status.HTTP_429_TOO_MANY_REQUESTS,
)
except Exception as e:
capture_exception(e)
prompt = request.data.get("prompt", False)
task = request.data.get("task", False)
@@ -66,9 +41,9 @@ class GPTIntegrationEndpoint(BaseAPIView):
final_text = task + "\n" + prompt
openai.api_key = settings.OPENAI_API_KEY
response = openai.Completion.create(
engine=settings.GPT_ENGINE,
prompt=final_text,
response = openai.ChatCompletion.create(
model=settings.GPT_ENGINE,
messages=[{"role": "user", "content": final_text}],
temperature=0.7,
max_tokens=1024,
)
@@ -76,13 +51,12 @@ class GPTIntegrationEndpoint(BaseAPIView):
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=project_id)
text = response.choices[0].text.strip()
text = response.choices[0].message.content.strip()
text_html = text.replace("\n", "<br/>")
return Response(
{
"response": text,
"response_html": text_html,
"count": count,
"project_detail": ProjectLiteSerializer(project).data,
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
},
+2 -2
View File
@@ -332,7 +332,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
# if there is no default state assign any random state
if default_state is None:
default_state = State.objects.filter(
~Q(name="Triage"), sproject_id=project_id
~Q(name="Triage"), project_id=project_id
).first()
# Get the maximum sequence_id
@@ -458,7 +458,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
actor=request.user,
project_id=project_id,
workspace_id=project.workspace_id,
comment=f"{request.user.email} importer the issue from {service}",
comment=f"imported the issue from {service}",
verb="created",
created_by=request.user,
)
+267 -2
View File
@@ -15,7 +15,6 @@ from sentry_sdk import capture_exception
from .base import BaseViewSet
from plane.api.permissions import ProjectBasePermission, ProjectLitePermission
from plane.db.models import (
Project,
Inbox,
InboxIssue,
Issue,
@@ -23,6 +22,7 @@ from plane.db.models import (
IssueLink,
IssueAttachment,
ProjectMember,
ProjectDeployBoard,
)
from plane.api.serializers import (
IssueSerializer,
@@ -377,4 +377,269 @@ class InboxIssueViewSet(BaseViewSet):
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
)
class InboxIssuePublicViewSet(BaseViewSet):
serializer_class = InboxIssueSerializer
model = InboxIssue
filterset_fields = [
"status",
]
def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"))
if project_deploy_board is not None:
return self.filter_queryset(
super()
.get_queryset()
.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
inbox_id=self.kwargs.get("inbox_id"),
)
.select_related("issue", "workspace", "project")
)
else:
return InboxIssue.objects.none()
def list(self, request, slug, project_id, inbox_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.objects.filter(
issue_inbox__inbox_id=inbox_id,
workspace__slug=slug,
project_id=project_id,
)
.filter(**filters)
.annotate(bridge_id=F("issue_inbox__id"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.prefetch_related(
Prefetch(
"issue_inbox",
queryset=InboxIssue.objects.only(
"status", "duplicate_to", "snoozed_till", "source"
),
)
)
)
issues_data = IssueStateInboxSerializer(issues, many=True).data
return Response(
issues_data,
status=status.HTTP_200_OK,
)
except ProjectDeployBoard.DoesNotExist:
return Response({"error": "Project Deploy Board does not exist"}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, slug, project_id, inbox_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
if not request.data.get("issue", {}).get("name", False):
return Response(
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
# Check for valid priority
if not request.data.get("issue", {}).get("priority", None) in [
"low",
"medium",
"high",
"urgent",
None,
]:
return Response(
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
)
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="backlog",
description="Default state for managing all Inbox Issues",
project_id=project_id,
color="#ff7700",
)
# 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,
)
# 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,
)
# create an inbox issue
InboxIssue.objects.create(
inbox_id=inbox_id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "in-app"),
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
if str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
# Get issue data
issue_data = request.data.pop("issue", False)
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
# viewers and guests since only viewers and guests
issue_data = {
"name": issue_data.get("name", issue.name),
"description_html": issue_data.get("description_html", issue.description_html),
"description": issue_data.get("description", issue.description)
}
issue_serializer = IssueCreateSerializer(
issue, data=issue_data, partial=True
)
if issue_serializer.is_valid():
current_instance = issue
# Log all the updates
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
if issue is not None:
issue_activity.delay(
type="issue.activity.updated",
requested_data=requested_data,
actor_id=str(request.user.id),
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=json.dumps(
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
issue_serializer.save()
return Response(issue_serializer.data, status=status.HTTP_200_OK)
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except InboxIssue.DoesNotExist:
return Response(
{"error": "Inbox Issue does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def retrieve(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
if str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except InboxIssue.DoesNotExist:
return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+15 -1
View File
@@ -20,6 +20,17 @@ class SlackProjectSyncViewSet(BaseViewSet):
serializer_class = SlackProjectSyncSerializer
model = SlackProjectSync
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
)
.filter(project__project_projectmember__member=self.request.user)
)
def create(self, request, slug, project_id, workspace_integration_id):
try:
serializer = SlackProjectSyncSerializer(data=request.data)
@@ -45,7 +56,10 @@ class SlackProjectSyncViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response({"error": "Slack is already enabled for the project"}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{"error": "Slack is already enabled for the project"},
status=status.HTTP_400_BAD_REQUEST,
)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration does not exist"},
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -53,6 +53,8 @@ class ModuleViewSet(BaseViewSet):
)
def get_queryset(self):
order_by = self.request.GET.get("order_by", "sort_order")
subquery = ModuleFavorite.objects.filter(
user=self.request.user,
module_id=OuterRef("pk"),
@@ -106,7 +108,7 @@ class ModuleViewSet(BaseViewSet):
filter=Q(issue_module__issue__state__group="backlog"),
)
)
.order_by("-is_favorite", "name")
.order_by(order_by, "name")
)
def perform_destroy(self, instance):
@@ -173,8 +175,9 @@ class ModuleViewSet(BaseViewSet):
.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")
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
@@ -305,6 +308,7 @@ class ModuleIssueViewSet(BaseViewSet):
try:
order_by = request.GET.get("order_by", "created_at")
group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.issue_objects.filter(issue_module__module_id=module_id)
@@ -343,9 +347,15 @@ class ModuleIssueViewSet(BaseViewSet):
issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by:
return Response(
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
return Response(
group_results(issues_data, group_by),
group_results(issues_data, group_by, sub_group_by),
status=status.HTTP_200_OK,
)
@@ -480,9 +490,6 @@ class ModuleLinkViewSet(BaseViewSet):
class ModuleFavoriteViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite
+363
View File
@@ -0,0 +1,363 @@
# Django imports
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
from sentry_sdk import capture_exception
from plane.utils.paginator import BasePaginator
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.db.models import (
Notification,
IssueAssignee,
IssueSubscriber,
Issue,
WorkspaceMember,
)
from plane.api.serializers import NotificationSerializer
class NotificationViewSet(BaseViewSet, BasePaginator):
model = Notification
serializer_class = NotificationSerializer
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
workspace__slug=self.kwargs.get("slug"),
receiver_id=self.request.user.id,
)
.select_related("workspace", "project," "triggered_by", "receiver")
)
def list(self, request, slug):
try:
snoozed = request.GET.get("snoozed", "false")
archived = request.GET.get("archived", "false")
read = request.GET.get("read", "true")
# Filter type
type = request.GET.get("type", "all")
notifications = (
Notification.objects.filter(
workspace__slug=slug, receiver_id=request.user.id
)
.select_related("workspace", "project", "triggered_by", "receiver")
.order_by("snoozed_till", "-created_at")
)
# Filter for snoozed notifications
if snoozed == "false":
notifications = notifications.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
)
if snoozed == "true":
notifications = notifications.filter(
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
)
if read == "false":
notifications = notifications.filter(read_at__isnull=True)
# Filter for archived or unarchive
if archived == "false":
notifications = notifications.filter(archived_at__isnull=True)
if archived == "true":
notifications = notifications.filter(archived_at__isnull=False)
# Subscribed issues
if type == "watching":
issue_ids = IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
# Assigned Issues
if type == "assigned":
issue_ids = IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
# Created issues
if type == "created":
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role__lt=15
).exists():
notifications = Notification.objects.none()
else:
issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True)
notifications = notifications.filter(
entity_identifier__in=issue_ids
)
# Pagination
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
return self.paginate(
request=request,
queryset=(notifications),
on_results=lambda notifications: NotificationSerializer(
notifications, many=True
).data,
)
serializer = NotificationSerializer(notifications, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, pk):
try:
notification = Notification.objects.get(
workspace__slug=slug, pk=pk, receiver=request.user
)
# Only read_at and snoozed_till can be updated
notification_data = {
"snoozed_till": request.data.get("snoozed_till", None),
}
serializer = NotificationSerializer(
notification, data=notification_data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Notification.DoesNotExist:
return Response(
{"error": "Notification does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def mark_read(self, request, slug, pk):
try:
notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.read_at = timezone.now()
notification.save()
serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def mark_unread(self, request, slug, pk):
try:
notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.read_at = None
notification.save()
serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def archive(self, request, slug, pk):
try:
notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.archived_at = timezone.now()
notification.save()
serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def unarchive(self, request, slug, pk):
try:
notification = Notification.objects.get(
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.archived_at = None
notification.save()
serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK)
except Notification.DoesNotExist:
return Response(
{"error": "Notification does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UnreadNotificationEndpoint(BaseAPIView):
def get(self, request, slug):
try:
# Watching Issues Count
watching_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
entity_identifier__in=IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True),
).count()
# My Issues Count
my_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
entity_identifier__in=IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True),
).count()
# Created Issues Count
created_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
entity_identifier__in=Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True),
).count()
return Response(
{
"watching_issues": watching_issues_count,
"my_issues": my_issues_count,
"created_issues": created_issues_count,
},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class MarkAllReadNotificationViewSet(BaseViewSet):
def create(self, request, slug):
try:
snoozed = request.data.get("snoozed", False)
archived = request.data.get("archived", False)
type = request.data.get("type", "all")
notifications = (
Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
)
.select_related("workspace", "project", "triggered_by", "receiver")
.order_by("snoozed_till", "-created_at")
)
# Filter for snoozed notifications
if snoozed:
notifications = notifications.filter(
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
)
else:
notifications = notifications.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
)
# Filter for archived or unarchive
if archived:
notifications = notifications.filter(archived_at__isnull=False)
else:
notifications = notifications.filter(archived_at__isnull=True)
# Subscribed issues
if type == "watching":
issue_ids = IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
# Assigned Issues
if type == "assigned":
issue_ids = IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
# Created issues
if type == "created":
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role__lt=15
).exists():
notifications = Notification.objects.none()
else:
issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True)
notifications = notifications.filter(
entity_identifier__in=issue_ids
)
updated_notifications = []
for notification in notifications:
notification.read_at = timezone.now()
updated_notifications.append(notification)
Notification.objects.bulk_update(
updated_notifications, ["read_at"], batch_size=100
)
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+1 -1
View File
@@ -301,7 +301,7 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView):
issue=issue,
actor=request.user,
project_id=project_id,
comment=f"{request.user.email} created the issue from {page_block.name} block",
comment=f"created the issue from {page_block.name} block",
verb="created",
)
+338 -48
View File
@@ -5,7 +5,15 @@ from datetime import datetime
# Django imports
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.db.models import Q, Exists, OuterRef, Func, F
from django.db.models import (
Q,
Exists,
OuterRef,
Func,
F,
Func,
Subquery,
)
from django.core.validators import validate_email
from django.conf import settings
@@ -13,6 +21,7 @@ from django.conf import settings
from rest_framework.response import Response
from rest_framework import status
from rest_framework import serializers
from rest_framework.permissions import AllowAny
from sentry_sdk import capture_exception
# Module imports
@@ -23,9 +32,17 @@ from plane.api.serializers import (
ProjectDetailSerializer,
ProjectMemberInviteSerializer,
ProjectFavoriteSerializer,
IssueLiteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
)
from plane.api.permissions import ProjectBasePermission
from plane.api.permissions import (
ProjectBasePermission,
ProjectEntityPermission,
ProjectMemberPermission,
ProjectLitePermission,
)
from plane.db.models import (
Project,
@@ -48,6 +65,7 @@ from plane.db.models import (
IssueAssignee,
ModuleMember,
Inbox,
ProjectDeployBoard,
)
from plane.bgtasks.project_invitation_task import project_invitation
@@ -91,20 +109,61 @@ class ProjectViewSet(BaseViewSet):
)
)
)
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_cycles=Cycle.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_modules=Module.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
member_role=ProjectMember.objects.filter(
project_id=OuterRef("pk"),
member_id=self.request.user.id,
).values("role")
)
.annotate(
is_deployed=Exists(
ProjectDeployBoard.objects.filter(
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
)
)
.distinct()
)
def list(self, request, slug):
try:
is_favorite = request.GET.get("is_favorite", "all")
subquery = ProjectFavorite.objects.filter(
user=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
sort_order_query = ProjectMember.objects.filter(
member=request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
).values("sort_order")
projects = (
self.get_queryset()
.annotate(is_favorite=Exists(subquery))
.order_by("-is_favorite", "name")
.annotate(sort_order=Subquery(sort_order_query))
.order_by("sort_order", "name")
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id")
@@ -126,6 +185,12 @@ class ProjectViewSet(BaseViewSet):
.values("count")
)
)
if is_favorite == "true":
projects = projects.filter(is_favorite=True)
if is_favorite == "false":
projects = projects.filter(is_favorite=False)
return Response(ProjectDetailSerializer(projects, many=True).data)
except Exception as e:
capture_exception(e)
@@ -145,40 +210,49 @@ class ProjectViewSet(BaseViewSet):
serializer.save()
# Add the user as Administrator to the project
ProjectMember.objects.create(
project_member = ProjectMember.objects.create(
project_id=serializer.data["id"], member=request.user, role=20
)
if serializer.data["project_lead"] is not None and str(
serializer.data["project_lead"]
) != str(request.user.id):
ProjectMember.objects.create(
project_id=serializer.data["id"],
member_id=serializer.data["project_lead"],
role=20,
)
# Default states
states = [
{
"name": "Backlog",
"color": "#5e6ad2",
"color": "#A3A3A3",
"sequence": 15000,
"group": "backlog",
"default": True,
},
{
"name": "Todo",
"color": "#eb5757",
"color": "#3A3A3A",
"sequence": 25000,
"group": "unstarted",
},
{
"name": "In Progress",
"color": "#26b5ce",
"color": "#F59E0B",
"sequence": 35000,
"group": "started",
},
{
"name": "Done",
"color": "#f2c94c",
"color": "#16A34A",
"sequence": 45000,
"group": "completed",
},
{
"name": "Cancelled",
"color": "#4cb782",
"color": "#EF4444",
"sequence": 55000,
"group": "cancelled",
},
@@ -200,9 +274,14 @@ class ProjectViewSet(BaseViewSet):
]
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
data = serializer.data
# Additional fields of the member
data["sort_order"] = project_member.sort_order
data["member_role"] = project_member.role
data["is_member"] = True
return Response(data, status=status.HTTP_201_CREATED)
return Response(
[serializer.errors[error][0] for error in serializer.errors],
serializer.errors,
status=status.HTTP_400_BAD_REQUEST,
)
except IntegrityError as e:
@@ -259,7 +338,7 @@ class ProjectViewSet(BaseViewSet):
group="backlog",
description="Default state for managing all Inbox Issues",
project_id=pk,
color="#ff7700"
color="#ff7700",
)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -307,7 +386,9 @@ class InviteProjectEndpoint(BaseAPIView):
validate_email(email)
# Check if user is already a member of workspace
if ProjectMember.objects.filter(
project_id=project_id, member__email=email
project_id=project_id,
member__email=email,
member__is_bot=False,
).exists():
return Response(
{"error": "User is already member of workspace"},
@@ -401,7 +482,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
# Delete joined project invites
project_invitations.delete()
return Response(status=status.HTTP_200_OK)
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
@@ -411,14 +492,14 @@ class UserProjectInvitationsViewset(BaseViewSet):
class ProjectMemberViewSet(BaseViewSet):
serializer_class = ProjectMemberSerializer
serializer_class = ProjectMemberAdminSerializer
model = ProjectMember
permission_classes = [
ProjectBasePermission,
]
search_fields = [
"member__email",
"member__display_name",
"member__first_name",
]
@@ -536,7 +617,7 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
except ProjectMember.DoesNotExist:
return Response(
{"error": "Project Member does not exist"}, status=status.HTTP_400
{"error": "Project Member does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
capture_exception(e)
@@ -550,45 +631,66 @@ class AddMemberToProjectEndpoint(BaseAPIView):
def post(self, request, slug, project_id):
try:
member_id = request.data.get("member_id", False)
role = request.data.get("role", False)
members = request.data.get("members", [])
if not member_id or not role:
# get the project
project = Project.objects.get(pk=project_id, workspace__slug=slug)
if not len(members):
return Response(
{"error": "Member ID and role is required"},
{"error": "Atleast one member is required"},
status=status.HTTP_400_BAD_REQUEST,
)
bulk_project_members = []
# Check if the user is a member in the workspace
if not WorkspaceMember.objects.filter(
workspace__slug=slug, member_id=member_id
).exists():
# TODO: Update this error message - nk
return Response(
{
"error": "User is not a member of the workspace. Invite the user to the workspace to add him to project"
},
status=status.HTTP_400_BAD_REQUEST,
project_members = (
ProjectMember.objects.filter(
workspace__slug=slug,
member_id__in=[member.get("member_id") for member in members],
)
# Check if the user is already member of project
if ProjectMember.objects.filter(
project=project_id, member_id=member_id
).exists():
return Response(
{"error": "User is already a member of the project"},
status=status.HTTP_400_BAD_REQUEST,
)
# Add the user to project
project_member = ProjectMember.objects.create(
project_id=project_id, member_id=member_id, role=role
.values("member_id", "sort_order")
.order_by("sort_order")
)
serializer = ProjectMemberSerializer(project_member)
for member in members:
sort_order = [
project_member.get("sort_order")
for project_member in project_members
if str(project_member.get("member_id"))
== str(member.get("member_id"))
]
bulk_project_members.append(
ProjectMember(
member_id=member.get("member_id"),
role=member.get("role", 10),
project_id=project_id,
workspace_id=project.workspace_id,
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
)
)
project_members = ProjectMember.objects.bulk_create(
bulk_project_members,
batch_size=10,
ignore_conflicts=True,
)
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except KeyError:
return Response(
{"error": "Incorrect data sent"}, status=status.HTTP_400_BAD_REQUEST
)
except Project.DoesNotExist:
return Response(
{"error": "Project does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError:
return Response(
{"error": "User not member of the workspace"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
@@ -810,16 +912,19 @@ class ProjectUserViewsEndpoint(BaseAPIView):
view_props = project_member.view_props
default_props = project_member.default_props
preferences = project_member.preferences
sort_order = project_member.sort_order
project_member.view_props = request.data.get("view_props", view_props)
project_member.default_props = request.data.get(
"default_props", default_props
)
project_member.preferences = request.data.get("preferences", preferences)
project_member.sort_order = request.data.get("sort_order", sort_order)
project_member.save()
return Response(status=status.HTTP_200_OK)
return Response(status=status.HTTP_204_NO_CONTENT)
except Project.DoesNotExist:
return Response(
{"error": "The requested resource does not exists"},
@@ -919,3 +1024,188 @@ class ProjectFavoritesViewSet(BaseViewSet):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectDeployBoardViewSet(BaseViewSet):
permission_classes = [
ProjectMemberPermission,
]
serializer_class = ProjectDeployBoardSerializer
model = ProjectDeployBoard
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
)
.select_related("project")
)
def create(self, request, slug, project_id):
try:
comments = request.data.get("comments", False)
reactions = request.data.get("reactions", False)
inbox = request.data.get("inbox", None)
votes = request.data.get("votes", False)
views = request.data.get(
"views",
{
"list": True,
"kanban": True,
"calendar": True,
"gantt": True,
"spreadsheet": True,
},
)
project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create(
anchor=f"{slug}/{project_id}",
project_id=project_id,
)
project_deploy_board.comments = comments
project_deploy_board.reactions = reactions
project_deploy_board.inbox = inbox
project_deploy_board.votes = votes
project_deploy_board.views = views
project_deploy_board.save()
serializer = ProjectDeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectMemberEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
try:
project_members = ProjectMember.objects.filter(
project_id=project_id,
workspace__slug=slug,
member__is_bot=False,
).select_related("project", "member")
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request, slug, project_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
serializer = ProjectDeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK)
except ProjectDeployBoard.DoesNotExist:
return Response(
{"error": "Project Deploy Board does not exists"},
status=status.HTTP_404_NOT_FOUND,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request, slug):
try:
projects = (
Project.objects.filter(workspace__slug=slug)
.annotate(
is_public=Exists(
ProjectDeployBoard.objects.filter(
workspace__slug=slug, project_id=OuterRef("pk")
)
)
)
.filter(is_public=True)
).values(
"id",
"identifier",
"name",
"description",
"emoji",
"icon_prop",
"cover_image",
)
return Response(projects, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class LeaveProjectEndpoint(BaseAPIView):
permission_classes = [
ProjectLitePermission,
]
def delete(self, request, slug, project_id):
try:
project_member = ProjectMember.objects.get(
workspace__slug=slug,
member=request.user,
project_id=project_id,
)
# Only Admin case
if (
project_member.role == 20
and ProjectMember.objects.filter(
workspace__slug=slug,
role=20,
project_id=project_id,
).count()
== 1
):
return Response(
{
"error": "You cannot leave the project since you are the only admin of the project you should delete the project"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Delete the member from workspace
project_member.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except ProjectMember.DoesNotExist:
return Response(
{"error": "Workspace member does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+104 -89
View File
@@ -20,7 +20,7 @@ class GlobalSearchEndpoint(BaseAPIView):
also show related workspace if found
"""
def filter_workspaces(self, query, slug, project_id):
def filter_workspaces(self, query, slug, project_id, workspace_search):
fields = ["name"]
q = Q()
for field in fields:
@@ -31,8 +31,8 @@ class GlobalSearchEndpoint(BaseAPIView):
.values("name", "id", "slug")
)
def filter_projects(self, query, slug, project_id):
fields = ["name"]
def filter_projects(self, query, slug, project_id, workspace_search):
fields = ["name", "identifier"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
@@ -46,8 +46,8 @@ class GlobalSearchEndpoint(BaseAPIView):
.values("name", "id", "identifier", "workspace__slug")
)
def filter_issues(self, query, slug, project_id):
fields = ["name", "sequence_id"]
def filter_issues(self, query, slug, project_id, workspace_search):
fields = ["name", "sequence_id", "project__identifier"]
q = Q()
for field in fields:
if field == "sequence_id":
@@ -56,111 +56,123 @@ class GlobalSearchEndpoint(BaseAPIView):
q |= Q(**{"sequence_id": sequence_id})
else:
q |= Q(**{f"{field}__icontains": query})
return (
Issue.issue_objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"sequence_id",
"project__identifier",
"project_id",
"workspace__slug",
)
issues = Issue.issue_objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
)
def filter_cycles(self, query, slug, project_id):
if workspace_search == "false" and project_id:
issues = issues.filter(project_id=project_id)
return issues.distinct().values(
"name",
"id",
"sequence_id",
"project__identifier",
"project_id",
"workspace__slug",
)
def filter_cycles(self, query, slug, project_id, workspace_search):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
Cycle.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
cycles = Cycle.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
)
def filter_modules(self, query, slug, project_id):
if workspace_search == "false" and project_id:
cycles = cycles.filter(project_id=project_id)
return cycles.distinct().values(
"name",
"id",
"project_id",
"project__identifier",
"workspace__slug",
)
def filter_modules(self, query, slug, project_id, workspace_search):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
Module.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
modules = Module.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
)
def filter_pages(self, query, slug, project_id):
if workspace_search == "false" and project_id:
modules = modules.filter(project_id=project_id)
return modules.distinct().values(
"name",
"id",
"project_id",
"project__identifier",
"workspace__slug",
)
def filter_pages(self, query, slug, project_id, workspace_search):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
Page.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
pages = Page.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
)
def filter_views(self, query, slug, project_id):
if workspace_search == "false" and project_id:
pages = pages.filter(project_id=project_id)
return pages.distinct().values(
"name",
"id",
"project_id",
"project__identifier",
"workspace__slug",
)
def filter_views(self, query, slug, project_id, workspace_search):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
IssueView.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
project_id=project_id,
)
.distinct()
.values(
"name",
"id",
"project_id",
"workspace__slug",
)
issue_views = IssueView.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
)
def get(self, request, slug, project_id):
if workspace_search == "false" and project_id:
issue_views = issue_views.filter(project_id=project_id)
return issue_views.distinct().values(
"name",
"id",
"project_id",
"project__identifier",
"workspace__slug",
)
def get(self, request, slug):
try:
query = request.query_params.get("search", False)
workspace_search = request.query_params.get("workspace_search", "false")
project_id = request.query_params.get("project_id", False)
if not query:
return Response(
{
@@ -191,7 +203,7 @@ class GlobalSearchEndpoint(BaseAPIView):
for model in MODELS_MAPPER.keys():
func = MODELS_MAPPER.get(model, None)
results[model] = func(query, slug, project_id)
results[model] = func(query, slug, project_id, workspace_search)
return Response({"results": results}, status=status.HTTP_200_OK)
except Exception as e:
@@ -206,8 +218,9 @@ class IssueSearchEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
try:
query = request.query_params.get("search", False)
workspace_search = request.query_params.get("workspace_search", "false")
parent = request.query_params.get("parent", "false")
blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false")
issue_relation = request.query_params.get("issue_relation", "false")
cycle = request.query_params.get("cycle", "false")
module = request.query_params.get("module", "false")
sub_issue = request.query_params.get("sub_issue", "false")
@@ -216,10 +229,12 @@ class IssueSearchEndpoint(BaseAPIView):
issues = Issue.issue_objects.filter(
workspace__slug=slug,
project_id=project_id,
project__project_projectmember__member=self.request.user,
)
if workspace_search == "false":
issues = issues.filter(project_id=project_id)
if query:
issues = search_issues(query, issues)
@@ -232,12 +247,12 @@ class IssueSearchEndpoint(BaseAPIView):
"parent_id", flat=True
)
)
if blocker_blocked_by == "true" and issue_id:
if issue_relation == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id),
~Q(blocked_issues__block=issue),
~Q(blocker_issues__blocked_by=issue),
~Q(issue_related__issue=issue),
~Q(issue_relation__related_issue=issue),
)
if sub_issue == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
@@ -251,12 +266,12 @@ class IssueSearchEndpoint(BaseAPIView):
if module == "true":
issues = issues.exclude(issue_module__isnull=False)
return Response(
issues.values(
"name",
"id",
"sequence_id",
"project__name",
"project__identifier",
"project_id",
"workspace__slug",
@@ -37,7 +37,9 @@ class UserEndpoint(BaseViewSet):
workspace_invites = WorkspaceMemberInvite.objects.filter(
email=request.user.email
).count()
assigned_issues = Issue.issue_objects.filter(assignees__in=[request.user]).count()
assigned_issues = Issue.issue_objects.filter(
assignees__in=[request.user]
).count()
serialized_data = UserSerializer(request.user).data
serialized_data["workspace"] = {
@@ -47,7 +49,9 @@ class UserEndpoint(BaseViewSet):
"fallback_workspace_slug": workspace.slug,
"invites": workspace_invites,
}
serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues
serialized_data.setdefault("issues", {})[
"assigned_issues"
] = assigned_issues
return Response(
serialized_data,
@@ -59,11 +63,15 @@ class UserEndpoint(BaseViewSet):
workspace_invites = WorkspaceMemberInvite.objects.filter(
email=request.user.email
).count()
assigned_issues = Issue.issue_objects.filter(assignees__in=[request.user]).count()
assigned_issues = Issue.issue_objects.filter(
assignees__in=[request.user]
).count()
fallback_workspace = Workspace.objects.filter(
workspace_member__member=request.user
).order_by("created_at").first()
fallback_workspace = (
Workspace.objects.filter(workspace_member__member=request.user)
.order_by("created_at")
.first()
)
serialized_data = UserSerializer(request.user).data
@@ -78,7 +86,9 @@ class UserEndpoint(BaseViewSet):
else None,
"invites": workspace_invites,
}
serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues
serialized_data.setdefault("issues", {})[
"assigned_issues"
] = assigned_issues
return Response(
serialized_data,
@@ -109,12 +119,29 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
)
class UserActivityEndpoint(BaseAPIView, BasePaginator):
def get(self, request):
class UpdateUserTourCompletedEndpoint(BaseAPIView):
def patch(self, request):
try:
queryset = IssueActivity.objects.filter(actor=request.user).select_related(
"actor", "workspace"
user = User.objects.get(pk=request.user.id)
user.is_tour_completed = request.data.get("is_tour_completed", False)
user.save()
return Response(
{"message": "Updated successfully"}, status=status.HTTP_200_OK
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UserActivityEndpoint(BaseAPIView, BasePaginator):
def get(self, request, slug):
try:
queryset = IssueActivity.objects.filter(
actor=request.user, workspace__slug=slug
).select_related("actor", "workspace", "issue", "project")
return self.paginate(
request=request,
+7
View File
@@ -19,6 +19,7 @@ from plane.db.models import (
IssueView,
Issue,
IssueViewFavorite,
IssueReaction,
)
from plane.utils.issue_filters import issue_filters
@@ -77,6 +78,12 @@ class ViewIssuesEndpoint(BaseAPIView):
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
)
serializer = IssueLiteSerializer(issues, many=True)
+610 -36
View File
@@ -3,6 +3,7 @@ import jwt
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
from uuid import uuid4
# Django imports
from django.db import IntegrityError
from django.db.models import Prefetch
@@ -12,15 +13,22 @@ from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.contrib.sites.shortcuts import get_current_site
from django.db.models import (
CharField,
Count,
Prefetch,
OuterRef,
Func,
F,
Q,
Count,
Case,
Value,
CharField,
When,
Max,
IntegerField,
)
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
from django.db.models.fields import DateField
from django.contrib.auth.hashers import make_password
# Third party modules
from rest_framework import status
@@ -37,6 +45,9 @@ from plane.api.serializers import (
UserLiteSerializer,
ProjectMemberSerializer,
WorkspaceThemeSerializer,
IssueActivitySerializer,
IssueLiteSerializer,
WorkspaceMemberAdminSerializer,
)
from plane.api.views.base import BaseAPIView
from . import BaseViewSet
@@ -58,9 +69,24 @@ from plane.db.models import (
PageFavorite,
Page,
IssueViewFavorite,
IssueLink,
IssueAttachment,
IssueSubscriber,
Project,
Label,
WorkspaceMember,
CycleIssue,
IssueReaction,
)
from plane.api.permissions import (
WorkSpaceBasePermission,
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
WorkspaceViewerPermission,
)
from plane.api.permissions import WorkSpaceBasePermission, WorkSpaceAdminPermission
from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.utils.issue_filters import issue_filters
from plane.utils.grouper import group_results
class WorkSpaceViewSet(BaseViewSet):
@@ -81,26 +107,48 @@ class WorkSpaceViewSet(BaseViewSet):
def get_queryset(self):
member_count = (
WorkspaceMember.objects.filter(workspace=OuterRef("id"))
WorkspaceMember.objects.filter(
workspace=OuterRef("id"), member__is_bot=False
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
issue_count = (
Issue.objects.filter(workspace=OuterRef("id"))
Issue.issue_objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
return self.filter_queryset(
super().get_queryset().select_related("owner")
).order_by("name").filter(workspace_member__member=self.request.user).annotate(total_members=member_count).annotate(total_issues=issue_count)
return (
self.filter_queryset(super().get_queryset().select_related("owner"))
.order_by("name")
.filter(workspace_member__member=self.request.user)
.annotate(total_members=member_count)
.annotate(total_issues=issue_count)
.select_related("owner")
)
def create(self, request):
try:
serializer = WorkSpaceSerializer(data=request.data)
slug = request.data.get("slug", False)
name = request.data.get("name", False)
if not name or not slug:
return Response(
{"error": "Both name and slug are required"},
status=status.HTTP_400_BAD_REQUEST,
)
if len(name) > 80 or len(slug) > 48:
return Response(
{"error": "The maximum length for name is 80 and for slug is 48"},
status=status.HTTP_400_BAD_REQUEST,
)
if serializer.is_valid():
serializer.save(owner=request.user)
# Create Workspace member
@@ -146,28 +194,36 @@ class UserWorkSpacesEndpoint(BaseAPIView):
def get(self, request):
try:
member_count = (
WorkspaceMember.objects.filter(workspace=OuterRef("id"))
WorkspaceMember.objects.filter(
workspace=OuterRef("id"), member__is_bot=False
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
issue_count = (
Issue.objects.filter(workspace=OuterRef("id"))
Issue.issue_objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
workspace = (
Workspace.objects.prefetch_related(
Prefetch("workspace_member", queryset=WorkspaceMember.objects.all())
(
Workspace.objects.prefetch_related(
Prefetch(
"workspace_member", queryset=WorkspaceMember.objects.all()
)
)
.filter(
workspace_member__member=request.user,
)
.select_related("owner")
)
.filter(
workspace_member__member=request.user,
)
.select_related("owner")
).annotate(total_members=member_count).annotate(total_issues=issue_count)
.annotate(total_members=member_count)
.annotate(total_issues=issue_count)
)
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -216,9 +272,20 @@ class InviteWorkspaceEndpoint(BaseAPIView):
)
# check for role level
requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user)
if len([email for email in emails if int(email.get("role", 10)) > requesting_user.role]):
return Response({"error": "You cannot invite a user with higher role"}, status=status.HTTP_400_BAD_REQUEST)
requesting_user = WorkspaceMember.objects.get(
workspace__slug=slug, member=request.user
)
if len(
[
email
for email in emails
if int(email.get("role", 10)) > requesting_user.role
]
):
return Response(
{"error": "You cannot invite a user with higher role"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
@@ -276,14 +343,18 @@ class InviteWorkspaceEndpoint(BaseAPIView):
# create the user if signup is disabled
if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
_ = User.objects.bulk_create([
User(
email=email.get("email"),
password=str(uuid4().hex),
is_password_autoset=True
)
for email in emails
], batch_size=100)
_ = User.objects.bulk_create(
[
User(
username=str(uuid4().hex),
email=invitation.email,
password=make_password(uuid4().hex),
is_password_autoset=True,
)
for invitation in workspace_invitations
],
batch_size=100,
)
for invitation in workspace_invitations:
workspace_invitation.delay(
@@ -400,6 +471,30 @@ class WorkspaceInvitationsViewset(BaseViewSet):
.select_related("workspace", "workspace__owner", "created_by")
)
def destroy(self, request, slug, pk):
try:
workspace_member_invite = WorkspaceMemberInvite.objects.get(
pk=pk, workspace__slug=slug
)
# delete the user if signup is disabled
if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
user = User.objects.filter(email=workspace_member_invite.email).first()
if user is not None:
user.delete()
workspace_member_invite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except WorkspaceMemberInvite.DoesNotExist:
return Response(
{"error": "Workspace member invite does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class UserWorkspaceInvitationsEndpoint(BaseViewSet):
serializer_class = WorkSpaceMemberInviteSerializer
@@ -437,7 +532,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
# Delete joined workspace invites
workspace_invitations.delete()
return Response(status=status.HTTP_200_OK)
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
@@ -447,7 +542,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
class WorkSpaceMemberViewSet(BaseViewSet):
serializer_class = WorkSpaceMemberSerializer
serializer_class = WorkspaceMemberAdminSerializer
model = WorkspaceMember
permission_classes = [
@@ -455,7 +550,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
]
search_fields = [
"member__email",
"member__display_name",
"member__first_name",
]
@@ -530,6 +625,21 @@ class WorkSpaceMemberViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
# Check for the only member in the workspace
if (
workspace_member.role == 20
and WorkspaceMember.objects.filter(
workspace__slug=slug,
role=20,
member__is_bot=False,
).count()
== 1
):
return Response(
{"error": "Cannot delete the only Admin for the workspace"},
status=status.HTTP_400_BAD_REQUEST,
)
# Delete the user also from all the projects
ProjectMember.objects.filter(
workspace__slug=slug, member=workspace_member.member
@@ -587,7 +697,7 @@ class TeamMemberViewSet(BaseViewSet):
]
search_fields = [
"member__email",
"member__display_name",
"member__first_name",
]
@@ -736,7 +846,7 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
workspace_member.view_props = request.data.get("view_props", {})
workspace_member.save()
return Response(status=status.HTTP_200_OK)
return Response(status=status.HTTP_204_NO_CONTENT)
except WorkspaceMember.DoesNotExist:
return Response(
{"error": "User not a member of workspace"},
@@ -865,7 +975,9 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
)
state_distribution = (
Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user])
Issue.issue_objects.filter(
workspace__slug=slug, assignees__in=[request.user]
)
.annotate(state_group=F("state__group"))
.values("state_group")
.annotate(state_count=Count("state_group"))
@@ -882,11 +994,11 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
upcoming_issues = Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
target_date__gte=timezone.now(),
start_date__gte=timezone.now(),
workspace__slug=slug,
assignees__in=[request.user],
completed_at__isnull=True,
).values("id", "name", "workspace__slug", "project_id", "target_date")
).values("id", "name", "workspace__slug", "project_id", "start_date")
return Response(
{
@@ -940,3 +1052,465 @@ class WorkspaceThemeViewSet(BaseViewSet):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
def get(self, request, slug, user_id):
try:
filters = issue_filters(request.query_params, "GET")
state_distribution = (
Issue.issue_objects.filter(
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
)
.filter(**filters)
.annotate(state_group=F("state__group"))
.values("state_group")
.annotate(state_count=Count("state_group"))
.order_by("state_group")
)
priority_order = ["urgent", "high", "medium", "low", "none"]
priority_distribution = (
Issue.issue_objects.filter(
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
)
.filter(**filters)
.values("priority")
.annotate(priority_count=Count("priority"))
.filter(priority_count__gte=1)
.annotate(
priority_order=Case(
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
default=Value(len(priority_order)),
output_field=IntegerField(),
)
)
.order_by("priority_order")
)
created_issues = (
Issue.issue_objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
created_by_id=user_id,
)
.filter(**filters)
.count()
)
assigned_issues_count = (
Issue.issue_objects.filter(
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
)
.filter(**filters)
.count()
)
pending_issues_count = (
Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
)
.filter(**filters)
.count()
)
completed_issues_count = (
Issue.issue_objects.filter(
workspace__slug=slug,
assignees__in=[user_id],
state__group="completed",
project__project_projectmember__member=request.user,
)
.filter(**filters)
.count()
)
subscribed_issues_count = (
IssueSubscriber.objects.filter(
workspace__slug=slug,
subscriber_id=user_id,
project__project_projectmember__member=request.user,
)
.filter(**filters)
.count()
)
upcoming_cycles = CycleIssue.objects.filter(
workspace__slug=slug,
cycle__start_date__gt=timezone.now().date(),
issue__assignees__in=[
user_id,
],
).values("cycle__name", "cycle__id", "cycle__project_id")
present_cycle = CycleIssue.objects.filter(
workspace__slug=slug,
cycle__start_date__lt=timezone.now().date(),
cycle__end_date__gt=timezone.now().date(),
issue__assignees__in=[
user_id,
],
).values("cycle__name", "cycle__id", "cycle__project_id")
return Response(
{
"state_distribution": state_distribution,
"priority_distribution": priority_distribution,
"created_issues": created_issues,
"assigned_issues": assigned_issues_count,
"completed_issues": completed_issues_count,
"pending_issues": pending_issues_count,
"subscribed_issues": subscribed_issues_count,
"present_cycles": present_cycle,
"upcoming_cycles": upcoming_cycles,
}
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceUserActivityEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug, user_id):
try:
projects = request.query_params.getlist("project", [])
queryset = IssueActivity.objects.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
workspace__slug=slug,
project__project_projectmember__member=request.user,
actor=user_id,
).select_related("actor", "workspace", "issue", "project")
if projects:
queryset = queryset.filter(project__in=projects)
return self.paginate(
request=request,
queryset=queryset,
on_results=lambda issue_activities: IssueActivitySerializer(
issue_activities, many=True
).data,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceUserProfileEndpoint(BaseAPIView):
def get(self, request, slug, user_id):
try:
user_data = User.objects.get(pk=user_id)
requesting_workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug, member=request.user
)
projects = []
if requesting_workspace_member.role >= 10:
projects = (
Project.objects.filter(
workspace__slug=slug,
project_projectmember__member=request.user,
)
.annotate(
created_issues=Count(
"project_issue",
filter=Q(project_issue__created_by_id=user_id),
)
)
.annotate(
assigned_issues=Count(
"project_issue",
filter=Q(project_issue__assignees__in=[user_id]),
)
)
.annotate(
completed_issues=Count(
"project_issue",
filter=Q(
project_issue__completed_at__isnull=False,
project_issue__assignees__in=[user_id],
),
)
)
.annotate(
pending_issues=Count(
"project_issue",
filter=Q(
project_issue__state__group__in=[
"backlog",
"unstarted",
"started",
],
project_issue__assignees__in=[user_id],
),
)
)
.values(
"id",
"name",
"identifier",
"emoji",
"icon_prop",
"created_issues",
"assigned_issues",
"completed_issues",
"pending_issues",
)
)
return Response(
{
"project_data": projects,
"user_data": {
"email": user_data.email,
"first_name": user_data.first_name,
"last_name": user_data.last_name,
"avatar": user_data.avatar,
"cover_image": user_data.cover_image,
"date_joined": user_data.date_joined,
"user_timezone": user_data.user_timezone,
"display_name": user_data.display_name,
},
},
status=status.HTTP_200_OK,
)
except WorkspaceMember.DoesNotExist:
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
permission_classes = [
WorkspaceViewerPermission,
]
def get(self, request, slug, user_id):
try:
filters = issue_filters(request.query_params, "GET")
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
Issue.issue_objects.filter(
Q(assignees__in=[user_id])
| Q(created_by_id=user_id)
| Q(issue_subscribers__subscriber_id=user_id),
workspace__slug=slug,
project__project_projectmember__member=request.user,
)
.filter(**filters)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.select_related("project", "workspace", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.order_by("-created_at")
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
).distinct()
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order
if order_by_param == "priority"
else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
output_field=CharField(),
)
).order_by("priority_order")
# State Ordering
elif order_by_param in [
"state__name",
"state__group",
"-state__name",
"-state__group",
]:
state_order = (
state_order
if order_by_param in ["state__name", "state__group"]
else state_order[::-1]
)
issue_queryset = issue_queryset.annotate(
state_order=Case(
*[
When(state__group=state_group, then=Value(i))
for i, state_group in enumerate(state_order)
],
default=Value(len(state_order)),
output_field=CharField(),
)
).order_by("state_order")
# assignee and label ordering
elif order_by_param in [
"labels__name",
"-labels__name",
"assignees__first_name",
"-assignees__first_name",
]:
issue_queryset = issue_queryset.annotate(
max_values=Max(
order_by_param[1::]
if order_by_param.startswith("-")
else order_by_param
)
).order_by(
"-max_values" if order_by_param.startswith("-") else "max_values"
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results
group_by = request.GET.get("group_by", False)
if group_by:
return Response(
group_results(issues, group_by), status=status.HTTP_200_OK
)
return Response(issues, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceLabelsEndpoint(BaseAPIView):
permission_classes = [
WorkspaceViewerPermission,
]
def get(self, request, slug):
try:
labels = Label.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
).values("parent", "name", "color", "id", "project_id", "workspace__slug")
return Response(labels, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceMembersEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
try:
workspace_members = WorkspaceMember.objects.filter(
workspace__slug=slug,
member__is_bot=False,
).select_related("workspace", "member")
serialzier = WorkSpaceMemberSerializer(workspace_members, many=True)
return Response(serialzier.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class LeaveWorkspaceEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def delete(self, request, slug):
try:
workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug, member=request.user
)
# Only Admin case
if (
workspace_member.role == 20
and WorkspaceMember.objects.filter(
workspace__slug=slug, role=20
).count()
== 1
):
return Response(
{
"error": "You cannot leave the workspace since you are the only admin of the workspace you should delete the workspace"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Delete the member from workspace
workspace_member.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except WorkspaceMember.DoesNotExist:
return Response(
{"error": "Workspace member does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+11 -11
View File
@@ -21,7 +21,7 @@ row_mapping = {
"state__name": "State",
"state__group": "State Group",
"labels__name": "Label",
"assignees__email": "Assignee Name",
"assignees__display_name": "Assignee Name",
"start_date": "Start Date",
"target_date": "Due Date",
"completed_at": "Completed At",
@@ -51,12 +51,12 @@ def analytic_export_task(email, data, slug):
segmented = segment
assignee_details = {}
if x_axis in ["assignees__email"] or segment in ["assignees__email"]:
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
assignee_details = (
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
.order_by("assignees__id")
.distinct("assignees__id")
.values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name")
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id")
)
if segment:
@@ -93,19 +93,19 @@ def analytic_export_task(email, data, slug):
else:
generated_row.append("0")
# x-axis replacement for names
if x_axis in ["assignees__email"]:
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(item)]
if x_axis in ["assignees__id"]:
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)]
if len(assignee):
generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
rows.append(tuple(generated_row))
# If segment is ["assignees__email"] then replace segment_zero rows with first and last names
if segmented in ["assignees__email"]:
# If segment is ["assignees__display_name"] then replace segment_zero rows with first and last names
if segmented in ["assignees__id"]:
for index, segm in enumerate(row_zero[2:]):
# find the name of the user
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(segm)]
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(segm)]
if len(assignee):
row_zero[index] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
row_zero[index + 2] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
rows = [tuple(row_zero)] + rows
csv_buffer = io.StringIO()
@@ -141,8 +141,8 @@ def analytic_export_task(email, data, slug):
else distribution.get(item)[0].get("estimate "),
]
# x-axis replacement to names
if x_axis in ["assignees__email"]:
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(item)]
if x_axis in ["assignees__id"]:
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)]
if len(assignee):
row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
+375
View File
@@ -0,0 +1,375 @@
# Python imports
import csv
import io
import json
import boto3
import zipfile
from urllib.parse import urlparse, urlunparse
# 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
def dateTimeConverter(time):
if time:
return time.strftime("%a, %d %b %Y %I:%M:%S %Z%z")
def dateConverter(time):
if time:
return time.strftime("%a, %d %b %Y")
def create_csv_file(data):
csv_buffer = io.StringIO()
csv_writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
for row in data:
csv_writer.writerow(row)
csv_buffer.seek(0)
return csv_buffer.getvalue()
def create_json_file(data):
return json.dumps(data)
def create_xlsx_file(data):
workbook = Workbook()
sheet = workbook.active
for row in data:
sheet.append(row)
xlsx_buffer = io.BytesIO()
workbook.save(xlsx_buffer)
xlsx_buffer.seek(0)
return xlsx_buffer.getvalue()
def create_zip_file(files):
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
for filename, file_content in files:
zipf.writestr(filename, file_content)
zip_buffer.seek(0)
return zip_buffer
def upload_to_s3(zip_file, workspace_id, token_id, slug):
file_name = f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.zip"
expires_in = 7 * 24 * 60 * 60
if settings.DOCKERIZED and settings.USE_MINIO:
s3 = boto3.client(
"s3",
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
s3.upload_fileobj(
zip_file,
settings.AWS_STORAGE_BUCKET_NAME,
file_name,
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
)
presigned_url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name},
ExpiresIn=expires_in,
)
# Create the new url with updated domain and protocol
presigned_url = presigned_url.replace(
"http://plane-minio:9000/uploads/",
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/",
)
else:
s3 = boto3.client(
"s3",
region_name=settings.AWS_REGION,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
s3.upload_fileobj(
zip_file,
settings.AWS_S3_BUCKET_NAME,
file_name,
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
)
presigned_url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
ExpiresIn=expires_in,
)
exporter_instance = ExporterHistory.objects.get(token=token_id)
if presigned_url:
exporter_instance.url = presigned_url
exporter_instance.status = "completed"
exporter_instance.key = file_name
else:
exporter_instance.status = "failed"
exporter_instance.save(update_fields=["status", "url", "key"])
def generate_table_row(issue):
return [
f"""{issue["project__identifier"]}-{issue["sequence_id"]}""",
issue["project__name"],
issue["name"],
issue["description_stripped"],
issue["state__name"],
issue["priority"],
f"{issue['created_by__first_name']} {issue['created_by__last_name']}"
if issue["created_by__first_name"] and issue["created_by__last_name"]
else "",
f"{issue['assignees__first_name']} {issue['assignees__last_name']}"
if issue["assignees__first_name"] and issue["assignees__last_name"]
else "",
issue["labels__name"],
issue["issue_cycle__cycle__name"],
dateConverter(issue["issue_cycle__cycle__start_date"]),
dateConverter(issue["issue_cycle__cycle__end_date"]),
issue["issue_module__module__name"],
dateConverter(issue["issue_module__module__start_date"]),
dateConverter(issue["issue_module__module__target_date"]),
dateTimeConverter(issue["created_at"]),
dateTimeConverter(issue["updated_at"]),
dateTimeConverter(issue["completed_at"]),
dateTimeConverter(issue["archived_at"]),
]
def generate_json_row(issue):
return {
"ID": f"""{issue["project__identifier"]}-{issue["sequence_id"]}""",
"Project": issue["project__name"],
"Name": issue["name"],
"Description": issue["description_stripped"],
"State": issue["state__name"],
"Priority": issue["priority"],
"Created By": f"{issue['created_by__first_name']} {issue['created_by__last_name']}"
if issue["created_by__first_name"] and issue["created_by__last_name"]
else "",
"Assignee": f"{issue['assignees__first_name']} {issue['assignees__last_name']}"
if issue["assignees__first_name"] and issue["assignees__last_name"]
else "",
"Labels": issue["labels__name"],
"Cycle Name": issue["issue_cycle__cycle__name"],
"Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]),
"Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]),
"Module Name": issue["issue_module__module__name"],
"Module Start Date": dateConverter(issue["issue_module__module__start_date"]),
"Module Target Date": dateConverter(issue["issue_module__module__target_date"]),
"Created At": dateTimeConverter(issue["created_at"]),
"Updated At": dateTimeConverter(issue["updated_at"]),
"Completed At": dateTimeConverter(issue["completed_at"]),
"Archived At": dateTimeConverter(issue["archived_at"]),
}
def update_json_row(rows, row):
matched_index = next(
(
index
for index, existing_row in enumerate(rows)
if existing_row["ID"] == row["ID"]
),
None,
)
if matched_index is not None:
existing_assignees, existing_labels = (
rows[matched_index]["Assignee"],
rows[matched_index]["Labels"],
)
assignee, label = row["Assignee"], row["Labels"]
if assignee is not None and assignee not in existing_assignees:
rows[matched_index]["Assignee"] += f", {assignee}"
if label is not None and label not in existing_labels:
rows[matched_index]["Labels"] += f", {label}"
else:
rows.append(row)
def update_table_row(rows, row):
matched_index = next(
(index for index, existing_row in enumerate(rows) if existing_row[0] == row[0]),
None,
)
if matched_index is not None:
existing_assignees, existing_labels = rows[matched_index][7:9]
assignee, label = row[7:9]
if assignee is not None and assignee not in existing_assignees:
rows[matched_index][7] += f", {assignee}"
if label is not None and label not in existing_labels:
rows[matched_index][8] += f", {label}"
else:
rows.append(row)
def generate_csv(header, project_id, issues, files):
"""
Generate CSV export for all the passed issues.
"""
rows = [
header,
]
for issue in issues:
row = generate_table_row(issue)
update_table_row(rows, row)
csv_file = create_csv_file(rows)
files.append((f"{project_id}.csv", csv_file))
def generate_json(header, project_id, issues, files):
rows = []
for issue in issues:
row = generate_json_row(issue)
update_json_row(rows, row)
json_file = create_json_file(rows)
files.append((f"{project_id}.json", json_file))
def generate_xlsx(header, project_id, issues, files):
rows = [header]
for issue in issues:
row = generate_table_row(issue)
update_table_row(rows, row)
xlsx_file = create_xlsx_file(rows)
files.append((f"{project_id}.xlsx", xlsx_file))
@shared_task
def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, slug):
try:
exporter_instance = ExporterHistory.objects.get(token=token_id)
exporter_instance.status = "processing"
exporter_instance.save(update_fields=["status"])
workspace_issues = (
(
Issue.objects.filter(
workspace__id=workspace_id,
project_id__in=project_ids,
project__project_projectmember__member=exporter_instance.initiated_by_id,
)
.select_related("project", "workspace", "state", "parent", "created_by")
.prefetch_related(
"assignees", "labels", "issue_cycle__cycle", "issue_module__module"
)
.values(
"id",
"project__identifier",
"project__name",
"project__id",
"sequence_id",
"name",
"description_stripped",
"priority",
"state__name",
"created_at",
"updated_at",
"completed_at",
"archived_at",
"issue_cycle__cycle__name",
"issue_cycle__cycle__start_date",
"issue_cycle__cycle__end_date",
"issue_module__module__name",
"issue_module__module__start_date",
"issue_module__module__target_date",
"created_by__first_name",
"created_by__last_name",
"assignees__first_name",
"assignees__last_name",
"labels__name",
)
)
.order_by("project__identifier", "sequence_id")
.distinct()
)
# CSV header
header = [
"ID",
"Project",
"Name",
"Description",
"State",
"Priority",
"Created By",
"Assignee",
"Labels",
"Cycle Name",
"Cycle Start Date",
"Cycle End Date",
"Module Name",
"Module Start Date",
"Module Target Date",
"Created At",
"Updated At",
"Completed At",
"Archived At",
]
EXPORTER_MAPPER = {
"csv": generate_csv,
"json": generate_json,
"xlsx": generate_xlsx,
}
files = []
if multiple:
for project_id in project_ids:
issues = workspace_issues.filter(project__id=project_id)
exporter = EXPORTER_MAPPER.get(provider)
if exporter is not None:
exporter(
header,
project_id,
issues,
files,
)
else:
exporter = EXPORTER_MAPPER.get(provider)
if exporter is not None:
exporter(
header,
workspace_id,
workspace_issues,
files,
)
zip_buffer = create_zip_file(files)
upload_to_s3(zip_buffer, workspace_id, token_id, slug)
except Exception as e:
exporter_instance = ExporterHistory.objects.get(token=token_id)
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)
return
@@ -0,0 +1,49 @@
# Python imports
import boto3
from datetime import timedelta
# Django imports
from django.conf import settings
from django.utils import timezone
from django.db.models import Q
# Third party imports
from celery import shared_task
from botocore.client import Config
# Module imports
from plane.db.models import ExporterHistory
@shared_task
def delete_old_s3_link():
# Get a list of keys and IDs to process
expired_exporter_history = ExporterHistory.objects.filter(
Q(url__isnull=False) & Q(created_at__lte=timezone.now() - timedelta(days=8))
).values_list("key", "id")
if settings.DOCKERIZED and settings.USE_MINIO:
s3 = boto3.client(
"s3",
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
else:
s3 = boto3.client(
"s3",
region_name="ap-south-1",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
for file_name, exporter_id in expired_exporter_history:
# Delete object from S3
if file_name:
if settings.DOCKERIZED and settings.USE_MINIO:
s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name)
else:
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name)
ExporterHistory.objects.filter(id=exporter_id).update(url=None)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,159 @@
# Python imports
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
# Module imports
from plane.db.models import Issue, Project, State
from plane.bgtasks.issue_activites_task import issue_activity
@shared_task
def archive_and_close_old_issues():
archive_old_issues()
close_old_issues()
def archive_old_issues():
try:
# Get all the projects whose archive_in is greater than 0
projects = Project.objects.filter(archive_in__gt=0)
for project in projects:
project_id = project.id
archive_in = project.archive_in
# Get all the issues whose updated_at in less that the archive_in month
issues = Issue.issue_objects.filter(
Q(
project=project_id,
archived_at__isnull=True,
updated_at__lte=(timezone.now() - timedelta(days=archive_in * 30)),
state__group__in=["completed", "cancelled"],
),
Q(issue_cycle__isnull=True)
| (
Q(issue_cycle__cycle__end_date__lt=timezone.now().date())
& Q(issue_cycle__isnull=False)
),
Q(issue_module__isnull=True)
| (
Q(issue_module__module__target_date__lt=timezone.now().date())
& Q(issue_module__isnull=False)
),
).filter(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True)
)
# Check if Issues
if issues:
issues_to_update = []
for issue in issues:
issue.archived_at = timezone.now()
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
if issues_to_update:
updated_issues = Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100
)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
)
for issue in updated_issues
]
return
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)
return
def close_old_issues():
try:
# Get all the projects whose close_in is greater than 0
projects = Project.objects.filter(close_in__gt=0).select_related(
"default_state"
)
for project in projects:
project_id = project.id
close_in = project.close_in
# Get all the issues whose updated_at in less that the close_in month
issues = Issue.issue_objects.filter(
Q(
project=project_id,
archived_at__isnull=True,
updated_at__lte=(timezone.now() - timedelta(days=close_in * 30)),
state__group__in=["backlog", "unstarted", "started"],
),
Q(issue_cycle__isnull=True)
| (
Q(issue_cycle__cycle__end_date__lt=timezone.now().date())
& Q(issue_cycle__isnull=False)
),
Q(issue_module__isnull=True)
| (
Q(issue_module__module__target_date__lt=timezone.now().date())
& Q(issue_module__isnull=False)
),
).filter(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True)
)
# Check if Issues
if issues:
if project.default_state is None:
close_state = State.objects.filter(group="cancelled").first()
else:
close_state = project.default_state
issues_to_update = []
for issue in issues:
issue.state = close_state
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
if issues_to_update:
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
)
for issue in updated_issues
]
return
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)
return
+15
View File
@@ -1,6 +1,7 @@
import os
from celery import Celery
from plane.settings.redis import redis_instance
from celery.schedules import crontab
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
@@ -13,5 +14,19 @@ app = Celery("plane")
# pickle the object when using Windows.
app.config_from_object("django.conf:settings", namespace="CELERY")
app.conf.beat_schedule = {
# Executes every day at 12 AM
"check-every-day-to-archive-and-close": {
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete_exporter_history": {
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
"schedule": crontab(hour=0, minute=0),
},
}
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler'
@@ -0,0 +1,42 @@
# Generated by Django 3.2.19 on 2023-07-04 16:55
from django.db import migrations, models
def update_company_organization_size(apps, schema_editor):
Model = apps.get_model("db", "Workspace")
updated_size = []
for obj in Model.objects.all():
obj.organization_size = str(obj.company_size)
updated_size.append(obj)
Model.objects.bulk_update(updated_size, ["organization_size"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
("db", "0034_auto_20230628_1046"),
]
operations = [
migrations.AddField(
model_name="workspace",
name="organization_size",
field=models.CharField(default="2-10", max_length=20),
),
migrations.RunPython(update_company_organization_size),
migrations.AlterField(
model_name="workspace",
name="name",
field=models.CharField(max_length=80, verbose_name="Workspace Name"),
),
migrations.AlterField(
model_name="workspace",
name="slug",
field=models.SlugField(max_length=48, unique=True),
),
migrations.RemoveField(
model_name="workspace",
name="company_size",
),
]
@@ -0,0 +1,18 @@
# Generated by Django 3.2.19 on 2023-07-05 07:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0035_auto_20230704_2225'),
]
operations = [
migrations.AlterField(
model_name='workspace',
name='organization_size',
field=models.CharField(max_length=20),
),
]
@@ -0,0 +1,266 @@
# Generated by Django 4.2.3 on 2023-07-19 06:52
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.user
import uuid
def onboarding_default_steps(apps, schema_editor):
default_onboarding_schema = {
"workspace_join": True,
"profile_complete": True,
"workspace_create": True,
"workspace_invite": True,
}
Model = apps.get_model("db", "User")
updated_user = []
for obj in Model.objects.filter(is_onboarded=True):
obj.onboarding_step = default_onboarding_schema
obj.is_tour_completed = True
updated_user.append(obj)
Model.objects.bulk_update(updated_user, ["onboarding_step", "is_tour_completed"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
("db", "0036_alter_workspace_organization_size"),
]
operations = [
migrations.AddField(
model_name="issue",
name="archived_at",
field=models.DateField(null=True),
),
migrations.AddField(
model_name="project",
name="archive_in",
field=models.IntegerField(
default=0,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(12),
],
),
),
migrations.AddField(
model_name="project",
name="close_in",
field=models.IntegerField(
default=0,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(12),
],
),
),
migrations.AddField(
model_name="project",
name="default_state",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="default_state",
to="db.state",
),
),
migrations.AddField(
model_name="user",
name="is_tour_completed",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="user",
name="onboarding_step",
field=models.JSONField(default=plane.db.models.user.get_default_onboarding),
),
migrations.RunPython(onboarding_default_steps),
migrations.CreateModel(
name="Notification",
fields=[
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("data", models.JSONField(null=True)),
("entity_identifier", models.UUIDField(null=True)),
("entity_name", models.CharField(max_length=255)),
("title", models.TextField()),
("message", models.JSONField(null=True)),
("message_html", models.TextField(blank=True, default="<p></p>")),
("message_stripped", models.TextField(blank=True, null=True)),
("sender", models.CharField(max_length=255)),
("read_at", models.DateTimeField(null=True)),
("snoozed_till", models.DateTimeField(null=True)),
("archived_at", models.DateTimeField(null=True)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"project",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to="db.project",
),
),
(
"receiver",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="received_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"triggered_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="triggered_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to="db.workspace",
),
),
],
options={
"verbose_name": "Notification",
"verbose_name_plural": "Notifications",
"db_table": "notifications",
"ordering": ("-created_at",),
},
),
migrations.CreateModel(
name="IssueSubscriber",
fields=[
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"issue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issue_subscribers",
to="db.issue",
),
),
(
"project",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="project_%(class)s",
to="db.project",
),
),
(
"subscriber",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issue_subscribers",
to=settings.AUTH_USER_MODEL,
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_%(class)s",
to="db.workspace",
),
),
],
options={
"verbose_name": "Issue Subscriber",
"verbose_name_plural": "Issue Subscribers",
"db_table": "issue_subscribers",
"ordering": ("-created_at",),
"unique_together": {("issue", "subscriber")},
},
),
]
@@ -0,0 +1,35 @@
# Generated by Django 4.2.3 on 2023-07-20 09:35
from django.db import migrations, models
def restructure_theming(apps, schema_editor):
Model = apps.get_model("db", "User")
updated_user = []
for obj in Model.objects.exclude(theme={}).all():
current_theme = obj.theme
updated_theme = {
"primary": current_theme.get("accent", ""),
"background": current_theme.get("bgBase", ""),
"sidebarBackground": current_theme.get("sidebar", ""),
"text": current_theme.get("textBase", ""),
"sidebarText": current_theme.get("textBase", ""),
"palette": f"""{current_theme.get("bgBase","")},{current_theme.get("textBase", "")},{current_theme.get("accent", "")},{current_theme.get("sidebar","")},{current_theme.get("textBase", "")}""",
"darkPalette": current_theme.get("darkPalette", "")
}
obj.theme = updated_theme
updated_user.append(obj)
Model.objects.bulk_update(
updated_user, ["theme"], batch_size=100
)
class Migration(migrations.Migration):
dependencies = [
("db", "0037_issue_archived_at_project_archive_in_and_more"),
]
operations = [
migrations.RunPython(restructure_theming)
]
@@ -0,0 +1,97 @@
# Generated by Django 4.2.3 on 2023-07-23 16:33
import random
from django.db import migrations, models
import plane.db.models.workspace
def rename_field(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
updated_activity = []
for obj in Model.objects.filter(field="assignee"):
obj.field = "assignees"
updated_activity.append(obj)
Model.objects.bulk_update(updated_activity, ["field"], batch_size=100)
def update_workspace_member_props(apps, schema_editor):
Model = apps.get_model("db", "WorkspaceMember")
updated_workspace_member = []
for obj in Model.objects.all():
if obj.view_props is None:
obj.view_props = {
"filters": {"type": None},
"groupByProperty": None,
"issueView": "list",
"orderBy": "-created_at",
"properties": {
"assignee": True,
"due_date": True,
"key": True,
"labels": True,
"priority": True,
"state": True,
"sub_issue_count": True,
"attachment_count": True,
"link": True,
"estimate": True,
"created_on": True,
"updated_on": True,
},
"showEmptyGroups": True,
}
else:
current_view_props = obj.view_props
obj.view_props = {
"filters": {"type": None},
"groupByProperty": None,
"issueView": "list",
"orderBy": "-created_at",
"showEmptyGroups": True,
"properties": current_view_props,
}
updated_workspace_member.append(obj)
Model.objects.bulk_update(updated_workspace_member, ["view_props"], batch_size=100)
def update_project_member_sort_order(apps, schema_editor):
Model = apps.get_model("db", "ProjectMember")
updated_project_members = []
for obj in Model.objects.all():
obj.sort_order = random.randint(1, 65536)
updated_project_members.append(obj)
Model.objects.bulk_update(updated_project_members, ["sort_order"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
("db", "0038_auto_20230720_1505"),
]
operations = [
migrations.RunPython(rename_field),
migrations.RunPython(update_workspace_member_props),
migrations.AlterField(
model_name='workspacemember',
name='view_props',
field=models.JSONField(default=plane.db.models.workspace.get_default_props),
),
migrations.AddField(
model_name='workspacemember',
name='default_props',
field=models.JSONField(default=plane.db.models.workspace.get_default_props),
),
migrations.AddField(
model_name='projectmember',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.RunPython(update_project_member_sort_order),
]
@@ -0,0 +1,81 @@
# Generated by Django 4.2.3 on 2023-08-01 06:02
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.project
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0039_auto_20230723_2203'),
]
operations = [
migrations.AddField(
model_name='projectmember',
name='preferences',
field=models.JSONField(default=plane.db.models.project.get_default_preferences),
),
migrations.AddField(
model_name='user',
name='cover_image',
field=models.URLField(blank=True, max_length=800, null=True),
),
migrations.CreateModel(
name='IssueReaction',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('reaction', models.CharField(max_length=20)),
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_reactions', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_reactions', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Issue Reaction',
'verbose_name_plural': 'Issue Reactions',
'db_table': 'issue_reactions',
'ordering': ('-created_at',),
'unique_together': {('issue', 'actor', 'reaction')},
},
),
migrations.CreateModel(
name='CommentReaction',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('reaction', models.CharField(max_length=20)),
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comment_reactions', to=settings.AUTH_USER_MODEL)),
('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comment_reactions', to='db.issuecomment')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Comment Reaction',
'verbose_name_plural': 'Comment Reactions',
'db_table': 'comment_reactions',
'ordering': ('-created_at',),
'unique_together': {('comment', 'actor', 'reaction')},
},
),
migrations.AlterField(
model_name='project',
name='identifier',
field=models.CharField(max_length=12, verbose_name='Project Identifier'),
),
migrations.AlterField(
model_name='projectidentifier',
name='name',
field=models.CharField(max_length=12),
),
]
@@ -0,0 +1,243 @@
# Generated by Django 4.2.3 on 2023-08-14 07:12
from django.conf import settings
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.exporter
import plane.db.models.project
import uuid
import random
import string
def generate_display_name(apps, schema_editor):
UserModel = apps.get_model("db", "User")
updated_users = []
for obj in UserModel.objects.all():
obj.display_name = (
obj.email.split("@")[0]
if len(obj.email.split("@"))
else "".join(random.choice(string.ascii_letters) for _ in range(6))
)
updated_users.append(obj)
UserModel.objects.bulk_update(updated_users, ["display_name"], batch_size=100)
def rectify_field_issue_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
updated_activity = []
for obj in Model.objects.filter(field="assignee"):
obj.field = "assignees"
updated_activity.append(obj)
Model.objects.bulk_update(updated_activity, ["field"], batch_size=100)
def update_assignee_issue_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
updated_activity = []
# Get all the users
User = apps.get_model("db", "User")
users = User.objects.values("id", "email", "display_name")
for obj in Model.objects.filter(field="assignees"):
if bool(obj.new_value) and not bool(obj.old_value):
# Get user from list
assigned_user = [
user for user in users if user.get("email") == obj.new_value
]
if assigned_user:
obj.new_value = assigned_user[0].get("display_name")
obj.new_identifier = assigned_user[0].get("id")
# Update the comment
words = obj.comment.split()
words[-1] = assigned_user[0].get("display_name")
obj.comment = " ".join(words)
if bool(obj.old_value) and not bool(obj.new_value):
# Get user from list
assigned_user = [
user for user in users if user.get("email") == obj.old_value
]
if assigned_user:
obj.old_value = assigned_user[0].get("display_name")
obj.old_identifier = assigned_user[0].get("id")
# Update the comment
words = obj.comment.split()
words[-1] = assigned_user[0].get("display_name")
obj.comment = " ".join(words)
updated_activity.append(obj)
Model.objects.bulk_update(
updated_activity,
["old_value", "new_value", "old_identifier", "new_identifier", "comment"],
batch_size=200,
)
def update_name_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
update_activity = []
for obj in Model.objects.filter(field="name"):
obj.comment = obj.comment.replace("start date", "name")
update_activity.append(obj)
Model.objects.bulk_update(update_activity, ["comment"], batch_size=1000)
def random_cycle_order(apps, schema_editor):
CycleModel = apps.get_model("db", "Cycle")
updated_cycles = []
for obj in CycleModel.objects.all():
obj.sort_order = random.randint(1, 65536)
updated_cycles.append(obj)
CycleModel.objects.bulk_update(updated_cycles, ["sort_order"], batch_size=100)
def random_module_order(apps, schema_editor):
ModuleModel = apps.get_model("db", "Module")
updated_modules = []
for obj in ModuleModel.objects.all():
obj.sort_order = random.randint(1, 65536)
updated_modules.append(obj)
ModuleModel.objects.bulk_update(updated_modules, ["sort_order"], batch_size=100)
def update_user_issue_properties(apps, schema_editor):
IssuePropertyModel = apps.get_model("db", "IssueProperty")
updated_issue_properties = []
for obj in IssuePropertyModel.objects.all():
obj.properties["start_date"] = True
updated_issue_properties.append(obj)
IssuePropertyModel.objects.bulk_update(
updated_issue_properties, ["properties"], batch_size=100
)
def workspace_member_properties(apps, schema_editor):
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
updated_workspace_members = []
for obj in WorkspaceMemberModel.objects.all():
obj.view_props["properties"]["start_date"] = True
obj.default_props["properties"]["start_date"] = True
updated_workspace_members.append(obj)
WorkspaceMemberModel.objects.bulk_update(
updated_workspace_members, ["view_props", "default_props"], batch_size=100
)
class Migration(migrations.Migration):
dependencies = [
('db', '0040_projectmember_preferences_user_cover_image_and_more'),
]
operations = [
migrations.AddField(
model_name='cycle',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.AddField(
model_name='issuecomment',
name='access',
field=models.CharField(choices=[('INTERNAL', 'INTERNAL'), ('EXTERNAL', 'EXTERNAL')], default='INTERNAL', max_length=100),
),
migrations.AddField(
model_name='module',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.AddField(
model_name='user',
name='display_name',
field=models.CharField(default='', max_length=255),
),
migrations.CreateModel(
name='ExporterHistory',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('project', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(default=uuid.uuid4), blank=True, null=True, size=None)),
('provider', models.CharField(choices=[('json', 'json'), ('csv', 'csv'), ('xlsx', 'xlsx')], max_length=50)),
('status', models.CharField(choices=[('queued', 'Queued'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='queued', max_length=50)),
('reason', models.TextField(blank=True)),
('key', models.TextField(blank=True)),
('url', models.URLField(blank=True, max_length=800, null=True)),
('token', models.CharField(default=plane.db.models.exporter.generate_token, max_length=255, unique=True)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('initiated_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_exporters', to=settings.AUTH_USER_MODEL)),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_exporters', to='db.workspace')),
],
options={
'verbose_name': 'Exporter',
'verbose_name_plural': 'Exporters',
'db_table': 'exporters',
'ordering': ('-created_at',),
},
),
migrations.CreateModel(
name='ProjectDeployBoard',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('anchor', models.CharField(db_index=True, default=plane.db.models.project.get_anchor, max_length=255, unique=True)),
('comments', models.BooleanField(default=False)),
('reactions', models.BooleanField(default=False)),
('votes', models.BooleanField(default=False)),
('views', models.JSONField(default=plane.db.models.project.get_default_views)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('inbox', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bord_inbox', to='db.inbox')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Project Deploy Board',
'verbose_name_plural': 'Project Deploy Boards',
'db_table': 'project_deploy_boards',
'ordering': ('-created_at',),
'unique_together': {('project', 'anchor')},
},
),
migrations.CreateModel(
name='IssueVote',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('vote', models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')])),
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Issue Vote',
'verbose_name_plural': 'Issue Votes',
'db_table': 'issue_votes',
'ordering': ('-created_at',),
'unique_together': {('issue', 'actor')},
},
),
migrations.AlterField(
model_name='modulelink',
name='title',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.RunPython(generate_display_name),
migrations.RunPython(rectify_field_issue_activity),
migrations.RunPython(update_assignee_issue_activity),
migrations.RunPython(update_name_activity),
migrations.RunPython(random_cycle_order),
migrations.RunPython(random_module_order),
migrations.RunPython(update_user_issue_properties),
migrations.RunPython(workspace_member_properties),
]
File diff suppressed because one or more lines are too long
@@ -0,0 +1,84 @@
# Generated by Django 4.2.3 on 2023-09-12 07:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from plane.db.models import IssueRelation
from sentry_sdk import capture_exception
import uuid
def create_issue_relation(apps, schema_editor):
try:
IssueBlockerModel = apps.get_model("db", "IssueBlocker")
updated_issue_relation = []
for blocked_issue in IssueBlockerModel.objects.all():
updated_issue_relation.append(
IssueRelation(
issue_id=blocked_issue.block_id,
related_issue_id=blocked_issue.blocked_by_id,
relation_type="blocked_by",
project_id=blocked_issue.project_id,
workspace_id=blocked_issue.workspace_id,
created_by_id=blocked_issue.created_by_id,
updated_by_id=blocked_issue.updated_by_id,
)
)
IssueRelation.objects.bulk_create(updated_issue_relation, batch_size=100)
except Exception as e:
print(e)
capture_exception(e)
def update_issue_priority_choice(apps, schema_editor):
IssueModel = apps.get_model("db", "Issue")
updated_issues = []
for obj in IssueModel.objects.all():
if obj.priority is None:
obj.priority = "none"
updated_issues.append(obj)
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0042_alter_analyticview_created_by_and_more'),
]
operations = [
migrations.CreateModel(
name='IssueRelation',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('relation_type', models.CharField(choices=[('duplicate', 'Duplicate'), ('relates_to', 'Relates To'), ('blocked_by', 'Blocked By')], default='blocked_by', max_length=20, verbose_name='Issue Relation Type')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_relation', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('related_issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_related', to='db.issue')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Issue Relation',
'verbose_name_plural': 'Issue Relations',
'db_table': 'issue_relations',
'ordering': ('-created_at',),
'unique_together': {('issue', 'related_issue')},
},
),
migrations.AddField(
model_name='issue',
name='is_draft',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='issue',
name='priority',
field=models.CharField(choices=[('urgent', 'Urgent'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low'), ('none', 'None')], default='none', max_length=30, verbose_name='Issue Priority'),
),
migrations.RunPython(create_issue_relation),
migrations.RunPython(update_issue_priority_choice),
]
@@ -0,0 +1,138 @@
# Generated by Django 4.2.3 on 2023-09-13 07:09
from django.db import migrations
def workspace_member_props(old_props):
new_props = {
"filters": {
"priority": old_props.get("filters", {}).get("priority", None),
"state": old_props.get("filters", {}).get("state", None),
"state_group": old_props.get("filters", {}).get("state_group", None),
"assignees": old_props.get("filters", {}).get("assignees", None),
"created_by": old_props.get("filters", {}).get("created_by", None),
"labels": old_props.get("filters", {}).get("labels", None),
"start_date": old_props.get("filters", {}).get("start_date", None),
"target_date": old_props.get("filters", {}).get("target_date", None),
"subscriber": old_props.get("filters", {}).get("subscriber", None),
},
"display_filters": {
"group_by": old_props.get("groupByProperty", None),
"order_by": old_props.get("orderBy", "-created_at"),
"type": old_props.get("filters", {}).get("type", None),
"sub_issue": old_props.get("showSubIssues", True),
"show_empty_groups": old_props.get("showEmptyGroups", True),
"layout": old_props.get("issueView", "list"),
"calendar_date_range": old_props.get("calendarDateRange", ""),
},
"display_properties": {
"assignee": old_props.get("properties", {}).get("assignee",None),
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
"created_on": old_props.get("properties", {}).get("created_on", None),
"due_date": old_props.get("properties", {}).get("due_date", None),
"estimate": old_props.get("properties", {}).get("estimate", None),
"key": old_props.get("properties", {}).get("key", None),
"labels": old_props.get("properties", {}).get("labels", None),
"link": old_props.get("properties", {}).get("link", None),
"priority": old_props.get("properties", {}).get("priority", None),
"start_date": old_props.get("properties", {}).get("start_date", None),
"state": old_props.get("properties", {}).get("state", None),
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
"updated_on": old_props.get("properties", {}).get("updated_on", None),
},
}
return new_props
def project_member_props(old_props):
new_props = {
"filters": {
"priority": old_props.get("filters", {}).get("priority", None),
"state": old_props.get("filters", {}).get("state", None),
"state_group": old_props.get("filters", {}).get("state_group", None),
"assignees": old_props.get("filters", {}).get("assignees", None),
"created_by": old_props.get("filters", {}).get("created_by", None),
"labels": old_props.get("filters", {}).get("labels", None),
"start_date": old_props.get("filters", {}).get("start_date", None),
"target_date": old_props.get("filters", {}).get("target_date", None),
"subscriber": old_props.get("filters", {}).get("subscriber", None),
},
"display_filters": {
"group_by": old_props.get("groupByProperty", None),
"order_by": old_props.get("orderBy", "-created_at"),
"type": old_props.get("filters", {}).get("type", None),
"sub_issue": old_props.get("showSubIssues", True),
"show_empty_groups": old_props.get("showEmptyGroups", True),
"layout": old_props.get("issueView", "list"),
"calendar_date_range": old_props.get("calendarDateRange", ""),
},
}
return new_props
def cycle_module_props(old_props):
new_props = {
"filters": {
"priority": old_props.get("filters", {}).get("priority", None),
"state": old_props.get("filters", {}).get("state", None),
"state_group": old_props.get("filters", {}).get("state_group", None),
"assignees": old_props.get("filters", {}).get("assignees", None),
"created_by": old_props.get("filters", {}).get("created_by", None),
"labels": old_props.get("filters", {}).get("labels", None),
"start_date": old_props.get("filters", {}).get("start_date", None),
"target_date": old_props.get("filters", {}).get("target_date", None),
"subscriber": old_props.get("filters", {}).get("subscriber", None),
},
}
return new_props
def update_workspace_member_view_props(apps, schema_editor):
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
updated_workspace_member = []
for obj in WorkspaceMemberModel.objects.all():
obj.view_props = workspace_member_props(obj.view_props)
obj.default_props = workspace_member_props(obj.default_props)
updated_workspace_member.append(obj)
WorkspaceMemberModel.objects.bulk_update(updated_workspace_member, ["view_props", "default_props"], batch_size=100)
def update_project_member_view_props(apps, schema_editor):
ProjectMemberModel = apps.get_model("db", "ProjectMember")
updated_project_member = []
for obj in ProjectMemberModel.objects.all():
obj.view_props = project_member_props(obj.view_props)
obj.default_props = project_member_props(obj.default_props)
updated_project_member.append(obj)
ProjectMemberModel.objects.bulk_update(updated_project_member, ["view_props", "default_props"], batch_size=100)
def update_cycle_props(apps, schema_editor):
CycleModel = apps.get_model("db", "Cycle")
updated_cycle = []
for obj in CycleModel.objects.all():
if "filter" in obj.view_props:
obj.view_props = cycle_module_props(obj.view_props)
updated_cycle.append(obj)
CycleModel.objects.bulk_update(updated_cycle, ["view_props"], batch_size=100)
def update_module_props(apps, schema_editor):
ModuleModel = apps.get_model("db", "Module")
updated_module = []
for obj in ModuleModel.objects.all():
if "filter" in obj.view_props:
obj.view_props = cycle_module_props(obj.view_props)
updated_module.append(obj)
ModuleModel.objects.bulk_update(updated_module, ["view_props"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0043_alter_analyticview_created_by_and_more'),
]
operations = [
migrations.RunPython(update_workspace_member_view_props),
migrations.RunPython(update_project_member_view_props),
migrations.RunPython(update_cycle_props),
migrations.RunPython(update_module_props),
]
+12
View File
@@ -18,6 +18,8 @@ from .project import (
ProjectMemberInvite,
ProjectIdentifier,
ProjectFavorite,
ProjectDeployBoard,
ProjectPublicMember,
)
from .issue import (
@@ -30,9 +32,14 @@ from .issue import (
IssueAssignee,
Label,
IssueBlocker,
IssueRelation,
IssueLink,
IssueSequence,
IssueAttachment,
IssueSubscriber,
IssueReaction,
CommentReaction,
IssueVote,
)
from .asset import FileAsset
@@ -66,4 +73,9 @@ from .page import Page, PageBlock, PageFavorite, PageLabel
from .estimate import Estimate, EstimatePoint
from .inbox import Inbox, InboxIssue
from .analytic import AnalyticView
from .notification import Notification
from .exporter import ExporterHistory
+12
View File
@@ -17,6 +17,7 @@ class Cycle(ProjectBaseModel):
related_name="owned_by_cycle",
)
view_props = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
class Meta:
verbose_name = "Cycle"
@@ -24,6 +25,17 @@ class Cycle(ProjectBaseModel):
db_table = "cycles"
ordering = ("-created_at",)
def save(self, *args, **kwargs):
if self._state.adding:
smallest_sort_order = Cycle.objects.filter(
project=self.project
).aggregate(smallest=models.Min("sort_order"))["smallest"]
if smallest_sort_order is not None:
self.sort_order = smallest_sort_order - 10000
super(Cycle, self).save(*args, **kwargs)
def __str__(self):
"""Return name of the cycle"""
return f"{self.name} <{self.project.name}>"
+56
View File
@@ -0,0 +1,56 @@
import uuid
# Python imports
from uuid import uuid4
# Django imports
from django.db import models
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
# Module imports
from . import BaseModel
def generate_token():
return uuid4().hex
class ExporterHistory(BaseModel):
workspace = models.ForeignKey(
"db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_exporters"
)
project = ArrayField(models.UUIDField(default=uuid.uuid4), blank=True, null=True)
provider = models.CharField(
max_length=50,
choices=(
("json", "json"),
("csv", "csv"),
("xlsx", "xlsx"),
),
)
status = models.CharField(
max_length=50,
choices=(
("queued", "Queued"),
("processing", "Processing"),
("completed", "Completed"),
("failed", "Failed"),
),
default="queued",
)
reason = models.TextField(blank=True)
key = models.TextField(blank=True)
url = models.URLField(max_length=800, blank=True, null=True)
token = models.CharField(max_length=255, default=generate_token, unique=True)
initiated_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="workspace_exporters"
)
class Meta:
verbose_name = "Exporter"
verbose_name_plural = "Exporters"
db_table = "exporters"
ordering = ("-created_at",)
def __str__(self):
"""Return name of the service"""
return f"{self.provider} <{self.workspace.name}>"
+136 -13
View File
@@ -28,6 +28,8 @@ class IssueManager(models.Manager):
| models.Q(issue_inbox__status=2)
| models.Q(issue_inbox__isnull=True)
)
.exclude(archived_at__isnull=False)
.exclude(is_draft=True)
)
@@ -37,6 +39,7 @@ class Issue(ProjectBaseModel):
("high", "High"),
("medium", "Medium"),
("low", "Low"),
("none", "None")
)
parent = models.ForeignKey(
"self",
@@ -63,8 +66,7 @@ class Issue(ProjectBaseModel):
max_length=30,
choices=PRIORITY_CHOICES,
verbose_name="Issue Priority",
null=True,
blank=True,
default="none",
)
start_date = models.DateField(null=True, blank=True)
target_date = models.DateField(null=True, blank=True)
@@ -81,6 +83,8 @@ class Issue(ProjectBaseModel):
)
sort_order = models.FloatField(default=65535)
completed_at = models.DateTimeField(null=True)
archived_at = models.DateField(null=True)
is_draft = models.BooleanField(default=False)
objects = models.Manager()
issue_objects = IssueManager()
@@ -106,11 +110,7 @@ class Issue(ProjectBaseModel):
~models.Q(name="Triage"), project=self.project
).first()
self.state = random_state
if random_state.group == "started":
self.start_date = timezone.now().date()
else:
if default_state.group == "started":
self.start_date = timezone.now().date()
self.state = default_state
except ImportError:
pass
@@ -125,8 +125,6 @@ class Issue(ProjectBaseModel):
PageBlock.objects.filter(issue_id=self.id).filter().update(
completed_at=timezone.now()
)
elif self.state.group == "started":
self.start_date = timezone.now().date()
else:
PageBlock.objects.filter(issue_id=self.id).filter().update(
completed_at=None
@@ -151,9 +149,6 @@ class Issue(ProjectBaseModel):
if largest_sort_order is not None:
self.sort_order = largest_sort_order + 10000
# If adding it to started state
if self.state.group == "started":
self.start_date = timezone.now().date()
# Strip the html tags using html parser
self.description_stripped = (
None
@@ -185,6 +180,37 @@ class IssueBlocker(ProjectBaseModel):
return f"{self.block.name} {self.blocked_by.name}"
class IssueRelation(ProjectBaseModel):
RELATION_CHOICES = (
("duplicate", "Duplicate"),
("relates_to", "Relates To"),
("blocked_by", "Blocked By"),
)
issue = models.ForeignKey(
Issue, related_name="issue_relation", on_delete=models.CASCADE
)
related_issue = models.ForeignKey(
Issue, related_name="issue_related", on_delete=models.CASCADE
)
relation_type = models.CharField(
max_length=20,
choices=RELATION_CHOICES,
verbose_name="Issue Relation Type",
default="blocked_by",
)
class Meta:
unique_together = ["issue", "related_issue"]
verbose_name = "Issue Relation"
verbose_name_plural = "Issue Relations"
db_table = "issue_relations"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.related_issue.name}"
class IssueAssignee(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
@@ -207,7 +233,7 @@ class IssueAssignee(ProjectBaseModel):
class IssueLink(ProjectBaseModel):
title = models.CharField(max_length=255, null=True)
title = models.CharField(max_length=255, null=True, blank=True)
url = models.URLField()
issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, related_name="issue_link"
@@ -300,7 +326,7 @@ class IssueComment(ProjectBaseModel):
comment_json = models.JSONField(blank=True, default=dict)
comment_html = models.TextField(blank=True, default="<p></p>")
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments")
# System can also create comment
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
@@ -308,6 +334,14 @@ class IssueComment(ProjectBaseModel):
related_name="comments",
null=True,
)
access = models.CharField(
choices=(
("INTERNAL", "INTERNAL"),
("EXTERNAL", "EXTERNAL"),
),
default="INTERNAL",
max_length=100,
)
def save(self, *args, **kwargs):
self.comment_stripped = (
@@ -401,6 +435,95 @@ class IssueSequence(ProjectBaseModel):
ordering = ("-created_at",)
class IssueSubscriber(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_subscribers"
)
subscriber = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="issue_subscribers",
)
class Meta:
unique_together = ["issue", "subscriber"]
verbose_name = "Issue Subscriber"
verbose_name_plural = "Issue Subscribers"
db_table = "issue_subscribers"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.subscriber.email}"
class IssueReaction(ProjectBaseModel):
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="issue_reactions",
)
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_reactions"
)
reaction = models.CharField(max_length=20)
class Meta:
unique_together = ["issue", "actor", "reaction"]
verbose_name = "Issue Reaction"
verbose_name_plural = "Issue Reactions"
db_table = "issue_reactions"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.actor.email}"
class CommentReaction(ProjectBaseModel):
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="comment_reactions",
)
comment = models.ForeignKey(
IssueComment, on_delete=models.CASCADE, related_name="comment_reactions"
)
reaction = models.CharField(max_length=20)
class Meta:
unique_together = ["comment", "actor", "reaction"]
verbose_name = "Comment Reaction"
verbose_name_plural = "Comment Reactions"
db_table = "comment_reactions"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.actor.email}"
class IssueVote(ProjectBaseModel):
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="votes")
actor = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="votes"
)
vote = models.IntegerField(
choices=(
(-1, "DOWNVOTE"),
(1, "UPVOTE"),
),
default=1,
)
class Meta:
unique_together = ["issue", "actor",]
verbose_name = "Issue Vote"
verbose_name_plural = "Issue Votes"
db_table = "issue_votes"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.actor.email}"
# TODO: Find a better method to save the model
@receiver(post_save, sender=Issue)
def create_issue_sequence(sender, instance, created, **kwargs):
+13 -1
View File
@@ -40,6 +40,7 @@ class Module(ProjectBaseModel):
through_fields=("module", "member"),
)
view_props = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
class Meta:
unique_together = ["name", "project"]
@@ -48,6 +49,17 @@ class Module(ProjectBaseModel):
db_table = "modules"
ordering = ("-created_at",)
def save(self, *args, **kwargs):
if self._state.adding:
smallest_sort_order = Module.objects.filter(
project=self.project
).aggregate(smallest=models.Min("sort_order"))["smallest"]
if smallest_sort_order is not None:
self.sort_order = smallest_sort_order - 10000
super(Module, self).save(*args, **kwargs)
def __str__(self):
return f"{self.name} {self.start_date} {self.target_date}"
@@ -86,7 +98,7 @@ class ModuleIssue(ProjectBaseModel):
class ModuleLink(ProjectBaseModel):
title = models.CharField(max_length=255, null=True)
title = models.CharField(max_length=255, blank=True, null=True)
url = models.URLField()
module = models.ForeignKey(
Module, on_delete=models.CASCADE, related_name="link_module"
+37
View File
@@ -0,0 +1,37 @@
# Django imports
from django.db import models
# Third party imports
from .base import BaseModel
class Notification(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", related_name="notifications", on_delete=models.CASCADE
)
project = models.ForeignKey(
"db.Project", related_name="notifications", on_delete=models.CASCADE, null=True
)
data = models.JSONField(null=True)
entity_identifier = models.UUIDField(null=True)
entity_name = models.CharField(max_length=255)
title = models.TextField()
message = models.JSONField(null=True)
message_html = models.TextField(blank=True, default="<p></p>")
message_stripped = models.TextField(blank=True, null=True)
sender = models.CharField(max_length=255)
triggered_by = models.ForeignKey("db.User", related_name="triggered_notifications", on_delete=models.SET_NULL, null=True)
receiver = models.ForeignKey("db.User", related_name="received_notifications", on_delete=models.CASCADE)
read_at = models.DateTimeField(null=True)
snoozed_till = models.DateTimeField(null=True)
archived_at = models.DateTimeField(null=True)
class Meta:
verbose_name = "Notification"
verbose_name_plural = "Notifications"
db_table = "notifications"
ordering = ("-created_at",)
def __str__(self):
"""Return name of the notifications"""
return f"{self.receiver.email} <{self.workspace.name}>"
+106 -9
View File
@@ -1,9 +1,13 @@
# Python imports
from uuid import uuid4
# Django imports
from django.db import models
from django.conf import settings
from django.template.defaultfilters import slugify
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.validators import MinValueValidator, MaxValueValidator
# Modeule imports
from plane.db.mixins import AuditModel
@@ -21,16 +25,33 @@ ROLE_CHOICES = (
def get_default_props():
return {
"filters": {"type": None},
"orderBy": "-created_at",
"collapsed": True,
"issueView": "list",
"filterIssue": None,
"groupByProperty": None,
"showEmptyGroups": True,
"filters": {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
},
"display_filters": {
"group_by": None,
"order_by": '-created_at',
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
},
}
def get_default_preferences():
return {"pages": {"block_display": True}}
class Project(BaseModel):
NETWORK_CHOICES = ((0, "Secret"), (2, "Public"))
name = models.CharField(max_length=255, verbose_name="Project Name")
@@ -46,7 +67,7 @@ class Project(BaseModel):
"db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_project"
)
identifier = models.CharField(
max_length=5,
max_length=12,
verbose_name="Project Identifier",
)
default_assignee = models.ForeignKey(
@@ -74,6 +95,15 @@ class Project(BaseModel):
estimate = models.ForeignKey(
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
)
archive_in = models.IntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
)
close_in = models.IntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
)
default_state = models.ForeignKey(
"db.State", on_delete=models.SET_NULL, null=True, related_name="default_state"
)
def __str__(self):
"""Return name of the project"""
@@ -137,6 +167,20 @@ class ProjectMember(ProjectBaseModel):
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
view_props = models.JSONField(default=get_default_props)
default_props = models.JSONField(default=get_default_props)
preferences = models.JSONField(default=get_default_preferences)
sort_order = models.FloatField(default=65535)
def save(self, *args, **kwargs):
if self._state.adding:
smallest_sort_order = ProjectMember.objects.filter(
workspace_id=self.project.workspace_id, member=self.member
).aggregate(smallest=models.Min("sort_order"))["smallest"]
# Project ordering
if smallest_sort_order is not None:
self.sort_order = smallest_sort_order - 10000
super(ProjectMember, self).save(*args, **kwargs)
class Meta:
unique_together = ["project", "member"]
@@ -158,7 +202,7 @@ class ProjectIdentifier(AuditModel):
project = models.OneToOneField(
Project, on_delete=models.CASCADE, related_name="project_identifier"
)
name = models.CharField(max_length=10)
name = models.CharField(max_length=12)
class Meta:
unique_together = ["name", "workspace"]
@@ -185,3 +229,56 @@ class ProjectFavorite(ProjectBaseModel):
def __str__(self):
"""Return user of the project"""
return f"{self.user.email} <{self.project.name}>"
def get_anchor():
return uuid4().hex
def get_default_views():
return {
"list": True,
"kanban": True,
"calendar": True,
"gantt": True,
"spreadsheet": True,
}
class ProjectDeployBoard(ProjectBaseModel):
anchor = models.CharField(
max_length=255, default=get_anchor, unique=True, db_index=True
)
comments = models.BooleanField(default=False)
reactions = models.BooleanField(default=False)
inbox = models.ForeignKey(
"db.Inbox", related_name="bord_inbox", on_delete=models.SET_NULL, null=True
)
votes = models.BooleanField(default=False)
views = models.JSONField(default=get_default_views)
class Meta:
unique_together = ["project", "anchor"]
verbose_name = "Project Deploy Board"
verbose_name_plural = "Project Deploy Boards"
db_table = "project_deploy_boards"
ordering = ("-created_at",)
def __str__(self):
"""Return project and anchor"""
return f"{self.anchor} <{self.project.name}>"
class ProjectPublicMember(ProjectBaseModel):
member = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="public_project_members",
)
class Meta:
unique_together = ["project", "member"]
verbose_name = "Project Public Member"
verbose_name_plural = "Project Public Members"
db_table = "project_public_members"
ordering = ("-created_at",)
+25 -5
View File
@@ -1,6 +1,8 @@
# Python imports
from enum import unique
import uuid
import string
import random
import pytz
# Django imports
from django.db import models
@@ -8,9 +10,6 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import AbstractBaseUser, UserManager, PermissionsMixin
from django.utils import timezone
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
@@ -19,6 +18,15 @@ from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
def get_default_onboarding():
return {
"profile_complete": False,
"workspace_create": False,
"workspace_invite": False,
"workspace_join": False,
}
class User(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField(
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
@@ -31,6 +39,7 @@ class User(AbstractBaseUser, PermissionsMixin):
first_name = models.CharField(max_length=255, blank=True)
last_name = models.CharField(max_length=255, blank=True)
avatar = models.CharField(max_length=255, blank=True)
cover_image = models.URLField(blank=True, null=True, max_length=800)
# tracking metrics
date_joined = models.DateTimeField(auto_now_add=True, verbose_name="Created At")
@@ -55,7 +64,8 @@ class User(AbstractBaseUser, PermissionsMixin):
billing_address = models.JSONField(null=True)
has_billing_address = models.BooleanField(default=False)
user_timezone = models.CharField(max_length=255, default="Asia/Kolkata")
USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
user_timezone = models.CharField(max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES)
last_active = models.DateTimeField(default=timezone.now, null=True)
last_login_time = models.DateTimeField(null=True)
@@ -73,6 +83,9 @@ class User(AbstractBaseUser, PermissionsMixin):
role = models.CharField(max_length=300, null=True, blank=True)
is_bot = models.BooleanField(default=False)
theme = models.JSONField(default=dict)
display_name = models.CharField(max_length=255, default="")
is_tour_completed = models.BooleanField(default=False)
onboarding_step = models.JSONField(default=get_default_onboarding)
USERNAME_FIELD = "email"
@@ -97,6 +110,13 @@ class User(AbstractBaseUser, PermissionsMixin):
self.token = uuid.uuid4().hex + uuid.uuid4().hex
self.token_updated_at = timezone.now()
if not self.display_name:
self.display_name = (
self.email.split("@")[0]
if len(self.email.split("@"))
else "".join(random.choice(string.ascii_letters) for _ in range(6))
)
if self.is_superuser:
self.is_staff = True
+45 -4
View File
@@ -14,16 +14,56 @@ ROLE_CHOICES = (
)
def get_default_props():
return {
"filters": {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
},
"display_filters": {
"group_by": None,
"order_by": '-created_at',
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
},
"display_properties": {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
}
}
class Workspace(BaseModel):
name = models.CharField(max_length=255, verbose_name="Workspace Name")
name = models.CharField(max_length=80, verbose_name="Workspace Name")
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="owner_workspace",
)
slug = models.SlugField(max_length=100, db_index=True, unique=True)
company_size = models.PositiveIntegerField(default=10)
slug = models.SlugField(max_length=48, db_index=True, unique=True)
organization_size = models.CharField(max_length=20)
def __str__(self):
"""Return name of the Workspace"""
@@ -47,7 +87,8 @@ class WorkspaceMember(BaseModel):
)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
company_role = models.TextField(null=True, blank=True)
view_props = models.JSONField(null=True, blank=True)
view_props = models.JSONField(default=get_default_props)
default_props = models.JSONField(default=get_default_props)
class Meta:
unique_together = ["workspace", "member"]
+4 -2
View File
@@ -35,6 +35,7 @@ INSTALLED_APPS = [
"rest_framework_simplejwt.token_blacklist",
"corsheaders",
"taggit",
"django_celery_beat",
]
MIDDLEWARE = [
@@ -48,7 +49,7 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"crum.CurrentRequestUserMiddleware",
"django.middleware.gzip.GZipMiddleware",
]
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
@@ -160,7 +161,7 @@ MEDIA_URL = "/media/"
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Asia/Kolkata"
TIME_ZONE = "UTC"
USE_I18N = True
@@ -213,3 +214,4 @@ SIMPLE_JWT = {
CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_IMPORTS = ("plane.bgtasks.issue_automation_task","plane.bgtasks.exporter_expired_task")
+29 -10
View File
@@ -10,16 +10,14 @@ from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa
DEBUG = int(os.environ.get(
"DEBUG", 1
)) == 1
DEBUG = int(os.environ.get("DEBUG", 1)) == 1
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("PGUSER", "plane"),
"USER": "",
"PASSWORD": "",
@@ -27,13 +25,11 @@ DATABASES = {
}
}
DOCKERIZED = int(os.environ.get(
"DOCKERIZED", 0
)) == 1
DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
if DOCKERIZED:
DATABASES["default"] = dj_database_url.config()
@@ -63,7 +59,29 @@ if os.environ.get("SENTRY_DSN", False):
send_default_pii=True,
environment="local",
traces_sample_rate=0.7,
profiles_sample_rate=1.0,
)
else:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "DEBUG",
},
"loggers": {
"*": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": True,
},
},
}
REDIS_HOST = "localhost"
REDIS_PORT = 6379
@@ -82,8 +100,9 @@ PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
@@ -94,4 +113,4 @@ CELERY_BROKER_URL = os.environ.get("REDIS_URL")
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
+26 -13
View File
@@ -13,13 +13,11 @@ from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa
# Database
DEBUG = int(os.environ.get(
"DEBUG", 0
)) == 1
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"ENGINE": "django.db.backends.postgresql",
"NAME": "plane",
"USER": os.environ.get("PGUSER", ""),
"PASSWORD": os.environ.get("PGPASSWORD", ""),
@@ -72,8 +70,14 @@ CORS_ALLOW_HEADERS = [
]
CORS_ALLOW_CREDENTIALS = True
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
INSTALLED_APPS += ("scout_apm.django",)
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
if bool(os.environ.get("SENTRY_DSN", False)):
sentry_sdk.init(
@@ -84,11 +88,12 @@ if bool(os.environ.get("SENTRY_DSN", False)):
traces_sample_rate=1,
send_default_pii=True,
environment="production",
profiles_sample_rate=1.0,
)
if DOCKERIZED and USE_MINIO:
INSTALLED_APPS += ("storages",)
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
# The AWS secret access key to use.
@@ -96,7 +101,9 @@ if DOCKERIZED and USE_MINIO:
# The name of the bucket to store files in.
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "http://plane-minio:9000")
AWS_S3_ENDPOINT_URL = os.environ.get(
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
)
# Default permissions
AWS_DEFAULT_ACL = "public-read"
AWS_QUERYSTRING_AUTH = False
@@ -187,7 +194,10 @@ else:
# extra characters appended.
AWS_S3_FILE_OVERWRITE = False
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
STORAGES["default"] = {
"BACKEND": "django_s3_storage.storage.S3Storage",
}
# AWS Settings End
# Enable Connection Pooling (if desired)
@@ -202,9 +212,6 @@ ALLOWED_HOSTS = [
]
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
@@ -241,8 +248,9 @@ PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
@@ -264,3 +272,8 @@ GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
# Scout Settings
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
SCOUT_NAME = "Plane"
+20 -17
View File
@@ -11,13 +11,12 @@ from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa
# Database
DEBUG = int(os.environ.get(
"DEBUG", 1
)) == 1
DEBUG = int(os.environ.get("DEBUG", 1)) == 1
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("PGUSER", "plane"),
"USER": "",
"PASSWORD": "",
@@ -48,13 +47,15 @@ ALLOWED_HOSTS = ["*"]
# TODO: Make it FALSE and LIST DOMAINS IN FULL PROD.
CORS_ALLOW_ALL_ORIGINS = True
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# Make true if running in a docker environment
DOCKERIZED = int(os.environ.get(
"DOCKERIZED", 0
)) == 1
DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
@@ -66,6 +67,7 @@ sentry_sdk.init(
traces_sample_rate=1,
send_default_pii=True,
environment="staging",
profiles_sample_rate=1.0,
)
# The AWS region to connect to.
@@ -150,7 +152,9 @@ AWS_S3_SIGNATURE_VERSION = None
AWS_S3_FILE_OVERWRITE = False
# AWS Settings End
STORAGES["default"] = {
"BACKEND": "django_s3_storage.storage.S3Storage",
}
# Enable Connection Pooling (if desired)
# DATABASES['default']['ENGINE'] = 'django_postgrespool'
@@ -163,11 +167,6 @@ ALLOWED_HOSTS = [
"*",
]
DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage"
# Simplified static file serving.
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
@@ -199,15 +198,19 @@ PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "text-davinci-003")
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
redis_url = os.environ.get("REDIS_URL")
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
broker_url = (
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
)
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
+2 -4
View File
@@ -3,11 +3,10 @@
"""
# from django.contrib import admin
from django.urls import path
from django.urls import path, include, re_path
from django.views.generic import TemplateView
from django.conf import settings
from django.conf.urls import include, url, static
# from django.conf.urls.static import static
@@ -18,11 +17,10 @@ urlpatterns = [
path("", include("plane.web.urls")),
]
urlpatterns = urlpatterns + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
url(r"^__debug__/", include(debug_toolbar.urls)),
re_path(r"^__debug__/", include(debug_toolbar.urls)),
] + urlpatterns
+2 -2
View File
@@ -96,7 +96,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
chart_data = {str(date): 0 for date in date_range}
completed_issues_distribution = (
Issue.objects.filter(
Issue.issue_objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_cycle__cycle_id=cycle_id,
@@ -118,7 +118,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
chart_data = {str(date): 0 for date in date_range}
completed_issues_distribution = (
Issue.objects.filter(
Issue.issue_objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_module__module_id=module_id,
+134 -32
View File
@@ -15,7 +15,7 @@ def resolve_keys(group_keys, value):
return value
def group_results(results_data, group_by):
def group_results(results_data, group_by, sub_group_by=False):
"""group results data into certain group_by
Args:
@@ -25,38 +25,140 @@ def group_results(results_data, group_by):
Returns:
obj: grouped results
"""
response_dict = dict()
if sub_group_by:
main_responsive_dict = dict()
if group_by == "priority":
response_dict = {
"urgent": [],
"high": [],
"medium": [],
"low": [],
"None": [],
}
if sub_group_by == "priority":
main_responsive_dict = {
"urgent": {},
"high": {},
"medium": {},
"low": {},
"none": {},
}
for value in results_data:
group_attribute = resolve_keys(group_by, value)
if isinstance(group_attribute, list):
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in response_dict:
response_dict[str(attrib)].append(value)
else:
response_dict[str(attrib)] = []
response_dict[str(attrib)].append(value)
else:
if str(None) in response_dict:
response_dict[str(None)].append(value)
for value in results_data:
main_group_attribute = resolve_keys(sub_group_by, value)
group_attribute = resolve_keys(group_by, value)
if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list):
if len(main_group_attribute):
for attrib in main_group_attribute:
if str(attrib) not in main_responsive_dict:
main_responsive_dict[str(attrib)] = {}
if str(group_attribute) in main_responsive_dict[str(attrib)]:
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
else:
main_responsive_dict[str(attrib)][str(group_attribute)] = []
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
else:
response_dict[str(None)] = []
response_dict[str(None)].append(value)
else:
if str(group_attribute) in response_dict:
response_dict[str(group_attribute)].append(value)
else:
response_dict[str(group_attribute)] = []
response_dict[str(group_attribute)].append(value)
if str(None) not in main_responsive_dict:
main_responsive_dict[str(None)] = {}
return response_dict
if str(group_attribute) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(group_attribute)].append(value)
else:
main_responsive_dict[str(None)][str(group_attribute)] = []
main_responsive_dict[str(None)][str(group_attribute)].append(value)
elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list):
if str(main_group_attribute) not in main_responsive_dict:
main_responsive_dict[str(main_group_attribute)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(attrib)] = []
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(None)] = []
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list):
if len(main_group_attribute):
for main_attrib in main_group_attribute:
if str(main_attrib) not in main_responsive_dict:
main_responsive_dict[str(main_attrib)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(main_attrib)]:
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
else:
main_responsive_dict[str(main_attrib)][str(attrib)] = []
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(main_attrib)]:
main_responsive_dict[str(main_attrib)][str(None)].append(value)
else:
main_responsive_dict[str(main_attrib)][str(None)] = []
main_responsive_dict[str(main_attrib)][str(None)].append(value)
else:
if str(None) not in main_responsive_dict:
main_responsive_dict[str(None)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(attrib)].append(value)
else:
main_responsive_dict[str(None)][str(attrib)] = []
main_responsive_dict[str(None)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(None)].append(value)
else:
main_responsive_dict[str(None)][str(None)] = []
main_responsive_dict[str(None)][str(None)].append(value)
else:
main_group_attribute = resolve_keys(sub_group_by, value)
group_attribute = resolve_keys(group_by, value)
if str(main_group_attribute) not in main_responsive_dict:
main_responsive_dict[str(main_group_attribute)] = {}
if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
return main_responsive_dict
else:
response_dict = dict()
if group_by == "priority":
response_dict = {
"urgent": [],
"high": [],
"medium": [],
"low": [],
"none": [],
}
for value in results_data:
group_attribute = resolve_keys(group_by, value)
if isinstance(group_attribute, list):
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in response_dict:
response_dict[str(attrib)].append(value)
else:
response_dict[str(attrib)] = []
response_dict[str(attrib)].append(value)
else:
if str(None) in response_dict:
response_dict[str(None)].append(value)
else:
response_dict[str(None)] = []
response_dict[str(None)].append(value)
else:
if str(group_attribute) in response_dict:
response_dict[str(group_attribute)].append(value)
else:
response_dict[str(group_attribute)] = []
response_dict[str(group_attribute)].append(value)
return response_dict
+58 -34
View File
@@ -1,6 +1,7 @@
from django.utils.timezone import make_aware
from django.utils.dateparse import parse_datetime
def filter_state(params, filter, method):
if method == "GET":
states = params.get("state").split(",")
@@ -12,6 +13,17 @@ def filter_state(params, filter, method):
return filter
def filter_state_group(params, filter, method):
if method == "GET":
state_group = params.get("state_group").split(",")
if len(state_group) and "" not in state_group:
filter["state__group__in"] = state_group
else:
if params.get("state_group", None) and len(params.get("state_group")):
filter["state__group__in"] = params.get("state_group")
return filter
def filter_estimate_point(params, filter, method):
if method == "GET":
estimate_points = params.get("estimate_point").split(",")
@@ -27,25 +39,10 @@ def filter_priority(params, filter, method):
if method == "GET":
priorities = params.get("priority").split(",")
if len(priorities) and "" not in priorities:
if len(priorities) == 1 and "null" in priorities:
filter["priority__isnull"] = True
elif len(priorities) > 1 and "null" in priorities:
filter["priority__isnull"] = True
filter["priority__in"] = [p for p in priorities if p != "null"]
else:
filter["priority__in"] = [p for p in priorities if p != "null"]
filter["priority__in"] = priorities
else:
if params.get("priority", None) and len(params.get("priority")):
priorities = params.get("priority")
if len(priorities) == 1 and "null" in priorities:
filter["priority__isnull"] = True
elif len(priorities) > 1 and "null" in priorities:
filter["priority__isnull"] = True
filter["priority__in"] = [p for p in priorities if p != "null"]
else:
filter["priority__in"] = [p for p in priorities if p != "null"]
filter["priority__in"] = params.get("priority")
return filter
@@ -112,10 +109,11 @@ def filter_created_at(params, filter, method):
else:
if params.get("created_at", None) and len(params.get("created_at")):
for query in params.get("created_at"):
if query.get("timeline", "after") == "after":
filter["created_at__date__gte"] = query.get("datetime")
created_at_query = query.split(";")
if len(created_at_query) == 2 and "after" in created_at_query:
filter["created_at__date__gte"] = created_at_query[0]
else:
filter["created_at__date__lte"] = query.get("datetime")
filter["created_at__date__lte"] = created_at_query[0]
return filter
@@ -132,10 +130,11 @@ def filter_updated_at(params, filter, method):
else:
if params.get("updated_at", None) and len(params.get("updated_at")):
for query in params.get("updated_at"):
if query.get("timeline", "after") == "after":
filter["updated_at__date__gte"] = query.get("datetime")
updated_at_query = query.split(";")
if len(updated_at_query) == 2 and "after" in updated_at_query:
filter["updated_at__date__gte"] = updated_at_query[0]
else:
filter["updated_at__date__lte"] = query.get("datetime")
filter["updated_at__date__lte"] = updated_at_query[0]
return filter
@@ -152,10 +151,11 @@ def filter_start_date(params, filter, method):
else:
if params.get("start_date", None) and len(params.get("start_date")):
for query in params.get("start_date"):
if query.get("timeline", "after") == "after":
filter["start_date__gte"] = query.get("datetime")
start_date_query = query.split(";")
if len(start_date_query) == 2 and "after" in start_date_query:
filter["start_date__gte"] = start_date_query[0]
else:
filter["start_date__lte"] = query.get("datetime")
filter["start_date__lte"] = start_date_query[0]
return filter
@@ -166,16 +166,17 @@ def filter_target_date(params, filter, method):
for query in target_dates:
target_date_query = query.split(";")
if len(target_date_query) == 2 and "after" in target_date_query:
filter["target_date__gte"] = target_date_query[0]
filter["target_date__gt"] = target_date_query[0]
else:
filter["target_date__lte"] = target_date_query[0]
filter["target_date__lt"] = target_date_query[0]
else:
if params.get("target_date", None) and len(params.get("target_date")):
for query in params.get("target_date"):
if query.get("timeline", "after") == "after":
filter["target_date__gte"] = query.get("datetime")
target_date_query = query.split(";")
if len(target_date_query) == 2 and "after" in target_date_query:
filter["target_date__gt"] = target_date_query[0]
else:
filter["target_date__lte"] = query.get("datetime")
filter["target_date__lt"] = target_date_query[0]
return filter
@@ -193,10 +194,11 @@ def filter_completed_at(params, filter, method):
else:
if params.get("completed_at", None) and len(params.get("completed_at")):
for query in params.get("completed_at"):
if query.get("timeline", "after") == "after":
filter["completed_at__date__gte"] = query.get("datetime")
completed_at_query = query.split(";")
if len(completed_at_query) == 2 and "after" in completed_at_query:
filter["completed_at__date__gte"] = completed_at_query[0]
else:
filter["completed_at__lte"] = query.get("datetime")
filter["completed_at__lte"] = completed_at_query[0]
return filter
@@ -268,11 +270,31 @@ def filter_sub_issue_toggle(params, filter, method):
return filter
def filter_subscribed_issues(params, filter, method):
if method == "GET":
subscribers = params.get("subscriber").split(",")
if len(subscribers) and "" not in subscribers:
filter["issue_subscribers__subscriber_id__in"] = subscribers
else:
if params.get("subscriber", None) and len(params.get("subscriber")):
filter["issue_subscribers__subscriber_id__in"] = params.get("subscriber")
return filter
def filter_start_target_date_issues(params, filter, method):
start_target_date = params.get("start_target_date", "false")
if start_target_date == "true":
filter["target_date__isnull"] = False
filter["start_date__isnull"] = False
return filter
def issue_filters(query_params, method):
filter = dict()
ISSUE_FILTER = {
"state": filter_state,
"state_group": filter_state_group,
"estimate_point": filter_estimate_point,
"priority": filter_priority,
"parent": filter_parent,
@@ -291,6 +313,8 @@ def issue_filters(query_params, method):
"module": filter_module,
"inbox_status": filter_inbox_status,
"sub_issue": filter_sub_issue_toggle,
"subscriber": filter_subscribed_issues,
"start_target_date": filter_start_target_date_issues,
}
for key, value in ISSUE_FILTER.items():
+26 -21
View File
@@ -1,31 +1,36 @@
# base requirements
Django==3.2.19
Django==4.2.5
django-braces==1.15.0
django-taggit==3.1.0
psycopg2==2.9.5
django-oauth-toolkit==2.2.0
mistune==2.0.4
django-taggit==4.0.0
psycopg==3.1.10
django-oauth-toolkit==2.3.0
mistune==3.0.1
djangorestframework==3.14.0
redis==4.5.4
redis==4.6.0
django-nested-admin==4.0.2
django-cors-headers==3.13.0
whitenoise==6.3.0
django-allauth==0.52.0
faker==13.4.0
django-filter==22.1
django-cors-headers==4.2.0
whitenoise==6.5.0
django-allauth==0.55.2
faker==18.11.2
django-filter==23.2
jsonmodels==2.6.0
djangorestframework-simplejwt==5.2.2
sentry-sdk==1.14.0
django-s3-storage==0.13.11
djangorestframework-simplejwt==5.3.0
sentry-sdk==1.30.0
django-s3-storage==0.14.0
django-crum==0.7.9
django-guardian==2.4.0
dj_rest_auth==2.2.5
google-auth==2.16.0
google-api-python-client==2.75.0
django-redis==5.2.0
uvicorn==0.20.0
google-auth==2.22.0
google-api-python-client==2.97.0
django-redis==5.3.0
uvicorn==0.23.2
channels==4.0.0
openai==0.27.2
slack-sdk==3.20.2
celery==5.2.7
openai==0.28.0
slack-sdk==3.21.3
celery==5.3.4
django_celery_beat==2.5.0
psycopg-binary==3.1.10
psycopg-c==3.1.10
scout-apm==2.26.1
openpyxl==3.1.2
+1 -1
View File
@@ -1,3 +1,3 @@
-r base.txt
django-debug-toolbar==3.8.1
django-debug-toolbar==4.1.0
+8 -9
View File
@@ -1,12 +1,11 @@
-r base.txt
dj-database-url==1.2.0
gunicorn==20.1.0
whitenoise==6.3.0
django-storages==1.13.2
boto3==1.26.136
django-anymail==9.0
twilio==7.16.2
django-debug-toolbar==3.8.1
gevent==22.10.2
dj-database-url==2.1.0
gunicorn==21.2.0
whitenoise==6.5.0
django-storages==1.14
boto3==1.28.40
django-anymail==10.1
django-debug-toolbar==4.1.0
gevent==23.7.0
psycogreen==1.0.2
+1 -1
View File
@@ -1 +1 @@
python-3.11.4
python-3.11.5
@@ -89,8 +89,8 @@
</tr>
<tr>
<td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style="color: #3b3f44; font-family: arial,helvetica,sans-serif; font-size: 16px; line-height: 1.5;">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="https://app.plane.so/" style="v-text-anchor:middle; height: 33px; width: 301px;" arcsize="12%" fillcolor="#ffffff" strokecolor="#3f76ff" strokeweight="1px" data-btn="1">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href={{magic_url}} style="v-text-anchor:middle; height: 33px; width: 301px;" arcsize="12%" fillcolor="#ffffff" strokecolor="#3f76ff" strokeweight="1px" data-btn="1">
<w:anchorlock/>
<div style="display:none;">
<center class="default-button">
@@ -98,11 +98,11 @@
</center>
</div>
</v:roundrect>
<![endif]--> <!--[if !mso]><!-- -->
<![endif]--> <!--[if !mso]><!-- -->
<a href={{magic_url}} class="r16-r default-button" target="_blank" data-btn="1" style="font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; border-style: solid; word-wrap: break-word; display: inline-block; -webkit-text-size-adjust: none; mso-hide: all; background-color: #ffffff; border-color: #3f76ff; border-radius: 4px; border-width: 1px; color: #ffffff; font-family: arial,helvetica,sans-serif; font-size: 16px; height: 18px; padding-bottom: 7px; padding-left: 20px; padding-right: 20px; padding-top: 7px; width: 258px;">
<p style="margin: 0;"><span style="color: #3F76FF; font-size: 14px;">Open Plane</span></p>
</a>
<!--<![endif]-->
<!--<![endif]-->
</td>
</tr>
<tr class="nl2go-responsive-hide">
@@ -364,4 +364,4 @@
</tr>
</table>
</body>
</html>
</html>
@@ -0,0 +1,9 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
Dear {{username}},<br/>
Your requested Issue's data has been successfully exported from Plane. The export includes all relevant information about issues you requested from your selected projects.</br>
Please find the attachment and download the CSV file. If you have any questions or need further assistance, please don't hesitate to contact our support team at <a href = "mailto: engineering@plane.com">engineering@plane.so</a>. We're here to help!</br>
Thank you for using Plane. We hope this export will aid you in effectively managing your projects.</br>
Regards,
Team Plane
</html>
@@ -90,8 +90,8 @@
</tr>
<tr>
<td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style="color: #3b3f44; font-family: arial,helvetica,sans-serif; font-size: 16px; line-height: 1.5;">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="https://app.plane.so/" style="v-text-anchor:middle; height: 33px; width: 301px;" arcsize="12%" fillcolor="#ffffff" strokecolor="#3f76ff" strokeweight="1px" data-btn="1">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href={{invitation_url}} style="v-text-anchor:middle; height: 33px; width: 301px;" arcsize="12%" fillcolor="#ffffff" strokecolor="#3f76ff" strokeweight="1px" data-btn="1">
<w:anchorlock/>
<div style="display:none;">
<center class="default-button">
@@ -99,11 +99,11 @@
</center>
</div>
</v:roundrect>
<![endif]--> <!--[if !mso]><!-- -->
<![endif]--> <!--[if !mso]><!-- -->
<a href={{invitation_url}} class="r16-r default-button" target="_blank" data-btn="1" style="font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; border-style: solid; word-wrap: break-word; display: inline-block; -webkit-text-size-adjust: none; mso-hide: all; background-color: #ffffff; border-color: #3f76ff; border-radius: 4px; border-width: 1px; color: #ffffff; font-family: arial,helvetica,sans-serif; font-size: 16px; height: 18px; padding-bottom: 7px; padding-left: 20px; padding-right: 20px; padding-top: 7px; width: 258px;">
<p style="margin: 0;"><span style="color: #3F76FF;">Accept the invite</span></p>
</a>
<!--<![endif]-->
<!--<![endif]-->
</td>
</tr>
<tr class="nl2go-responsive-hide">
@@ -346,4 +346,4 @@
</tr>
</table>
</body>
</html>
</html>
@@ -90,8 +90,8 @@
</tr>
<tr>
<td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style="color: #3b3f44; font-family: arial,helvetica,sans-serif; font-size: 16px; line-height: 1.5;">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="https://app.plane.so/" style="v-text-anchor:middle; height: 33px; width: 301px;" arcsize="12%" fillcolor="#ffffff" strokecolor="#3f76ff" strokeweight="1px" data-btn="1">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href={{invitation_url}} style="v-text-anchor:middle; height: 33px; width: 301px;" arcsize="12%" fillcolor="#ffffff" strokecolor="#3f76ff" strokeweight="1px" data-btn="1">
<w:anchorlock/>
<div style="display:none;">
<center class="default-button">
@@ -99,11 +99,11 @@
</center>
</div>
</v:roundrect>
<![endif]--> <!--[if !mso]><!-- -->
<![endif]--> <!--[if !mso]><!-- -->
<a href={{invitation_url}} class="r16-r default-button" target="_blank" data-btn="1" style="font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; border-style: solid; word-wrap: break-word; display: inline-block; -webkit-text-size-adjust: none; mso-hide: all; background-color: #ffffff; border-color: #3f76ff; border-radius: 4px; border-width: 1px; color: #ffffff; font-family: arial,helvetica,sans-serif; font-size: 16px; height: 18px; padding-bottom: 7px; padding-left: 20px; padding-right: 20px; padding-top: 7px; width: 258px;">
<p style="margin: 0;"><span style="color: #3F76FF;">Accept the invite</span></p>
</a>
<!--<![endif]-->
<!--<![endif]-->
</td>
</tr>
<tr class="nl2go-responsive-hide">
@@ -346,4 +346,4 @@
</tr>
</table>
</body>
</html>
</html>
-70
View File
@@ -1,70 +0,0 @@
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope=app --docker
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
FROM node:18-alpine AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 plane
RUN adduser --system --uid 1001 captain
USER captain
COPY --from=installer /app/apps/app/next.config.js .
COPY --from=installer /app/apps/app/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/app/.next ./apps/app/.next
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
USER root
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN chmod +x /usr/local/bin/start.sh
USER captain
ENV NEXT_TELEMETRY_DISABLED 1
EXPOSE 3000
@@ -1,62 +0,0 @@
// ui
import { LineGraph } from "components/ui";
// types
import { IDefaultAnalyticsResponse } from "types";
// constants
import { MONTHS_LIST } from "constants/calendar";
type Props = {
defaultAnalytics: IDefaultAnalyticsResponse;
};
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => {
const currentMonth = new Date().getMonth();
const startMonth = Math.floor(currentMonth / 3) * 3 + 1;
const quarterMonthsList = [startMonth, startMonth + 1, startMonth + 2];
return (
<div className="py-3 border border-brand-base rounded-[10px]">
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
<LineGraph
data={[
{
id: "issues_closed",
color: "rgb(var(--color-accent))",
data: MONTHS_LIST.map((month) => ({
x: month.label.substring(0, 3),
y:
defaultAnalytics.issue_completed_month_wise.find(
(data) => data.month === month.value
)?.count || 0,
})),
},
]}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => {
if (quarterMonthsList.includes(data.month)) return data.count;
return 0;
})}
height="300px"
colors={(datum) => datum.color}
curve="monotoneX"
margin={{ top: 20 }}
enableSlices="x"
sliceTooltip={(datum) => (
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
{datum.slice.points[0].data.yFormatted}
<span className="text-brand-secondary"> issues closed in </span>
{datum.slice.points[0].data.xFormatted}
</div>
)}
theme={{
background: "rgb(var(--color-bg-base))",
}}
enableArea
/>
) : (
<div className="text-brand-secondary text-center text-sm py-8">No matching data found.</div>
)}
</div>
);
};
@@ -1,45 +0,0 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
// cmdk
import { Command } from "cmdk";
import { THEMES_OBJ } from "constants/themes";
import { useTheme } from "next-themes";
import { SettingIcon } from "components/icons";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
};
export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
const [mounted, setMounted] = useState(false);
const { setTheme } = useTheme();
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
{THEMES_OBJ.map((theme) => (
<Command.Item
key={theme.value}
onSelect={() => {
setTheme(theme.value);
setIsPaletteOpen(false);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
{theme.label}
</div>
</Command.Item>
))}
</>
);
};
@@ -1,928 +0,0 @@
import { useRouter } from "next/router";
import React, { useCallback, useEffect, useState } from "react";
import useSWR, { mutate } from "swr";
// icons
import {
ArrowRightIcon,
ChartBarIcon,
ChatBubbleOvalLeftEllipsisIcon,
DocumentTextIcon,
FolderPlusIcon,
InboxIcon,
LinkIcon,
MagnifyingGlassIcon,
RocketLaunchIcon,
Squares2X2Icon,
TrashIcon,
UserMinusIcon,
UserPlusIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
import {
AssignmentClipboardIcon,
ContrastIcon,
DiscordIcon,
DocumentIcon,
GithubIcon,
LayerDiagonalIcon,
PeopleGroupIcon,
SettingIcon,
ViewListIcon,
} from "components/icons";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// cmdk
import { Command } from "cmdk";
// hooks
import useProjectDetails from "hooks/use-project-details";
import useTheme from "hooks/use-theme";
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
import useDebounce from "hooks/use-debounce";
// components
import {
ShortcutsModal,
ChangeIssueState,
ChangeIssuePriority,
ChangeIssueAssignee,
ChangeInterfaceTheme,
} from "components/command-palette";
import { BulkDeleteIssuesModal } from "components/core";
import { CreateUpdateCycleModal } from "components/cycles";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { CreateUpdateModuleModal } from "components/modules";
import { CreateProjectModal } from "components/project";
import { CreateUpdateViewModal } from "components/views";
import { CreateUpdatePageModal } from "components/pages";
import { Spinner } from "components/ui";
// helpers
import {
capitalizeFirstLetter,
copyTextToClipboard,
replaceUnderscoreIfSnakeCase,
} from "helpers/string.helper";
// services
import issuesService from "services/issues.service";
import workspaceService from "services/workspace.service";
import inboxService from "services/inbox.service";
// types
import { IIssue, IWorkspaceSearchResults } from "types";
// fetch keys
import { INBOX_LIST, ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
export const CommandPalette: React.FC = () => {
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false);
const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false);
const [isCreateViewModalOpen, setIsCreateViewModalOpen] = useState(false);
const [isCreateModuleModalOpen, setIsCreateModuleModalOpen] = useState(false);
const [isBulkDeleteIssuesModalOpen, setIsBulkDeleteIssuesModalOpen] = useState(false);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [results, setResults] = useState<IWorkspaceSearchResults>({
results: {
workspace: [],
project: [],
issue: [],
cycle: [],
module: [],
issue_view: [],
page: [],
},
});
const [resultsCount, setResultsCount] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const [placeholder, setPlaceholder] = React.useState("Type a command or search...");
const [pages, setPages] = React.useState<string[]>([]);
const page = pages[pages.length - 1];
const router = useRouter();
const { workspaceSlug, projectId, issueId, inboxId } = router.query;
const { user } = useUser();
const { projectDetails } = useProjectDetails();
const { setToastAlert } = useToast();
const { toggleCollapsed } = useTheme();
const { data: issueDetails } = useSWR<IIssue | undefined>(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () =>
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
: null
);
const { data: inboxList } = useSWR(
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => inboxService.getInboxes(workspaceSlug as string, projectId as string)
: null
);
const updateIssue = useCallback(
async (formData: Partial<IIssue>) => {
if (!workspaceSlug || !projectId || !issueId) return;
mutate<IIssue>(
ISSUE_DETAILS(issueId as string),
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
...formData,
};
},
false
);
const payload = { ...formData };
await issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
.then(() => {
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
mutate(ISSUE_DETAILS(issueId as string));
})
.catch((e) => {
console.error(e);
});
},
[workspaceSlug, issueId, projectId, user]
);
const handleIssueAssignees = (assignee: string) => {
if (!issueDetails) return;
setIsPaletteOpen(false);
const updatedAssignees = issueDetails.assignees ?? [];
if (updatedAssignees.includes(assignee)) {
updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
} else {
updatedAssignees.push(assignee);
}
updateIssue({ assignees_list: updatedAssignees });
};
const copyIssueUrlToClipboard = useCallback(() => {
if (!router.query.issueId) return;
const url = new URL(window.location.href);
copyTextToClipboard(url.href)
.then(() => {
setToastAlert({
type: "success",
title: "Copied to clipboard",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Some error occurred",
});
});
}, [router, setToastAlert]);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const singleShortcutKeys = ["p", "v", "d", "h", "q", "m"];
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
if (!key) return;
const keyPressed = key.toLowerCase();
if (
!(e.target instanceof HTMLTextAreaElement) &&
!(e.target instanceof HTMLInputElement) &&
!(e.target as Element).classList?.contains("remirror-editor")
) {
if ((ctrlKey || metaKey) && keyPressed === "k") {
e.preventDefault();
setIsPaletteOpen(true);
} else if ((ctrlKey || metaKey) && keyPressed === "c") {
if (altKey) {
e.preventDefault();
copyIssueUrlToClipboard();
}
} else if (keyPressed === "c") {
e.preventDefault();
setIsIssueModalOpen(true);
} else if ((ctrlKey || metaKey) && keyPressed === "b") {
e.preventDefault();
toggleCollapsed();
} else if (key === "Delete") {
e.preventDefault();
setIsBulkDeleteIssuesModalOpen(true);
} else if (
singleShortcutKeys.includes(keyPressed) &&
(ctrlKey || metaKey || altKey || shiftKey)
) {
e.preventDefault();
} else if (keyPressed === "p") {
setIsProjectModalOpen(true);
} else if (keyPressed === "v") {
setIsCreateViewModalOpen(true);
} else if (keyPressed === "d") {
setIsCreateUpdatePageModalOpen(true);
} else if (keyPressed === "h") {
setIsShortcutsModalOpen(true);
} else if (keyPressed === "q") {
setIsCreateCycleModalOpen(true);
} else if (keyPressed === "m") {
setIsCreateModuleModalOpen(true);
}
}
},
[toggleCollapsed, copyIssueUrlToClipboard]
);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
useEffect(
() => {
if (!workspaceSlug || !projectId) return;
setIsLoading(true);
// this is done prevent subsequent api request
// or searchTerm has not been updated within last 500ms.
if (debouncedSearchTerm) {
setIsSearching(true);
workspaceService
.searchWorkspace(workspaceSlug as string, projectId as string, debouncedSearchTerm)
.then((results) => {
setResults(results);
const count = Object.keys(results.results).reduce(
(accumulator, key) => (results.results as any)[key].length + accumulator,
0
);
setResultsCount(count);
})
.finally(() => {
setIsLoading(false);
setIsSearching(false);
});
} else {
setResults({
results: {
workspace: [],
project: [],
issue: [],
cycle: [],
module: [],
issue_view: [],
page: [],
},
});
setIsLoading(false);
setIsSearching(false);
}
},
[debouncedSearchTerm, workspaceSlug, projectId] // Only call effect if debounced search term changes
);
if (!user) return null;
const createNewWorkspace = () => {
setIsPaletteOpen(false);
router.push("/create-workspace");
};
const createNewProject = () => {
setIsPaletteOpen(false);
setIsProjectModalOpen(true);
};
const createNewIssue = () => {
setIsPaletteOpen(false);
setIsIssueModalOpen(true);
};
const createNewCycle = () => {
setIsPaletteOpen(false);
setIsCreateCycleModalOpen(true);
};
const createNewView = () => {
setIsPaletteOpen(false);
setIsCreateViewModalOpen(true);
};
const createNewPage = () => {
setIsPaletteOpen(false);
setIsCreateUpdatePageModalOpen(true);
};
const createNewModule = () => {
setIsPaletteOpen(false);
setIsCreateModuleModalOpen(true);
};
const deleteIssue = () => {
setIsPaletteOpen(false);
setDeleteIssueModal(true);
};
const redirect = (path: string) => {
setIsPaletteOpen(false);
router.push(path);
};
return (
<>
<ShortcutsModal isOpen={isShortcutsModalOpen} setIsOpen={setIsShortcutsModalOpen} />
{workspaceSlug && (
<CreateProjectModal
isOpen={isProjectModalOpen}
setIsOpen={setIsProjectModalOpen}
user={user}
/>
)}
{projectId && (
<>
<CreateUpdateCycleModal
isOpen={isCreateCycleModalOpen}
handleClose={() => setIsCreateCycleModalOpen(false)}
user={user}
/>
<CreateUpdateModuleModal
isOpen={isCreateModuleModalOpen}
setIsOpen={setIsCreateModuleModalOpen}
user={user}
/>
<CreateUpdateViewModal
handleClose={() => setIsCreateViewModalOpen(false)}
isOpen={isCreateViewModalOpen}
user={user}
/>
<CreateUpdatePageModal
isOpen={isCreateUpdatePageModalOpen}
handleClose={() => setIsCreateUpdatePageModalOpen(false)}
user={user}
/>
</>
)}
{issueId && issueDetails && (
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueDetails}
user={user}
/>
)}
<CreateUpdateIssueModal
isOpen={isIssueModalOpen}
handleClose={() => setIsIssueModalOpen(false)}
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
/>
<BulkDeleteIssuesModal
isOpen={isBulkDeleteIssuesModalOpen}
setIsOpen={setIsBulkDeleteIssuesModalOpen}
user={user}
/>
<Transition.Root
show={isPaletteOpen}
afterLeave={() => {
setSearchTerm("");
}}
as={React.Fragment}
>
<Dialog as="div" className="relative z-30" onClose={() => setIsPaletteOpen(false)}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all">
<Command
filter={(value, search) => {
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
return 0;
}}
onKeyDown={(e) => {
// when search is empty and page is undefined
// when user tries to close the modal with esc
if (e.key === "Escape" && !page && !searchTerm) {
setIsPaletteOpen(false);
}
// Escape goes to previous page
// Backspace goes to previous page when search is empty
if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) {
e.preventDefault();
setPages((pages) => pages.slice(0, -1));
setPlaceholder("Type a command or search...");
}
}}
>
{issueId && issueDetails && (
<div className="flex p-3">
<p className="overflow-hidden truncate rounded-md bg-brand-surface-1 p-1 px-2 text-xs font-medium text-brand-secondary">
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
{issueDetails?.name}
</p>
</div>
)}
<div className="relative">
<MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-brand-secondary"
aria-hidden="true"
/>
<Command.Input
className="w-full border-0 border-b border-brand-base bg-transparent p-4 pl-11 text-brand-base placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder={placeholder}
value={searchTerm}
onValueChange={(e) => {
setSearchTerm(e);
}}
autoFocus
/>
</div>
<Command.List className="max-h-96 overflow-scroll p-2">
{!isLoading &&
resultsCount === 0 &&
searchTerm !== "" &&
debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-brand-secondary">
No results found.
</div>
)}
{(isLoading || isSearching) && (
<Command.Loading>
<div className="flex h-full w-full items-center justify-center py-8">
<Spinner />
</div>
</Command.Loading>
)}
{debouncedSearchTerm !== "" && (
<>
{Object.keys(results.results).map((key) => {
const section = (results.results as any)[key];
if (section.length > 0) {
return (
<Command.Group
heading={capitalizeFirstLetter(replaceUnderscoreIfSnakeCase(key))}
key={key}
>
{section.map((item: any) => {
let path = "";
let value = item.name;
let Icon: any = ArrowRightIcon;
if (key === "workspace") {
path = `/${item.slug}`;
Icon = FolderPlusIcon;
} else if (key == "project") {
path = `/${item.workspace__slug}/projects/${item.id}/issues`;
Icon = AssignmentClipboardIcon;
} else if (key === "issue") {
path = `/${item.workspace__slug}/projects/${item.project_id}/issues/${item.id}`;
// user can search id-num idnum or issue name
value = `${item.project__identifier}-${item.sequence_id} ${item.project__identifier}${item.sequence_id} ${item.name}`;
Icon = LayerDiagonalIcon;
} else if (key === "issue_view") {
path = `/${item.workspace__slug}/projects/${item.project_id}/views/${item.id}`;
Icon = ViewListIcon;
} else if (key === "module") {
path = `/${item.workspace__slug}/projects/${item.project_id}/modules/${item.id}`;
Icon = PeopleGroupIcon;
} else if (key === "page") {
path = `/${item.workspace__slug}/projects/${item.project_id}/pages/${item.id}`;
Icon = DocumentTextIcon;
} else if (key === "cycle") {
path = `/${item.workspace__slug}/projects/${item.project_id}/cycles/${item.id}`;
Icon = ContrastIcon;
}
return (
<Command.Item
key={item.id}
onSelect={() => {
router.push(path);
setIsPaletteOpen(false);
}}
value={value}
className="focus:outline-none"
>
<div className="flex items-center gap-2 overflow-hidden text-brand-secondary">
<Icon
className="h-4 w-4 text-brand-secondary"
color="#6b7280"
/>
<p className="block flex-1 truncate">{item.name}</p>
</div>
</Command.Item>
);
})}
</Command.Group>
);
}
})}
</>
)}
{!page && (
<>
{issueId && (
<>
<Command.Item
onSelect={() => {
setPlaceholder("Change state...");
setSearchTerm("");
setPages([...pages, "change-issue-state"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
Change state...
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Change priority...");
setSearchTerm("");
setPages([...pages, "change-issue-priority"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<ChartBarIcon className="h-4 w-4 text-brand-secondary" />
Change priority...
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Assign to...");
setSearchTerm("");
setPages([...pages, "change-issue-assignee"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<UsersIcon className="h-4 w-4 text-brand-secondary" />
Assign to...
</div>
</Command.Item>
<Command.Item
onSelect={() => {
handleIssueAssignees(user.id);
setSearchTerm("");
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
{issueDetails?.assignees.includes(user.id) ? (
<>
<UserMinusIcon className="h-4 w-4 text-brand-secondary" />
Un-assign from me
</>
) : (
<>
<UserPlusIcon className="h-4 w-4 text-brand-secondary" />
Assign to me
</>
)}
</div>
</Command.Item>
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
<div className="flex items-center gap-2 text-brand-secondary">
<TrashIcon className="h-4 w-4 text-brand-secondary" />
Delete issue
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
copyIssueUrlToClipboard();
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<LinkIcon className="h-4 w-4 text-brand-secondary" />
Copy issue URL to clipboard
</div>
</Command.Item>
</>
)}
<Command.Group heading="Issue">
<Command.Item
onSelect={createNewIssue}
className="focus:bg-brand-surface-2"
>
<div className="flex items-center gap-2 text-brand-secondary">
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
Create new issue
</div>
<kbd>C</kbd>
</Command.Item>
</Command.Group>
{workspaceSlug && (
<Command.Group heading="Project">
<Command.Item
onSelect={createNewProject}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
Create new project
</div>
<kbd>P</kbd>
</Command.Item>
</Command.Group>
)}
{projectId && (
<>
<Command.Group heading="Cycle">
<Command.Item
onSelect={createNewCycle}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<ContrastIcon className="h-4 w-4" color="#6b7280" />
Create new cycle
</div>
<kbd>Q</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="Module">
<Command.Item
onSelect={createNewModule}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
Create new module
</div>
<kbd>M</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="View">
<Command.Item onSelect={createNewView} className="focus:outline-none">
<div className="flex items-center gap-2 text-brand-secondary">
<ViewListIcon className="h-4 w-4" color="#6b7280" />
Create new view
</div>
<kbd>V</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="Page">
<Command.Item onSelect={createNewPage} className="focus:outline-none">
<div className="flex items-center gap-2 text-brand-secondary">
<DocumentTextIcon className="h-4 w-4" color="#6b7280" />
Create new page
</div>
<kbd>D</kbd>
</Command.Item>
</Command.Group>
{projectDetails && projectDetails.inbox_view && (
<Command.Group heading="Inbox">
<Command.Item
onSelect={() =>
redirect(
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`
)
}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<InboxIcon className="h-4 w-4" color="#6b7280" />
Open inbox
</div>
</Command.Item>
</Command.Group>
)}
</>
)}
<Command.Group heading="Workspace Settings">
<Command.Item
onSelect={() => {
setPlaceholder("Search workspace settings...");
setSearchTerm("");
setPages([...pages, "settings"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4" color="#6b7280" />
Search settings...
</div>
</Command.Item>
</Command.Group>
<Command.Group heading="Account">
<Command.Item
onSelect={createNewWorkspace}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<FolderPlusIcon className="h-4 w-4 text-brand-secondary" />
Create new workspace
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Change interface theme...");
setSearchTerm("");
setPages([...pages, "change-interface-theme"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Change interface theme...
</div>
</Command.Item>
</Command.Group>
<Command.Group heading="Help">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
const e = new KeyboardEvent("keydown", {
key: "h",
});
document.dispatchEvent(e);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<RocketLaunchIcon className="h-4 w-4 text-brand-secondary" />
Open keyboard shortcuts
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
window.open("https://docs.plane.so/", "_blank");
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<DocumentIcon className="h-4 w-4 text-brand-secondary" />
Open Plane documentation
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<DiscordIcon className="h-4 w-4" color="#6b7280" />
Join our Discord
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
window.open(
"https://github.com/makeplane/plane/issues/new/choose",
"_blank"
);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<GithubIcon className="h-4 w-4" color="#6b7280" />
Report a bug
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
(window as any).$crisp.push(["do", "chat:open"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-brand-secondary" />
Chat with us
</div>
</Command.Item>
</Command.Group>
</>
)}
{page === "settings" && workspaceSlug && (
<>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
General
</div>
</Command.Item>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Members
</div>
</Command.Item>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Billing and Plans
</div>
</Command.Item>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Integrations
</div>
</Command.Item>
<Command.Item
onSelect={() => redirect(`/${workspaceSlug}/settings/import-export`)}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" />
Import/Export
</div>
</Command.Item>
</>
)}
{page === "change-issue-state" && issueDetails && (
<>
<ChangeIssueState
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
</>
)}
{page === "change-issue-priority" && issueDetails && (
<ChangeIssuePriority
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
)}
{page === "change-issue-assignee" && issueDetails && (
<ChangeIssueAssignee
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
)}
{page === "change-interface-theme" && (
<ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />
)}
</Command.List>
</Command>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</>
);
};
@@ -1,110 +0,0 @@
// hooks
import useProjectIssuesView from "hooks/use-issues-view";
// components
import { SingleBoard } from "components/core/board-view/single-board";
// icons
import { getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types";
type Props = {
type: "issue" | "cycle" | "module";
states: IState[] | undefined;
addIssueToState: (groupTitle: string) => void;
makeIssueCopy: (issue: IIssue) => void;
handleEditIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
};
export const AllBoards: React.FC<Props> = ({
type,
states,
addIssueToState,
makeIssueCopy,
handleEditIssue,
openIssuesListModal,
handleDeleteIssue,
handleTrashBox,
removeIssue,
isCompleted = false,
user,
userAuth,
}) => {
const {
groupedByIssues,
groupByProperty: selectedGroup,
showEmptyGroups,
} = useProjectIssuesView();
return (
<>
{groupedByIssues ? (
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
{Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (!showEmptyGroups && groupedByIssues[singleGroup].length === 0) return null;
return (
<SingleBoard
key={index}
type={type}
currentState={currentState}
groupTitle={singleGroup}
handleEditIssue={handleEditIssue}
makeIssueCopy={makeIssueCopy}
addIssueToState={() => addIssueToState(singleGroup)}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={openIssuesListModal ?? null}
handleTrashBox={handleTrashBox}
removeIssue={removeIssue}
isCompleted={isCompleted}
user={user}
userAuth={userAuth}
/>
);
})}
{!showEmptyGroups && (
<div className="h-full w-96 flex-shrink-0 space-y-3 p-1">
<h2 className="text-lg font-semibold">Hidden groups</h2>
<div className="space-y-3">
{Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (groupedByIssues[singleGroup].length === 0)
return (
<div
key={index}
className="flex items-center justify-between gap-2 rounded bg-brand-surface-1 p-2 shadow"
>
<div className="flex items-center gap-2">
{currentState &&
getStateGroupIcon(currentState.group, "16", "16", currentState.color)}
<h4 className="text-sm capitalize">
{selectedGroup === "state"
? addSpaceIfCamelCase(currentState?.name ?? "")
: addSpaceIfCamelCase(singleGroup)}
</h4>
</div>
<span className="text-xs text-brand-secondary">0</span>
</div>
);
})}
</div>
</div>
)}
</div>
) : null}
</>
);
};
@@ -1,181 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import issuesService from "services/issues.service";
import projectService from "services/project.service";
// hooks
import useIssuesView from "hooks/use-issues-view";
// component
import { Avatar } from "components/ui";
// icons
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssueLabels, IState } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
currentState?: IState | null;
groupTitle: string;
addIssueToState: () => void;
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
isCompleted?: boolean;
};
export const BoardHeader: React.FC<Props> = ({
currentState,
groupTitle,
addIssueToState,
isCollapsed,
setIsCollapsed,
isCompleted = false,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView();
const { data: issueLabels } = useSWR<IIssueLabels[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
let bgColor = "#000000";
if (selectedGroup === "state") bgColor = currentState?.color ?? "#000000";
if (selectedGroup === "priority")
groupTitle === "high"
? (bgColor = "#dc2626")
: groupTitle === "medium"
? (bgColor = "#f97316")
: groupTitle === "low"
? (bgColor = "#22c55e")
: (bgColor = "#ff0000");
const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle);
switch (selectedGroup) {
case "state":
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
case "labels":
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
break;
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
title =
member?.first_name && member.first_name !== ""
? `${member.first_name} ${member.last_name}`
: member?.email ?? "";
break;
}
return title;
};
const getGroupIcon = () => {
let icon;
switch (selectedGroup) {
case "state":
icon = currentState && getStateGroupIcon(currentState.group, "16", "16", bgColor);
break;
case "priority":
icon = getPriorityIcon(groupTitle, "text-lg");
break;
case "labels":
const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
icon = (
<span
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
style={{ backgroundColor: labelColor }}
/>
);
break;
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
icon = <Avatar user={member} height="24px" width="24px" fontSize="12px" />;
break;
}
return icon;
};
return (
<div
className={`flex items-center justify-between px-1 ${
!isCollapsed ? "flex-col rounded-md bg-brand-surface-1" : ""
}`}
>
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
<div
className={`flex cursor-pointer items-center gap-x-3 ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`}
>
<span className="flex items-center">{getGroupIcon()}</span>
<h2
className="text-lg font-semibold capitalize"
style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}}
>
{getGroupTitle()}
</h2>
<span
className={`${
isCollapsed ? "ml-0.5" : ""
} min-w-[2.5rem] rounded-full bg-brand-surface-2 py-1 text-center text-xs`}
>
{groupedByIssues?.[groupTitle].length ?? 0}
</span>
</div>
</div>
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
onClick={() => {
setIsCollapsed((prevData) => !prevData);
}}
>
{isCollapsed ? (
<ArrowsPointingInIcon className="h-4 w-4" />
) : (
<ArrowsPointingOutIcon className="h-4 w-4" />
)}
</button>
{!isCompleted && selectedGroup !== "created_by" && (
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-brand-secondary outline-none duration-300 hover:bg-brand-surface-2"
onClick={addIssueToState}
>
<PlusIcon className="h-4 w-4" />
</button>
)}
</div>
</div>
);
};
@@ -1,193 +0,0 @@
import { useState } from "react";
import { useRouter } from "next/router";
// react-beautiful-dnd
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { Draggable } from "react-beautiful-dnd";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useIssuesProperties from "hooks/use-issue-properties";
// components
import { BoardHeader, SingleBoardIssue } from "components/core";
// ui
import { CustomMenu } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types";
type Props = {
type?: "issue" | "cycle" | "module";
currentState?: IState | null;
groupTitle: string;
handleEditIssue: (issue: IIssue) => void;
makeIssueCopy: (issue: IIssue) => void;
addIssueToState: () => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
handleTrashBox: (isDragging: boolean) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
isCompleted?: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
};
export const SingleBoard: React.FC<Props> = ({
type,
currentState,
groupTitle,
handleEditIssue,
makeIssueCopy,
addIssueToState,
handleDeleteIssue,
openIssuesListModal,
handleTrashBox,
removeIssue,
isCompleted = false,
user,
userAuth,
}) => {
// collapse/expand
const [isCollapsed, setIsCollapsed] = useState(true);
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssuesView();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
return (
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
<BoardHeader
addIssueToState={addIssueToState}
currentState={currentState}
groupTitle={groupTitle}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
isCompleted={isCompleted}
/>
{isCollapsed && (
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
<div
className={`relative h-full ${
orderBy !== "sort_order" && snapshot.isDraggingOver ? "bg-brand-base/20" : ""
} ${!isCollapsed ? "hidden" : "flex flex-col"}`}
ref={provided.innerRef}
{...provided.droppableProps}
>
{orderBy !== "sort_order" && (
<>
<div
className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden"
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-brand-surface-1 opacity-50`}
/>
<div
className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden"
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-brand-base p-2 text-xs`}
>
This board is ordered by{" "}
{replaceUnderscoreIfSnakeCase(
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
)}
</div>
</>
)}
<div className="pt-3 overflow-hidden overflow-y-scroll">
{groupedByIssues?.[groupTitle].map((issue, index) => (
<Draggable
key={issue.id}
draggableId={issue.id}
index={index}
isDragDisabled={
isNotAllowed || selectedGroup === "created_by" || selectedGroup === "labels"
}
>
{(provided, snapshot) => (
<SingleBoardIssue
key={index}
provided={provided}
snapshot={snapshot}
type={type}
index={index}
selectedGroup={selectedGroup}
issue={issue}
groupTitle={groupTitle}
properties={properties}
editIssue={() => handleEditIssue(issue)}
makeIssueCopy={() => makeIssueCopy(issue)}
handleDeleteIssue={handleDeleteIssue}
handleTrashBox={handleTrashBox}
removeIssue={() => {
if (removeIssue && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
}}
isCompleted={isCompleted}
user={user}
userAuth={userAuth}
/>
)}
</Draggable>
))}
<span
style={{
display: orderBy === "sort_order" ? "inline" : "none",
}}
>
{provided.placeholder}
</span>
</div>
{selectedGroup !== "created_by" && (
<div>
{type === "issue" ? (
<button
type="button"
className="flex items-center gap-2 font-medium text-brand-accent outline-none p-1"
onClick={addIssueToState}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
) : (
!isCompleted && (
<CustomMenu
customButton={
<button
type="button"
className="flex items-center gap-2 font-medium text-brand-accent outline-none"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
noBorder
>
<CustomMenu.MenuItem onClick={addIssueToState}>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)
)}
</div>
)}
</div>
)}
</StrictModeDroppable>
)}
</div>
);
};

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