Compare commits

...

92 Commits

Author SHA1 Message Date
Anmol Singh Bhatia
73d5a80c38 fix: sub work item 2025-03-28 16:53:00 +05:30
Anmol Singh Bhatia
92cdc4ecbb fix: create sub work item operation 2025-02-13 18:26:36 +05:30
Vamsi Krishna
b88ae112f9 fix: updated lang key for defaults (#6599) 2025-02-12 19:59:05 +05:30
sriram veeraghanta
2d20278c9b fix: esbuild deps resolution 2025-02-12 18:44:31 +05:30
Prateek Shourya
8cff059868 improvement: add timeout before resetting data in workspace delete form (#6598) 2025-02-12 17:39:12 +05:30
Akshita Goyal
6a3ccafe35 fix: code splitting for workspace delete modal (#6581)
* fix: code splitting for delete modal

* fix: redirected to profile post deletion

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


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

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

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


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

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

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

* chore: changed the filtering logic

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

* chore: platform-specific command icons

---------

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

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

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


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

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

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

* fix: update label creation permissions

---------

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

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

* chore: separate out pie chart tooltip

* chore: remove unused any types

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

* fix: user role and member count for new workspace

* chore: set role to 20 while workspace creation

---------

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

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

* add comment

* fix double tap

* make label focus

* fix title focus

* focus discard and save

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

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

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

* fix: hover

* fix: added current plan

* feat: Return user role

* chore: remove unused imports

* fix: css

* fix: added user role in workspace switcher

* fix: return role as integer

* fix: role casing

* fix: refactor

* fix: plan pill fix

* fix: design updates

* fix: refactor

* fix: member translation

* fix: css improvements

* fix: truncate issue

* fix: workspace switcher dropdown email truncate

* fix: workspace switcher dropdown email truncate

* fix: role

---------

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

* fix: quickstart guide

* fix: link handling

* fix: home completed state

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


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

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

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

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

* fix: translation key

* chore: empty state refactor (#6404)

* chore: asset path helper hook added

* chore: detailed and simple empty state component added

* chore: section empty state component added

* chore: language translation for all empty states

* chore: new empty state implementation

* improvement: add more translations

* improvement: user permissions and workspace draft empty state

* chore: update translation structure

* chore: inbox empty states

* chore: disabled project features empty state

* chore: active cycle progress empty state

* chore: notification empty state

* chore: connections translation

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

* chore: project pages empty state and translations

* chore: project module and view related empty state

* chore: remove project draft related empty state

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

* chore: project cycles related empty state

* chore: project settings empty state

* chore: profile issue and acitivity empty state

* chore: workspace settings realted constants

* chore: stickies and home widgets empty state

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

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

* chore: minor updates

* fix: build errors

---------

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

* fix: language support fo profile (#6461)

* fix: ln support fo profile

* fix: merge changes

* fix: merge changes

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

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

* chore: updated translation structure

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

* chore: updated spanish translation

* chore: removed translation for issue priorities

* fix: build errors

* chore: minor updates

---------

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

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

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

* chore: workspace drafts constant moved to plane constant package

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

* feat: home language support without stickies

* fix: home sidebar

* fix: added missing keys

* fix: show all btn

* fix: recents empty state

* chore: translation update

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

* chore: workspace constant language support and refactor

* chore: workspace constant language support and refactor

* chore: code refactor

* chore: code refactor

* merge conflict

* chore: code refactor

---------

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

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

* chore: notification language support and refactor

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

* chore: ln support for inbox constants

* fix: snooze duration

* fix: enum

* fix: translation keys

* fix: inbox status icon

* fix: status icon

* fix: naming

---------

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

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

* fix: ln support for views constants

* fix: added translation

* fix: translation keys

* fix: access

* chore: code refactor

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

* chore: ln support workspace projects constants

* fix: translation key

* fix: removed state translation

* fix: removed state translation

* fi: added translations

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

* chore: themes language support and refactor

* chore: theme language support and refactor

* fix

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

* chore: ln support for cycles constant file

* fix: added chinese

* fix: lint

* fix: translation key

* fix: build errors

* minor updates

* chore: minor translation update

* chore: minor translation update

* refactor: move labels contants to packages

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

* chore: timezones constant moved to plane package

* chore: metadata constant code refactor

* chore: code refactor

* fix: dashboard constants moved

* chore: code refactor (#6478)

* refactor: spreadsheet constants

* chore: drafts language support (#6485)

* chore: workspace drafts language support

* chore: code refactor

* feat: ln support for notifications (#6486)

* feat: ln support for notifications

* fix: translations

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

* fix: removed use-client

* chore: removed unnecessary commnets

* chore: workspace draft language support (#6490)

* chore: workspace drafts language support

* chore: code refactor

* chore: draft language support

* Feat constant event tracker (#6479)

* fix: event tracjer constants

* fix: constants event tracker

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

* feat: added translation to projects list page

* chore: restructured translation file

* chore: module language support (#6499)

* chore: module language support added

* chore: code refactor

* chore: workspace views language support (#6492)

* chore: workspace views language support

* chore: code refactor

* feat: custom analytics language support (#6494)

* feat: custom analytics language support

* fix: key

* fix: refactoring

---------

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

* chore: minor improvements

* feat: language support for intake (#6498)

* feat: language support for intake

* fix: key name

* refactor: authentications related translations

* feat: language support issues  (#6501)

* enhancement: added translations for issue list view

* chore: added translations for issue detail widgets

* chore: added missing translations

* chore: modified issue to work items

* chore: updated translations

* Feat: workspace settings language support (#6508)

* feat: language support for workspace settings

* fix: lint

* fix: export title

* chore project settings language support (#6502)

* chore: project settings language support

* chore: code refactor

* refactor: workspace creation related translations

* chore: renamed issues to work items

* fix: build errors

* fix: lint

* chore: modified translations

* chore: remove duplicate

* improvement: french translation

* chore: chinese translation improvement

* fix: japanese translations

* chore: added spanish translation

* minor improvements

* fix: miscelleous language translations

* fix: clear_all key

* fix: moved user permission constants (#6516)

* feat: language support for  issues (#6513)

* chore: added language support to issue detail widgets

* improvement: added translation for issue detail

* enhancement: added language trasnlation to issue layouts

* chore: translation improvement (#6518)

* feat: language support description (#6519)

* enhancement: added language support for description

* fix: updated keys

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

* chore: replace missing issue occurances to work items

* fix: build errors

* minor improvements

* fix: profile links

* Feat ln cycles (#6528)

* feat: added language support for cycles

* feat: added language support for cycles

* chore: added core.json

* fix: translation keys

* fix: translation keys (#6530)

* fix: changed sidebar keys

* fix: removed extras

* fix: updated keys

* chore: optimize translation imports

* fix: updated keys (#6534)

* fix: updated keys

* fix-sub work items toasts

* chore: add missing translation and minor fixes

* chore: code refactor

* fix: language support keys (#6553)

* minor improvements

* minor fixes

* fix: remove lucide import from constants package

* chore: regenerate all translations

* chore: addded chinese and japanese translation files

* chore: remove all  from translations

* fix: added member

* fix: language support keys (#6558)

* fix: renamed keys

* fix: space app

* chore: renamed issues to work items

* chore: update site manifest

* chore: updated translations

* fix: lang keys

* chore: update translations

---------

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

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

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

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

* fix: make delete_comment_activity include comment_id

The delete issues comment webhook requires comment_id

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

* chore: renamed the display value

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

* chore: revert prop changes

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

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

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

* feat: remove sort order calculation

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

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

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

* style: table cell padding

* style: table cell padding

* fix: insert resizable tables

---------

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

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

* style fixes

* style: css and autofocus improved

* fix: fixed weeks for datepicker to ensure static height

---------

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

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

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

* chore: replace margin with padding
2025-01-24 15:23:41 +05:30
Prateek Shourya
0b53912295 [WEB-3207] chore: add state_id, priority and assignee_ids to create issue relation response (#6448) 2025-01-23 14:16:06 +05:30
Prateek Shourya
586a320d86 fix: minor fixes for issue relations list and retrival (#6444) 2025-01-23 12:42:35 +05:30
865 changed files with 25239 additions and 16287 deletions

View File

@@ -25,6 +25,10 @@ on:
required: false
default: false
type: boolean
push:
branches:
- preview
- canary
env:
TARGET_BRANCH: ${{ github.ref_name }}

View File

@@ -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

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

@@ -207,6 +207,7 @@ class IssueSerializer(BaseSerializer):
for assignee_id in assignees
],
batch_size=10,
ignore_conflicts=True,
)
if labels is not None:
@@ -224,6 +225,7 @@ class IssueSerializer(BaseSerializer):
for label_id in labels
],
batch_size=10,
ignore_conflicts=True,
)
# Time updation occues even when other related models are updated

View File

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

View File

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

View File

@@ -203,6 +203,7 @@ class IssueCreateSerializer(BaseSerializer):
for user in assignees
],
batch_size=10,
ignore_conflicts=True,
)
if labels is not None:
@@ -220,6 +221,7 @@ class IssueCreateSerializer(BaseSerializer):
for label in labels
],
batch_size=10,
ignore_conflicts=True,
)
# Time updation occues even when other related models are updated
@@ -283,10 +285,26 @@ class IssueRelationSerializer(BaseSerializer):
)
name = serializers.CharField(source="related_issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
priority = serializers.CharField(source="related_issue.priority", read_only=True)
assignee_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = IssueRelation
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
"state_id",
"priority",
"assignee_ids",
]
read_only_fields = ["workspace", "project"]
@@ -298,10 +316,26 @@ class RelatedIssueSerializer(BaseSerializer):
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
name = serializers.CharField(source="issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
state_id = serializers.UUIDField(source="issue.state.id", read_only=True)
priority = serializers.CharField(source="issue.priority", read_only=True)
assignee_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = IssueRelation
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
"state_id",
"priority",
"assignee_ids",
]
read_only_fields = ["workspace", "project"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,6 +48,7 @@ from .workspace.favorite import (
WorkspaceFavoriteGroupEndpoint,
)
from .workspace.recent_visit import UserRecentVisitViewSet
from .workspace.user_preference import WorkspaceUserPreferenceViewSet
from .workspace.member import (
WorkSpaceMemberViewSet,
@@ -189,6 +190,7 @@ from .analytic.base import (
SavedAnalyticEndpoint,
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
ProjectStatsEndpoint,
)
from .notification.base import (

View File

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

View File

@@ -47,6 +47,7 @@ from plane.db.models import (
User,
Project,
ProjectMember,
UserRecentVisit,
)
from plane.utils.analytics_plot import burndown_plot
from plane.bgtasks.recent_visited_task import recent_visited_task
@@ -133,11 +134,11 @@ class CycleViewSet(BaseViewSet):
)
)
.annotate(
pending_issues=Count(
cancelled_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group__in=["backlog", "unstarted", "started"],
issue_cycle__issue__state__group__in=["cancelled"],
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
@@ -226,7 +227,7 @@ class CycleViewSet(BaseViewSet):
"is_favorite",
"total_issues",
"completed_issues",
"pending_issues",
"cancelled_issues",
"assignee_ids",
"status",
"version",
@@ -258,7 +259,7 @@ class CycleViewSet(BaseViewSet):
# meta fields
"is_favorite",
"total_issues",
"pending_issues",
"cancelled_issues",
"completed_issues",
"assignee_ids",
"status",
@@ -543,6 +544,13 @@ class CycleViewSet(BaseViewSet):
entity_identifier=pk,
project_id=project_id,
).delete()
# Delete the cycle from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="cycle",
).delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@@ -44,6 +44,7 @@ from plane.db.models import (
Project,
ProjectMember,
CycleIssue,
UserRecentVisit,
)
from plane.utils.grouper import (
issue_group_values,
@@ -671,6 +672,13 @@ class IssueViewSet(BaseViewSet):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue.delete()
# delete the issue from recent visits
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="issue",
).delete(soft=False)
issue_activity.delay(
type="issue.activity.deleted",
requested_data=json.dumps({"issue_id": str(pk)}),

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:

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import json
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.core.serializers.json import DjangoJSONEncoder
# Third Party imports
@@ -25,12 +25,9 @@ from plane.app.serializers import (
from plane.app.permissions import ProjectMemberPermission, allow_permission, ROLE
from plane.db.models import (
UserFavorite,
Cycle,
Intake,
DeployBoard,
IssueUserProperty,
Issue,
Module,
Project,
ProjectIdentifier,
ProjectMember,
@@ -39,7 +36,7 @@ from plane.db.models import (
WorkspaceMember,
)
from plane.utils.cache import cache_response
from plane.bgtasks.webhook_task import model_activity
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.utils.exception_logger import log_exception
@@ -73,36 +70,6 @@ class ProjectViewSet(BaseViewSet):
)
)
)
.annotate(
is_member=Exists(
ProjectMember.objects.filter(
member=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
is_active=True,
)
)
)
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_cycles=Cycle.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_modules=Module.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
member_role=ProjectMember.objects.filter(
project_id=OuterRef("pk"),
@@ -133,7 +100,7 @@ class ProjectViewSet(BaseViewSet):
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug):
def list_detail(self, request, slug):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
projects = self.get_queryset().order_by("sort_order", "name")
if WorkspaceMember.objects.filter(
@@ -170,6 +137,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()

View File

@@ -53,6 +53,23 @@ class StateViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def partial_update(self, request, slug, project_id, pk):
try:
state = State.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
serializer = StateSerializer(state, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"name": "The state name is already taken"},
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):

View File

@@ -24,6 +24,7 @@ from plane.db.models import (
ProjectMember,
Project,
CycleIssue,
UserRecentVisit,
)
from plane.utils.grouper import (
issue_group_values,
@@ -495,6 +496,13 @@ class IssueViewViewSet(BaseViewSet):
entity_identifier=pk,
entity_type="view",
).delete()
# Delete the page from recent visit
UserRecentVisit.objects.filter(
project_id=project_id,
workspace__slug=slug,
entity_identifier=pk,
entity_name="view",
).delete(soft=False)
else:
return Response(
{"error": "Only admin or owner can delete the view"},

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,78 @@
# Module imports
from ..base import BaseAPIView
from plane.db.models.workspace import WorkspaceUserPreference
from plane.app.serializers.workspace import WorkspaceUserPreferenceSerializer
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import Workspace
# Third party imports
from rest_framework.response import Response
from rest_framework import status
class WorkspaceUserPreferenceViewSet(BaseAPIView):
model = WorkspaceUserPreference
def get_serializer_class(self):
return WorkspaceUserPreferenceSerializer
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
get_preference = WorkspaceUserPreference.objects.filter(
user=request.user, workspace_id=workspace.id
)
create_preference_keys = []
keys = [
key
for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices
if key not in ["projects"]
]
for preference in keys:
if preference not in get_preference.values_list("key", flat=True):
create_preference_keys.append(preference)
preference = WorkspaceUserPreference.objects.bulk_create(
[
WorkspaceUserPreference(
key=key, user=request.user, workspace=workspace
)
for key in create_preference_keys
],
batch_size=10,
ignore_conflicts=True,
)
preference = WorkspaceUserPreference.objects.filter(
user=request.user, workspace_id=workspace.id
)
return Response(
preference.values("key", "is_pinned", "sort_order"),
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def patch(self, request, slug, key):
preference = WorkspaceUserPreference.objects.filter(
key=key, workspace__slug=slug, user=request.user
).first()
if preference:
serializer = WorkspaceUserPreferenceSerializer(
preference, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
{"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND
)

View File

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

View File

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

View File

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

View File

@@ -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,

View File

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

View File

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

View File

@@ -69,7 +69,8 @@ from .workspace import (
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceUserLink,
WorkspaceHomePreference
WorkspaceHomePreference,
WorkspaceUserPreference,
)
from .favorite import UserFavorite

View File

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

View File

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

View File

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

View File

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

View File

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

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))

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

View File

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

View File

@@ -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

View File

@@ -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",

View File

@@ -22,7 +22,11 @@
"devDependencies": {
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"turbo": "^2.3.3"
"turbo": "^2.4.1"
},
"resolutions": {
"nanoid": "3.3.8",
"esbuild": "0.25.0"
},
"packageManager": "yarn@1.22.22",
"name": "plane"

View File

@@ -0,0 +1,5 @@
.next
.turbo
out/
dist/
build/

View File

@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

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",

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";

View File

@@ -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",

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;
};

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,
}

View File

@@ -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";

View File

@@ -1 +0,0 @@
export const SIDEBAR_CLICKED = "Sidenav clicked";

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",
},
];

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",
},
];

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";

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 },
];

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" },
];

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;

View File

@@ -0,0 +1,3 @@
export * from "./common";
export * from "./filter";
export * from "./layout";

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);

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";

View File

@@ -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",
},
];

View File

@@ -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,
},
];

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,
};

View File

@@ -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/",
},
];

View File

@@ -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",
},
};

View File

@@ -0,0 +1 @@
export const SPREADSHEET_SELECT_GROUP = "spreadsheet-issues";

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;

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,
};

View File

@@ -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",

View File

@@ -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",

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: {},
};

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" },
];

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,182 @@ 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",
},
];

