Compare commits

..
Author SHA1 Message Date
sriram veeraghanta f70eae2f3b fix: package version update 2025-02-24 20:37:55 +05:30
sriram veeraghantaandGitHub cf8823fa96 Merge pull request #6664 from makeplane/canary
release: v0.25.0
2025-02-24 20:34:31 +05:30
Bavisetti NarayanandGitHub 5f3d02606c [WEB-3449] chore: changed the logic from utc to project date conversion (#6663)
* chore: changed the logic from utc to project date conversion

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

* synced docker-compose and swarm-compose

* updated the BRANCH variable

* fixes

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

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

* removed property restart from docker compose file

* added restart_policy condition in docker compose

* fix: changed restart policy to `any`

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

* updated selfhost readme

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

* add sleep in redeployStack service

* fixed typo in swarm and install script

* updated coderabbit suggestions

* added Replica Envs for services

* removed additional replica envs from docker compose file

---------

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

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

* fix archive permissions

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

* fix: reset password endpoints

* fix: intake issue list

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

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

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

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

* fix: page details instances

* fix: build errors

* refactor: page store hooks

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

* fix: animation ref type

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

* refactor: asset store

* refactor: space app file handlers

* fix: separate webhook connection params

* chore: handle undefined status

* chore: add type to upload status

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

* fix: types and errors

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

* fix: removed action btns from date range selector

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

* fix: minor formatting fix

---------

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

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

* fix: meta endpoint to return correct error message

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

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

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

* chore: code splitting for search

* fix: refactor

* fix: quick link error validation

* fix: refactor

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

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

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

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

* dev: extended sidebar implementation and code refactor

* chore: ux improvements

* chore: sidebar preference endpoint updated

* chore: sidebar preference endpoint updated

* chore: sidebar preference endpoint updated

* chore: code refactor

* chore: code refactor

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

* chore: scrollbar color token added to tailwind config

* dev: scroll area component

* chore-scroll-area-component-improvement

* fix: build error

* chore: code refactor

---------

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

* fix: command palette

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

* chore: scrollbar color token added to tailwind config

* dev: scroll area component

* chore-scroll-area-component-improvement

* fix: build error

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

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

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

* chore: add detail endpoint

* chore: getIssueMetaFromURL and retrieveWithIdentifier endpoint added

* chore: issue store updated

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

* fix: issue details permission

* fix: work item detail header

* chore: generateWorkItemLink helper function added

* chore: copyTextToClipboard helper function updated

* chore: workItemLink updated

* chore: workItemLink updated

* chore: workItemLink updated

* fix: issues navigation tab active status

* fix: invalid workitem error state

* chore: peek view parent issue redirection improvement

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

* fix: workitem empty state redirection and header

* fix: workitem empty state redirection and header

* chore: code refactor

* chore: project auth wrapper improvement

---------

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

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

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

* feat: copy invite link from workspace invitations list

* invitation reponse cleanup and logo url fix

---------

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

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

* fix: redirected to profile post deletion

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


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

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

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


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

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

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

* chore: changed the filtering logic

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

* chore: platform-specific command icons

---------

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

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

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


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

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

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

* fix: update label creation permissions

---------

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

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

* chore: separate out pie chart tooltip

* chore: remove unused any types

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

* fix: user role and member count for new workspace

* chore: set role to 20 while workspace creation

---------

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

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

* add comment

* fix double tap

* make label focus

* fix title focus

* focus discard and save

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

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

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

* fix: hover

* fix: added current plan

* feat: Return user role

* chore: remove unused imports

* fix: css

* fix: added user role in workspace switcher

* fix: return role as integer

* fix: role casing

* fix: refactor

* fix: plan pill fix

* fix: design updates

* fix: refactor

* fix: member translation

* fix: css improvements

* fix: truncate issue

* fix: workspace switcher dropdown email truncate

* fix: workspace switcher dropdown email truncate

* fix: role

---------

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

* fix: quickstart guide

* fix: link handling

* fix: home completed state

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


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

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

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

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

* fix: translation key

* chore: empty state refactor (#6404)

* chore: asset path helper hook added

* chore: detailed and simple empty state component added

* chore: section empty state component added

* chore: language translation for all empty states

* chore: new empty state implementation

* improvement: add more translations

* improvement: user permissions and workspace draft empty state

* chore: update translation structure

* chore: inbox empty states

* chore: disabled project features empty state

* chore: active cycle progress empty state

* chore: notification empty state

* chore: connections translation

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

* chore: project pages empty state and translations

* chore: project module and view related empty state

* chore: remove project draft related empty state

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

* chore: project cycles related empty state

* chore: project settings empty state

* chore: profile issue and acitivity empty state

* chore: workspace settings realted constants

* chore: stickies and home widgets empty state

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

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

* chore: minor updates

* fix: build errors

---------

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

* fix: language support fo profile (#6461)

* fix: ln support fo profile

* fix: merge changes

* fix: merge changes

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

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

* chore: updated translation structure

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

* chore: updated spanish translation

* chore: removed translation for issue priorities

* fix: build errors

* chore: minor updates

---------

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

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

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

* chore: workspace drafts constant moved to plane constant package

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

* feat: home language support without stickies

* fix: home sidebar

* fix: added missing keys

* fix: show all btn

* fix: recents empty state

* chore: translation update

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

* chore: workspace constant language support and refactor

* chore: workspace constant language support and refactor

* chore: code refactor

* chore: code refactor

* merge conflict

* chore: code refactor

---------

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

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

* chore: notification language support and refactor

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

* chore: ln support for inbox constants

* fix: snooze duration

* fix: enum

* fix: translation keys

* fix: inbox status icon

* fix: status icon

* fix: naming

---------

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

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

* fix: ln support for views constants

* fix: added translation

* fix: translation keys

* fix: access

* chore: code refactor

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

* chore: ln support workspace projects constants

* fix: translation key

* fix: removed state translation

* fix: removed state translation

* fi: added translations

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

* chore: themes language support and refactor

* chore: theme language support and refactor

* fix

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

* chore: ln support for cycles constant file

* fix: added chinese

* fix: lint

* fix: translation key

* fix: build errors

* minor updates

* chore: minor translation update

* chore: minor translation update

* refactor: move labels contants to packages

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

* chore: timezones constant moved to plane package

* chore: metadata constant code refactor

* chore: code refactor

* fix: dashboard constants moved

* chore: code refactor (#6478)

* refactor: spreadsheet constants

* chore: drafts language support (#6485)

* chore: workspace drafts language support

* chore: code refactor

* feat: ln support for notifications (#6486)

* feat: ln support for notifications

* fix: translations

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

* fix: removed use-client

* chore: removed unnecessary commnets

* chore: workspace draft language support (#6490)

* chore: workspace drafts language support

* chore: code refactor

* chore: draft language support

* Feat constant event tracker (#6479)

* fix: event tracjer constants

* fix: constants event tracker

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

* feat: added translation to projects list page

* chore: restructured translation file

* chore: module language support (#6499)

* chore: module language support added

* chore: code refactor

* chore: workspace views language support (#6492)

* chore: workspace views language support

* chore: code refactor

* feat: custom analytics language support (#6494)

* feat: custom analytics language support

* fix: key

* fix: refactoring

---------

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

* chore: minor improvements

* feat: language support for intake (#6498)

* feat: language support for intake

* fix: key name

* refactor: authentications related translations

* feat: language support issues  (#6501)

* enhancement: added translations for issue list view

* chore: added translations for issue detail widgets

* chore: added missing translations

* chore: modified issue to work items

* chore: updated translations

* Feat: workspace settings language support (#6508)

* feat: language support for workspace settings

* fix: lint

* fix: export title

* chore project settings language support (#6502)

* chore: project settings language support

* chore: code refactor

* refactor: workspace creation related translations

* chore: renamed issues to work items

* fix: build errors

* fix: lint

* chore: modified translations

* chore: remove duplicate

* improvement: french translation

* chore: chinese translation improvement

* fix: japanese translations

* chore: added spanish translation

* minor improvements

* fix: miscelleous language translations

* fix: clear_all key

* fix: moved user permission constants (#6516)

* feat: language support for  issues (#6513)

* chore: added language support to issue detail widgets

* improvement: added translation for issue detail

* enhancement: added language trasnlation to issue layouts

* chore: translation improvement (#6518)

* feat: language support description (#6519)

* enhancement: added language support for description

* fix: updated keys

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

* chore: replace missing issue occurances to work items

* fix: build errors

* minor improvements

* fix: profile links

* Feat ln cycles (#6528)

* feat: added language support for cycles

* feat: added language support for cycles

* chore: added core.json

* fix: translation keys

* fix: translation keys (#6530)

* fix: changed sidebar keys

* fix: removed extras

* fix: updated keys

* chore: optimize translation imports

* fix: updated keys (#6534)

* fix: updated keys

* fix-sub work items toasts

* chore: add missing translation and minor fixes

* chore: code refactor

* fix: language support keys (#6553)

* minor improvements

* minor fixes

* fix: remove lucide import from constants package

* chore: regenerate all translations

* chore: addded chinese and japanese translation files

* chore: remove all  from translations

* fix: added member

* fix: language support keys (#6558)

* fix: renamed keys

* fix: space app

* chore: renamed issues to work items

* chore: update site manifest

* chore: updated translations

* fix: lang keys

* chore: update translations

---------

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

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

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

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

* fix: make delete_comment_activity include comment_id

The delete issues comment webhook requires comment_id

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

* chore: renamed the display value

* chore: reverted the accounts code
2025-01-31 18:04:40 +05:30
Vamsi KrishnaandGitHub 0deec92d91 fix: cycle labels overflow issue (#6526) 2025-01-31 16:00:20 +05:30
Anmol Singh BhatiaandGitHub d2a6307bb0 fix: page version history application error (#6529) 2025-01-31 15:59:40 +05:30
Anmol Singh BhatiaandGitHub 66be0b1862 fix: version history z index (#6531) 2025-01-31 15:59:15 +05:30
sriram veeraghantaandGitHub 9ed4591edc Merge pull request #6183 from makeplane/canary
release: v0.24.1
2024-12-10 21:43:09 +05:30
1005 changed files with 29763 additions and 17559 deletions
+6
View File
@@ -25,6 +25,10 @@ on:
required: false
default: false
type: boolean
push:
branches:
- preview
- canary
env:
TARGET_BRANCH: ${{ github.ref_name }}
@@ -316,6 +320,7 @@ jobs:
retention-days: 2
path: |
${{ github.workspace }}/deploy/selfhost/setup.sh
${{ github.workspace }}/deploy/selfhost/swarm.sh
${{ github.workspace }}/deploy/selfhost/restore.sh
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
${{ github.workspace }}/deploy/selfhost/variables.env
@@ -358,6 +363,7 @@ jobs:
generate_release_notes: true
files: |
${{ github.workspace }}/deploy/selfhost/setup.sh
${{ github.workspace }}/deploy/selfhost/swarm.sh
${{ github.workspace }}/deploy/selfhost/restore.sh
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
${{ github.workspace }}/deploy/selfhost/variables.env
@@ -71,7 +71,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn lint --filter=admin
@@ -84,7 +84,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn lint --filter=space
@@ -97,7 +97,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn lint --filter=web
@@ -109,7 +109,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn build --filter=admin
@@ -121,7 +121,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn build --filter=space
@@ -133,6 +133,6 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
- run: yarn install
- run: yarn build --filter=web
+130 -4
View File
@@ -62,17 +62,143 @@ To ensure consistency throughout the source code, please keep these rules in min
- All features or bug fixes must be tested by one or more specs (unit-tests).
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
## Need help? Questions and suggestions
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Discord Server](https://discord.com/invite/A92xrEGCge).
## Ways to contribute
- Try Plane Cloud and the self hosting platform and give feedback
- Add new integrations
- Add or update translations
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
- Share your thoughts and suggestions with us
- Help create tutorials and blog posts
- Request a feature by submitting a proposal
- Report a bug
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
## Contributing to language support
This guide is designed to help contributors understand how to add or update translations in the application.
### Understanding translation structure
#### File organization
Translations are organized by language in the locales directory. Each language has its own folder containing JSON files for translations. Here's how it looks:
```
packages/i18n/src/locales/
├── en/
│ ├── core.json # Critical translations
│ └── translations.json
├── fr/
│ └── translations.json
└── [language]/
└── translations.json
```
#### Nested structure
To keep translations organized, we use a nested structure for keys. This makes it easier to manage and locate specific translations. For example:
```json
{
"issue": {
"label": "Work item",
"title": {
"label": "Work item title"
}
}
}
```
### Translation formatting guide
We use [IntlMessageFormat](https://formatjs.github.io/docs/intl-messageformat/) to handle dynamic content, such as variables and pluralization. Here's how to format your translations:
#### Examples
- **Simple variables**
```json
{
"greeting": "Hello, {name}!"
}
```
- **Pluralization**
```json
{
"items": "{count, plural, one {Work item} other {Work items}}"
}
```
### Contributing guidelines
#### Updating existing translations
1. Locate the key in `locales/<language>/translations.json`.
2. Update the value while ensuring the key structure remains intact.
3. Preserve any existing ICU formats (e.g., variables, pluralization).
#### Adding new translation keys
1. When introducing a new key, ensure it is added to **all** language files, even if translations are not immediately available. Use English as a placeholder if needed.
2. Keep the nesting structure consistent across all languages.
3. If the new key requires dynamic content (e.g., variables or pluralization), ensure the ICU format is applied uniformly across all languages.
### Adding new languages
Adding a new language involves several steps to ensure it integrates seamlessly with the project. Follow these instructions carefully:
1. **Update type definitions**
Add the new language to the TLanguage type in the language definitions file:
```typescript
// types/language.ts
export type TLanguage = "en" | "fr" | "your-lang";
```
2. **Add language configuration**
Include the new language in the list of supported languages:
```typescript
// constants/language.ts
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "English", value: "en" },
{ label: "Your Language", value: "your-lang" }
];
```
3. **Create translation files**
1. Create a new folder for your language under locales (e.g., `locales/your-lang/`).
2. Add a `translations.json` file inside the folder.
3. Copy the structure from an existing translation file and translate all keys.
4. **Update import logic**
Modify the language import logic to include your new language:
```typescript
private importLanguageFile(language: TLanguage): Promise<any> {
switch (language) {
case "your-lang":
return import("../locales/your-lang/translations.json");
// ...
}
}
```
### Quality checklist
Before submitting your contribution, please ensure the following:
- All translation keys exist in every language file.
- Nested structures match across all language files.
- ICU message formats are correctly implemented.
- All languages load without errors in the application.
- Dynamic values and pluralization work as expected.
- There are no missing or untranslated keys.
#### Pro tips
- When in doubt, refer to the English translations for context.
- Verify pluralization works with different numbers.
- Ensure dynamic values (e.g., `{name}`) are correctly interpolated.
- Double-check that nested key access paths are accurate.
Happy translating! 🌍✨
## Need help? Questions and suggestions
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Discord Server](https://discord.com/invite/A92xrEGCge).
+1 -1
View File
@@ -123,7 +123,7 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
in line with{" "}
<a
href="https://docs.plane.so/self-hosting/telemetry"
href="https://developers.plane.so/self-hosting/telemetry"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
+3 -3
View File
@@ -7,15 +7,15 @@ import { DefaultLayout } from "@/layouts/default-layout";
export const metadata: Metadata = {
title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.",
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
openGraph: {
title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.",
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
url: "https://plane.so/",
},
keywords:
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration",
"software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration",
twitter: {
site: "@planepowers",
},
@@ -336,7 +336,7 @@ export const InstanceSetupForm: FC = (props) => {
</label>
<a
tabIndex={-1}
href="https://docs.plane.so/telemetry"
href="https://developers.plane.so/self-hosting/telemetry"
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-blue-500 hover:text-blue-600"
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.24.1",
"version": "0.25.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -19,13 +19,13 @@
"@plane/ui": "*",
"@plane/utils": "*",
"@plane/services": "*",
"@sentry/nextjs": "^8.32.0",
"@sentry/nextjs": "^8.54.0",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14",
"axios": "^1.7.9",
"lodash": "^4.17.21",
"lucide-react": "^0.356.0",
"lucide-react": "^0.469.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"next": "^14.2.20",
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.24.1"
"version": "0.25.0"
}
+79 -63
View File
@@ -1,6 +1,7 @@
# Django imports
from django.utils import timezone
from lxml import html
from django.db import IntegrityError
# Third party imports
from rest_framework import serializers
@@ -138,47 +139,56 @@ class IssueSerializer(BaseSerializer):
updated_by_id = issue.updated_by_id
if assignees is not None and len(assignees):
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=assignee_id,
try:
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for assignee_id in assignees
],
batch_size=10,
)
except IntegrityError:
pass
else:
try:
# Then assign it to default assignee
if default_assignee_id is not None:
IssueAssignee.objects.create(
assignee_id=default_assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for assignee_id in assignees
],
batch_size=10,
)
else:
# Then assign it to default assignee
if default_assignee_id is not None:
IssueAssignee.objects.create(
assignee_id=default_assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
except IntegrityError:
pass
if labels is not None and len(labels):
IssueLabel.objects.bulk_create(
[
IssueLabel(
label_id=label_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label_id in labels
],
batch_size=10,
)
try:
IssueLabel.objects.bulk_create(
[
IssueLabel(
label_id=label_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label_id in labels
],
batch_size=10,
)
except IntegrityError:
pass
return issue
@@ -194,39 +204,45 @@ class IssueSerializer(BaseSerializer):
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete()
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=assignee_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for assignee_id in assignees
],
batch_size=10,
ignore_conflicts=True,
)
try:
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=assignee_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for assignee_id in assignees
],
batch_size=10,
ignore_conflicts=True,
)
except IntegrityError:
pass
if labels is not None:
IssueLabel.objects.filter(issue=instance).delete()
IssueLabel.objects.bulk_create(
[
IssueLabel(
label_id=label_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label_id in labels
],
batch_size=10,
ignore_conflicts=True,
)
try:
IssueLabel.objects.bulk_create(
[
IssueLabel(
label_id=label_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label_id in labels
],
batch_size=10,
ignore_conflicts=True,
)
except IntegrityError:
pass
# Time updation occues even when other related models are updated
instance.updated_at = timezone.now()
+14 -1
View File
@@ -28,7 +28,7 @@ from plane.db.models import (
Workspace,
UserFavorite,
)
from plane.bgtasks.webhook_task import model_activity
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from .base import BaseAPIView
@@ -326,6 +326,19 @@ class ProjectAPIEndpoint(BaseAPIView):
entity_type="project", entity_identifier=pk, project_id=pk
).delete()
project.delete()
webhook_activity.delay(
event="project",
verb="deleted",
field=None,
old_value=None,
new_value=None,
actor_id=request.user.id,
slug=slug,
current_site=request.META.get("HTTP_ORIGIN"),
event_id=project.id,
old_identifier=None,
new_identifier=None,
)
return Response(status=status.HTTP_204_NO_CONTENT)
+79 -62
View File
@@ -2,6 +2,7 @@
from django.utils import timezone
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from django.db import IntegrityError
# Third Party imports
from rest_framework import serializers
@@ -134,47 +135,56 @@ class IssueCreateSerializer(BaseSerializer):
updated_by_id = issue.updated_by_id
if assignees is not None and len(assignees):
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee=user,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user in assignees
],
batch_size=10,
)
try:
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee=user,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user in assignees
],
batch_size=10,
)
except IntegrityError:
pass
else:
# Then assign it to default assignee
if default_assignee_id is not None:
IssueAssignee.objects.create(
assignee_id=default_assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
if labels is not None and len(labels):
IssueLabel.objects.bulk_create(
[
IssueLabel(
label=label,
try:
IssueAssignee.objects.create(
assignee_id=default_assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
)
except IntegrityError:
pass
if labels is not None and len(labels):
try:
IssueLabel.objects.bulk_create(
[
IssueLabel(
label=label,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
)
except IntegrityError:
pass
return issue
@@ -190,39 +200,45 @@ class IssueCreateSerializer(BaseSerializer):
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete()
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee=user,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user in assignees
],
batch_size=10,
ignore_conflicts=True,
)
try:
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee=user,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user in assignees
],
batch_size=10,
ignore_conflicts=True,
)
except IntegrityError:
pass
if labels is not None:
IssueLabel.objects.filter(issue=instance).delete()
IssueLabel.objects.bulk_create(
[
IssueLabel(
label=label,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
ignore_conflicts=True,
)
try:
IssueLabel.objects.bulk_create(
[
IssueLabel(
label=label,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
ignore_conflicts=True,
)
except IntegrityError:
pass
# Time updation occues even when other related models are updated
instance.updated_at = timezone.now()
@@ -506,6 +522,7 @@ class IssueAttachmentLiteSerializer(DynamicBaseSerializer):
"asset",
"attributes",
# "issue_id",
"created_by",
"updated_at",
"updated_by",
"asset_url",
+2 -21
View File
@@ -90,17 +90,7 @@ class ProjectLiteSerializer(BaseSerializer):
class ProjectListSerializer(DynamicBaseSerializer):
total_issues = serializers.IntegerField(read_only=True)
archived_issues = serializers.IntegerField(read_only=True)
archived_sub_issues = serializers.IntegerField(read_only=True)
draft_issues = serializers.IntegerField(read_only=True)
draft_sub_issues = serializers.IntegerField(read_only=True)
sub_issues = serializers.IntegerField(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
member_role = serializers.IntegerField(read_only=True)
anchor = serializers.CharField(read_only=True)
@@ -113,14 +103,9 @@ class ProjectListSerializer(DynamicBaseSerializer):
if project_members is not None:
# Filter members by the project ID
return [
{
"id": member.id,
"member_id": member.member_id,
"member__display_name": member.member.display_name,
"member__avatar": member.member.avatar,
"member__avatar_url": member.member.avatar_url,
}
member.member_id
for member in project_members
if member.is_active and not member.member.is_bot
]
return []
@@ -134,10 +119,6 @@ class ProjectDetailSerializer(BaseSerializer):
default_assignee = UserLiteSerializer(read_only=True)
project_lead = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
member_role = serializers.IntegerField(read_only=True)
anchor = serializers.CharField(read_only=True)
+44 -6
View File
@@ -32,10 +32,9 @@ from django.core.exceptions import ValidationError
class WorkSpaceSerializer(DynamicBaseSerializer):
owner = UserLiteSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
logo_url = serializers.CharField(read_only=True)
role = serializers.IntegerField(read_only=True)
def validate_slug(self, value):
# Check if the slug is restricted
@@ -60,7 +59,7 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = ["name", "slug", "id"]
fields = ["name", "slug", "id", "logo_url"]
read_only_fields = fields
@@ -91,9 +90,11 @@ class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
class WorkSpaceMemberInviteSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True)
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
workspace = WorkspaceLiteSerializer(read_only=True)
invite_link = serializers.SerializerMethodField()
def get_invite_link(self, obj):
return f"/workspace-invitations/?invitation_id={obj.id}&email={obj.email}&slug={obj.workspace.slug}"
class Meta:
model = WorkspaceMemberInvite
@@ -107,6 +108,7 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
"responded_at",
"created_at",
"updated_at",
"invite_link",
]
@@ -147,6 +149,42 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
return value
def create(self, validated_data):
# Filtering the WorkspaceUserLink with the given url to check if the link already exists.
url = validated_data.get("url")
workspace_user_link = WorkspaceUserLink.objects.filter(
url=url,
workspace_id=validated_data.get("workspace_id"),
owner_id=validated_data.get("owner_id")
)
if workspace_user_link.exists():
raise serializers.ValidationError(
{"error": "URL already exists for this workspace and owner"}
)
return super().create(validated_data)
def update(self, instance, validated_data):
# Filtering the WorkspaceUserLink with the given url to check if the link already exists.
url = validated_data.get("url")
workspace_user_link = WorkspaceUserLink.objects.filter(
url=url,
workspace_id=instance.workspace_id,
owner=instance.owner
)
if workspace_user_link.exclude(pk=instance.id).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this workspace and owner"}
)
return super().update(instance, validated_data)
class IssueRecentVisitSerializer(serializers.ModelSerializer):
project_identifier = serializers.SerializerMethodField()
+6
View File
@@ -7,6 +7,7 @@ from plane.app.views import (
SavedAnalyticEndpoint,
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
ProjectStatsEndpoint,
)
@@ -43,4 +44,9 @@ urlpatterns = [
DefaultAnalyticsEndpoint.as_view(),
name="default-analytics",
),
path(
"workspaces/<str:slug>/project-stats/",
ProjectStatsEndpoint.as_view(),
name="project-analytics",
),
]
+12
View File
@@ -26,6 +26,8 @@ from plane.app.views import (
IssueBulkUpdateDateEndpoint,
IssueVersionEndpoint,
IssueDescriptionVersionEndpoint,
IssueMetaEndpoint,
IssueDetailIdentifierEndpoint,
)
urlpatterns = [
@@ -278,4 +280,14 @@ urlpatterns = [
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/meta/",
IssueMetaEndpoint.as_view(),
name="issue-meta",
),
path(
"workspaces/<str:slug>/work-items/<str:project_identifier>-<str:issue_identifier>/",
IssueDetailIdentifierEndpoint.as_view(),
name="issue-detail-identifier",
),
]
+5
View File
@@ -23,6 +23,11 @@ urlpatterns = [
ProjectViewSet.as_view({"get": "list", "post": "create"}),
name="project",
),
path(
"workspaces/<str:slug>/projects/details/",
ProjectViewSet.as_view({"get": "list_detail"}),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:pk>/",
ProjectViewSet.as_view(
+3
View File
@@ -116,6 +116,8 @@ from .issue.base import (
IssuePaginatedViewSet,
IssueDetailEndpoint,
IssueBulkUpdateDateEndpoint,
IssueMetaEndpoint,
IssueDetailIdentifierEndpoint,
)
from .issue.activity import IssueActivityEndpoint
@@ -190,6 +192,7 @@ from .analytic.base import (
SavedAnalyticEndpoint,
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
ProjectStatsEndpoint,
)
from .notification.base import (
+82 -2
View File
@@ -3,7 +3,7 @@ from django.db.models import Count, F, Sum, Q
from django.db.models.functions import ExtractMonth
from django.utils import timezone
from django.db.models.functions import Concat
from django.db.models import Case, When, Value
from django.db.models import Case, When, Value, OuterRef, Func
from django.db import models
# Third party imports
@@ -15,7 +15,16 @@ from plane.app.permissions import WorkSpaceAdminPermission
from plane.app.serializers import AnalyticViewSerializer
from plane.app.views.base import BaseAPIView, BaseViewSet
from plane.bgtasks.analytic_plot_export import analytic_export_task
from plane.db.models import AnalyticView, Issue, Workspace
from plane.db.models import (
AnalyticView,
Issue,
Workspace,
Project,
ProjectMember,
Cycle,
Module,
)
from plane.utils.analytics_plot import build_graph_plot
from plane.utils.issue_filters import issue_filters
from plane.app.permissions import allow_permission, ROLE
@@ -441,3 +450,74 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
},
status=status.HTTP_200_OK,
)
class ProjectStatsEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
fields = request.GET.get("fields", "").split(",")
project_ids = request.GET.get("project_ids", "")
valid_fields = {
"total_issues",
"completed_issues",
"total_members",
"total_cycles",
"total_modules",
}
requested_fields = set(filter(None, fields)) & valid_fields
if not requested_fields:
requested_fields = valid_fields
projects = Project.objects.filter(workspace__slug=slug)
if project_ids:
projects = projects.filter(id__in=project_ids.split(","))
annotations = {}
if "total_issues" in requested_fields:
annotations["total_issues"] = (
Issue.issue_objects.filter(project_id=OuterRef("pk"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "completed_issues" in requested_fields:
annotations["completed_issues"] = (
Issue.issue_objects.filter(
project_id=OuterRef("pk"), state__group="completed"
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "total_cycles" in requested_fields:
annotations["total_cycles"] = (
Cycle.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "total_modules" in requested_fields:
annotations["total_modules"] = (
Module.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "total_members" in requested_fields:
annotations["total_members"] = (
ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
projects = projects.annotate(**annotations).values("id", *requested_fields)
return Response(projects, status=status.HTTP_200_OK)
+19 -3
View File
@@ -5,6 +5,7 @@ import uuid
from django.conf import settings
from django.http import HttpResponseRedirect
from django.utils import timezone
from django.db import IntegrityError
# Third party imports
from rest_framework import status
@@ -679,15 +680,30 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
[self.save_project_cover(asset, project_id) for asset in assets]
if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION:
assets.update(issue_id=entity_id)
# For some cases, the bulk api is called after the issue is deleted creating
# an integrity error
try:
assets.update(issue_id=entity_id)
except IntegrityError:
pass
if asset.entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
assets.update(comment_id=entity_id)
# For some cases, the bulk api is called after the comment is deleted
# creating an integrity error
try:
assets.update(comment_id=entity_id)
except IntegrityError:
pass
if asset.entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
assets.update(page_id=entity_id)
if asset.entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION:
assets.update(draft_issue_id=entity_id)
# For some cases, the bulk api is called after the draft issue is deleted
# creating an integrity error
try:
assets.update(draft_issue_id=entity_id)
except IntegrityError:
pass
return Response(status=status.HTTP_204_NO_CONTENT)
+4 -4
View File
@@ -134,11 +134,11 @@ class CycleViewSet(BaseViewSet):
)
)
.annotate(
pending_issues=Count(
cancelled_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group__in=["backlog", "unstarted", "started"],
issue_cycle__issue__state__group__in=["cancelled"],
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
@@ -227,7 +227,7 @@ class CycleViewSet(BaseViewSet):
"is_favorite",
"total_issues",
"completed_issues",
"pending_issues",
"cancelled_issues",
"assignee_ids",
"status",
"version",
@@ -259,7 +259,7 @@ class CycleViewSet(BaseViewSet):
# meta fields
"is_favorite",
"total_issues",
"pending_issues",
"cancelled_issues",
"completed_issues",
"assignee_ids",
"status",
+8 -6
View File
@@ -3,7 +3,7 @@ import os
from typing import List, Dict, Tuple
# Third party import
import litellm
from openai import OpenAI
import requests
from rest_framework import status
@@ -116,12 +116,14 @@ def get_llm_response(task, prompt, api_key: str, model: str, provider: str) -> T
if provider.lower() == "gemini":
model = f"gemini/{model}"
response = litellm.completion(
client = OpenAI(api_key=api_key)
chat_completion = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": final_text}],
api_key=api_key,
messages=[
{"role": "user", "content": final_text}
]
)
text = response.choices[0].message.content.strip()
text = chat_completion.choices[0].message.content
return text, None
except Exception as e:
log_exception(e)
@@ -175,7 +177,7 @@ class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug):
api_key, model, provider = get_llm_config()
if not api_key or not model or not provider:
return Response(
{"error": "LLM provider API key and model are required"},
+5 -2
View File
@@ -174,14 +174,17 @@ class IntakeIssueViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
intake_id = Intake.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
if not intake:
return Response({"error": "Intake not found"}, status=status.HTTP_404_NOT_FOUND)
project = Project.objects.get(pk=project_id)
filters = issue_filters(request.GET, "GET", "issue__")
intake_issue = (
IntakeIssue.objects.filter(
intake_id=intake_id.id, project_id=project_id, **filters
intake_id=intake.id, project_id=project_id, **filters
)
.select_related("issue")
.prefetch_related("issue__labels")
+190 -1
View File
@@ -547,7 +547,7 @@ class IssueViewSet(BaseViewSet):
)
"""
if the role is guest and guest_view_all_features is false and owned by is not
if the role is guest and guest_view_all_features is false and owned by is not
the requesting user then dont show the issue
"""
@@ -1096,3 +1096,192 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
return Response(
{"message": "Issues updated successfully"}, status=status.HTTP_200_OK
)
class IssueMetaEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="PROJECT")
def get(self, request, slug, project_id, issue_id):
issue = Issue.issue_objects.only("sequence_id", "project__identifier").get(
id=issue_id, project_id=project_id, workspace__slug=slug
)
return Response(
{
"sequence_id": issue.sequence_id,
"project_identifier": issue.project.identifier,
},
status=status.HTTP_200_OK,
)
class IssueDetailIdentifierEndpoint(BaseAPIView):
def strict_str_to_int(self, s):
if not s.isdigit() and not (s.startswith('-') and s[1:].isdigit()):
raise ValueError("Invalid integer string")
return int(s)
def get(self, request, slug, project_identifier, issue_identifier):
# Check if the issue identifier is a valid integer
try:
issue_identifier = self.strict_str_to_int(issue_identifier)
except ValueError:
return Response(
{"error": "Invalid issue identifier"},
status=status.HTTP_400_BAD_REQUEST,
)
# Fetch the project
project = Project.objects.get(
identifier__iexact=project_identifier,
workspace__slug=slug,
)
# Check if the user is a member of the project
if not ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project.id,
member=request.user,
is_active=True,
).exists():
return Response(
{"error": "You are not allowed to view this issue"},
status=status.HTTP_403_FORBIDDEN,
)
# Fetch the issue
issue = (
Issue.issue_objects.filter(project_id=project.id)
.filter(workspace__slug=slug)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[
:1
]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(sequence_id=issue_identifier)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("issue", "actor"),
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project.id,
issue__sequence_id=issue_identifier,
subscriber=request.user,
)
)
)
).first()
# Check if the issue exists
if not issue:
return Response(
{"error": "The required object does not exist."},
status=status.HTTP_404_NOT_FOUND,
)
"""
if the role is guest and guest_view_all_features is false and owned by is not
the requesting user then dont show the issue
"""
if (
ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project.id,
member=request.user,
role=5,
is_active=True,
).exists()
and not project.guest_view_all_features
and not issue.created_by == request.user
):
return Response(
{"error": "You are not allowed to view this issue"},
status=status.HTTP_400_BAD_REQUEST,
)
recent_visited_task.delay(
slug=slug,
entity_name="issue",
entity_identifier=str(issue.id),
user_id=str(request.user.id),
project_id=str(project.id),
)
# Serialize the issue
serializer = IssueDetailSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
+26 -17
View File
@@ -5,6 +5,7 @@ import json
from django.utils import timezone
from django.db.models import Exists
from django.core.serializers.json import DjangoJSONEncoder
from django.db import IntegrityError
# Third Party imports
from rest_framework.response import Response
@@ -164,24 +165,32 @@ class CommentReactionViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def create(self, request, slug, project_id, comment_id):
serializer = CommentReactionSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id, actor_id=request.user.id, comment_id=comment_id
try:
serializer = CommentReactionSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
actor_id=request.user.id,
comment_id=comment_id,
)
issue_activity.delay(
type="comment_reaction.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "Reaction already exists for the user"},
status=status.HTTP_400_BAD_REQUEST,
)
issue_activity.delay(
type="comment_reaction.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def destroy(self, request, slug, project_id, comment_id, reaction_code):
+18 -2
View File
@@ -35,7 +35,9 @@ class LabelViewSet(BaseViewSet):
.order_by("sort_order")
)
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
@invalidate_cache(
path="/api/workspaces/:slug/labels/", url_params=True, user=False, multiple=True
)
@allow_permission([ROLE.ADMIN])
def create(self, request, slug, project_id):
try:
@@ -53,6 +55,20 @@ class LabelViewSet(BaseViewSet):
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
@allow_permission([ROLE.ADMIN])
def partial_update(self, request, *args, **kwargs):
# Check if the label name is unique within the project
if (
"name" in request.data
and Label.objects.filter(
project_id=kwargs["project_id"], name=request.data["name"]
)
.exclude(pk=kwargs["pk"])
.exists()
):
return Response(
{"error": "Label with the same name already exists in the project"},
status=status.HTTP_400_BAD_REQUEST,
)
# call the parent method to perform the update
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
@@ -72,7 +88,7 @@ class BulkCreateIssueLabelsEndpoint(BaseAPIView):
Label(
name=label.get("name", "Migrated"),
description=label.get("description", "Migrated Issue"),
color=f"#{random.randint(0, 0xFFFFFF+1):06X}",
color=f"#{random.randint(0, 0xFFFFFF + 1):06X}",
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
+2 -3
View File
@@ -272,10 +272,9 @@ class IssueRelationViewSet(BaseViewSet):
issue_relations = IssueRelation.objects.filter(
workspace__slug=slug,
project_id=project_id,
).filter(
Q(issue_id=related_issue, related_issue_id=issue_id) |
Q(issue_id=issue_id, related_issue_id=related_issue)
Q(issue_id=related_issue, related_issue_id=issue_id)
| Q(issue_id=issue_id, related_issue_id=related_issue)
)
issue_relations = issue_relations.first()
current_instance = json.dumps(
+1 -1
View File
@@ -280,7 +280,7 @@ class ModuleIssueViewSet(BaseViewSet):
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
{"module_name": module_issue.first().module.name}
{"module_name": module_issue.first().module.name if (module_issue.first() and module_issue.first().module) else None}
),
epoch=int(timezone.now().timestamp()),
notification=True,
+11 -1
View File
@@ -40,7 +40,7 @@ from ..base import BaseAPIView, BaseViewSet
from plane.bgtasks.page_transaction_task import page_transaction
from plane.bgtasks.page_version_task import page_version
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.bgtasks.copy_s3_object import copy_s3_objects
def unarchive_archive_page_and_descendants(page_id, archived_at):
# Your SQL query
@@ -597,6 +597,16 @@ class PageDuplicateEndpoint(BaseAPIView):
page_transaction.delay(
{"description_html": page.description_html}, None, page.id
)
# Copy the s3 objects uploaded in the page
copy_s3_objects.delay(
entity_name="PAGE",
entity_identifier=page.id,
project_id=project_id,
slug=slug,
user_id=request.user.id,
)
page = (
Page.objects.filter(pk=page.id)
.annotate(
+83 -89
View File
@@ -6,7 +6,7 @@ import json
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.core.serializers.json import DjangoJSONEncoder
# Third Party imports
@@ -25,12 +25,9 @@ from plane.app.serializers import (
from plane.app.permissions import ProjectMemberPermission, allow_permission, ROLE
from plane.db.models import (
UserFavorite,
Cycle,
Intake,
DeployBoard,
IssueUserProperty,
Issue,
Module,
Project,
ProjectIdentifier,
ProjectMember,
@@ -39,7 +36,7 @@ from plane.db.models import (
WorkspaceMember,
)
from plane.utils.cache import cache_response
from plane.bgtasks.webhook_task import model_activity
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.utils.exception_logger import log_exception
@@ -73,36 +70,6 @@ class ProjectViewSet(BaseViewSet):
)
)
)
.annotate(
is_member=Exists(
ProjectMember.objects.filter(
member=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
is_active=True,
)
)
)
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_cycles=Cycle.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_modules=Module.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
member_role=ProjectMember.objects.filter(
project_id=OuterRef("pk"),
@@ -133,7 +100,7 @@ class ProjectViewSet(BaseViewSet):
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug):
def list_detail(self, request, slug):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
projects = self.get_queryset().order_by("sort_order", "name")
if WorkspaceMember.objects.filter(
@@ -170,6 +137,73 @@ class ProjectViewSet(BaseViewSet):
).data
return Response(projects, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug):
sort_order = ProjectMember.objects.filter(
member=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
is_active=True,
).values("sort_order")
projects = (
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
.select_related(
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate(
member_role=ProjectMember.objects.filter(
project_id=OuterRef("pk"),
member_id=self.request.user.id,
is_active=True,
).values("role")
)
.annotate(inbox_view=F("intake_view"))
.annotate(sort_order=Subquery(sort_order))
.distinct()
).values(
"id",
"name",
"identifier",
"sort_order",
"logo_props",
"member_role",
"archived_at",
"workspace",
"cycle_view",
"issue_views_view",
"module_view",
"page_view",
"inbox_view",
"project_lead",
"created_at",
"updated_at",
"created_by",
"updated_by",
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=5
).exists():
projects = projects.filter(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=15
).exists():
projects = projects.filter(
Q(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
| Q(network=2)
)
return Response(projects, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
@@ -182,58 +216,6 @@ class ProjectViewSet(BaseViewSet):
)
.filter(archived_at__isnull=True)
.filter(pk=pk)
.annotate(
total_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("pk")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("pk"), parent__isnull=False
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
archived_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"), archived_at__isnull=False
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
archived_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
archived_at__isnull=False,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
draft_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"), is_draft=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
draft_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
is_draft=True,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
).first()
if project is None:
@@ -462,7 +444,19 @@ class ProjectViewSet(BaseViewSet):
):
project = Project.objects.get(pk=pk)
project.delete()
webhook_activity.delay(
event="project",
verb="deleted",
field=None,
old_value=None,
new_value=None,
actor_id=request.user.id,
slug=slug,
current_site=request.META.get("HTTP_ORIGIN"),
event_id=project.id,
old_identifier=None,
new_identifier=None,
)
# Delete the project members
DeployBoard.objects.filter(project_id=pk, workspace__slug=slug).delete()
+1 -1
View File
@@ -120,7 +120,7 @@ class WebhookLogsEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def get(self, request, slug, webhook_id):
webhook_logs = WebhookLog.objects.filter(
workspace__slug=slug, webhook_id=webhook_id
workspace__slug=slug, webhook=webhook_id
)
serializer = WebhookLogSerializer(webhook_logs, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
+16 -17
View File
@@ -7,9 +7,11 @@ from datetime import date
from dateutil.relativedelta import relativedelta
from django.db import IntegrityError
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
from django.db.models.fields import DateField
from django.db.models.functions import Cast, ExtractDay, ExtractWeek
# Django imports
from django.http import HttpResponse
from django.utils import timezone
@@ -62,12 +64,6 @@ class WorkSpaceViewSet(BaseViewSet):
.values("count")
)
issue_count = (
Issue.issue_objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
return (
self.filter_queryset(super().get_queryset().select_related("owner"))
.order_by("name")
@@ -76,8 +72,6 @@ class WorkSpaceViewSet(BaseViewSet):
workspace_member__is_active=True,
)
.annotate(total_members=member_count)
.annotate(total_issues=issue_count)
.select_related("owner")
)
def create(self, request):
@@ -123,7 +117,14 @@ class WorkSpaceViewSet(BaseViewSet):
role=20,
company_role=request.data.get("company_role", ""),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
# Get total members and role
total_members=WorkspaceMember.objects.filter(workspace_id=serializer.data["id"]).count()
data = serializer.data
data["total_members"] = total_members
data["role"] = 20
return Response(data, status=status.HTTP_201_CREATED)
return Response(
[serializer.errors[error][0] for error in serializer.errors],
status=status.HTTP_400_BAD_REQUEST,
@@ -166,11 +167,9 @@ class UserWorkSpacesEndpoint(BaseAPIView):
.values("count")
)
issue_count = (
Issue.issue_objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
role = (
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member=request.user, is_active=True)
.values("role")
)
workspace = (
@@ -182,19 +181,19 @@ class UserWorkSpacesEndpoint(BaseAPIView):
),
)
)
.select_related("owner")
.annotate(total_members=member_count)
.annotate(total_issues=issue_count)
.annotate(role=role, total_members=member_count)
.filter(
workspace_member__member=request.user, workspace_member__is_active=True
)
.distinct()
)
workspaces = WorkSpaceSerializer(
self.filter_queryset(workspace),
fields=fields if fields else None,
many=True,
).data
return Response(workspaces, status=status.HTTP_200_OK)
@@ -4,6 +4,7 @@ from rest_framework.response import Response
# Django modules
from django.db.models import Q
from django.db import IntegrityError
# Module imports
from plane.app.views.base import BaseAPIView
@@ -31,16 +32,21 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
serializer = UserFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
user_id=request.user.id,
workspace=workspace,
project_id=request.data.get("project_id", None),
try:
workspace = Workspace.objects.get(slug=slug)
serializer = UserFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
user_id=request.user.id,
workspace=workspace,
project_id=request.data.get("project_id", None),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "Favorite already exists"}, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def patch(self, request, slug, favorite_id):
@@ -251,8 +251,7 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
super()
.get_queryset()
.filter(email=self.request.user.email)
.select_related("workspace", "workspace__owner", "created_by")
.annotate(total_members=Count("workspace__workspace_member"))
.select_related("workspace")
)
@invalidate_cache(path="/api/workspaces/", user=False)
@@ -21,7 +21,7 @@ class QuickLinkViewSet(BaseViewSet):
serializer = WorkspaceUserLinkSerializer(data=request.data)
if serializer.is_valid():
serializer.save(workspace_id=workspace.id, owner=request.user)
serializer.save(workspace_id=workspace.id, owner_id=request.user.id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -30,7 +30,6 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView):
keys = [
key
for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices
if key not in ["projects"]
]
for preference in keys:
@@ -40,20 +39,28 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView):
preference = WorkspaceUserPreference.objects.bulk_create(
[
WorkspaceUserPreference(
key=key, user=request.user, workspace=workspace
key=key, user=request.user, workspace=workspace, sort_order=(65535 + (i*10000))
)
for key in create_preference_keys
for i, key in enumerate(create_preference_keys)
],
batch_size=10,
ignore_conflicts=True,
)
preference = WorkspaceUserPreference.objects.filter(
preferences = WorkspaceUserPreference.objects.filter(
user=request.user, workspace_id=workspace.id
)
).order_by("sort_order").values("key", "is_pinned", "sort_order")
user_preferences = {}
for preference in preferences:
user_preferences[(str(preference["key"]))] = {
"is_pinned": preference["is_pinned"],
"sort_order": preference["sort_order"],
}
return Response(
preference.values("key", "is_pinned", "sort_order"),
user_preferences,
status=status.HTTP_200_OK,
)
@@ -100,8 +100,20 @@ class ResetPasswordEndpoint(View):
def post(self, request, uidb64, token):
try:
# Decode the id from the uidb64
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
try:
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
except (ValueError, User.DoesNotExist):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD_TOKEN"],
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = urljoin(
base_host(request=request, is_app=True),
"accounts/reset-password?" + urlencode(params),
)
return HttpResponseRedirect(url)
# check if the token is valid for the user
if not PasswordResetTokenGenerator().check_token(user, token):
+150
View File
@@ -0,0 +1,150 @@
# Python imports
import uuid
import base64
import requests
from bs4 import BeautifulSoup
# Django imports
from django.conf import settings
# Module imports
from plane.db.models import FileAsset, Page, Issue
from plane.utils.exception_logger import log_exception
from plane.settings.storage import S3Storage
from celery import shared_task
def get_entity_id_field(entity_type, entity_id):
entity_mapping = {
FileAsset.EntityTypeContext.WORKSPACE_LOGO: {"workspace_id": entity_id},
FileAsset.EntityTypeContext.PROJECT_COVER: {"project_id": entity_id},
FileAsset.EntityTypeContext.USER_AVATAR: {"user_id": entity_id},
FileAsset.EntityTypeContext.USER_COVER: {"user_id": entity_id},
FileAsset.EntityTypeContext.ISSUE_ATTACHMENT: {"issue_id": entity_id},
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION: {"issue_id": entity_id},
FileAsset.EntityTypeContext.PAGE_DESCRIPTION: {"page_id": entity_id},
FileAsset.EntityTypeContext.COMMENT_DESCRIPTION: {"comment_id": entity_id},
FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION: {
"draft_issue_id": entity_id
},
}
return entity_mapping.get(entity_type, {})
def extract_asset_ids(html, tag):
try:
soup = BeautifulSoup(html, "html.parser")
return [tag.get("src") for tag in soup.find_all(tag) if tag.get("src")]
except Exception as e:
log_exception(e)
return []
def replace_asset_ids(html, tag, duplicated_assets):
try:
soup = BeautifulSoup(html, "html.parser")
for mention_tag in soup.find_all(tag):
for asset in duplicated_assets:
if mention_tag.get("src") == asset["old_asset_id"]:
mention_tag["src"] = asset["new_asset_id"]
return str(soup)
except Exception as e:
log_exception(e)
return html
def update_description(entity, duplicated_assets, tag):
updated_html = replace_asset_ids(entity.description_html, tag, duplicated_assets)
entity.description_html = updated_html
entity.save()
return updated_html
# Get the description binary and description from the live server
def sync_with_external_service(entity_name, description_html):
try:
data = {
"description_html": description_html,
"variant": "rich" if entity_name == "PAGE" else "document",
}
response = requests.post(
f"{settings.LIVE_BASE_URL}/convert-document/",
json=data,
headers=None,
)
if response.status_code == 200:
return response.json()
except requests.RequestException as e:
log_exception(e)
return {}
@shared_task
def copy_s3_objects(entity_name, entity_identifier, project_id, slug, user_id):
"""
Step 1: Extract asset ids from the description_html of the entity
Step 2: Duplicate the assets
Step 3: Update the description_html of the entity with the new asset ids (change the src of img tag)
Step 4: Request the live server to generate the description_binary and description for the entity
"""
try:
model_class = {"PAGE": Page, "ISSUE": Issue}.get(entity_name)
if not model_class:
raise ValueError(f"Unsupported entity_name: {entity_name}")
entity = model_class.objects.get(id=entity_identifier)
asset_ids = extract_asset_ids(entity.description_html, "image-component")
duplicated_assets = []
workspace = entity.workspace
storage = S3Storage()
original_assets = FileAsset.objects.filter(
workspace=workspace, project_id=project_id, id__in=asset_ids
)
for original_asset in original_assets:
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
duplicated_asset = FileAsset.objects.create(
attributes={
"name": original_asset.attributes.get("name"),
"type": original_asset.attributes.get("type"),
"size": original_asset.attributes.get("size"),
},
asset=destination_key,
size=original_asset.size,
workspace=workspace,
created_by_id=user_id,
entity_type=original_asset.entity_type,
project_id=project_id,
storage_metadata=original_asset.storage_metadata,
**get_entity_id_field(original_asset.entity_type, entity_identifier),
)
storage.copy_object(original_asset.asset, destination_key)
duplicated_assets.append(
{
"new_asset_id": str(duplicated_asset.id),
"old_asset_id": str(original_asset.id),
}
)
if duplicated_assets:
FileAsset.objects.filter(
pk__in=[item["new_asset_id"] for item in duplicated_assets]
).update(is_uploaded=True)
updated_html = update_description(
entity, duplicated_assets, "image-component"
)
external_data = sync_with_external_service(entity_name, updated_html)
if external_data:
entity.description = external_data.get("description")
entity.description_binary = base64.b64decode(
external_data.get("description_binary")
)
entity.save()
return
except Exception as e:
log_exception(e)
return []
+4 -1
View File
@@ -82,7 +82,10 @@ def soft_delete_related_objects(app_label, model_name, instance_pk, using=None):
)
else:
# Handle other relationships
related_queryset = getattr(instance, related_name).all()
related_queryset = getattr(instance, related_name)(
manager="objects"
).all()
for related_obj in related_queryset:
if hasattr(related_obj, "deleted_at"):
if not related_obj.deleted_at:
@@ -9,10 +9,10 @@ from celery import shared_task
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
# Module imports
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
from plane.db.models import (
CommentReaction,
Cycle,
@@ -32,7 +32,7 @@ from plane.settings.redis import redis_instance
from plane.utils.exception_logger import log_exception
from plane.bgtasks.webhook_task import webhook_activity
from plane.utils.issue_relation_mapper import get_inverse_relation
from plane.utils.valid_uuid import is_valid_uuid
# Track Changes in name
def track_name(
@@ -738,8 +738,10 @@ def delete_comment_activity(
issue_activities,
epoch,
):
requested_data = json.loads(requested_data) if requested_data is not None else None
issue_activities.append(
IssueActivity(
issue_comment_id=requested_data.get("comment_id", None),
issue_id=issue_id,
project_id=project_id,
workspace_id=workspace_id,
@@ -788,14 +790,15 @@ def create_cycle_issue_activity(
issue_id=updated_record.get("issue_id"),
actor_id=actor_id,
verb="updated",
old_value=old_cycle.name,
new_value=new_cycle.name,
old_value=old_cycle.name if old_cycle else "",
new_value=new_cycle.name if new_cycle else "",
field="cycles",
project_id=project_id,
workspace_id=workspace_id,
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
old_identifier=old_cycle.id,
new_identifier=new_cycle.id,
comment=f"""updated cycle from {old_cycle.name if old_cycle else ""}
to {new_cycle.name if new_cycle else ""}""",
old_identifier=old_cycle.id if old_cycle else None,
new_identifier=new_cycle.id if new_cycle else None,
epoch=epoch,
)
)
@@ -891,11 +894,11 @@ def create_module_issue_activity(
actor_id=actor_id,
verb="created",
old_value="",
new_value=module.name,
new_value=module.name if module else "",
field="modules",
project_id=project_id,
workspace_id=workspace_id,
comment=f"added module {module.name}",
comment=f"added module {module.name if module else ''}",
new_identifier=requested_data.get("module_id"),
epoch=epoch,
)
@@ -1411,7 +1414,7 @@ def delete_issue_relation_activity(
),
project_id=project_id,
workspace_id=workspace_id,
comment=f'deleted {requested_data.get("relation_type")} relation',
comment=f"deleted {requested_data.get('relation_type')} relation",
old_identifier=requested_data.get("related_issue"),
epoch=epoch,
)
@@ -1565,9 +1568,14 @@ def issue_activity(
try:
issue_activities = []
# check if project_id is valid
if not is_valid_uuid(project_id):
return
project = Project.objects.get(pk=project_id)
workspace_id = project.workspace_id
if issue_id is not None:
if origin:
ri = redis_instance()
@@ -1,5 +1,6 @@
# Python imports
from django.utils import timezone
from django.db import DatabaseError
# Third party imports
from celery import shared_task
@@ -22,8 +23,12 @@ def recent_visited_task(entity_name, entity_identifier, user_id, project_id, slu
).first()
if recent_visited:
recent_visited.visited_at = timezone.now()
recent_visited.save(update_fields=["visited_at"])
# Check if the database is available
try:
recent_visited.visited_at = timezone.now()
recent_visited.save(update_fields=["visited_at"])
except DatabaseError:
pass
else:
recent_visited_count = UserRecentVisit.objects.filter(
user_id=user_id, workspace_id=workspace.id
+9 -5
View File
@@ -136,7 +136,7 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site):
# Log the webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -153,7 +153,7 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site):
# Log the failed webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -304,7 +304,7 @@ def webhook_send_task(
# Log the webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -319,7 +319,7 @@ def webhook_send_task(
# Log the failed webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -387,7 +387,11 @@ def webhook_activity(
webhook=webhook.id,
slug=slug,
event=event,
event_data=get_model_data(event=event, event_id=event_id),
event_data=(
{"id": event_id}
if verb == "deleted"
else get_model_data(event=event, event_id=event_id)
),
action=verb,
current_site=current_site,
activity={
@@ -0,0 +1,33 @@
# Generated by Django 4.2.17 on 2025-01-30 16:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0090_rename_dashboard_deprecateddashboard_and_more'),
]
operations = [
migrations.AddField(
model_name='issuecomment',
name='edited_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='profile',
name='is_smooth_cursor_enabled',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='userrecentvisit',
name='entity_name',
field=models.CharField(max_length=30),
),
migrations.AlterField(
model_name='webhooklog',
name='webhook',
field=models.UUIDField(),
)
]
+1
View File
@@ -467,6 +467,7 @@ class IssueComment(ProjectBaseModel):
)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
edited_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
self.comment_stripped = (
+1 -1
View File
@@ -17,7 +17,7 @@ class EntityNameEnum(models.TextChoices):
class UserRecentVisit(WorkspaceBaseModel):
entity_identifier = models.UUIDField(null=True)
entity_name = models.CharField(max_length=30, choices=EntityNameEnum.choices)
entity_name = models.CharField(max_length=30)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
+2
View File
@@ -186,6 +186,8 @@ class Profile(TimeAuditModel):
billing_address = models.JSONField(null=True)
has_billing_address = models.BooleanField(default=False)
company_name = models.CharField(max_length=255, blank=True)
is_smooth_cursor_enabled = models.BooleanField(default=False)
# mobile
is_mobile_onboarded = models.BooleanField(default=False)
mobile_onboarding_step = models.JSONField(default=get_mobile_default_onboarding)
+2 -2
View File
@@ -66,7 +66,7 @@ class WebhookLog(BaseModel):
"db.Workspace", on_delete=models.CASCADE, related_name="webhook_logs"
)
# Associated webhook
webhook = models.ForeignKey(Webhook, on_delete=models.CASCADE, related_name="logs")
webhook = models.UUIDField()
# Basic request details
event_type = models.CharField(max_length=255, blank=True, null=True)
@@ -89,4 +89,4 @@ class WebhookLog(BaseModel):
ordering = ("-created_at",)
def __str__(self):
return f"{self.event_type} {str(self.webhook.url)}"
return f"{self.event_type} {str(self.webhook)}"
+6 -4
View File
@@ -391,11 +391,13 @@ class WorkspaceHomePreference(BaseModel):
class WorkspaceUserPreference(BaseModel):
"""Preference for the workspace for a user"""
class UserPreferenceKeys(models.TextChoices):
PROJECTS = "projects", "Projects"
ANALYTICS = "analytics", "Analytics"
CYCLES = "cycles", "Cycles"
class UserPreferenceKeys(models.TextChoices):
VIEWS = "views", "Views"
ACTIVE_CYCLES = "active_cycles", "Active Cycles"
ANALYTICS = "analytics", "Analytics"
DRAFTS = "drafts", "Drafts"
YOUR_WORK = "your_work", "Your Work"
ARCHIVES = "archives", "Archives"
workspace = models.ForeignKey(
"db.Workspace",
+2
View File
@@ -336,6 +336,8 @@ CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure"
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
APP_BASE_URL = os.environ.get("APP_BASE_URL")
LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL")
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
+14
View File
@@ -151,3 +151,17 @@ class S3Storage(S3Boto3Storage):
"ETag": response.get("ETag"),
"Metadata": response.get("Metadata", {}),
}
def copy_object(self, object_name, new_object_name):
"""Copy an S3 object to a new location"""
try:
response = self.s3_client.copy_object(
Bucket=self.aws_storage_bucket_name,
CopySource={"Bucket": self.aws_storage_bucket_name, "Key": object_name},
Key=new_object_name,
)
except ClientError as e:
log_exception(e)
return None
return response
+2 -2
View File
@@ -14,9 +14,9 @@ class ProjectMetaDataEndpoint(BaseAPIView):
def get(self, request, anchor):
try:
deploy_board = DeployBoard.objects.filter(
deploy_board = DeployBoard.objects.get(
anchor=anchor, entity_name="project"
).first()
)
except DeployBoard.DoesNotExist:
return Response(
{"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND
+1 -1
View File
@@ -77,7 +77,7 @@ def convert_to_utc(
current_datetime_in_project_tz = timezone.now().astimezone(local_tz)
current_datetime_in_utc = current_datetime_in_project_tz.astimezone(pytz.utc)
if utc_datetime.date() == current_datetime_in_utc.date():
if localized_datetime.date() == current_datetime_in_project_tz.date():
return current_datetime_in_utc
return utc_datetime
+8
View File
@@ -0,0 +1,8 @@
import uuid
def is_valid_uuid(uuid_str):
try:
uuid.UUID(uuid_str, version=4)
return True
except ValueError:
return False
+4 -4
View File
@@ -4,7 +4,7 @@
Django==4.2.18
# rest framework
djangorestframework==3.15.2
# postgres
# postgres
psycopg==3.1.18
psycopg-binary==3.1.18
psycopg-c==3.1.18
@@ -37,7 +37,7 @@ uvicorn==0.29.0
# sockets
channels==4.1.0
# ai
litellm==1.51.0
openai==1.63.2
# slack
slack-sdk==3.27.1
# apm
@@ -51,7 +51,7 @@ beautifulsoup4==4.12.3
# analytics
posthog==3.5.0
# crypto
cryptography==43.0.1
cryptography==44.0.1
# html validator
lxml==5.2.1
# s3
@@ -66,4 +66,4 @@ PyJWT==2.8.0
opentelemetry-api==1.28.1
opentelemetry-sdk==1.28.1
opentelemetry-instrumentation-django==0.49b1
opentelemetry-exporter-otlp==1.28.1
opentelemetry-exporter-otlp==1.28.1
+1 -1
View File
@@ -2,4 +2,4 @@
# debug toolbar
django-debug-toolbar==4.3.0
# formatter
ruff==0.4.2
ruff==0.9.7
+136 -14
View File
@@ -55,18 +55,30 @@ Installing plane is a very easy and minimal step process.
- User context used must have access to docker services. In most cases, use sudo su to switch as root user
- Use the terminal (or gitbash) window to run all the future steps
### Downloading Latest Stable Release
### Downloading Latest Release
```
mkdir plane-selfhost
cd plane-selfhost
```
#### For *Docker Compose* based setup
```
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh
chmod +x setup.sh
```
#### For *Docker Swarm* based setup
```
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/swarm.sh
chmod +x setup.sh
```
---
### Proceed with setup
@@ -77,8 +89,9 @@ Lets get started by running the `./setup.sh` command.
This will prompt you with the below options.
#### Docker Compose
```bash
Select a Action you want to perform:
Select an Action you want to perform:
1) Install (x86_64)
2) Start
3) Stop
@@ -87,17 +100,42 @@ Select a Action you want to perform:
6) View Logs
7) Backup Data
8) Exit
Action [2]: 1
```
For the 1st time setup, type "1" as action input.
This will create a create a folder `plane-app` or `plane-app-preview` (in case of preview deployment) and will download 2 files inside that
This will create a folder `plane-app` and will download 2 files inside that
- `docker-compose.yaml`
- `plane.env`
Again the `options [1-8]` will be popped up and this time hit `8` to exit.
Again the `options [1-8]` will be popped up, and this time hit `8` to exit.
#### Docker Swarm
```bash
Select an Action you want to perform:
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 1
```
For the 1st time setup, type "1" as action input.
This will create a create a folder `plane-app` and will download 2 files inside that
- `docker-compose.yaml`
- `plane.env`
Again the `options [1-7]` will be popped up, and this time hit `7` to exit.
---
@@ -116,7 +154,7 @@ There are many other settings you can play with, but we suggest you configure `E
---
### Continue with setup - Start Server
### Continue with setup - Start Server (Docker Compose)
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `2` to start the sevices
@@ -147,9 +185,11 @@ You have successfully self hosted `Plane` instance. Access the application by go
---
### Stopping the Server
### Stopping the Server / Remove Stack
In case you want to make changes to `.env` variables, we suggest you to stop the services before doing that.
In case you want to make changes to `plane.env` variables, we suggest you to stop the services before doing that.
#### Docker Compose
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `3` to stop the sevices
@@ -171,14 +211,34 @@ If all goes well, you must see something like this
![Stop Services](images/stopped.png)
#### Docker Swarm
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `2` to stop the sevices
```bash
Select an Action you want to perform:
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 2
```
If all goes well, you will see the confirmation from docker cli
---
### Restarting the Server
### Restarting the Server / Redeploy Stack
In case you want to make changes to `.env` variables, without stopping the server or you noticed some abnormalies in services, you can restart the services with RESTART option.
In case you want to make changes to `plane.env` variables, without stopping the server or you noticed some abnormalies in services, you can restart the services with `RESTART` / `REDEPLOY` option.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `4` to restart the sevices
#### Docker Compose
```bash
Select a Action you want to perform:
1) Install (x86_64)
@@ -197,14 +257,32 @@ If all goes well, you must see something like this
![Restart Services](images/restart.png)
#### Docker Swarm
```bash
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 4
```
If all goes well, you will see the confirmation from docker cli
---
### Upgrading Plane Version
### Upgrading Plane Version
It is always advised to keep Plane up to date with the latest release.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release.
#### Docker Compose
```bash
Select a Action you want to perform:
1) Install (x86_64)
@@ -231,13 +309,41 @@ Once done, choose `8` to exit from prompt.
Once done with making changes in `plane.env` file, jump on to `Start Server`
#### Docker Swarm
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release.
```bash
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 5
```
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`.
Once done, choose `7` to exit from prompt.
> It is very important for you to validate the `plane.env` for the new changes.
Once done with making changes in `plane.env` file, jump on to `Redeploy Stack`
---
### View Logs
There would a time when you might want to check what is happening inside the API, Worker or any other container.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `6` to view logs.
Lets again run the `./setup.sh` command. You will again be prompted with the below options.
This time select `6` to view logs.
#### Docker Compose
```bash
Select a Action you want to perform:
@@ -253,7 +359,22 @@ Select a Action you want to perform:
Action [2]: 6
```
#### Docker Swarm
```bash
1) Deploy Stack
2) Remove Stack
3) View Stack Status
4) Redeploy Stack
5) Upgrade
6) View Logs
7) Exit
Action [3]: 6
```
#### Service Menu Options for Logs
This will further open sub-menu with list of services
```bash
Select a Service you want to view the logs for:
@@ -267,9 +388,10 @@ Select a Service you want to view the logs for:
8) Redis
9) Postgres
10) Minio
11) RabbitMQ
0) Back to Main Menu
Service:
Service: 3
```
Select any of the service to view the logs e.g. `3`. Expect something similar to this
@@ -323,7 +445,7 @@ Similarly, you can view the logs of other services.
---
### Backup Data
### Backup Data (Docker Compose)
There would a time when you might want to backup your data from docker volumes to external storage like S3 or drives.
@@ -355,7 +477,7 @@ Backup completed successfully. Backup files are stored in /....../plane-app/back
---
### Restore Data
### Restore Data (Docker Compose)
When you want to restore the previously backed-up data, follow the instructions below.
+55 -48
View File
@@ -15,7 +15,7 @@ x-redis-env: &redis-env
x-minio-env: &minio-env
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key}
x-aws-s3-env: &aws-s3-env
AWS_REGION: ${AWS_REGION:-}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-access-key}
@@ -28,8 +28,7 @@ x-proxy-env: &proxy-env
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
x-mq-env: &mq-env
# RabbitMQ Settings
x-mq-env: &mq-env # RabbitMQ Settings
RABBITMQ_HOST: ${RABBITMQ_HOST:-plane-mq}
RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
@@ -50,33 +49,27 @@ x-app-env: &app-env
USE_MINIO: ${USE_MINIO:-1}
DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
ADMIN_BASE_URL: ${ADMIN_BASE_URL}
SPACE_BASE_URL: ${SPACE_BASE_URL}
APP_BASE_URL: ${APP_BASE_URL}
AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
services:
web:
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: node web/server.js web
deploy:
replicas: ${WEB_REPLICAS:-1}
restart_policy:
condition: on-failure
depends_on:
- api
- worker
space:
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: node space/server.js space
deploy:
replicas: ${SPACE_REPLICAS:-1}
restart_policy:
condition: on-failure
depends_on:
- api
- worker
@@ -84,42 +77,39 @@ services:
admin:
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: node admin/server.js admin
deploy:
replicas: ${ADMIN_REPLICAS:-1}
restart_policy:
condition: on-failure
depends_on:
- api
- web
live:
image: ${DOCKERHUB_USER:-makeplane}/plane-live:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: node live/dist/server.js live
environment:
<<: [ *live-env ]
<<: [*live-env]
deploy:
replicas: ${LIVE_REPLICAS:-1}
restart_policy:
condition: on-failure
depends_on:
- api
- web
api:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-api.sh
deploy:
replicas: ${API_REPLICAS:-1}
restart_policy:
condition: on-failure
volumes:
- logs_api:/code/plane/logs
environment:
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- plane-db
- plane-redis
@@ -127,14 +117,15 @@ services:
worker:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-worker.sh
deploy:
replicas: ${WORKER_REPLICAS:-1}
restart_policy:
condition: on-failure
volumes:
- logs_worker:/code/plane/logs
environment:
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
@@ -143,14 +134,15 @@ services:
beat-worker:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-beat.sh
deploy:
replicas: ${BEAT_WORKER_REPLICAS:-1}
restart_policy:
condition: on-failure
volumes:
- logs_beat-worker:/code/plane/logs
environment:
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
@@ -159,23 +151,27 @@ services:
migrator:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: "no"
command: ./bin/docker-entrypoint-migrator.sh
deploy:
replicas: 1
restart_policy:
condition: on-failure
volumes:
- logs_migrator:/code/plane/logs
environment:
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- plane-db
- plane-redis
# Comment this if you already have a database running
plane-db:
image: postgres:15.7-alpine
pull_policy: if_not_present
restart: unless-stopped
command: postgres -c 'max_connections=1000'
deploy:
replicas: 1
restart_policy:
condition: on-failure
environment:
<<: *db-env
volumes:
@@ -183,24 +179,32 @@ services:
plane-redis:
image: valkey/valkey:7.2.5-alpine
pull_policy: if_not_present
restart: unless-stopped
deploy:
replicas: 1
restart_policy:
condition: on-failure
volumes:
- redisdata:/data
plane-mq:
image: rabbitmq:3.13.6-management-alpine
restart: always
deploy:
replicas: 1
restart_policy:
condition: on-failure
environment:
<<: *mq-env
volumes:
- rabbitmq_data:/var/lib/rabbitmq
# Comment this if you using any external s3 compatible storage
plane-minio:
image: minio/minio:latest
pull_policy: if_not_present
restart: unless-stopped
command: server /export --console-address ":9090"
deploy:
replicas: 1
restart_policy:
condition: on-failure
environment:
<<: *minio-env
volumes:
@@ -209,13 +213,17 @@ services:
# Comment this if you already have a reverse proxy running
proxy:
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
ports:
- ${NGINX_PORT}:80
- target: 80
published: ${NGINX_PORT:-80}
protocol: tcp
mode: host
environment:
<<: *proxy-env
deploy:
replicas: 1
restart_policy:
condition: on-failure
depends_on:
- web
- api
@@ -224,7 +232,6 @@ services:
volumes:
pgdata:
redisdata:
uploads:
logs_api:
logs_worker:
+12 -10
View File
@@ -131,18 +131,17 @@ function updateEnvFile() {
fi
if [ -f "$file" ]; then
# Check if key exists in the file
if ! grep -q "^$key=" "$file"; then
# check if key exists in the file
grep -q "^$key=" "$file"
if [ $? -ne 0 ]; then
echo "$key=$value" >> "$file"
return
else
# Escape special characters in value for sed
value=$(printf '%s\n' "$value" | sed -e 's/[\/&]/\\&/g')
else
if [ "$OS_NAME" == "Darwin" ]; then
sed -i '' "s|^$key=.*|$key=$value|" "$file"
value=$(echo "$value" | sed 's/|/\\|/g')
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
else
sed -i "s|^$key=.*|$key=$value|" "$file"
sed -i "s/^$key=.*/$key=$value/g" "$file"
fi
fi
else
@@ -458,12 +457,13 @@ function viewLogs(){
echo " 8) Redis"
echo " 9) Postgres"
echo " 10) Minio"
echo " 11) RabbitMQ"
echo " 0) Back to Main Menu"
echo
read -p "Service: " DOCKER_SERVICE_NAME
until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 10 )); do
echo "Invalid selection. Please enter a number between 1 and 11."
until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 11 )); do
echo "Invalid selection. Please enter a number between 0 and 11."
read -p "Service: " DOCKER_SERVICE_NAME
done
@@ -482,6 +482,7 @@ function viewLogs(){
8) viewSpecificLogs "plane-redis";;
9) viewSpecificLogs "plane-db";;
10) viewSpecificLogs "plane-minio";;
11) viewSpecificLogs "plane-mq";;
0) askForAction;;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
@@ -500,6 +501,7 @@ function viewLogs(){
redis) viewSpecificLogs "plane-redis";;
postgres) viewSpecificLogs "plane-db";;
minio) viewSpecificLogs "plane-minio";;
rabbitmq) viewSpecificLogs "plane-mq";;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
else
+612
View File
@@ -0,0 +1,612 @@
#!/bin/bash
BRANCH=${BRANCH:-master}
SERVICE_FOLDER=plane-app
SCRIPT_DIR=$PWD
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE="stable"
export DOCKERHUB_USER=makeplane
export GH_REPO=makeplane/plane
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deploy/selfhost"
OS_NAME=$(uname)
# Create necessary directories
mkdir -p $PLANE_INSTALL_DIR/archive
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yml
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
function print_header() {
clear
cat <<"EOF"
--------------------------------------------
____ _ /////////
| _ \| | __ _ _ __ ___ /////////
| |_) | |/ _` | '_ \ / _ \ ///// /////
| __/| | (_| | | | | __/ ///// /////
|_| |_|\__,_|_| |_|\___| ////
////
--------------------------------------------
Project management tool from the future
--------------------------------------------
EOF
}
function checkLatestRelease(){
echo "Checking for the latest release..." >&2
local latest_release=$(curl -s https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g')
if [ -z "$latest_release" ]; then
echo "Failed to check for the latest release. Exiting..." >&2
exit 1
fi
echo $latest_release
}
# Function to read stack name from env file
function readStackName() {
if [ -f "$DOCKER_ENV_PATH" ]; then
local saved_stack_name=$(grep "^STACK_NAME=" "$DOCKER_ENV_PATH" | cut -d'=' -f2)
if [ -n "$saved_stack_name" ]; then
stack_name=$saved_stack_name
return 1
fi
fi
return 0
}
# Function to get stack name (either from env or user input)
function getStackName() {
read -p "Enter stack name [plane]: " input_stack_name
if [ -z "$input_stack_name" ]; then
input_stack_name="plane"
fi
stack_name=$input_stack_name
updateEnvFile "STACK_NAME" "$stack_name" "$DOCKER_ENV_PATH"
echo "Using stack name: $stack_name"
}
function syncEnvFile(){
echo "Syncing environment variables..." >&2
if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then
# READ keys of plane.env and update the values from plane.env.bak
while IFS= read -r line
do
# ignore if the line is empty or starts with #
if [ -z "$line" ] || [[ $line == \#* ]]; then
continue
fi
key=$(echo "$line" | cut -d'=' -f1)
value=$(getEnvValue "$key" "$PLANE_INSTALL_DIR/plane.env.bak")
if [ -n "$value" ]; then
updateEnvFile "$key" "$value" "$DOCKER_ENV_PATH"
fi
done < "$DOCKER_ENV_PATH"
value=$(getEnvValue "STACK_NAME" "$PLANE_INSTALL_DIR/plane.env.bak")
if [ -n "$value" ]; then
updateEnvFile "STACK_NAME" "$value" "$DOCKER_ENV_PATH"
fi
fi
echo "Environment variables synced successfully" >&2
rm -f $PLANE_INSTALL_DIR/plane.env.bak
}
function getEnvValue() {
local key=$1
local file=$2
if [ -z "$key" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
grep -q "^$key=" "$file"
if [ $? -eq 0 ]; then
local value
value=$(grep "^$key=" "$file" | cut -d'=' -f2)
echo "$value"
else
echo ""
fi
fi
}
function updateEnvFile() {
local key=$1
local value=$2
local file=$3
if [ -z "$key" ] || [ -z "$value" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
# check if key exists in the file
grep -q "^$key=" "$file"
if [ $? -ne 0 ]; then
echo "$key=$value" >> "$file"
return
else
if [ "$OS_NAME" == "Darwin" ]; then
value=$(echo "$value" | sed 's/|/\\|/g')
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
else
sed -i "s/^$key=.*/$key=$value/g" "$file"
fi
fi
else
echo "File not found: $file"
exit 1
fi
}
function download() {
cd $SCRIPT_DIR || exit 1
TS=$(date +%s)
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yml" ]
then
mv $PLANE_INSTALL_DIR/docker-compose.yml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml
fi
echo $RELEASE_DOWNLOAD_URL
echo $FALLBACK_DOWNLOAD_URL
echo $APP_RELEASE
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yml
else
# Fallback to download from the raw github url
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yml
else
echo "Failed to download docker-compose.yml. HTTP Status: $STATUS"
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml"
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml $PLANE_INSTALL_DIR/docker-compose.yml
exit 1
fi
fi
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
else
# Fallback to download from the raw github url
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)")
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$STATUS" -eq 200 ]; then
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
else
echo "Failed to download variables.env. HTTP Status: $STATUS"
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env"
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml $PLANE_INSTALL_DIR/docker-compose.yml
exit 1
fi
fi
if [ -f "$DOCKER_ENV_PATH" ];
then
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env"
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak"
fi
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
syncEnvFile
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
}
function deployStack() {
# Check if docker compose file and env file exist
if [ ! -f "$DOCKER_FILE_PATH" ] || [ ! -f "$DOCKER_ENV_PATH" ]; then
echo "Configuration files not found"
echo "Downloading it now......"
APP_RELEASE=$(checkLatestRelease)
download
fi
if [ -z "$stack_name" ]; then
getStackName
fi
echo "Starting ${stack_name} stack..."
# Pull envs
if [ -f "$DOCKER_ENV_PATH" ]; then
set -o allexport; source $DOCKER_ENV_PATH; set +o allexport;
else
echo "Environment file not found: $DOCKER_ENV_PATH"
exit 1
fi
# Deploy the stack
docker stack deploy -c $DOCKER_FILE_PATH $stack_name
echo "Waiting for services to be deployed..."
sleep 10
# Check migrator service
local migrator_service=$(docker service ls --filter name=${stack_name}_migrator -q)
if [ -n "$migrator_service" ]; then
echo ">> Waiting for Data Migration to finish"
while docker service ls --filter name=${stack_name}_migrator | grep -q "running"; do
echo -n "."
sleep 1
done
echo ""
# Get the most recent container for the migrator service
local migrator_container=$(docker ps -a --filter name=${stack_name}_migrator --latest -q)
if [ -n "$migrator_container" ]; then
# Get the exit code of the container
local exit_code=$(docker inspect --format='{{.State.ExitCode}}' $migrator_container)
if [ "$exit_code" != "0" ]; then
echo "Server failed to start ❌"
echo "Migration failed with exit code: $exit_code"
echo "Please check the logs for the 'migrator' service and resolve the issue(s)."
echo "Stop the services by running the command: ./swarm.sh stop"
exit 1
else
echo " Data Migration completed successfully ✅"
fi
else
echo "Warning: Could not find migrator container to check exit status"
fi
fi
# Check API service
local api_service=$(docker service ls --filter name=${stack_name}_api -q)
while docker service ls --filter name=${stack_name}_api | grep -q "running"; do
local running_container=$(docker ps --filter "name=${stack_name}_api" --filter "status=running" -q)
if [ -n "$running_container" ]; then
if docker container logs $running_container 2>/dev/null | grep -q "Application Startup Complete"; then
break
fi
fi
sleep 2
done
if [ -z "$api_service" ]; then
echo "Plane Server failed to start ❌"
echo "Please check the logs for the 'api' service and resolve the issue(s)."
echo "Stop the services by running the command: ./swarm.sh stop"
exit 1
fi
echo " Plane Server started successfully ✅"
echo ""
echo " You can access the application at $WEB_URL"
echo ""
}
function removeStack() {
if [ -z "$stack_name" ]; then
echo "Stack name not found"
exit 1
fi
echo "Removing ${stack_name} stack..."
docker stack rm "$stack_name"
echo "Waiting for services to be removed..."
while docker stack ls | grep -q "$stack_name"; do
sleep 1
done
sleep 20
echo "Services stopped successfully ✅"
}
function viewStatus() {
echo "Checking status of ${stack_name} stack..."
if [ -z "$stack_name" ]; then
echo "Stack name not found"
exit 1
fi
docker stack ps "$stack_name"
}
function redeployStack() {
removeStack
echo "ReDeploying ${stack_name} stack..."
deployStack
}
function upgrade() {
echo "Checking status of ${stack_name} stack..."
if [ -z "$stack_name" ]; then
echo "Stack name not found"
exit 1
fi
local latest_release=$(checkLatestRelease)
echo ""
echo "Current release: $APP_RELEASE"
if [ "$latest_release" == "$APP_RELEASE" ]; then
echo ""
echo "You are already using the latest release"
exit 0
fi
echo "Latest release: $latest_release"
echo ""
# Check for confirmation to upgrade
echo "Do you want to upgrade to the latest release ($latest_release)?"
read -p "Continue? [y/N]: " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Exiting..."
exit 0
fi
export APP_RELEASE=$latest_release
# check if stack exists
echo "Upgrading ${stack_name} stack..."
# check env file and take backup
if [ -f "$DOCKER_ENV_PATH" ]; then
cp "$DOCKER_ENV_PATH" "${DOCKER_ENV_PATH}.bak"
fi
download
redeployStack
}
function viewSpecificLogs() {
local service=$1
# Input validation
if [ -z "$service" ]; then
echo "Error: Please specify a service name"
return 1
fi
# Main loop for service logs
while true; do
# Get all running containers for the service
local running_containers=$(docker ps --filter "name=${stack_name}_${service}" --filter "status=running" -q)
# If no running containers found, try service logs
if [ -z "$running_containers" ]; then
echo "No running containers found for ${stack_name}_${service}, checking service logs..."
if docker service inspect ${stack_name}_${service} >/dev/null 2>&1; then
echo "Press Ctrl+C or 'q' to exit logs"
docker service logs ${stack_name}_${service} -f
break
else
echo "Error: No running containers or services found for ${stack_name}_${service}"
return 1
fi
return
fi
# If multiple containers are running, let user choose
if [ $(echo "$running_containers" | grep -v '^$' | wc -l) -gt 1 ]; then
clear
echo "Multiple containers found for ${stack_name}_${service}:"
local i=1
# Use regular arrays instead of associative arrays
container_ids=()
container_names=()
while read -r container_id; do
if [ -n "$container_id" ]; then
local container_name=$(docker inspect --format '{{.Name}}' "$container_id" | sed 's/\///')
container_ids[$i]=$container_id
container_names[$i]=$container_name
echo "[$i] ${container_names[$i]} (${container_ids[$i]})"
i=$((i+1))
fi
done <<< "$running_containers"
echo -e "\nPlease select a container number:"
read -r selection
if [[ "$selection" =~ ^[0-9]+$ ]] && [ -n "${container_ids[$selection]}" ]; then
local selected_container=${container_ids[$selection]}
clear
echo "Showing logs for container: ${container_names[$selection]}"
echo "Press Ctrl+C or 'q' to return to container selection"
# Start watching logs in the background
docker container logs -f "$selected_container" &
local log_pid=$!
while true; do
read -r -n 1 input
if [[ $input == "q" ]]; then
kill $log_pid 2>/dev/null
wait $log_pid 2>/dev/null
break
fi
done
clear
else
echo "Error: Invalid selection"
sleep 2
fi
else
# Single container case
local container_name=$(docker inspect --format '{{.Name}}' "$running_containers" | sed 's/\///')
echo "Showing logs for container: $container_name"
echo "Press Ctrl+C or 'q' to exit logs"
docker container logs -f "$running_containers" &
local log_pid=$!
while true; do
read -r -n 1 input
if [[ $input == "q" ]]; then
kill $log_pid 2>/dev/null
wait $log_pid 2>/dev/null
break
fi
done
break
fi
done
}
function viewLogs(){
ARG_SERVICE_NAME=$2
if [ -z "$ARG_SERVICE_NAME" ];
then
echo
echo "Select a Service you want to view the logs for:"
echo " 1) Web"
echo " 2) Space"
echo " 3) API"
echo " 4) Worker"
echo " 5) Beat-Worker"
echo " 6) Migrator"
echo " 7) Proxy"
echo " 8) Redis"
echo " 9) Postgres"
echo " 10) Minio"
echo " 11) RabbitMQ"
echo " 0) Back to Main Menu"
echo
read -p "Service: " DOCKER_SERVICE_NAME
until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 11 )); do
echo "Invalid selection. Please enter a number between 0 and 11."
read -p "Service: " DOCKER_SERVICE_NAME
done
if [ -z "$DOCKER_SERVICE_NAME" ];
then
echo "INVALID SERVICE NAME SUPPLIED"
else
case $DOCKER_SERVICE_NAME in
1) viewSpecificLogs "web";;
2) viewSpecificLogs "space";;
3) viewSpecificLogs "api";;
4) viewSpecificLogs "worker";;
5) viewSpecificLogs "beat-worker";;
6) viewSpecificLogs "migrator";;
7) viewSpecificLogs "proxy";;
8) viewSpecificLogs "plane-redis";;
9) viewSpecificLogs "plane-db";;
10) viewSpecificLogs "plane-minio";;
11) viewSpecificLogs "plane-mq";;
0) askForAction;;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
fi
elif [ -n "$ARG_SERVICE_NAME" ];
then
ARG_SERVICE_NAME=$(echo "$ARG_SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
case $ARG_SERVICE_NAME in
web) viewSpecificLogs "web";;
space) viewSpecificLogs "space";;
api) viewSpecificLogs "api";;
worker) viewSpecificLogs "worker";;
beat-worker) viewSpecificLogs "beat-worker";;
migrator) viewSpecificLogs "migrator";;
proxy) viewSpecificLogs "proxy";;
redis) viewSpecificLogs "plane-redis";;
postgres) viewSpecificLogs "plane-db";;
minio) viewSpecificLogs "plane-minio";;
rabbitmq) viewSpecificLogs "plane-mq";;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
else
echo "INVALID SERVICE NAME SUPPLIED"
fi
}
function askForAction() {
# Rest of askForAction remains the same but use $stack_name instead of $STACK_NAME
local DEFAULT_ACTION=$1
if [ -z "$DEFAULT_ACTION" ]; then
echo
echo "Select an Action you want to perform:"
echo " 1) Deploy Stack"
echo " 2) Remove Stack"
echo " 3) View Stack Status"
echo " 4) Redeploy Stack"
echo " 5) Upgrade"
echo " 6) View Logs"
echo " 7) Exit"
echo
read -p "Action [3]: " ACTION
until [[ -z "$ACTION" || "$ACTION" =~ ^[1-6]$ ]]; do
echo "$ACTION: invalid selection."
read -p "Action [3]: " ACTION
done
if [ -z "$ACTION" ]; then
ACTION=3
fi
echo
fi
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "deploy" ]; then
deployStack
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "remove" ]; then
removeStack
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "status" ]; then
viewStatus
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "redeploy" ]; then
redeployStack
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ]; then
upgrade
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ]; then
viewLogs "$@"
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "exit" ]; then
exit 0
else
echo "INVALID ACTION SUPPLIED"
fi
}
# Initialize stack name at script start
if [ -z "$stack_name" ]; then
readStackName
fi
# Sync environment variables
if [ -f "$DOCKER_ENV_PATH" ]; then
DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH")
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi
if [ -z "$APP_RELEASE" ]; then
APP_RELEASE=stable
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
fi
fi
# Main execution
print_header
askForAction "$@"
+3
View File
@@ -5,6 +5,9 @@ WEB_REPLICAS=1
SPACE_REPLICAS=1
ADMIN_REPLICAS=1
API_REPLICAS=1
WORKER_REPLICAS=1
BEAT_WORKER_REPLICAS=1
LIVE_REPLICAS=1
NGINX_PORT=80
WEB_URL=http://${APP_DOMAIN}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "live",
"version": "0.24.1",
"version": "0.25.0",
"description": "",
"main": "./src/server.ts",
"private": true,
@@ -23,7 +23,7 @@
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/types": "*",
"@sentry/node": "^8.28.0",
"@sentry/node": "^9.0.1",
"@sentry/profiling-node": "^8.28.0",
"@tiptap/core": "2.10.4",
"@tiptap/html": "2.11.0",
+6 -2
View File
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
"version": "0.24.1",
"version": "0.25.0",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
@@ -22,7 +22,11 @@
"devDependencies": {
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"turbo": "^2.3.3"
"turbo": "^2.4.2"
},
"resolutions": {
"nanoid": "3.3.8",
"esbuild": "0.25.0"
},
"packageManager": "yarn@1.22.22",
"name": "plane"
+5
View File
@@ -0,0 +1,5 @@
.next
.turbo
out/
dist/
build/
+5
View File
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.24.1",
"version": "0.25.0",
"private": true,
"main": "./src/index.ts"
}
+6 -3
View File
@@ -2,8 +2,11 @@
import { TXAxisValues, TYAxisValues } from "@plane/types";
export const ANALYTICS_TABS = [
{ key: "scope_and_demand", title: "Scope and Demand" },
{ key: "custom", title: "Custom Analytics" },
{
key: "scope_and_demand",
i18n_title: "workspace_analytics.tabs.scope_and_demand",
},
{ key: "custom", i18n_title: "workspace_analytics.tabs.custom" },
];
export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
@@ -62,7 +65,7 @@ export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] =
[
{
value: "issue_count",
label: "Issue Count",
label: "Work item Count",
},
{
value: "estimate",
+2
View File
@@ -0,0 +1,2 @@
export const LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracking-wide";
export const AXIS_LINE_CLASSNAME = "text-custom-text-400/70";
@@ -1,56 +1,40 @@
// types
import { TCycleLayoutOptions, TCycleTabOptions } from "@plane/types";
export const CYCLE_TABS_LIST: {
key: TCycleTabOptions;
name: string;
}[] = [
{
key: "active",
name: "Active",
},
{
key: "all",
name: "All",
},
];
export const CYCLE_STATUS: {
label: string;
i18n_label: string;
value: "current" | "upcoming" | "completed" | "draft";
title: string;
i18n_title: string;
color: string;
textColor: string;
bgColor: string;
}[] = [
{
label: "day left",
i18n_label: "project_cycles.status.days_left",
value: "current",
title: "In progress",
i18n_title: "project_cycles.status.in_progress",
color: "#F59E0B",
textColor: "text-amber-500",
bgColor: "bg-amber-50",
},
{
label: "Yet to start",
i18n_label: "project_cycles.status.yet_to_start",
value: "upcoming",
title: "Yet to start",
i18n_title: "project_cycles.status.yet_to_start",
color: "#3F76FF",
textColor: "text-blue-500",
bgColor: "bg-indigo-50",
},
{
label: "Completed",
i18n_label: "project_cycles.status.completed",
value: "completed",
title: "Completed",
i18n_title: "project_cycles.status.completed",
color: "#16A34A",
textColor: "text-green-600",
bgColor: "bg-green-50",
},
{
label: "Draft",
i18n_label: "project_cycles.status.draft",
value: "draft",
title: "Draft",
i18n_title: "project_cycles.status.draft",
color: "#525252",
textColor: "text-custom-text-300",
bgColor: "bg-custom-background-90",
+92
View File
@@ -0,0 +1,92 @@
// types
import { TIssuesListTypes } from "@plane/types";
export enum EDurationFilters {
NONE = "none",
TODAY = "today",
THIS_WEEK = "this_week",
THIS_MONTH = "this_month",
THIS_YEAR = "this_year",
CUSTOM = "custom",
}
// filter duration options
export const DURATION_FILTER_OPTIONS: {
key: EDurationFilters;
label: string;
}[] = [
{
key: EDurationFilters.NONE,
label: "All time",
},
{
key: EDurationFilters.TODAY,
label: "Due today",
},
{
key: EDurationFilters.THIS_WEEK,
label: "Due this week",
},
{
key: EDurationFilters.THIS_MONTH,
label: "Due this month",
},
{
key: EDurationFilters.THIS_YEAR,
label: "Due this year",
},
{
key: EDurationFilters.CUSTOM,
label: "Custom",
},
];
// random background colors for project cards
export const PROJECT_BACKGROUND_COLORS = [
"bg-gray-500/20",
"bg-green-500/20",
"bg-red-500/20",
"bg-orange-500/20",
"bg-blue-500/20",
"bg-yellow-500/20",
"bg-pink-500/20",
"bg-purple-500/20",
];
// assigned and created issues widgets tabs list
export const FILTERED_ISSUES_TABS_LIST: {
key: TIssuesListTypes;
label: string;
}[] = [
{
key: "upcoming",
label: "Upcoming",
},
{
key: "overdue",
label: "Overdue",
},
{
key: "completed",
label: "Marked completed",
},
];
// assigned and created issues widgets tabs list
export const UNFILTERED_ISSUES_TABS_LIST: {
key: TIssuesListTypes;
label: string;
}[] = [
{
key: "pending",
label: "Pending",
},
{
key: "completed",
label: "Marked completed",
},
];
export type TLinkOptions = {
userId: string | undefined;
};
-5
View File
@@ -1,5 +0,0 @@
export enum E_ARCHIVE_ERROR_CODES {
"INVALID_ARCHIVE_STATE_GROUP" = 4091,
"INVALID_ISSUE_START_DATE" = 4101,
"INVALID_ISSUE_TARGET_DATE" = 4102,
}
@@ -104,7 +104,10 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
module_id: payload.module_id,
archived_at: payload.archived_at,
state: payload.state,
view_id: path?.includes("workspace-views") || path?.includes("views") ? path.split("/").pop() : "",
view_id:
path?.includes("workspace-views") || path?.includes("views")
? path.split("/").pop()
: "",
};
if (eventName === ISSUE_UPDATED) {
@@ -166,12 +169,12 @@ export const MODULE_LINK_CREATED = "Module link created";
export const MODULE_LINK_UPDATED = "Module link updated";
export const MODULE_LINK_DELETED = "Module link deleted";
// Issue Events
export const ISSUE_CREATED = "Issue created";
export const ISSUE_UPDATED = "Issue updated";
export const ISSUE_DELETED = "Issue deleted";
export const ISSUE_ARCHIVED = "Issue archived";
export const ISSUE_RESTORED = "Issue restored";
export const ISSUE_OPENED = "Issue opened";
export const ISSUE_CREATED = "Work item created";
export const ISSUE_UPDATED = "Work item updated";
export const ISSUE_DELETED = "Work item deleted";
export const ISSUE_ARCHIVED = "Work item archived";
export const ISSUE_RESTORED = "Work item restored";
export const ISSUE_OPENED = "Work item opened";
// Project State Events
export const STATE_CREATED = "State created";
export const STATE_UPDATED = "State updated";
-1
View File
@@ -1 +0,0 @@
export const SIDEBAR_CLICKED = "Sidenav clicked";
+53
View File
@@ -2,3 +2,56 @@ export enum E_SORT_ORDER {
ASC = "asc",
DESC = "desc",
}
export const DATE_AFTER_FILTER_OPTIONS = [
{
name: "1 week from now",
value: "1_weeks;after;fromnow",
},
{
name: "2 weeks from now",
value: "2_weeks;after;fromnow",
},
{
name: "1 month from now",
value: "1_months;after;fromnow",
},
{
name: "2 months from now",
value: "2_months;after;fromnow",
},
];
export const DATE_BEFORE_FILTER_OPTIONS = [
{
name: "1 week ago",
value: "1_weeks;before;fromnow",
},
{
name: "2 weeks ago",
value: "2_weeks;before;fromnow",
},
{
name: "1 month ago",
i18n_name: "date_filters.1_month_ago",
value: "1_months;before;fromnow",
},
];
export const PROJECT_CREATED_AT_FILTER_OPTIONS = [
{
name: "Today",
value: "today;custom;custom",
},
{
name: "Yesterday",
value: "yesterday;custom;custom",
},
{
name: "Last 7 days",
value: "last_7_days;custom;custom",
},
{
name: "Last 30 days",
value: "last_30_days;custom;custom",
},
];
+91
View File
@@ -0,0 +1,91 @@
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
export enum EInboxIssueCurrentTab {
OPEN = "open",
CLOSED = "closed",
}
export enum EInboxIssueStatus {
PENDING = -2,
DECLINED = -1,
SNOOZED = 0,
ACCEPTED = 1,
DUPLICATE = 2,
}
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
export type TInboxIssueStatus = EInboxIssueStatus;
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
duplicate_to: string | undefined;
source: string;
issue: TIssue;
created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
};
export const INBOX_STATUS: {
key: string;
status: TInboxIssueStatus;
i18n_title: string;
i18n_description: () => string;
}[] = [
{
key: "pending",
i18n_title: "inbox_issue.status.pending.title",
status: EInboxIssueStatus.PENDING,
i18n_description: () => `inbox_issue.status.pending.description`,
},
{
key: "declined",
i18n_title: "inbox_issue.status.declined.title",
status: EInboxIssueStatus.DECLINED,
i18n_description: () => `inbox_issue.status.declined.description`,
},
{
key: "snoozed",
i18n_title: "inbox_issue.status.snoozed.title",
status: EInboxIssueStatus.SNOOZED,
i18n_description: () => `inbox_issue.status.snoozed.description`,
},
{
key: "accepted",
i18n_title: "inbox_issue.status.accepted.title",
status: EInboxIssueStatus.ACCEPTED,
i18n_description: () => `inbox_issue.status.accepted.description`,
},
{
key: "duplicate",
i18n_title: "inbox_issue.status.duplicate.title",
status: EInboxIssueStatus.DUPLICATE,
i18n_description: () => `inbox_issue.status.duplicate.description`,
},
];
export const INBOX_ISSUE_ORDER_BY_OPTIONS = [
{
key: "issue__created_at",
i18n_label: "inbox_issue.order_by.created_at",
},
{
key: "issue__updated_at",
i18n_label: "inbox_issue.order_by.updated_at",
},
{
key: "issue__sequence_id",
i18n_label: "inbox_issue.order_by.id",
},
];
export const INBOX_ISSUE_SORT_BY_OPTIONS = [
{
key: "asc",
i18n_label: "common.sort.asc",
},
{
key: "desc",
i18n_label: "common.sort.desc",
},
];
+16 -1
View File
@@ -1,16 +1,31 @@
export * from "./ai";
export * from "./analytics";
export * from "./auth";
export * from "./chart";
export * from "./endpoints";
export * from "./event";
export * from "./file";
export * from "./filter";
export * from "./graph";
export * from "./instance";
export * from "./issue";
export * from "./metadata";
export * from "./notification";
export * from "./state";
export * from "./swr";
export * from "./tab-indices";
export * from "./user";
export * from "./workspace";
export * from "./stickies";
export * from "./cycle";
export * from "./module";
export * from "./project";
export * from "./views";
export * from "./themes";
export * from "./inbox";
export * from "./profile";
export * from "./workspace-drafts";
export * from "./label";
export * from "./event-tracker";
export * from "./spreadsheet";
export * from "./dashboard";
export * from "./page";
-185
View File
@@ -1,185 +0,0 @@
import { List, Kanban } from "lucide-react";
export const ALL_ISSUES = "All Issues";
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export type TIssueFilterKeys = "priority" | "state" | "labels";
export type TIssueLayout =
| "list"
| "kanban"
| "calendar"
| "spreadsheet"
| "gantt";
export type TIssueFilterPriorityObject = {
key: TIssuePriorities;
title: string;
className: string;
icon: string;
};
export enum EIssueGroupByToServerOptions {
"state" = "state_id",
"priority" = "priority",
"labels" = "labels__id",
"state_detail.group" = "state__group",
"assignees" = "assignees__id",
"cycle" = "cycle_id",
"module" = "issue_module__module_id",
"target_date" = "target_date",
"project" = "project_id",
"created_by" = "created_by",
"team_project" = "project_id",
}
export enum EIssueGroupBYServerToProperty {
"state_id" = "state_id",
"priority" = "priority",
"labels__id" = "label_ids",
"state__group" = "state__group",
"assignees__id" = "assignee_ids",
"cycle_id" = "cycle_id",
"issue_module__module_id" = "module_ids",
"target_date" = "target_date",
"project_id" = "project_id",
"created_by" = "created_by",
}
export enum EServerGroupByToFilterOptions {
"state_id" = "state",
"priority" = "priority",
"labels__id" = "labels",
"state__group" = "state_group",
"assignees__id" = "assignees",
"cycle_id" = "cycle",
"issue_module__module_id" = "module",
"target_date" = "target_date",
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
export enum EIssueLayoutTypes {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
GANTT = "gantt_chart",
SPREADSHEET = "spreadsheet",
}
export enum EIssuesStoreType {
GLOBAL = "GLOBAL",
PROFILE = "PROFILE",
TEAM = "TEAM",
PROJECT = "PROJECT",
CYCLE = "CYCLE",
MODULE = "MODULE",
TEAM_VIEW = "TEAM_VIEW",
PROJECT_VIEW = "PROJECT_VIEW",
ARCHIVED = "ARCHIVED",
DRAFT = "DRAFT",
DEFAULT = "DEFAULT",
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
EPIC = "EPIC",
}
export enum EIssueFilterType {
FILTERS = "filters",
DISPLAY_FILTERS = "display_filters",
DISPLAY_PROPERTIES = "display_properties",
KANBAN_FILTERS = "kanban_filters",
}
export enum EIssueCommentAccessSpecifier {
EXTERNAL = "EXTERNAL",
INTERNAL = "INTERNAL",
}
export enum EIssueListRow {
HEADER = "HEADER",
ISSUE = "ISSUE",
NO_ISSUES = "NO_ISSUES",
QUICK_ADD = "QUICK_ADD",
}
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
} = {
list: {
filters: ["priority", "state", "labels"],
},
kanban: {
filters: ["priority", "state", "labels"],
},
calendar: {
filters: ["priority", "state", "labels"],
},
spreadsheet: {
filters: ["priority", "state", "labels"],
},
gantt: {
filters: ["priority", "state", "labels"],
},
};
export const ISSUE_PRIORITIES: {
key: TIssuePriorities;
title: string;
}[] = [
{ key: "urgent", title: "Urgent" },
{ key: "high", title: "High" },
{ key: "medium", title: "Medium" },
{ key: "low", title: "Low" },
{ key: "none", title: "None" },
];
export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
{
key: "urgent",
title: "Urgent",
className: "bg-red-500 border-red-500 text-white",
icon: "error",
},
{
key: "high",
title: "High",
className: "text-orange-500 border-custom-border-300",
icon: "signal_cellular_alt",
},
{
key: "medium",
title: "Medium",
className: "text-yellow-500 border-custom-border-300",
icon: "signal_cellular_alt_2_bar",
},
{
key: "low",
title: "Low",
className: "text-green-500 border-custom-border-300",
icon: "signal_cellular_alt_1_bar",
},
{
key: "none",
title: "None",
className: "text-gray-500 border-custom-border-300",
icon: "block",
},
];
export const SITES_ISSUE_LAYOUTS: {
key: TIssueLayout;
title: string;
icon: any;
}[] = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
// { key: "calendar", title: "Calendar", icon: Calendar },
// { key: "spreadsheet", title: "Spreadsheet", icon: Sheet },
// { key: "gantt", title: "Gantt chart", icon: GanttChartSquare },
];
+217
View File
@@ -0,0 +1,217 @@
import {
TIssueGroupByOptions,
TIssueOrderByOptions,
IIssueDisplayProperties,
} from "@plane/types";
export const ALL_ISSUES = "All Issues";
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export type TIssueFilterPriorityObject = {
key: TIssuePriorities;
titleTranslationKey: string;
className: string;
icon: string;
};
export enum EIssueGroupByToServerOptions {
"state" = "state_id",
"priority" = "priority",
"labels" = "labels__id",
"state_detail.group" = "state__group",
"assignees" = "assignees__id",
"cycle" = "cycle_id",
"module" = "issue_module__module_id",
"target_date" = "target_date",
"project" = "project_id",
"created_by" = "created_by",
"team_project" = "project_id",
}
export enum EIssueGroupBYServerToProperty {
"state_id" = "state_id",
"priority" = "priority",
"labels__id" = "label_ids",
"state__group" = "state__group",
"assignees__id" = "assignee_ids",
"cycle_id" = "cycle_id",
"issue_module__module_id" = "module_ids",
"target_date" = "target_date",
"project_id" = "project_id",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
export enum EIssuesStoreType {
GLOBAL = "GLOBAL",
PROFILE = "PROFILE",
TEAM = "TEAM",
PROJECT = "PROJECT",
CYCLE = "CYCLE",
MODULE = "MODULE",
TEAM_VIEW = "TEAM_VIEW",
PROJECT_VIEW = "PROJECT_VIEW",
ARCHIVED = "ARCHIVED",
DRAFT = "DRAFT",
DEFAULT = "DEFAULT",
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
EPIC = "EPIC",
}
export enum EIssueCommentAccessSpecifier {
EXTERNAL = "EXTERNAL",
INTERNAL = "INTERNAL",
}
export enum EIssueListRow {
HEADER = "HEADER",
ISSUE = "ISSUE",
NO_ISSUES = "NO_ISSUES",
QUICK_ADD = "QUICK_ADD",
}
export const ISSUE_PRIORITIES: {
key: TIssuePriorities;
title: string;
}[] = [
{
key: "urgent",
title: "Urgent",
},
{
key: "high",
title: "High",
},
{
key: "medium",
title: "Medium",
},
{
key: "low",
title: "Low",
},
{
key: "none",
title: "None",
},
];
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
"state",
"priority",
"assignees",
"labels",
"module",
"cycle",
];
export type TCreateModalStoreTypes =
| EIssuesStoreType.TEAM
| EIssuesStoreType.PROJECT
| EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.PROFILE
| EIssuesStoreType.CYCLE
| EIssuesStoreType.MODULE
| EIssuesStoreType.EPIC;
export const ISSUE_GROUP_BY_OPTIONS: {
key: TIssueGroupByOptions;
titleTranslationKey: string;
}[] = [
{ key: "state", titleTranslationKey: "common.states" },
{ key: "state_detail.group", titleTranslationKey: "common.state_groups" },
{ key: "priority", titleTranslationKey: "common.priority" },
{ key: "team_project", titleTranslationKey: "common.team_project" }, // required this on team issues
{ key: "project", titleTranslationKey: "common.project" }, // required this on my issues
{ key: "cycle", titleTranslationKey: "common.cycle" }, // required this on my issues
{ key: "module", titleTranslationKey: "common.module" }, // required this on my issues
{ key: "labels", titleTranslationKey: "common.labels" },
{ key: "assignees", titleTranslationKey: "common.assignees" },
{ key: "created_by", titleTranslationKey: "common.created_by" },
{ key: null, titleTranslationKey: "common.none" },
];
export const ISSUE_ORDER_BY_OPTIONS: {
key: TIssueOrderByOptions;
titleTranslationKey: string;
}[] = [
{ key: "sort_order", titleTranslationKey: "common.order_by.manual" },
{ key: "-created_at", titleTranslationKey: "common.order_by.last_created" },
{ key: "-updated_at", titleTranslationKey: "common.order_by.last_updated" },
{ key: "start_date", titleTranslationKey: "common.order_by.start_date" },
{ key: "target_date", titleTranslationKey: "common.order_by.due_date" },
{ key: "-priority", titleTranslationKey: "common.priority" },
];
export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] =
[
"assignee",
"start_date",
"due_date",
"labels",
"key",
"priority",
"state",
"sub_issue_count",
"link",
"attachment_count",
"estimate",
"created_on",
"updated_on",
"modules",
"cycle",
"issue_type",
];
export const ISSUE_DISPLAY_PROPERTIES: {
key: keyof IIssueDisplayProperties;
titleTranslationKey: string;
}[] = [
{
key: "key",
titleTranslationKey: "issue.display.properties.id",
},
{
key: "issue_type",
titleTranslationKey: "issue.display.properties.issue_type",
},
{
key: "assignee",
titleTranslationKey: "common.assignee",
},
{
key: "start_date",
titleTranslationKey: "common.order_by.start_date",
},
{
key: "due_date",
titleTranslationKey: "common.order_by.due_date",
},
{ key: "labels", titleTranslationKey: "common.labels" },
{
key: "priority",
titleTranslationKey: "common.priority",
},
{ key: "state", titleTranslationKey: "common.state" },
{
key: "sub_issue_count",
titleTranslationKey: "issue.display.properties.sub_issue_count",
},
{
key: "attachment_count",
titleTranslationKey: "issue.display.properties.attachment_count",
},
{ key: "link", titleTranslationKey: "common.link" },
{
key: "estimate",
titleTranslationKey: "common.estimate",
},
{ key: "modules", titleTranslationKey: "common.module" },
{ key: "cycle", titleTranslationKey: "common.cycle" },
];
+530
View File
@@ -0,0 +1,530 @@
import {
ILayoutDisplayFiltersOptions,
TIssueActivityComment,
} from "@plane/types";
import {
TIssueFilterPriorityObject,
ISSUE_DISPLAY_PROPERTIES_KEYS,
EIssuesStoreType,
} from "./common";
import { TIssueLayout } from "./layout";
export type TIssueFilterKeys = "priority" | "state" | "labels";
export enum EServerGroupByToFilterOptions {
"state_id" = "state",
"priority" = "priority",
"labels__id" = "labels",
"state__group" = "state_group",
"assignees__id" = "assignees",
"cycle_id" = "cycle",
"issue_module__module_id" = "module",
"target_date" = "target_date",
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueFilterType {
FILTERS = "filters",
DISPLAY_FILTERS = "display_filters",
DISPLAY_PROPERTIES = "display_properties",
KANBAN_FILTERS = "kanban_filters",
}
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
} = {
list: {
filters: ["priority", "state", "labels"],
},
kanban: {
filters: ["priority", "state", "labels"],
},
calendar: {
filters: ["priority", "state", "labels"],
},
spreadsheet: {
filters: ["priority", "state", "labels"],
},
gantt: {
filters: ["priority", "state", "labels"],
},
};
export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
{
key: "urgent",
titleTranslationKey: "issue.priority.urgent",
className: "bg-red-500 border-red-500 text-white",
icon: "error",
},
{
key: "high",
titleTranslationKey: "issue.priority.high",
className: "text-orange-500 border-custom-border-300",
icon: "signal_cellular_alt",
},
{
key: "medium",
titleTranslationKey: "issue.priority.medium",
className: "text-yellow-500 border-custom-border-300",
icon: "signal_cellular_alt_2_bar",
},
{
key: "low",
titleTranslationKey: "issue.priority.low",
className: "text-green-500 border-custom-border-300",
icon: "signal_cellular_alt_1_bar",
},
{
key: "none",
titleTranslationKey: "common.none",
className: "text-gray-500 border-custom-border-300",
icon: "block",
},
];
export type TFiltersByLayout = {
[layoutType: string]: ILayoutDisplayFiltersOptions;
};
export type TIssueFiltersToDisplayByPageType = {
[pageType: string]: TFiltersByLayout;
};
export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
profile_issues: {
list: {
filters: [
"priority",
"state_group",
"labels",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels", null],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {
filters: [
"priority",
"state_group",
"labels",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels"],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
archived_issues: {
list: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"cycle",
"module",
"state_detail.group",
"priority",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
draft_issues: {
list: {
filters: [
"priority",
"state_group",
"cycle",
"module",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state_detail.group",
"cycle",
"module",
"priority",
"project",
"labels",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
kanban: {
filters: [
"priority",
"state_group",
"cycle",
"module",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state_detail.group",
"cycle",
"module",
"priority",
"project",
"labels",
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
my_issues: {
spreadsheet: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
list: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: false,
values: [],
},
},
},
issues: {
list: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
],
sub_group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
"target_date",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
calendar: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"issue_type",
],
display_properties: ["key", "issue_type"],
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
spreadsheet: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
gantt_chart: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ["key", "issue_type"],
display_filters: {
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
},
};
export const ISSUE_STORE_TO_FILTERS_MAP: Partial<
Record<EIssuesStoreType, TFiltersByLayout>
> = {
[EIssuesStoreType.PROJECT]: ISSUE_DISPLAY_FILTERS_BY_PAGE.issues,
};
export enum EActivityFilterType {
ACTIVITY = "ACTIVITY",
COMMENT = "COMMENT",
}
export type TActivityFilters = EActivityFilterType;
export const ACTIVITY_FILTER_TYPE_OPTIONS: Record<
TActivityFilters,
{ labelTranslationKey: string }
> = {
[EActivityFilterType.ACTIVITY]: {
labelTranslationKey: "common.updates",
},
[EActivityFilterType.COMMENT]: {
labelTranslationKey: "common.comments",
},
};
export type TActivityFilterOption = {
key: TActivityFilters;
labelTranslationKey: string;
isSelected: boolean;
onClick: () => void;
};
export const defaultActivityFilters: TActivityFilters[] = [
EActivityFilterType.ACTIVITY,
EActivityFilterType.COMMENT,
];
export const filterActivityOnSelectedFilters = (
activity: TIssueActivityComment[],
filters: TActivityFilters[]
): TIssueActivityComment[] =>
activity.filter((activity) =>
filters.includes(activity.activity_type as TActivityFilters)
);
export const ENABLE_ISSUE_DEPENDENCIES = false;
+3
View File
@@ -0,0 +1,3 @@
export * from "./common";
export * from "./filter";
export * from "./layout";
+76
View File
@@ -0,0 +1,76 @@
export type TIssueLayout =
| "list"
| "kanban"
| "calendar"
| "spreadsheet"
| "gantt";
export enum EIssueLayoutTypes {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
GANTT = "gantt_chart",
SPREADSHEET = "spreadsheet",
}
export type TIssueLayoutMap = Record<
EIssueLayoutTypes,
{
key: EIssueLayoutTypes;
i18n_title: string;
i18n_label: string;
}
>;
export const SITES_ISSUE_LAYOUTS: {
key: TIssueLayout;
titleTranslationKey: string;
icon: any;
}[] = [
{
key: "list",
icon: "List",
titleTranslationKey: "issue.layouts.list",
},
{
key: "kanban",
icon: "Kanban",
titleTranslationKey: "issue.layouts.kanban",
},
// { key: "calendar", title: "Calendar", icon: Calendar },
// { key: "spreadsheet", title: "Spreadsheet", icon: Sheet },
// { key: "gantt", title: "Gantt chart", icon: GanttChartSquare },
];
export const ISSUE_LAYOUT_MAP: TIssueLayoutMap = {
[EIssueLayoutTypes.LIST]: {
key: EIssueLayoutTypes.LIST,
i18n_title: "issue.layouts.title.list",
i18n_label: "issue.layouts.list",
},
[EIssueLayoutTypes.KANBAN]: {
key: EIssueLayoutTypes.KANBAN,
i18n_title: "issue.layouts.title.kanban",
i18n_label: "issue.layouts.kanban",
},
[EIssueLayoutTypes.CALENDAR]: {
key: EIssueLayoutTypes.CALENDAR,
i18n_title: "issue.layouts.title.calendar",
i18n_label: "issue.layouts.calendar",
},
[EIssueLayoutTypes.SPREADSHEET]: {
key: EIssueLayoutTypes.SPREADSHEET,
i18n_title: "issue.layouts.title.spreadsheet",
i18n_label: "issue.layouts.spreadsheet",
},
[EIssueLayoutTypes.GANTT]: {
key: EIssueLayoutTypes.GANTT,
i18n_title: "issue.layouts.title.gantt",
i18n_label: "issue.layouts.gantt",
},
};
export const ISSUE_LAYOUTS: {
key: EIssueLayoutTypes;
i18n_title: string;
}[] = Object.values(ISSUE_LAYOUT_MAP);
+3 -3
View File
@@ -3,9 +3,9 @@ export const SITE_NAME =
export const SITE_TITLE =
"Plane | Simple, extensible, open-source project management tool.";
export const SITE_DESCRIPTION =
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.";
"Open-source project management tool to manage work items, cycles, and product roadmaps easily";
export const SITE_KEYWORDS =
"software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration";
"software development, plan, ship, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration";
export const SITE_URL = "https://app.plane.so/";
export const TWITTER_USER_NAME =
"Plane | Simple, extensible, open-source project management tool.";
@@ -18,6 +18,6 @@ export const SPACE_SITE_TITLE =
export const SPACE_SITE_DESCRIPTION =
"Plane Publish is a customer feedback management tool built on top of plane.so";
export const SPACE_SITE_KEYWORDS =
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration";
"software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration";
export const SPACE_SITE_URL = "https://app.plane.so/";
export const SPACE_TWITTER_USER_NAME = "planepowers";
@@ -1,51 +1,54 @@
import { GanttChartSquare, LayoutGrid, List } from "lucide-react";
// types
import { TModuleLayoutOptions, TModuleOrderByOptions, TModuleStatus } from "@plane/types";
import {
TModuleLayoutOptions,
TModuleOrderByOptions,
TModuleStatus,
} from "@plane/types";
export const MODULE_STATUS: {
label: string;
i18n_label: string;
value: TModuleStatus;
color: string;
textColor: string;
bgColor: string;
}[] = [
{
label: "Backlog",
i18n_label: "project_modules.status.backlog",
value: "backlog",
color: "#a3a3a2",
textColor: "text-custom-text-400",
bgColor: "bg-custom-background-80",
},
{
label: "Planned",
i18n_label: "project_modules.status.planned",
value: "planned",
color: "#3f76ff",
textColor: "text-blue-500",
bgColor: "bg-indigo-50",
},
{
label: "In Progress",
i18n_label: "project_modules.status.in_progress",
value: "in-progress",
color: "#f39e1f",
textColor: "text-amber-500",
bgColor: "bg-amber-50",
},
{
label: "Paused",
i18n_label: "project_modules.status.paused",
value: "paused",
color: "#525252",
textColor: "text-custom-text-300",
bgColor: "bg-custom-background-90",
},
{
label: "Completed",
i18n_label: "project_modules.status.completed",
value: "completed",
color: "#16a34a",
textColor: "text-green-600",
bgColor: "bg-green-100",
},
{
label: "Cancelled",
i18n_label: "project_modules.status.cancelled",
value: "cancelled",
color: "#ef4444",
textColor: "text-red-500",
@@ -53,47 +56,50 @@ export const MODULE_STATUS: {
},
];
export const MODULE_VIEW_LAYOUTS: { key: TModuleLayoutOptions; icon: any; title: string }[] = [
export const MODULE_VIEW_LAYOUTS: {
key: TModuleLayoutOptions;
i18n_title: string;
}[] = [
{
key: "list",
icon: List,
title: "List layout",
i18n_title: "project_modules.layout.list",
},
{
key: "board",
icon: LayoutGrid,
title: "Gallery layout",
i18n_title: "project_modules.layout.board",
},
{
key: "gantt",
icon: GanttChartSquare,
title: "Timeline layout",
i18n_title: "project_modules.layout.timeline",
},
];
export const MODULE_ORDER_BY_OPTIONS: { key: TModuleOrderByOptions; label: string }[] = [
export const MODULE_ORDER_BY_OPTIONS: {
key: TModuleOrderByOptions;
i18n_label: string;
}[] = [
{
key: "name",
label: "Name",
i18n_label: "project_modules.order_by.name",
},
{
key: "progress",
label: "Progress",
i18n_label: "project_modules.order_by.progress",
},
{
key: "issues_length",
label: "Number of issues",
i18n_label: "project_modules.order_by.issues",
},
{
key: "target_date",
label: "Due date",
i18n_label: "project_modules.order_by.due_date",
},
{
key: "created_at",
label: "Created date",
i18n_label: "project_modules.order_by.created_at",
},
{
key: "sort_order",
label: "Manual",
i18n_label: "project_modules.order_by.manual",
},
];
@@ -29,12 +29,13 @@ export type TNotificationTab = ENotificationTab.ALL | ENotificationTab.MENTIONS;
export const NOTIFICATION_TABS = [
{
label: "All",
i18n_label: "notification.tabs.all",
value: ENotificationTab.ALL,
count: (unReadNotification: TUnreadNotificationsCount) => unReadNotification?.total_unread_notifications_count || 0,
count: (unReadNotification: TUnreadNotificationsCount) =>
unReadNotification?.total_unread_notifications_count || 0,
},
{
label: "Mentions",
i18n_label: "notification.tabs.mentions",
value: ENotificationTab.MENTIONS,
count: (unReadNotification: TUnreadNotificationsCount) =>
unReadNotification?.mention_unread_notifications_count || 0,
@@ -43,15 +44,15 @@ export const NOTIFICATION_TABS = [
export const FILTER_TYPE_OPTIONS = [
{
label: "Assigned to me",
i18n_label: "notification.filter.assigned",
value: ENotificationFilterType.ASSIGNED,
},
{
label: "Created by me",
i18n_label: "notification.filter.created",
value: ENotificationFilterType.CREATED,
},
{
label: "Subscribed by me",
i18n_label: "notification.filter.subscribed",
value: ENotificationFilterType.SUBSCRIBED,
},
];
@@ -59,7 +60,7 @@ export const FILTER_TYPE_OPTIONS = [
export const NOTIFICATION_SNOOZE_OPTIONS = [
{
key: "1_day",
label: "1 day",
i18n_label: "notification.snooze.1_day",
value: () => {
const date = new Date();
return new Date(date.getTime() + 24 * 60 * 60 * 1000);
@@ -67,7 +68,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
},
{
key: "3_days",
label: "3 days",
i18n_label: "notification.snooze.3_days",
value: () => {
const date = new Date();
return new Date(date.getTime() + 3 * 24 * 60 * 60 * 1000);
@@ -75,7 +76,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
},
{
key: "5_days",
label: "5 days",
i18n_label: "notification.snooze.5_days",
value: () => {
const date = new Date();
return new Date(date.getTime() + 5 * 24 * 60 * 60 * 1000);
@@ -83,7 +84,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
},
{
key: "1_week",
label: "1 week",
i18n_label: "notification.snooze.1_week",
value: () => {
const date = new Date();
return new Date(date.getTime() + 7 * 24 * 60 * 60 * 1000);
@@ -91,7 +92,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
},
{
key: "2_weeks",
label: "2 weeks",
i18n_label: "notification.snooze.2_weeks",
value: () => {
const date = new Date();
return new Date(date.getTime() + 14 * 24 * 60 * 60 * 1000);
@@ -99,7 +100,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
},
{
key: "custom",
label: "Custom",
i18n_label: "notification.snooze.custom",
value: undefined,
},
];
+14
View File
@@ -0,0 +1,14 @@
export enum EPageAccess {
PUBLIC = 0,
PRIVATE = 1,
}
export type TCreatePageModal = {
isOpen: boolean;
pageAccess?: EPageAccess;
};
export const DEFAULT_CREATE_PAGE_MODAL_DATA: TCreatePageModal = {
isOpen: false,
pageAccess: EPageAccess.PUBLIC,
};
@@ -1,48 +1,38 @@
import React from "react";
// icons
import { Activity, Bell, CircleUser, KeyRound, LucideProps, Settings2 } from "lucide-react";
export const PROFILE_ACTION_LINKS: {
key: string;
label: string;
i18n_label: string;
href: string;
highlight: (pathname: string) => boolean;
Icon: React.FC<LucideProps>;
}[] = [
{
key: "profile",
label: "Profile",
i18n_label: "profile.actions.profile",
href: `/profile`,
highlight: (pathname: string) => pathname === "/profile/",
Icon: CircleUser,
},
{
key: "security",
label: "Security",
i18n_label: "profile.actions.security",
href: `/profile/security`,
highlight: (pathname: string) => pathname === "/profile/security/",
Icon: KeyRound,
},
{
key: "activity",
label: "Activity",
i18n_label: "profile.actions.activity",
href: `/profile/activity`,
highlight: (pathname: string) => pathname === "/profile/activity/",
Icon: Activity,
},
{
key: "appearance",
label: "Appearance",
i18n_label: "profile.actions.appearance",
href: `/profile/appearance`,
highlight: (pathname: string) => pathname.includes("/profile/appearance"),
Icon: Settings2,
},
{
key: "notifications",
label: "Notifications",
i18n_label: "profile.actions.notifications",
href: `/profile/notifications`,
highlight: (pathname: string) => pathname === "/profile/notifications/",
Icon: Bell,
},
];
@@ -50,7 +40,7 @@ export const PROFILE_VIEWER_TAB = [
{
key: "summary",
route: "",
label: "Summary",
i18n_label: "profile.tabs.summary",
selected: "/",
},
];
@@ -59,24 +49,25 @@ export const PROFILE_ADMINS_TAB = [
{
key: "assigned",
route: "assigned",
label: "Assigned",
i18n_label: "profile.tabs.assigned",
selected: "/assigned/",
},
{
key: "created",
route: "created",
label: "Created",
i18n_label: "profile.tabs.created",
selected: "/created/",
},
{
key: "subscribed",
route: "subscribed",
label: "Subscribed",
i18n_label: "profile.tabs.subscribed",
selected: "/subscribed/",
},
{
key: "activity",
route: "activity",
label: "Activity",
i18n_label: "profile.tabs.activity",
selected: "/activity/",
},
];
@@ -1,41 +1,65 @@
// icons
import { Globe2, Lock, LucideIcon } from "lucide-react";
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
import {
TProjectAppliedDisplayFilterKeys,
TProjectOrderByOptions,
} from "@plane/types";
export const NETWORK_CHOICES: {
export type TNetworkChoiceIconKey = "Lock" | "Globe2";
export type TNetworkChoice = {
key: 0 | 2;
label: string;
labelKey: string;
i18n_label: string;
description: string;
icon: LucideIcon;
}[] = [
iconKey: TNetworkChoiceIconKey;
};
export const NETWORK_CHOICES: TNetworkChoice[] = [
{
key: 0,
label: "Private",
description: "Accessible only by invite",
icon: Lock,
labelKey: "Private",
i18n_label: "workspace_projects.network.private.title",
description: "workspace_projects.network.private.description", //"Accessible only by invite",
iconKey: "Lock",
},
{
key: 2,
label: "Public",
description: "Anyone in the workspace except Guests can join",
icon: Globe2,
labelKey: "Public",
i18n_label: "workspace_projects.network.public.title",
description: "workspace_projects.network.public.description", //"Anyone in the workspace except Guests can join",
iconKey: "Globe2",
},
];
export const GROUP_CHOICES = {
backlog: "Backlog",
unstarted: "Unstarted",
started: "Started",
completed: "Completed",
cancelled: "Cancelled",
backlog: {
key: "backlog",
i18n_label: "workspace_projects.state.backlog",
},
unstarted: {
key: "unstarted",
i18n_label: "workspace_projects.state.unstarted",
},
started: {
key: "started",
i18n_label: "workspace_projects.state.started",
},
completed: {
key: "completed",
i18n_label: "workspace_projects.state.completed",
},
cancelled: {
key: "cancelled",
i18n_label: "workspace_projects.state.cancelled",
},
};
export const PROJECT_AUTOMATION_MONTHS = [
{ label: "1 month", value: 1 },
{ label: "3 months", value: 3 },
{ label: "6 months", value: 6 },
{ label: "9 months", value: 9 },
{ label: "12 months", value: 12 },
{ i18n_label: "common.months_count", value: 1 },
{ i18n_label: "common.months_count", value: 3 },
{ i18n_label: "common.months_count", value: 6 },
{ i18n_label: "common.months_count", value: 9 },
{ i18n_label: "common.months_count", value: 12 },
];
export const PROJECT_UNSPLASH_COVERS = [
@@ -59,55 +83,55 @@ export const PROJECT_UNSPLASH_COVERS = [
export const PROJECT_ORDER_BY_OPTIONS: {
key: TProjectOrderByOptions;
label: string;
i18n_label: string;
}[] = [
{
key: "sort_order",
label: "Manual",
i18n_label: "workspace_projects.sort.manual",
},
{
key: "name",
label: "Name",
i18n_label: "workspace_projects.sort.name",
},
{
key: "created_at",
label: "Created date",
i18n_label: "workspace_projects.sort.created_at",
},
{
key: "members_length",
label: "Number of members",
i18n_label: "workspace_projects.sort.members_length",
},
];
export const PROJECT_DISPLAY_FILTER_OPTIONS: {
key: TProjectAppliedDisplayFilterKeys;
label: string;
i18n_label: string;
}[] = [
{
key: "my_projects",
label: "My projects",
i18n_label: "workspace_projects.scope.my_projects",
},
{
key: "archived_projects",
label: "Archived",
i18n_label: "workspace_projects.scope.archived_projects",
},
];
export const PROJECT_ERROR_MESSAGES = {
permissionError: {
title: "You don't have permission to perform this action.",
message: undefined,
i18n_title: "workspace_projects.error.permission",
i18n_message: undefined,
},
cycleDeleteError: {
title: "Error",
message: "Failed to delete cycle",
i18n_title: "error",
i18n_message: "workspace_projects.error.cycle_delete",
},
moduleDeleteError: {
title: "Error",
message: "Failed to delete module",
i18n_title: "error",
i18n_message: "workspace_projects.error.module_delete",
},
issueDeleteError: {
title: "Error",
message: "Failed to delete issue",
i18n_title: "error",
i18n_message: "workspace_projects.error.issue_delete",
},
};
+1
View File
@@ -0,0 +1 @@
export const SPREADSHEET_SELECT_GROUP = "spreadsheet-issues";
+14
View File
@@ -5,6 +5,11 @@ export type TStateGroups =
| "completed"
| "cancelled";
export type TDraggableData = {
groupKey: TStateGroups;
id: string;
};
export const STATE_GROUPS: {
[key in TStateGroups]: {
key: TStateGroups;
@@ -43,6 +48,13 @@ export const ARCHIVABLE_STATE_GROUPS = [
STATE_GROUPS.completed.key,
STATE_GROUPS.cancelled.key,
];
export const COMPLETED_STATE_GROUPS = [STATE_GROUPS.completed.key];
export const PENDING_STATE_GROUPS = [
STATE_GROUPS.backlog.key,
STATE_GROUPS.unstarted.key,
STATE_GROUPS.started.key,
STATE_GROUPS.cancelled.key,
];
export const PROGRESS_STATE_GROUPS_DETAILS = [
{
@@ -66,3 +78,5 @@ export const PROGRESS_STATE_GROUPS_DETAILS = [
color: "#A3A3A3",
},
];
export const DISPLAY_WORKFLOW_PRO_CTA = false;
+8
View File
@@ -6,3 +6,11 @@ export const DEFAULT_SWR_CONFIG = {
refreshInterval: 600000,
errorRetryCount: 3,
};
export const WEB_SWR_CONFIG = {
refreshWhenHidden: false,
revalidateIfStale: true,
revalidateOnFocus: true,
revalidateOnMount: true,
errorRetryCount: 3,
};
@@ -2,7 +2,6 @@ export const ISSUE_FORM_TAB_INDICES = [
"name",
"description_html",
"feeling_lucky",
"ai_assistant",
"state_id",
"priority",
"assignee_ids",
@@ -54,7 +53,14 @@ export const PROJECT_CREATE_TAB_INDICES = [
"logo_props",
];
export const PROJECT_CYCLE_TAB_INDICES = ["name", "description", "date_range", "cancel", "submit", "project_id"];
export const PROJECT_CYCLE_TAB_INDICES = [
"name",
"description",
"date_range",
"cancel",
"submit",
"project_id",
];
export const PROJECT_MODULE_TAB_INDICES = [
"name",
@@ -67,9 +73,21 @@ export const PROJECT_MODULE_TAB_INDICES = [
"submit",
];
export const PROJECT_VIEW_TAB_INDICES = ["name", "description", "filters", "cancel", "submit"];
export const PROJECT_VIEW_TAB_INDICES = [
"name",
"description",
"filters",
"cancel",
"submit",
];
export const PROJECT_PAGE_TAB_INDICES = ["name", "public", "private", "cancel", "submit"];
export const PROJECT_PAGE_TAB_INDICES = [
"name",
"public",
"private",
"cancel",
"submit",
];
export enum ETabIndices {
ISSUE_FORM = "issue-form",
@@ -1,9 +1,15 @@
export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"];
export const THEMES = [
"light",
"dark",
"light-contrast",
"dark-contrast",
"custom",
];
export interface I_THEME_OPTION {
key: string;
value: string;
label: string;
i18n_label: string;
type: string;
icon: {
border: string;
@@ -16,7 +22,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "system_preference",
value: "system",
label: "System preference",
i18n_label: "System preference",
type: "light",
icon: {
border: "#DEE2E6",
@@ -27,7 +33,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "light",
value: "light",
label: "Light",
i18n_label: "Light",
type: "light",
icon: {
border: "#DEE2E6",
@@ -38,7 +44,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "dark",
value: "dark",
label: "Dark",
i18n_label: "Dark",
type: "dark",
icon: {
border: "#2E3234",
@@ -49,7 +55,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "light_contrast",
value: "light-contrast",
label: "Light high contrast",
i18n_label: "Light high contrast",
type: "light",
icon: {
border: "#000000",
@@ -60,7 +66,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "dark_contrast",
value: "dark-contrast",
label: "Dark high contrast",
i18n_label: "Dark high contrast",
type: "dark",
icon: {
border: "#FFFFFF",
@@ -71,7 +77,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "custom",
value: "custom",
label: "Custom theme",
i18n_label: "Custom theme",
type: "light",
icon: {
border: "#FFC9C9",
+37
View File
@@ -36,3 +36,40 @@ export enum EUserProjectRoles {
MEMBER = 15,
GUEST = 5,
}
export type TUserPermissionsLevel = EUserPermissionsLevel;
export enum EUserPermissions {
ADMIN = 20,
MEMBER = 15,
GUEST = 5,
}
export type TUserPermissions = EUserPermissions;
export type TUserAllowedPermissionsObject = {
create: TUserPermissions[];
update: TUserPermissions[];
delete: TUserPermissions[];
read: TUserPermissions[];
};
export type TUserAllowedPermissions = {
workspace: {
[key: string]: Partial<TUserAllowedPermissionsObject>;
};
project: {
[key: string]: Partial<TUserAllowedPermissionsObject>;
};
};
export const USER_ALLOWED_PERMISSIONS: TUserAllowedPermissions = {
workspace: {
dashboard: {
read: [
EUserPermissions.ADMIN,
EUserPermissions.MEMBER,
EUserPermissions.GUEST,
],
},
},
project: {},
};
+23
View File
@@ -0,0 +1,23 @@
export enum EViewAccess {
PRIVATE,
PUBLIC,
}
export const VIEW_ACCESS_SPECIFIERS: {
key: EViewAccess;
i18n_label: string;
}[] = [
{ key: EViewAccess.PUBLIC, i18n_label: "common.access.public" },
{ key: EViewAccess.PRIVATE, i18n_label: "common.access.private" },
];
export const VIEW_SORTING_KEY_OPTIONS = [
{ key: "name", i18n_label: "project_view.sort_by.name" },
{ key: "created_at", i18n_label: "project_view.sort_by.created_at" },
{ key: "updated_at", i18n_label: "project_view.sort_by.updated_at" },
];
export const VIEW_SORT_BY_OPTIONS = [
{ key: "asc", i18n_label: "common.order_by.asc" },
{ key: "desc", i18n_label: "common.order_by.desc" },
];
+250
View File
@@ -1,3 +1,6 @@
import { TStaticViewTypes } from "@plane/types";
import { EUserWorkspaceRoles } from "./user";
export const ORGANIZATION_SIZE = [
"Just myself", // TODO: translate
"2-10",
@@ -74,3 +77,250 @@ export const RESTRICTED_URLS = [
"instances",
"instance",
];
export const WORKSPACE_SETTINGS = {
general: {
key: "general",
i18n_label: "workspace_settings.settings.general.title",
href: `/settings`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
},
members: {
key: "members",
i18n_label: "workspace_settings.settings.members.title",
href: `/settings/members`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
},
"billing-and-plans": {
key: "billing-and-plans",
i18n_label: "workspace_settings.settings.billing_and_plans.title",
href: `/settings/billing`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing/`,
},
export: {
key: "export",
i18n_label: "workspace_settings.settings.exports.title",
href: `/settings/exports`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`,
},
webhooks: {
key: "webhooks",
i18n_label: "workspace_settings.settings.webhooks.title",
href: `/settings/webhooks`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`,
},
"api-tokens": {
key: "api-tokens",
i18n_label: "workspace_settings.settings.api_tokens.title",
href: `/settings/api-tokens`,
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`,
},
};
export const WORKSPACE_SETTINGS_LINKS: {
key: string;
i18n_label: string;
href: string;
access: EUserWorkspaceRoles[];
highlight: (pathname: string, baseUrl: string) => boolean;
}[] = [
WORKSPACE_SETTINGS["general"],
WORKSPACE_SETTINGS["members"],
WORKSPACE_SETTINGS["billing-and-plans"],
WORKSPACE_SETTINGS["export"],
WORKSPACE_SETTINGS["webhooks"],
WORKSPACE_SETTINGS["api-tokens"],
];
export const ROLE = {
[EUserWorkspaceRoles.GUEST]: "Guest",
[EUserWorkspaceRoles.MEMBER]: "Member",
[EUserWorkspaceRoles.ADMIN]: "Admin",
};
export const ROLE_DETAILS = {
[EUserWorkspaceRoles.GUEST]: {
i18n_title: "role_details.guest.title",
i18n_description: "role_details.guest.description",
},
[EUserWorkspaceRoles.MEMBER]: {
i18n_title: "role_details.member.title",
i18n_description: "role_details.member.description",
},
[EUserWorkspaceRoles.ADMIN]: {
i18n_title: "role_details.admin.title",
i18n_description: "role_details.admin.description",
},
};
export const USER_ROLES = [
{
value: "Product / Project Manager",
i18n_label: "user_roles.product_or_project_manager",
},
{
value: "Development / Engineering",
i18n_label: "user_roles.development_or_engineering",
},
{
value: "Founder / Executive",
i18n_label: "user_roles.founder_or_executive",
},
{
value: "Freelancer / Consultant",
i18n_label: "user_roles.freelancer_or_consultant",
},
{ value: "Marketing / Growth", i18n_label: "user_roles.marketing_or_growth" },
{
value: "Sales / Business Development",
i18n_label: "user_roles.sales_or_business_development",
},
{
value: "Support / Operations",
i18n_label: "user_roles.support_or_operations",
},
{
value: "Student / Professor",
i18n_label: "user_roles.student_or_professor",
},
{ value: "Human Resources", i18n_label: "user_roles.human_resources" },
{ value: "Other", i18n_label: "user_roles.other" },
];
export const IMPORTERS_LIST = [
{
provider: "github",
type: "import",
i18n_title: "importer.github.title",
i18n_description: "importer.github.description",
},
{
provider: "jira",
type: "import",
i18n_title: "importer.jira.title",
i18n_description: "importer.jira.description",
},
];
export const EXPORTERS_LIST = [
{
provider: "csv",
type: "export",
i18n_title: "exporter.csv.title",
i18n_description: "exporter.csv.description",
},
{
provider: "xlsx",
type: "export",
i18n_title: "exporter.excel.title",
i18n_description: "exporter.csv.description",
},
{
provider: "json",
type: "export",
i18n_title: "exporter.json.title",
i18n_description: "exporter.csv.description",
},
];
export const DEFAULT_GLOBAL_VIEWS_LIST: {
key: TStaticViewTypes;
i18n_label: string;
}[] = [
{
key: "all-issues",
i18n_label: "default_global_view.all_issues",
},
{
key: "assigned",
i18n_label: "default_global_view.assigned",
},
{
key: "created",
i18n_label: "default_global_view.created",
},
{
key: "subscribed",
i18n_label: "default_global_view.subscribed",
},
];
export interface IWorkspaceSidebarNavigationItem {
key: string;
labelTranslationKey: string;
href: string;
access: EUserWorkspaceRoles[];
}
export const WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS: Record<string, IWorkspaceSidebarNavigationItem> = {
"your-work": {
key: "your_work",
labelTranslationKey: "your_work",
href: `/profile/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
},
views: {
key: "views",
labelTranslationKey: "views",
href: `/workspace-views/all-issues/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
},
analytics: {
key: "analytics",
labelTranslationKey: "analytics",
href: `/analytics/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
},
drafts: {
key: "drafts",
labelTranslationKey: "drafts",
href: `/drafts/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
},
archives: {
key: "archives",
labelTranslationKey: "archives",
href: `/projects/archives/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
},
};
export const WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS: IWorkspaceSidebarNavigationItem[] = [
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["views"],
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["analytics"],
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["your-work"],
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["drafts"],
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["archives"],
];
export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS: Record<string, IWorkspaceSidebarNavigationItem> = {
home: {
key: "home",
labelTranslationKey: "home.title",
href: `/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
},
inbox: {
key: "inbox",
labelTranslationKey: "notification.label",
href: `/notifications/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
},
projects: {
key: "projects",
labelTranslationKey: "projects",
href: `/projects/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
},
};
export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS: IWorkspaceSidebarNavigationItem[] = [
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["home"],
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["inbox"],
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["projects"],
];
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.24.1",
"version": "0.25.0",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
@@ -61,7 +61,7 @@
"jsx-dom-cjs": "^8.0.3",
"linkifyjs": "^4.1.3",
"lowlight": "^3.0.0",
"lucide-react": "^0.378.0",
"lucide-react": "^0.469.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-utils": "^1.2.2",
"tippy.js": "^6.3.7",
@@ -1,3 +1,4 @@
import { Extensions } from "@tiptap/core";
import React from "react";
// components
import { DocumentContentLoader, PageRenderer } from "@/components/editors";
@@ -35,7 +36,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
user,
} = props;
const extensions = [];
const extensions: Extensions = [];
if (embedHandler?.issue) {
extensions.push(
IssueWidget({
@@ -1,3 +1,4 @@
import { Extensions } from "@tiptap/core";
import { forwardRef, MutableRefObject } from "react";
// components
import { PageRenderer } from "@/components/editors";
@@ -10,7 +11,13 @@ import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types
import { EditorReadOnlyRefApi, TDisplayConfig, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
import {
EditorReadOnlyRefApi,
TDisplayConfig,
TExtensions,
TReadOnlyFileHandler,
TReadOnlyMentionHandler,
} from "@/types";
interface IDocumentReadOnlyEditor {
disabledExtensions: TExtensions[];
@@ -20,7 +27,7 @@ interface IDocumentReadOnlyEditor {
displayConfig?: TDisplayConfig;
editorClassName?: string;
embedHandler: any;
fileHandler: Pick<TFileHandler, "getAssetSrc">;
fileHandler: TReadOnlyFileHandler;
tabIndex?: number;
handleEditorReady?: (value: boolean) => void;
mentionHandler: TReadOnlyMentionHandler;
@@ -41,7 +48,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
initialValue,
mentionHandler,
} = props;
const extensions = [];
const extensions: Extensions = [];
if (embedHandler?.issue) {
extensions.push(
IssueWidget({
@@ -23,6 +23,7 @@ export const AIFeaturesMenu: React.FC<Props> = (props) => {
menuRef.current.remove();
menuRef.current.style.visibility = "visible";
// @ts-expect-error - Tippy types are incorrect
popup.current = tippy(document.body, {
getReferenceClientRect: null,
content: menuRef.current,
@@ -34,6 +34,7 @@ export const BlockMenu = (props: BlockMenuProps) => {
menuRef.current.remove();
menuRef.current.style.visibility = "visible";
// @ts-expect-error - Tippy types are incorrect
popup.current = tippy(document.body, {
getReferenceClientRect: null,
content: menuRef.current,

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