View File

@@ -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",

View File

@@ -16,6 +16,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
const {
onTransaction,
aiHandler,
bubbleMenuEnabled = true,
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
@@ -75,8 +76,9 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
return (
<PageRenderer
displayConfig={displayConfig}
aiHandler={aiHandler}
bubbleMenuEnabled={bubbleMenuEnabled}
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassNames}
id={id}

View File

@@ -15,12 +15,13 @@ import { Editor, ReactRenderer } from "@tiptap/react";
// components
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
import { LinkView, LinkViewProps } from "@/components/links";
import { AIFeaturesMenu, BlockMenu } from "@/components/menus";
import { AIFeaturesMenu, BlockMenu, EditorBubbleMenu } from "@/components/menus";
// types
import { TAIHandler, TDisplayConfig } from "@/types";
type IPageRenderer = {
aiHandler?: TAIHandler;
bubbleMenuEnabled: boolean;
displayConfig: TDisplayConfig;
editor: Editor;
editorContainerClassName: string;
@@ -29,7 +30,7 @@ type IPageRenderer = {
};
export const PageRenderer = (props: IPageRenderer) => {
const { aiHandler, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
// states
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
const [isOpen, setIsOpen] = useState(false);
@@ -141,6 +142,7 @@ export const PageRenderer = (props: IPageRenderer) => {
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
{editor.isEditable && (
<div>
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
<BlockMenu editor={editor} />
<AIFeaturesMenu menu={aiHandler?.menu} />
</div>

View File

@@ -69,6 +69,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
return (
<PageRenderer
bubbleMenuEnabled={false}
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}

View File

@@ -1,6 +1,6 @@
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
import { Editor } from "@tiptap/core";
import { Check, Link, Trash } from "lucide-react";
import { Check, Link, Trash2 } from "lucide-react";
import { Dispatch, FC, SetStateAction, useCallback, useRef, useState } from "react";
// plane utils
import { cn } from "@plane/utils";
// helpers
@@ -15,22 +15,26 @@ type Props = {
export const BubbleMenuLinkSelector: FC<Props> = (props) => {
const { editor, isOpen, setIsOpen } = props;
// states
const [error, setError] = useState(false);
// refs
const inputRef = useRef<HTMLInputElement>(null);
const onLinkSubmit = useCallback(() => {
const handleLinkSubmit = useCallback(() => {
const input = inputRef.current;
const url = input?.value;
if (url && isValidHttpUrl(url)) {
if (!input) return;
let url = input.value;
if (!url) return;
if (!url.startsWith("http")) url = `http://${url}`;
if (isValidHttpUrl(url)) {
setLinkEditor(editor, url);
setIsOpen(false);
setError(false);
} else {
setError(true);
}
}, [editor, inputRef, setIsOpen]);
useEffect(() => {
inputRef.current && inputRef.current?.focus();
});
return (
<div className="relative h-full">
<button
@@ -47,52 +51,62 @@ export const BubbleMenuLinkSelector: FC<Props> = (props) => {
e.stopPropagation();
}}
>
<span>Link</span>
Link
<Link className="flex-shrink-0 size-3" />
</button>
{isOpen && (
<div
className="dow-xl fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 animate-in fade-in slide-in-from-top-1"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onLinkSubmit();
}
}}
>
<input
ref={inputRef}
type="url"
placeholder="Paste a link"
onClick={(e) => {
e.stopPropagation();
}}
className="flex-1 border-r border-custom-border-300 bg-custom-background-100 p-1 text-sm outline-none placeholder:text-custom-text-400"
defaultValue={editor.getAttributes("link").href || ""}
/>
{editor.getAttributes("link").href ? (
<button
type="button"
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
onClick={(e) => {
unsetLinkEditor(editor);
setIsOpen(false);
e.stopPropagation();
<div className="fixed top-full z-[99999] mt-1 w-60 animate-in fade-in slide-in-from-top-1 rounded bg-custom-background-100 shadow-custom-shadow-rg">
<div
className={cn("flex rounded border border-custom-border-300 transition-colors", {
"border-red-500": error,
})}
>
<input
ref={inputRef}
type="url"
placeholder="Enter or paste a link"
onClick={(e) => e.stopPropagation()}
className="flex-1 border-r border-custom-border-300 bg-custom-background-100 py-2 px-1.5 text-xs outline-none placeholder:text-custom-text-400 rounded"
defaultValue={editor.getAttributes("link").href || ""}
onKeyDown={(e) => {
setError(false);
if (e.key === "Enter") {
e.preventDefault();
handleLinkSubmit();
}
}}
>
<Trash className="h-4 w-4" />
</button>
) : (
<button
className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90"
type="button"
onClick={(e) => {
onLinkSubmit();
e.stopPropagation();
}}
>
<Check className="h-4 w-4" />
</button>
onFocus={() => setError(false)}
autoFocus
/>
{editor.getAttributes("link").href ? (
<button
type="button"
className="grid place-items-center rounded-sm p-1 text-red-500 hover:bg-red-500/20 transition-all"
onClick={(e) => {
unsetLinkEditor(editor);
setIsOpen(false);
e.stopPropagation();
}}
>
<Trash2 className="size-4" />
</button>
) : (
<button
type="button"
className="h-full aspect-square grid place-items-center p-1 rounded-sm text-custom-text-300 hover:bg-custom-background-80 transition-all"
onClick={(e) => {
e.stopPropagation();
handleLinkSubmit();
}}
>
<Check className="size-4" />
</button>
)}
</div>
{error && (
<p className="text-xs text-red-500 my-1 px-2 pointer-events-none animate-in fade-in slide-in-from-top-0">
Please enter a valid URL
</p>
)}
</div>
)}

View File

@@ -93,6 +93,19 @@ export const CustomColorExtension = Mark.create({
};
},
addStorage() {
return {
markdown: {
serialize: {
open: "",
close: "",
mixable: true,
expelEnclosingWhitespace: true,
},
},
};
},
parseHTML() {
return [
{

View File

@@ -51,6 +51,10 @@ export const ListKeymap = ({ tabIndex }: { tabIndex?: number }) =>
} else if (this.editor.commands.liftListItem("taskItem")) {
return true;
}
// if tabIndex is set, we don't want to handle Tab key
if (tabIndex !== undefined && tabIndex !== null) {
return false;
}
return true;
},
Delete: ({ editor }) => {

View File

@@ -134,10 +134,6 @@ const SideMenu = (options: SideMenuPluginProps) => {
rect.left -= 8;
}
if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) {
rect.left += 8;
}
rect.width = options.dragHandleWidth;
if (!editorSideMenu) return;

View File

@@ -39,7 +39,12 @@ export interface TableOptions {
declare module "@tiptap/core" {
interface Commands<ReturnType> {
table: {
insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => ReturnType;
insertTable: (options?: {
rows?: number;
cols?: number;
withHeaderRow?: boolean;
columnWidth?: number;
}) => ReturnType;
addColumnBefore: () => ReturnType;
addColumnAfter: () => ReturnType;
deleteColumn: () => ReturnType;
@@ -108,9 +113,9 @@ export const Table = Node.create({
addCommands() {
return {
insertTable:
({ rows = 3, cols = 3, withHeaderRow = false } = {}) =>
({ rows = 3, cols = 3, withHeaderRow = false, columnWidth = 150 } = {}) =>
({ tr, dispatch, editor }) => {
const node = createTable(editor.schema, rows, cols, withHeaderRow);
const node = createTable(editor.schema, rows, cols, withHeaderRow, undefined, columnWidth);
if (dispatch) {
const offset = tr.selection.anchor + 1;

View File

@@ -2,11 +2,12 @@ import { Fragment, Node as ProsemirrorNode, NodeType } from "@tiptap/pm/model";
export function createCell(
cellType: NodeType,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
attrs?: Record<string, any>
): ProsemirrorNode | null | undefined {
if (cellContent) {
return cellType.createChecked(null, cellContent);
return cellType.createChecked(attrs, cellContent);
}
return cellType.createAndFill();
return cellType.createAndFill(attrs);
}

View File

@@ -8,21 +8,22 @@ export function createTable(
rowsCount: number,
colsCount: number,
withHeaderRow: boolean,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
columnWidth: number = 100
): ProsemirrorNode {
const types = getTableNodeTypes(schema);
const headerCells: ProsemirrorNode[] = [];
const cells: ProsemirrorNode[] = [];
for (let index = 0; index < colsCount; index += 1) {
const cell = createCell(types.cell, cellContent);
const cell = createCell(types.cell, cellContent, { colwidth: [columnWidth] });
if (cell) {
cells.push(cell);
}
if (withHeaderRow) {
const headerCell = createCell(types.header_cell, cellContent);
const headerCell = createCell(types.header_cell, cellContent, { colwidth: [columnWidth] });
if (headerCell) {
headerCells.push(headerCell);

View File

@@ -138,8 +138,9 @@ export const insertTableCommand = (editor: Editor, range?: Range) => {
}
}
}
if (range) editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3 }).run();
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3 }).run();
if (range)
editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run();
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run();
};
export const insertImage = ({

View File

@@ -202,7 +202,7 @@ export const useEditor = (props: CustomEditorProps) => {
getDocument: () => {
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
const documentHTML = editor?.getHTML() ?? "<p></p>";
const documentJSON = editor.getJSON() ?? null;
const documentJSON = editor?.getJSON() ?? null;
return {
binary: documentBinary,

View File

@@ -88,16 +88,18 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
const elements = document.elementsFromPoint(coords.x, coords.y);
for (const elem of elements) {
// Check for table wrapper first
if (elem.matches(".table-wrapper")) {
return elem;
}
if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) {
return elem;
}
// if the element is a <p> tag that is the first child of a td or th
if (
(elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) &&
elem?.textContent?.trim() !== ""
) {
return elem; // Return only if p tag is not empty in td or th
// Skip table cells
if (elem.closest(".table-wrapper")) {
continue;
}
// apply general selector

View File

@@ -138,8 +138,9 @@ export interface IRichTextEditor extends IEditorProps {
export interface ICollaborativeDocumentEditor
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
editable: boolean;
aiHandler?: TAIHandler;
bubbleMenuEnabled?: boolean;
editable: boolean;
embedHandler: TEmbedConfig;
handleEditorReady?: (value: boolean) => void;
id: string;

View File

@@ -105,14 +105,14 @@ ul[data-type="taskList"] li > div {
}
ul[data-type="taskList"] li > label input[type="checkbox"] {
border: 1px solid rgba(var(--color-border-300)) !important;
border: 1px solid rgba(var(--color-text-100), 0.2) !important;
outline: none;
border-radius: 2px;
transform: scale(1.05);
}
.ProseMirror[contenteditable="true"] input[type="checkbox"]:hover {
background-color: rgba(var(--color-background-80));
background-color: rgba(var(--color-text-100), 0.1);
}
.ProseMirror[contenteditable="false"] input[type="checkbox"] {
@@ -408,12 +408,14 @@ p.editor-paragraph-block {
padding-top: 4px;
}
&:last-child {
padding-bottom: 4px;
}
&:not(td p.editor-paragraph-block, th p.editor-paragraph-block) {
&:last-child {
padding-bottom: 4px;
}
&:not(:last-child) {
padding-bottom: 8px;
&:not(:last-child) {
padding-bottom: 8px;
}
}
font-size: var(--font-size-regular);

View File

@@ -16,7 +16,7 @@
.table-wrapper table th {
min-width: 1em;
border: 1px solid rgba(var(--color-border-200));
padding: 10px 20px;
padding: 7px 10px;
vertical-align: top;
box-sizing: border-box;
position: relative;
@@ -48,7 +48,7 @@
/* table dropdown */
.table-wrapper table .column-resize-handle {
position: absolute;
right: -2px;
right: 0;
top: 0;
width: 2px;
height: 100%;

View File

@@ -0,0 +1,171 @@
{
"sidebar": {
"projects": "Projects",
"pages": "Pages",
"new_work_item": "New work item",
"home": "Home",
"your_work": "Your work",
"inbox": "Inbox",
"workspace": "Workspace",
"views": "Views",
"analytics": "Analytics",
"work_items": "Work items",
"cycles": "Cycles",
"modules": "Modules",
"intake": "Intake",
"drafts": "Drafts",
"favorites": "Favorites",
"pro": "Pro",
"upgrade": "Upgrade"
},
"auth": {
"common": {
"email": {
"label": "Email",
"placeholder": "name@company.com",
"errors": {
"required": "Email is required",
"invalid": "Email is invalid"
}
},
"password": {
"label": "Password",
"set_password": "Set a password",
"placeholder": "Enter password",
"confirm_password": {
"label": "Confirm password",
"placeholder": "Confirm password"
},
"current_password": {
"label": "Current password"
},
"new_password": {
"label": "New password",
"placeholder": "Enter new password"
},
"change_password": {
"label": {
"default": "Change password",
"submitting": "Changing password"
}
},
"errors": {
"match": "Passwords don't match",
"empty": "Please enter your password",
"length": "Password length should me more than 8 characters",
"strength": {
"weak": "Password is weak",
"strong": "Password is strong"
}
},
"submit": "Set password",
"toast": {
"change_password": {
"success": {
"title": "Success!",
"message": "Password changed successfully."
},
"error": {
"title": "Error!",
"message": "Something went wrong. Please try again."
}
}
}
},
"unique_code": {
"label": "Unique code",
"placeholder": "gets-sets-flys",
"paste_code": "Paste the code sent to your email",
"requesting_new_code": "Requesting new code",
"sending_code": "Sending code"
},
"already_have_an_account": "Already have an account?",
"login": "Log in",
"create_account": "Create an account",
"new_to_plane": "New to Plane?",
"back_to_sign_in": "Back to sign in",
"resend_in": "Resend in {seconds} seconds",
"sign_in_with_unique_code": "Sign in with unique code",
"forgot_password": "Forgot your password?"
},
"sign_up": {
"header": {
"label": "Create an account to start managing work with your team.",
"step": {
"email": {
"header": "Sign up",
"sub_header": ""
},
"password": {
"header": "Sign up",
"sub_header": "Sign up using an email-password combination."
},
"unique_code": {
"header": "Sign up",
"sub_header": "Sign up using a unique code sent to the email address above."
}
}
},
"errors": {
"password": {
"strength": "Try setting-up a strong password to proceed"
}
}
},
"sign_in": {
"header": {
"label": "Log in to start managing work with your team.",
"step": {
"email": {
"header": "Log in or sign up",
"sub_header": ""
},
"password": {
"header": "Log in or sign up",
"sub_header": "Use your email-password combination to log in."
},
"unique_code": {
"header": "Log in or sign up",
"sub_header": "Log in using a unique code sent to the email address above."
}
}
}
},
"forgot_password": {
"title": "Reset your password",
"description": "Enter your user account's verified email address and we will send you a password reset link.",
"email_sent": "We sent the reset link to your email address",
"send_reset_link": "Send reset link",
"errors": {
"smtp_not_enabled": "We see that your god hasn't enabled SMTP, we will not be able to send a password reset link"
},
"toast": {
"success": {
"title": "Email sent",
"message": "Check your inbox for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder."
},
"error": {
"title": "Error!",
"message": "Something went wrong. Please try again."
}
}
},
"reset_password": {
"title": "Set new password",
"description": "Secure your account with a strong password"
},
"set_password": {
"title": "Secure your account",
"description": "Setting password helps you login securely"
},
"sign_out": {
"toast": {
"error": {
"title": "Error!",
"message": "Failed to sign out. Please try again."
}
}
}
}
}

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