Compare commits

..

63 Commits

Author SHA1 Message Date
Aaryan Khandelwal 0106689c89 fix: build errors 2025-02-07 17:44:34 +05:30
Aaryan Khandelwal 31dc1f193f fix: merge conflicts resolved from preview 2025-02-07 17:28:37 +05:30
Aaryan Khandelwal c2070a09ed fix: merge conflicts resolved from preview 2025-02-07 17:28:22 +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
Aaryan Khandelwal 5b7ee22c02 chore: rename power k css file 2025-01-30 12:55:51 +05:30
Aaryan Khandelwal 35b552d6f8 refactor: power k 2025-01-29 21:12:33 +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
Vamsi Krishna 8f3a0be177 [WEB-3194]fix: stickies modal close while redirection (#6440)
* fix: stickies modal close while redirection

* chore: added deafult for optional prop
2025-01-22 14:22:24 +05:30
Prateek Shourya 0679e140a2 [WEB-3192] fix: issue relation redirection (#6441) 2025-01-22 14:01:21 +05:30
guru_sainath b611f5110f chore: issue and issue description version endpoints (#6434) 2025-01-21 20:34:43 +05:30
Akshita Goyal 0f7bc6979f fix: new sticky color + recent sticky api call + sticky max height (#6438) 2025-01-21 20:34:00 +05:30
Prateek Shourya 12501d0597 fix: add optional chaning check for actor details (#6437) 2025-01-21 20:33:38 +05:30
Aaryan Khandelwal 3a86fff7c1 [WEB-3045] fix: sticky placeholder, gray color value (#6436)
* fix: sticky placeholder

* chore: update gray color
2025-01-21 20:32:45 +05:30
Aaryan Khandelwal 58a4b45463 [WEB-3045] fix: stickies bugs (#6433)
* fix: stickies bugs

* fix: sticky height fixed

---------

Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
2025-01-21 16:37:27 +05:30
849 changed files with 22333 additions and 12876 deletions
+3 -3
View File
@@ -7,15 +7,15 @@ import { DefaultLayout } from "@/layouts/default-layout";
export const metadata: Metadata = {
title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.",
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
openGraph: {
title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.",
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.",
url: "https://plane.so/",
},
keywords:
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration",
"software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration",
twitter: {
site: "@planepowers",
},
+2 -2
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",
@@ -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
+2
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:
@@ -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
+2
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
+5
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",
),
]
+14 -1
View File
@@ -28,7 +28,7 @@ from plane.db.models import (
Workspace,
UserFavorite,
)
from plane.bgtasks.webhook_task import model_activity
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from .base import BaseAPIView
@@ -326,6 +326,19 @@ class ProjectAPIEndpoint(BaseAPIView):
entity_type="project", entity_identifier=pk, project_id=pk
).delete()
project.delete()
webhook_activity.delay(
event="project",
verb="deleted",
field=None,
old_value=None,
new_value=None,
actor_id=request.user.id,
slug=slug,
current_site=request.META.get("HTTP_ORIGIN"),
event_id=project.id,
old_identifier=None,
new_identifier=None,
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -72,6 +72,8 @@ from .issue import (
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
IssueVersionDetailSerializer,
IssueDescriptionVersionDetailSerializer,
)
from .module import (
+99 -2
View File
@@ -33,6 +33,8 @@ from plane.db.models import (
IssueVote,
IssueRelation,
State,
IssueVersion,
IssueDescriptionVersion,
)
@@ -201,6 +203,7 @@ class IssueCreateSerializer(BaseSerializer):
for user in assignees
],
batch_size=10,
ignore_conflicts=True,
)
if labels is not None:
@@ -218,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
@@ -281,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"]
@@ -296,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"]
@@ -667,3 +703,64 @@ class IssueSubscriberSerializer(BaseSerializer):
model = IssueSubscriber
fields = "__all__"
read_only_fields = ["workspace", "project", "issue"]
class IssueVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueVersion
fields = [
"id",
"workspace",
"project",
"issue",
"parent",
"state",
"estimate_point",
"name",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
"is_draft",
"external_source",
"external_id",
"type",
"cycle",
"modules",
"meta",
"name",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]
class IssueDescriptionVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueDescriptionVersion
fields = [
"id",
"workspace",
"project",
"issue",
"description_binary",
"description_html",
"description_stripped",
"description_json",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]
+2 -21
View File
@@ -90,17 +90,7 @@ class ProjectLiteSerializer(BaseSerializer):
class ProjectListSerializer(DynamicBaseSerializer):
total_issues = serializers.IntegerField(read_only=True)
archived_issues = serializers.IntegerField(read_only=True)
archived_sub_issues = serializers.IntegerField(read_only=True)
draft_issues = serializers.IntegerField(read_only=True)
draft_sub_issues = serializers.IntegerField(read_only=True)
sub_issues = serializers.IntegerField(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
member_role = serializers.IntegerField(read_only=True)
anchor = serializers.CharField(read_only=True)
@@ -113,14 +103,9 @@ class ProjectListSerializer(DynamicBaseSerializer):
if project_members is not None:
# Filter members by the project ID
return [
{
"id": member.id,
"member_id": member.member_id,
"member__display_name": member.member.display_name,
"member__avatar": member.member.avatar,
"member__avatar_url": member.member.avatar_url,
}
member.member_id
for member in project_members
if member.is_active and not member.member.is_bot
]
return []
@@ -134,10 +119,6 @@ class ProjectDetailSerializer(BaseSerializer):
default_assignee = UserLiteSerializer(read_only=True)
project_lead = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
member_role = serializers.IntegerField(read_only=True)
anchor = serializers.CharField(read_only=True)
@@ -22,6 +22,7 @@ from plane.db.models import (
ProjectMember,
WorkspaceHomePreference,
Sticky,
WorkspaceUserPreference,
)
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
@@ -258,3 +259,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"]
+6
View File
@@ -7,6 +7,7 @@ from plane.app.views import (
SavedAnalyticEndpoint,
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
ProjectStatsEndpoint,
)
@@ -43,4 +44,9 @@ urlpatterns = [
DefaultAnalyticsEndpoint.as_view(),
name="default-analytics",
),
path(
"workspaces/<str:slug>/project-stats/",
ProjectStatsEndpoint.as_view(),
name="project-analytics",
),
]
+22
View File
@@ -24,6 +24,8 @@ from plane.app.views import (
IssueDetailEndpoint,
IssueAttachmentV2Endpoint,
IssueBulkUpdateDateEndpoint,
IssueVersionEndpoint,
IssueDescriptionVersionEndpoint,
)
urlpatterns = [
@@ -256,4 +258,24 @@ urlpatterns = [
IssueBulkUpdateDateEndpoint.as_view(),
name="project-issue-dates",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
]
+5
View File
@@ -23,6 +23,11 @@ urlpatterns = [
ProjectViewSet.as_view({"get": "list", "post": "create"}),
name="project",
),
path(
"workspaces/<str:slug>/projects/details/",
ProjectViewSet.as_view({"get": "list_detail"}),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:pk>/",
ProjectViewSet.as_view(
+12
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",
),
]
+4
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,
@@ -141,6 +142,8 @@ from .issue.sub_issue import SubIssuesEndpoint
from .issue.subscriber import IssueSubscriberViewSet
from .issue.version import IssueVersionEndpoint, IssueDescriptionVersionEndpoint
from .module.base import (
ModuleViewSet,
ModuleLinkViewSet,
@@ -187,6 +190,7 @@ from .analytic.base import (
SavedAnalyticEndpoint,
ExportAnalyticsEndpoint,
DefaultAnalyticsEndpoint,
ProjectStatsEndpoint,
)
from .notification.base import (
+82 -2
View File
@@ -3,7 +3,7 @@ from django.db.models import Count, F, Sum, Q
from django.db.models.functions import ExtractMonth
from django.utils import timezone
from django.db.models.functions import Concat
from django.db.models import Case, When, Value
from django.db.models import Case, When, Value, OuterRef, Func
from django.db import models
# Third party imports
@@ -15,7 +15,16 @@ from plane.app.permissions import WorkSpaceAdminPermission
from plane.app.serializers import AnalyticViewSerializer
from plane.app.views.base import BaseAPIView, BaseViewSet
from plane.bgtasks.analytic_plot_export import analytic_export_task
from plane.db.models import AnalyticView, Issue, Workspace
from plane.db.models import (
AnalyticView,
Issue,
Workspace,
Project,
ProjectMember,
Cycle,
Module,
)
from plane.utils.analytics_plot import build_graph_plot
from plane.utils.issue_filters import issue_filters
from plane.app.permissions import allow_permission, ROLE
@@ -441,3 +450,74 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
},
status=status.HTTP_200_OK,
)
class ProjectStatsEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
fields = request.GET.get("fields", "").split(",")
project_ids = request.GET.get("project_ids", "")
valid_fields = {
"total_issues",
"completed_issues",
"total_members",
"total_cycles",
"total_modules",
}
requested_fields = set(filter(None, fields)) & valid_fields
if not requested_fields:
requested_fields = valid_fields
projects = Project.objects.filter(workspace__slug=slug)
if project_ids:
projects = projects.filter(id__in=project_ids.split(","))
annotations = {}
if "total_issues" in requested_fields:
annotations["total_issues"] = (
Issue.issue_objects.filter(project_id=OuterRef("pk"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "completed_issues" in requested_fields:
annotations["completed_issues"] = (
Issue.issue_objects.filter(
project_id=OuterRef("pk"), state__group="completed"
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "total_cycles" in requested_fields:
annotations["total_cycles"] = (
Cycle.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "total_modules" in requested_fields:
annotations["total_modules"] = (
Module.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
if "total_members" in requested_fields:
annotations["total_members"] = (
ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
projects = projects.annotate(**annotations).values("id", *requested_fields)
return Response(projects, status=status.HTTP_200_OK)
+8
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
@@ -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)
+8
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)}),
+118
View File
@@ -0,0 +1,118 @@
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.db.models import IssueVersion, IssueDescriptionVersion
from ..base import BaseAPIView
from plane.app.serializers import (
IssueVersionDetailSerializer,
IssueDescriptionVersionDetailSerializer,
)
from plane.app.permissions import allow_permission, ROLE
from plane.utils.global_paginator import paginate
from plane.utils.timezone_converter import user_timezone_converter
class IssueVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
paginated_data, datetime_fields, timezone
)
return paginated_data
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
issue_version = IssueVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
serializer = IssueVersionDetailSerializer(issue_version)
return Response(serializer.data, status=status.HTTP_200_OK)
cursor = request.GET.get("cursor", None)
required_fields = [
"id",
"workspace",
"project",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
issue_versions_queryset = IssueVersion.objects.filter(
workspace__slug=slug, project_id=project_id, issue_id=issue_id
)
paginated_data = paginate(
base_queryset=issue_versions_queryset,
queryset=issue_versions_queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
),
)
return Response(paginated_data, status=status.HTTP_200_OK)
class IssueDescriptionVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
paginated_data, datetime_fields, timezone
)
return paginated_data
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
issue_description_version = IssueDescriptionVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
serializer = IssueDescriptionVersionDetailSerializer(
issue_description_version
)
return Response(serializer.data, status=status.HTTP_200_OK)
cursor = request.GET.get("cursor", None)
required_fields = [
"id",
"workspace",
"project",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
workspace__slug=slug, project_id=project_id, issue_id=issue_id
)
paginated_data = paginate(
base_queryset=issue_description_versions_queryset,
queryset=issue_description_versions_queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
),
)
return Response(paginated_data, status=status.HTTP_200_OK)
+8
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)
+8
View File
@@ -33,6 +33,7 @@ from plane.db.models import (
ProjectMember,
ProjectPage,
Project,
UserRecentVisit,
)
from plane.utils.error_codes import ERROR_CODES
from ..base import BaseAPIView, BaseViewSet
@@ -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)
+83 -89
View File
@@ -6,7 +6,7 @@ import json
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.core.serializers.json import DjangoJSONEncoder
# Third Party imports
@@ -25,12 +25,9 @@ from plane.app.serializers import (
from plane.app.permissions import ProjectMemberPermission, allow_permission, ROLE
from plane.db.models import (
UserFavorite,
Cycle,
Intake,
DeployBoard,
IssueUserProperty,
Issue,
Module,
Project,
ProjectIdentifier,
ProjectMember,
@@ -39,7 +36,7 @@ from plane.db.models import (
WorkspaceMember,
)
from plane.utils.cache import cache_response
from plane.bgtasks.webhook_task import model_activity
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.utils.exception_logger import log_exception
@@ -73,36 +70,6 @@ class ProjectViewSet(BaseViewSet):
)
)
)
.annotate(
is_member=Exists(
ProjectMember.objects.filter(
member=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
is_active=True,
)
)
)
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_cycles=Cycle.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
total_modules=Module.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
member_role=ProjectMember.objects.filter(
project_id=OuterRef("pk"),
@@ -133,7 +100,7 @@ class ProjectViewSet(BaseViewSet):
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug):
def list_detail(self, request, slug):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
projects = self.get_queryset().order_by("sort_order", "name")
if WorkspaceMember.objects.filter(
@@ -170,6 +137,73 @@ class ProjectViewSet(BaseViewSet):
).data
return Response(projects, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug):
sort_order = ProjectMember.objects.filter(
member=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
is_active=True,
).values("sort_order")
projects = (
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
.select_related(
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate(
member_role=ProjectMember.objects.filter(
project_id=OuterRef("pk"),
member_id=self.request.user.id,
is_active=True,
).values("role")
)
.annotate(inbox_view=F("intake_view"))
.annotate(sort_order=Subquery(sort_order))
.distinct()
).values(
"id",
"name",
"identifier",
"sort_order",
"logo_props",
"member_role",
"archived_at",
"workspace",
"cycle_view",
"issue_views_view",
"module_view",
"page_view",
"inbox_view",
"project_lead",
"created_at",
"updated_at",
"created_by",
"updated_by",
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=5
).exists():
projects = projects.filter(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=15
).exists():
projects = projects.filter(
Q(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
| Q(network=2)
)
return Response(projects, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
@@ -182,58 +216,6 @@ class ProjectViewSet(BaseViewSet):
)
.filter(archived_at__isnull=True)
.filter(pk=pk)
.annotate(
total_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("pk")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("pk"), parent__isnull=False
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
archived_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"), archived_at__isnull=False
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
archived_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
archived_at__isnull=False,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
draft_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"), is_draft=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
draft_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
is_draft=True,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
).first()
if project is None:
@@ -462,7 +444,19 @@ class ProjectViewSet(BaseViewSet):
):
project = Project.objects.get(pk=pk)
project.delete()
webhook_activity.delay(
event="project",
verb="deleted",
field=None,
old_value=None,
new_value=None,
actor_id=request.user.id,
slug=slug,
current_site=request.META.get("HTTP_ORIGIN"),
event_id=project.id,
old_identifier=None,
new_identifier=None,
)
# Delete the project members
DeployBoard.objects.filter(project_id=pk, workspace__slug=slug).delete()
+17
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):
+8
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"},
+1 -1
View File
@@ -120,7 +120,7 @@ class WebhookLogsEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def get(self, request, slug, webhook_id):
webhook_logs = WebhookLog.objects.filter(
workspace__slug=slug, webhook_id=webhook_id
workspace__slug=slug, webhook=webhook_id
)
serializer = WebhookLogSerializer(webhook_logs, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -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
)
-1
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(),
@@ -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,
+9 -5
View File
@@ -136,7 +136,7 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site):
# Log the webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -153,7 +153,7 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site):
# Log the failed webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -304,7 +304,7 @@ def webhook_send_task(
# Log the webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -319,7 +319,7 @@ def webhook_send_task(
# Log the failed webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
@@ -387,7 +387,11 @@ def webhook_activity(
webhook=webhook.id,
slug=slug,
event=event,
event_data=get_model_data(event=event, event_id=event_id),
event_data=(
{"id": event_id}
if verb == "deleted"
else get_model_data(event=event, event_id=event_id)
),
action=verb,
current_site=current_site,
activity={
@@ -0,0 +1,33 @@
# Generated by Django 4.2.17 on 2025-01-30 16:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0090_rename_dashboard_deprecateddashboard_and_more'),
]
operations = [
migrations.AddField(
model_name='issuecomment',
name='edited_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='profile',
name='is_smooth_cursor_enabled',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='userrecentvisit',
name='entity_name',
field=models.CharField(max_length=30),
),
migrations.AlterField(
model_name='webhooklog',
name='webhook',
field=models.UUIDField(),
)
]
+2 -1
View File
@@ -69,7 +69,8 @@ from .workspace import (
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceUserLink,
WorkspaceHomePreference
WorkspaceHomePreference,
WorkspaceUserPreference,
)
from .favorite import UserFavorite
+1
View File
@@ -467,6 +467,7 @@ class IssueComment(ProjectBaseModel):
)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
edited_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
self.comment_stripped = (
+1 -1
View File
@@ -17,7 +17,7 @@ class EntityNameEnum(models.TextChoices):
class UserRecentVisit(WorkspaceBaseModel):
entity_identifier = models.UUIDField(null=True)
entity_name = models.CharField(max_length=30, choices=EntityNameEnum.choices)
entity_name = models.CharField(max_length=30)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
+2
View File
@@ -186,6 +186,8 @@ class Profile(TimeAuditModel):
billing_address = models.JSONField(null=True)
has_billing_address = models.BooleanField(default=False)
company_name = models.CharField(max_length=255, blank=True)
is_smooth_cursor_enabled = models.BooleanField(default=False)
# mobile
is_mobile_onboarded = models.BooleanField(default=False)
mobile_onboarding_step = models.JSONField(default=get_mobile_default_onboarding)
+2 -2
View File
@@ -66,7 +66,7 @@ class WebhookLog(BaseModel):
"db.Workspace", on_delete=models.CASCADE, related_name="webhook_logs"
)
# Associated webhook
webhook = models.ForeignKey(Webhook, on_delete=models.CASCADE, related_name="logs")
webhook = models.UUIDField()
# Basic request details
event_type = models.CharField(max_length=255, blank=True, null=True)
@@ -89,4 +89,4 @@ class WebhookLog(BaseModel):
ordering = ("-created_at",)
def __str__(self):
return f"{self.event_type} {str(self.webhook.url)}"
return f"{self.event_type} {str(self.webhook)}"
+4 -3
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",
+26 -9
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):
+5
View File
@@ -0,0 +1,5 @@
.next
.turbo
out/
dist/
build/
+5
View File
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
+6 -3
View File
@@ -2,8 +2,11 @@
import { TXAxisValues, TYAxisValues } from "@plane/types";
export const ANALYTICS_TABS = [
{ key: "scope_and_demand", title: "Scope and Demand" },
{ key: "custom", title: "Custom Analytics" },
{
key: "scope_and_demand",
i18n_title: "workspace_analytics.tabs.scope_and_demand",
},
{ key: "custom", i18n_title: "workspace_analytics.tabs.custom" },
];
export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
@@ -62,7 +65,7 @@ export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] =
[
{
value: "issue_count",
label: "Issue Count",
label: "Work item Count",
},
{
value: "estimate",
@@ -1,56 +1,40 @@
// types
import { TCycleLayoutOptions, TCycleTabOptions } from "@plane/types";
export const CYCLE_TABS_LIST: {
key: TCycleTabOptions;
name: string;
}[] = [
{
key: "active",
name: "Active",
},
{
key: "all",
name: "All",
},
];
export const CYCLE_STATUS: {
label: string;
i18n_label: string;
value: "current" | "upcoming" | "completed" | "draft";
title: string;
i18n_title: string;
color: string;
textColor: string;
bgColor: string;
}[] = [
{
label: "day left",
i18n_label: "project_cycles.status.days_left",
value: "current",
title: "In progress",
i18n_title: "project_cycles.status.in_progress",
color: "#F59E0B",
textColor: "text-amber-500",
bgColor: "bg-amber-50",
},
{
label: "Yet to start",
i18n_label: "project_cycles.status.yet_to_start",
value: "upcoming",
title: "Yet to start",
i18n_title: "project_cycles.status.yet_to_start",
color: "#3F76FF",
textColor: "text-blue-500",
bgColor: "bg-indigo-50",
},
{
label: "Completed",
i18n_label: "project_cycles.status.completed",
value: "completed",
title: "Completed",
i18n_title: "project_cycles.status.completed",
color: "#16A34A",
textColor: "text-green-600",
bgColor: "bg-green-50",
},
{
label: "Draft",
i18n_label: "project_cycles.status.draft",
value: "draft",
title: "Draft",
i18n_title: "project_cycles.status.draft",
color: "#525252",
textColor: "text-custom-text-300",
bgColor: "bg-custom-background-90",
+92
View File
@@ -0,0 +1,92 @@
// types
import { TIssuesListTypes } from "@plane/types";
export enum EDurationFilters {
NONE = "none",
TODAY = "today",
THIS_WEEK = "this_week",
THIS_MONTH = "this_month",
THIS_YEAR = "this_year",
CUSTOM = "custom",
}
// filter duration options
export const DURATION_FILTER_OPTIONS: {
key: EDurationFilters;
label: string;
}[] = [
{
key: EDurationFilters.NONE,
label: "All time",
},
{
key: EDurationFilters.TODAY,
label: "Due today",
},
{
key: EDurationFilters.THIS_WEEK,
label: "Due this week",
},
{
key: EDurationFilters.THIS_MONTH,
label: "Due this month",
},
{
key: EDurationFilters.THIS_YEAR,
label: "Due this year",
},
{
key: EDurationFilters.CUSTOM,
label: "Custom",
},
];
// random background colors for project cards
export const PROJECT_BACKGROUND_COLORS = [
"bg-gray-500/20",
"bg-green-500/20",
"bg-red-500/20",
"bg-orange-500/20",
"bg-blue-500/20",
"bg-yellow-500/20",
"bg-pink-500/20",
"bg-purple-500/20",
];
// assigned and created issues widgets tabs list
export const FILTERED_ISSUES_TABS_LIST: {
key: TIssuesListTypes;
label: string;
}[] = [
{
key: "upcoming",
label: "Upcoming",
},
{
key: "overdue",
label: "Overdue",
},
{
key: "completed",
label: "Marked completed",
},
];
// assigned and created issues widgets tabs list
export const UNFILTERED_ISSUES_TABS_LIST: {
key: TIssuesListTypes;
label: string;
}[] = [
{
key: "pending",
label: "Pending",
},
{
key: "completed",
label: "Marked completed",
},
];
export type TLinkOptions = {
userId: string | undefined;
};
-5
View File
@@ -1,5 +0,0 @@
export enum E_ARCHIVE_ERROR_CODES {
"INVALID_ARCHIVE_STATE_GROUP" = 4091,
"INVALID_ISSUE_START_DATE" = 4101,
"INVALID_ISSUE_TARGET_DATE" = 4102,
}
@@ -104,7 +104,10 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
module_id: payload.module_id,
archived_at: payload.archived_at,
state: payload.state,
view_id: path?.includes("workspace-views") || path?.includes("views") ? path.split("/").pop() : "",
view_id:
path?.includes("workspace-views") || path?.includes("views")
? path.split("/").pop()
: "",
};
if (eventName === ISSUE_UPDATED) {
@@ -166,12 +169,12 @@ export const MODULE_LINK_CREATED = "Module link created";
export const MODULE_LINK_UPDATED = "Module link updated";
export const MODULE_LINK_DELETED = "Module link deleted";
// Issue Events
export const ISSUE_CREATED = "Issue created";
export const ISSUE_UPDATED = "Issue updated";
export const ISSUE_DELETED = "Issue deleted";
export const ISSUE_ARCHIVED = "Issue archived";
export const ISSUE_RESTORED = "Issue restored";
export const ISSUE_OPENED = "Issue opened";
export const ISSUE_CREATED = "Work item created";
export const ISSUE_UPDATED = "Work item updated";
export const ISSUE_DELETED = "Work item deleted";
export const ISSUE_ARCHIVED = "Work item archived";
export const ISSUE_RESTORED = "Work item restored";
export const ISSUE_OPENED = "Work item opened";
// Project State Events
export const STATE_CREATED = "State created";
export const STATE_UPDATED = "State updated";
-1
View File
@@ -1 +0,0 @@
export const SIDEBAR_CLICKED = "Sidenav clicked";
+53
View File
@@ -2,3 +2,56 @@ export enum E_SORT_ORDER {
ASC = "asc",
DESC = "desc",
}
export const DATE_AFTER_FILTER_OPTIONS = [
{
name: "1 week from now",
value: "1_weeks;after;fromnow",
},
{
name: "2 weeks from now",
value: "2_weeks;after;fromnow",
},
{
name: "1 month from now",
value: "1_months;after;fromnow",
},
{
name: "2 months from now",
value: "2_months;after;fromnow",
},
];
export const DATE_BEFORE_FILTER_OPTIONS = [
{
name: "1 week ago",
value: "1_weeks;before;fromnow",
},
{
name: "2 weeks ago",
value: "2_weeks;before;fromnow",
},
{
name: "1 month ago",
i18n_name: "date_filters.1_month_ago",
value: "1_months;before;fromnow",
},
];
export const PROJECT_CREATED_AT_FILTER_OPTIONS = [
{
name: "Today",
value: "today;custom;custom",
},
{
name: "Yesterday",
value: "yesterday;custom;custom",
},
{
name: "Last 7 days",
value: "last_7_days;custom;custom",
},
{
name: "Last 30 days",
value: "last_30_days;custom;custom",
},
];
+91
View File
@@ -0,0 +1,91 @@
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
export enum EInboxIssueCurrentTab {
OPEN = "open",
CLOSED = "closed",
}
export enum EInboxIssueStatus {
PENDING = -2,
DECLINED = -1,
SNOOZED = 0,
ACCEPTED = 1,
DUPLICATE = 2,
}
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
export type TInboxIssueStatus = EInboxIssueStatus;
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
duplicate_to: string | undefined;
source: string;
issue: TIssue;
created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
};
export const INBOX_STATUS: {
key: string;
status: TInboxIssueStatus;
i18n_title: string;
i18n_description: () => string;
}[] = [
{
key: "pending",
i18n_title: "inbox_issue.status.pending.title",
status: EInboxIssueStatus.PENDING,
i18n_description: () => `inbox_issue.status.pending.description`,
},
{
key: "declined",
i18n_title: "inbox_issue.status.declined.title",
status: EInboxIssueStatus.DECLINED,
i18n_description: () => `inbox_issue.status.declined.description`,
},
{
key: "snoozed",
i18n_title: "inbox_issue.status.snoozed.title",
status: EInboxIssueStatus.SNOOZED,
i18n_description: () => `inbox_issue.status.snoozed.description`,
},
{
key: "accepted",
i18n_title: "inbox_issue.status.accepted.title",
status: EInboxIssueStatus.ACCEPTED,
i18n_description: () => `inbox_issue.status.accepted.description`,
},
{
key: "duplicate",
i18n_title: "inbox_issue.status.duplicate.title",
status: EInboxIssueStatus.DUPLICATE,
i18n_description: () => `inbox_issue.status.duplicate.description`,
},
];
export const INBOX_ISSUE_ORDER_BY_OPTIONS = [
{
key: "issue__created_at",
i18n_label: "inbox_issue.order_by.created_at",
},
{
key: "issue__updated_at",
i18n_label: "inbox_issue.order_by.updated_at",
},
{
key: "issue__sequence_id",
i18n_label: "inbox_issue.order_by.id",
},
];
export const INBOX_ISSUE_SORT_BY_OPTIONS = [
{
key: "asc",
i18n_label: "common.sort.asc",
},
{
key: "desc",
i18n_label: "common.sort.desc",
},
];
+15 -1
View File
@@ -2,15 +2,29 @@ export * from "./ai";
export * from "./analytics";
export * from "./auth";
export * from "./endpoints";
export * from "./event";
export * from "./file";
export * from "./filter";
export * from "./graph";
export * from "./instance";
export * from "./issue";
export * from "./metadata";
export * from "./notification";
export * from "./state";
export * from "./swr";
export * from "./tab-indices";
export * from "./user";
export * from "./workspace";
export * from "./stickies";
export * from "./cycle";
export * from "./module";
export * from "./project";
export * from "./views";
export * from "./themes";
export * from "./inbox";
export * from "./profile";
export * from "./workspace-drafts";
export * from "./label";
export * from "./event-tracker";
export * from "./spreadsheet";
export * from "./dashboard";
export * from "./page";
-185
View File
@@ -1,185 +0,0 @@
import { List, Kanban } from "lucide-react";
export const ALL_ISSUES = "All Issues";
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export type TIssueFilterKeys = "priority" | "state" | "labels";
export type TIssueLayout =
| "list"
| "kanban"
| "calendar"
| "spreadsheet"
| "gantt";
export type TIssueFilterPriorityObject = {
key: TIssuePriorities;
title: string;
className: string;
icon: string;
};
export enum EIssueGroupByToServerOptions {
"state" = "state_id",
"priority" = "priority",
"labels" = "labels__id",
"state_detail.group" = "state__group",
"assignees" = "assignees__id",
"cycle" = "cycle_id",
"module" = "issue_module__module_id",
"target_date" = "target_date",
"project" = "project_id",
"created_by" = "created_by",
"team_project" = "project_id",
}
export enum EIssueGroupBYServerToProperty {
"state_id" = "state_id",
"priority" = "priority",
"labels__id" = "label_ids",
"state__group" = "state__group",
"assignees__id" = "assignee_ids",
"cycle_id" = "cycle_id",
"issue_module__module_id" = "module_ids",
"target_date" = "target_date",
"project_id" = "project_id",
"created_by" = "created_by",
}
export enum EServerGroupByToFilterOptions {
"state_id" = "state",
"priority" = "priority",
"labels__id" = "labels",
"state__group" = "state_group",
"assignees__id" = "assignees",
"cycle_id" = "cycle",
"issue_module__module_id" = "module",
"target_date" = "target_date",
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
export enum EIssueLayoutTypes {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
GANTT = "gantt_chart",
SPREADSHEET = "spreadsheet",
}
export enum EIssuesStoreType {
GLOBAL = "GLOBAL",
PROFILE = "PROFILE",
TEAM = "TEAM",
PROJECT = "PROJECT",
CYCLE = "CYCLE",
MODULE = "MODULE",
TEAM_VIEW = "TEAM_VIEW",
PROJECT_VIEW = "PROJECT_VIEW",
ARCHIVED = "ARCHIVED",
DRAFT = "DRAFT",
DEFAULT = "DEFAULT",
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
EPIC = "EPIC",
}
export enum EIssueFilterType {
FILTERS = "filters",
DISPLAY_FILTERS = "display_filters",
DISPLAY_PROPERTIES = "display_properties",
KANBAN_FILTERS = "kanban_filters",
}
export enum EIssueCommentAccessSpecifier {
EXTERNAL = "EXTERNAL",
INTERNAL = "INTERNAL",
}
export enum EIssueListRow {
HEADER = "HEADER",
ISSUE = "ISSUE",
NO_ISSUES = "NO_ISSUES",
QUICK_ADD = "QUICK_ADD",
}
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
} = {
list: {
filters: ["priority", "state", "labels"],
},
kanban: {
filters: ["priority", "state", "labels"],
},
calendar: {
filters: ["priority", "state", "labels"],
},
spreadsheet: {
filters: ["priority", "state", "labels"],
},
gantt: {
filters: ["priority", "state", "labels"],
},
};
export const ISSUE_PRIORITIES: {
key: TIssuePriorities;
title: string;
}[] = [
{ key: "urgent", title: "Urgent" },
{ key: "high", title: "High" },
{ key: "medium", title: "Medium" },
{ key: "low", title: "Low" },
{ key: "none", title: "None" },
];
export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
{
key: "urgent",
title: "Urgent",
className: "bg-red-500 border-red-500 text-white",
icon: "error",
},
{
key: "high",
title: "High",
className: "text-orange-500 border-custom-border-300",
icon: "signal_cellular_alt",
},
{
key: "medium",
title: "Medium",
className: "text-yellow-500 border-custom-border-300",
icon: "signal_cellular_alt_2_bar",
},
{
key: "low",
title: "Low",
className: "text-green-500 border-custom-border-300",
icon: "signal_cellular_alt_1_bar",
},
{
key: "none",
title: "None",
className: "text-gray-500 border-custom-border-300",
icon: "block",
},
];
export const SITES_ISSUE_LAYOUTS: {
key: TIssueLayout;
title: string;
icon: any;
}[] = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
// { key: "calendar", title: "Calendar", icon: Calendar },
// { key: "spreadsheet", title: "Spreadsheet", icon: Sheet },
// { key: "gantt", title: "Gantt chart", icon: GanttChartSquare },
];
+217
View File
@@ -0,0 +1,217 @@
import {
TIssueGroupByOptions,
TIssueOrderByOptions,
IIssueDisplayProperties,
} from "@plane/types";
export const ALL_ISSUES = "All Issues";
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export type TIssueFilterPriorityObject = {
key: TIssuePriorities;
titleTranslationKey: string;
className: string;
icon: string;
};
export enum EIssueGroupByToServerOptions {
"state" = "state_id",
"priority" = "priority",
"labels" = "labels__id",
"state_detail.group" = "state__group",
"assignees" = "assignees__id",
"cycle" = "cycle_id",
"module" = "issue_module__module_id",
"target_date" = "target_date",
"project" = "project_id",
"created_by" = "created_by",
"team_project" = "project_id",
}
export enum EIssueGroupBYServerToProperty {
"state_id" = "state_id",
"priority" = "priority",
"labels__id" = "label_ids",
"state__group" = "state__group",
"assignees__id" = "assignee_ids",
"cycle_id" = "cycle_id",
"issue_module__module_id" = "module_ids",
"target_date" = "target_date",
"project_id" = "project_id",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
export enum EIssuesStoreType {
GLOBAL = "GLOBAL",
PROFILE = "PROFILE",
TEAM = "TEAM",
PROJECT = "PROJECT",
CYCLE = "CYCLE",
MODULE = "MODULE",
TEAM_VIEW = "TEAM_VIEW",
PROJECT_VIEW = "PROJECT_VIEW",
ARCHIVED = "ARCHIVED",
DRAFT = "DRAFT",
DEFAULT = "DEFAULT",
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
EPIC = "EPIC",
}
export enum EIssueCommentAccessSpecifier {
EXTERNAL = "EXTERNAL",
INTERNAL = "INTERNAL",
}
export enum EIssueListRow {
HEADER = "HEADER",
ISSUE = "ISSUE",
NO_ISSUES = "NO_ISSUES",
QUICK_ADD = "QUICK_ADD",
}
export const ISSUE_PRIORITIES: {
key: TIssuePriorities;
title: string;
}[] = [
{
key: "urgent",
title: "Urgent",
},
{
key: "high",
title: "High",
},
{
key: "medium",
title: "Medium",
},
{
key: "low",
title: "Low",
},
{
key: "none",
title: "None",
},
];
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
"state",
"priority",
"assignees",
"labels",
"module",
"cycle",
];
export type TCreateModalStoreTypes =
| EIssuesStoreType.TEAM
| EIssuesStoreType.PROJECT
| EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.PROFILE
| EIssuesStoreType.CYCLE
| EIssuesStoreType.MODULE
| EIssuesStoreType.EPIC;
export const ISSUE_GROUP_BY_OPTIONS: {
key: TIssueGroupByOptions;
titleTranslationKey: string;
}[] = [
{ key: "state", titleTranslationKey: "common.states" },
{ key: "state_detail.group", titleTranslationKey: "common.state_groups" },
{ key: "priority", titleTranslationKey: "common.priority" },
{ key: "team_project", titleTranslationKey: "common.team_project" }, // required this on team issues
{ key: "project", titleTranslationKey: "common.project" }, // required this on my issues
{ key: "cycle", titleTranslationKey: "common.cycle" }, // required this on my issues
{ key: "module", titleTranslationKey: "common.module" }, // required this on my issues
{ key: "labels", titleTranslationKey: "common.labels" },
{ key: "assignees", titleTranslationKey: "common.assignees" },
{ key: "created_by", titleTranslationKey: "common.created_by" },
{ key: null, titleTranslationKey: "common.none" },
];
export const ISSUE_ORDER_BY_OPTIONS: {
key: TIssueOrderByOptions;
titleTranslationKey: string;
}[] = [
{ key: "sort_order", titleTranslationKey: "common.order_by.manual" },
{ key: "-created_at", titleTranslationKey: "common.order_by.last_created" },
{ key: "-updated_at", titleTranslationKey: "common.order_by.last_updated" },
{ key: "start_date", titleTranslationKey: "common.order_by.start_date" },
{ key: "target_date", titleTranslationKey: "common.order_by.due_date" },
{ key: "-priority", titleTranslationKey: "common.priority" },
];
export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] =
[
"assignee",
"start_date",
"due_date",
"labels",
"key",
"priority",
"state",
"sub_issue_count",
"link",
"attachment_count",
"estimate",
"created_on",
"updated_on",
"modules",
"cycle",
"issue_type",
];
export const ISSUE_DISPLAY_PROPERTIES: {
key: keyof IIssueDisplayProperties;
titleTranslationKey: string;
}[] = [
{
key: "key",
titleTranslationKey: "issue.display.properties.id",
},
{
key: "issue_type",
titleTranslationKey: "issue.display.properties.issue_type",
},
{
key: "assignee",
titleTranslationKey: "common.assignee",
},
{
key: "start_date",
titleTranslationKey: "common.order_by.start_date",
},
{
key: "due_date",
titleTranslationKey: "common.order_by.due_date",
},
{ key: "labels", titleTranslationKey: "common.labels" },
{
key: "priority",
titleTranslationKey: "common.priority",
},
{ key: "state", titleTranslationKey: "common.state" },
{
key: "sub_issue_count",
titleTranslationKey: "issue.display.properties.sub_issue_count",
},
{
key: "attachment_count",
titleTranslationKey: "issue.display.properties.attachment_count",
},
{ key: "link", titleTranslationKey: "common.link" },
{
key: "estimate",
titleTranslationKey: "common.estimate",
},
{ key: "modules", titleTranslationKey: "common.module" },
{ key: "cycle", titleTranslationKey: "common.cycle" },
];
+530
View File
@@ -0,0 +1,530 @@
import {
ILayoutDisplayFiltersOptions,
TIssueActivityComment,
} from "@plane/types";
import {
TIssueFilterPriorityObject,
ISSUE_DISPLAY_PROPERTIES_KEYS,
EIssuesStoreType,
} from "./common";
import { TIssueLayout } from "./layout";
export type TIssueFilterKeys = "priority" | "state" | "labels";
export enum EServerGroupByToFilterOptions {
"state_id" = "state",
"priority" = "priority",
"labels__id" = "labels",
"state__group" = "state_group",
"assignees__id" = "assignees",
"cycle_id" = "cycle",
"issue_module__module_id" = "module",
"target_date" = "target_date",
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueFilterType {
FILTERS = "filters",
DISPLAY_FILTERS = "display_filters",
DISPLAY_PROPERTIES = "display_properties",
KANBAN_FILTERS = "kanban_filters",
}
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
} = {
list: {
filters: ["priority", "state", "labels"],
},
kanban: {
filters: ["priority", "state", "labels"],
},
calendar: {
filters: ["priority", "state", "labels"],
},
spreadsheet: {
filters: ["priority", "state", "labels"],
},
gantt: {
filters: ["priority", "state", "labels"],
},
};
export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
{
key: "urgent",
titleTranslationKey: "issue.priority.urgent",
className: "bg-red-500 border-red-500 text-white",
icon: "error",
},
{
key: "high",
titleTranslationKey: "issue.priority.high",
className: "text-orange-500 border-custom-border-300",
icon: "signal_cellular_alt",
},
{
key: "medium",
titleTranslationKey: "issue.priority.medium",
className: "text-yellow-500 border-custom-border-300",
icon: "signal_cellular_alt_2_bar",
},
{
key: "low",
titleTranslationKey: "issue.priority.low",
className: "text-green-500 border-custom-border-300",
icon: "signal_cellular_alt_1_bar",
},
{
key: "none",
titleTranslationKey: "common.none",
className: "text-gray-500 border-custom-border-300",
icon: "block",
},
];
export type TFiltersByLayout = {
[layoutType: string]: ILayoutDisplayFiltersOptions;
};
export type TIssueFiltersToDisplayByPageType = {
[pageType: string]: TFiltersByLayout;
};
export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
profile_issues: {
list: {
filters: [
"priority",
"state_group",
"labels",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels", null],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {
filters: [
"priority",
"state_group",
"labels",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels"],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
archived_issues: {
list: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"cycle",
"module",
"state_detail.group",
"priority",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
draft_issues: {
list: {
filters: [
"priority",
"state_group",
"cycle",
"module",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state_detail.group",
"cycle",
"module",
"priority",
"project",
"labels",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
kanban: {
filters: [
"priority",
"state_group",
"cycle",
"module",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state_detail.group",
"cycle",
"module",
"priority",
"project",
"labels",
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
my_issues: {
spreadsheet: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
list: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: false,
values: [],
},
},
},
issues: {
list: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
],
sub_group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
"target_date",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
calendar: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"issue_type",
],
display_properties: ["key", "issue_type"],
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
spreadsheet: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
gantt_chart: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ["key", "issue_type"],
display_filters: {
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
},
};
export const ISSUE_STORE_TO_FILTERS_MAP: Partial<
Record<EIssuesStoreType, TFiltersByLayout>
> = {
[EIssuesStoreType.PROJECT]: ISSUE_DISPLAY_FILTERS_BY_PAGE.issues,
};
export enum EActivityFilterType {
ACTIVITY = "ACTIVITY",
COMMENT = "COMMENT",
}
export type TActivityFilters = EActivityFilterType;
export const ACTIVITY_FILTER_TYPE_OPTIONS: Record<
TActivityFilters,
{ labelTranslationKey: string }
> = {
[EActivityFilterType.ACTIVITY]: {
labelTranslationKey: "common.updates",
},
[EActivityFilterType.COMMENT]: {
labelTranslationKey: "common.comments",
},
};
export type TActivityFilterOption = {
key: TActivityFilters;
labelTranslationKey: string;
isSelected: boolean;
onClick: () => void;
};
export const defaultActivityFilters: TActivityFilters[] = [
EActivityFilterType.ACTIVITY,
EActivityFilterType.COMMENT,
];
export const filterActivityOnSelectedFilters = (
activity: TIssueActivityComment[],
filters: TActivityFilters[]
): TIssueActivityComment[] =>
activity.filter((activity) =>
filters.includes(activity.activity_type as TActivityFilters)
);
export const ENABLE_ISSUE_DEPENDENCIES = false;
+3
View File
@@ -0,0 +1,3 @@
export * from "./common";
export * from "./filter";
export * from "./layout";
+76
View File
@@ -0,0 +1,76 @@
export type TIssueLayout =
| "list"
| "kanban"
| "calendar"
| "spreadsheet"
| "gantt";
export enum EIssueLayoutTypes {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
GANTT = "gantt_chart",
SPREADSHEET = "spreadsheet",
}
export type TIssueLayoutMap = Record<
EIssueLayoutTypes,
{
key: EIssueLayoutTypes;
i18n_title: string;
i18n_label: string;
}
>;
export const SITES_ISSUE_LAYOUTS: {
key: TIssueLayout;
titleTranslationKey: string;
icon: any;
}[] = [
{
key: "list",
icon: "List",
titleTranslationKey: "issue.layouts.list",
},
{
key: "kanban",
icon: "Kanban",
titleTranslationKey: "issue.layouts.kanban",
},
// { key: "calendar", title: "Calendar", icon: Calendar },
// { key: "spreadsheet", title: "Spreadsheet", icon: Sheet },
// { key: "gantt", title: "Gantt chart", icon: GanttChartSquare },
];
export const ISSUE_LAYOUT_MAP: TIssueLayoutMap = {
[EIssueLayoutTypes.LIST]: {
key: EIssueLayoutTypes.LIST,
i18n_title: "issue.layouts.title.list",
i18n_label: "issue.layouts.list",
},
[EIssueLayoutTypes.KANBAN]: {
key: EIssueLayoutTypes.KANBAN,
i18n_title: "issue.layouts.title.kanban",
i18n_label: "issue.layouts.kanban",
},
[EIssueLayoutTypes.CALENDAR]: {
key: EIssueLayoutTypes.CALENDAR,
i18n_title: "issue.layouts.title.calendar",
i18n_label: "issue.layouts.calendar",
},
[EIssueLayoutTypes.SPREADSHEET]: {
key: EIssueLayoutTypes.SPREADSHEET,
i18n_title: "issue.layouts.title.spreadsheet",
i18n_label: "issue.layouts.spreadsheet",
},
[EIssueLayoutTypes.GANTT]: {
key: EIssueLayoutTypes.GANTT,
i18n_title: "issue.layouts.title.gantt",
i18n_label: "issue.layouts.gantt",
},
};
export const ISSUE_LAYOUTS: {
key: EIssueLayoutTypes;
i18n_title: string;
}[] = Object.values(ISSUE_LAYOUT_MAP);
+3 -3
View File
@@ -3,9 +3,9 @@ export const SITE_NAME =
export const SITE_TITLE =
"Plane | Simple, extensible, open-source project management tool.";
export const SITE_DESCRIPTION =
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.";
"Open-source project management tool to manage work items, cycles, and product roadmaps easily";
export const SITE_KEYWORDS =
"software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration";
"software development, plan, ship, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration";
export const SITE_URL = "https://app.plane.so/";
export const TWITTER_USER_NAME =
"Plane | Simple, extensible, open-source project management tool.";
@@ -18,6 +18,6 @@ export const SPACE_SITE_TITLE =
export const SPACE_SITE_DESCRIPTION =
"Plane Publish is a customer feedback management tool built on top of plane.so";
export const SPACE_SITE_KEYWORDS =
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration";
"software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration";
export const SPACE_SITE_URL = "https://app.plane.so/";
export const SPACE_TWITTER_USER_NAME = "planepowers";
@@ -1,51 +1,54 @@
import { GanttChartSquare, LayoutGrid, List } from "lucide-react";
// types
import { TModuleLayoutOptions, TModuleOrderByOptions, TModuleStatus } from "@plane/types";
import {
TModuleLayoutOptions,
TModuleOrderByOptions,
TModuleStatus,
} from "@plane/types";
export const MODULE_STATUS: {
label: string;
i18n_label: string;
value: TModuleStatus;
color: string;
textColor: string;
bgColor: string;
}[] = [
{
label: "Backlog",
i18n_label: "project_modules.status.backlog",
value: "backlog",
color: "#a3a3a2",
textColor: "text-custom-text-400",
bgColor: "bg-custom-background-80",
},
{
label: "Planned",
i18n_label: "project_modules.status.planned",
value: "planned",
color: "#3f76ff",
textColor: "text-blue-500",
bgColor: "bg-indigo-50",
},
{
label: "In Progress",
i18n_label: "project_modules.status.in_progress",
value: "in-progress",
color: "#f39e1f",
textColor: "text-amber-500",
bgColor: "bg-amber-50",
},
{
label: "Paused",
i18n_label: "project_modules.status.paused",
value: "paused",
color: "#525252",
textColor: "text-custom-text-300",
bgColor: "bg-custom-background-90",
},
{
label: "Completed",
i18n_label: "project_modules.status.completed",
value: "completed",
color: "#16a34a",
textColor: "text-green-600",
bgColor: "bg-green-100",
},
{
label: "Cancelled",
i18n_label: "project_modules.status.cancelled",
value: "cancelled",
color: "#ef4444",
textColor: "text-red-500",
@@ -53,47 +56,50 @@ export const MODULE_STATUS: {
},
];
export const MODULE_VIEW_LAYOUTS: { key: TModuleLayoutOptions; icon: any; title: string }[] = [
export const MODULE_VIEW_LAYOUTS: {
key: TModuleLayoutOptions;
i18n_title: string;
}[] = [
{
key: "list",
icon: List,
title: "List layout",
i18n_title: "project_modules.layout.list",
},
{
key: "board",
icon: LayoutGrid,
title: "Gallery layout",
i18n_title: "project_modules.layout.board",
},
{
key: "gantt",
icon: GanttChartSquare,
title: "Timeline layout",
i18n_title: "project_modules.layout.timeline",
},
];
export const MODULE_ORDER_BY_OPTIONS: { key: TModuleOrderByOptions; label: string }[] = [
export const MODULE_ORDER_BY_OPTIONS: {
key: TModuleOrderByOptions;
i18n_label: string;
}[] = [
{
key: "name",
label: "Name",
i18n_label: "project_modules.order_by.name",
},
{
key: "progress",
label: "Progress",
i18n_label: "project_modules.order_by.progress",
},
{
key: "issues_length",
label: "Number of issues",
i18n_label: "project_modules.order_by.issues",
},
{
key: "target_date",
label: "Due date",
i18n_label: "project_modules.order_by.due_date",
},
{
key: "created_at",
label: "Created date",
i18n_label: "project_modules.order_by.created_at",
},
{
key: "sort_order",
label: "Manual",
i18n_label: "project_modules.order_by.manual",
},
];
@@ -29,12 +29,13 @@ export type TNotificationTab = ENotificationTab.ALL | ENotificationTab.MENTIONS;
export const NOTIFICATION_TABS = [
{
label: "All",
i18n_label: "notification.tabs.all",
value: ENotificationTab.ALL,
count: (unReadNotification: TUnreadNotificationsCount) => unReadNotification?.total_unread_notifications_count || 0,
count: (unReadNotification: TUnreadNotificationsCount) =>
unReadNotification?.total_unread_notifications_count || 0,
},
{
label: "Mentions",
i18n_label: "notification.tabs.mentions",
value: ENotificationTab.MENTIONS,
count: (unReadNotification: TUnreadNotificationsCount) =>
unReadNotification?.mention_unread_notifications_count || 0,
@@ -43,15 +44,15 @@ export const NOTIFICATION_TABS = [
export const FILTER_TYPE_OPTIONS = [
{
label: "Assigned to me",
i18n_label: "notification.filter.assigned",
value: ENotificationFilterType.ASSIGNED,
},
{
label: "Created by me",
i18n_label: "notification.filter.created",
value: ENotificationFilterType.CREATED,
},
{
label: "Subscribed by me",
i18n_label: "notification.filter.subscribed",
value: ENotificationFilterType.SUBSCRIBED,
},
];
@@ -59,7 +60,7 @@ export const FILTER_TYPE_OPTIONS = [
export const NOTIFICATION_SNOOZE_OPTIONS = [
{
key: "1_day",
label: "1 day",
i18n_label: "notification.snooze.1_day",
value: () => {
const date = new Date();
return new Date(date.getTime() + 24 * 60 * 60 * 1000);
@@ -67,7 +68,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
},
{
key: "3_days",
label: "3 days",
i18n_label: "notification.snooze.3_days",
value: () => {
const date = new Date();
return new Date(date.getTime() + 3 * 24 * 60 * 60 * 1000);
@@ -75,7 +76,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
},
{
key: "5_days",
label: "5 days",
i18n_label: "notification.snooze.5_days",
value: () => {
const date = new Date();
return new Date(date.getTime() + 5 * 24 * 60 * 60 * 1000);
@@ -83,7 +84,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
},
{
key: "1_week",
label: "1 week",
i18n_label: "notification.snooze.1_week",
value: () => {
const date = new Date();
return new Date(date.getTime() + 7 * 24 * 60 * 60 * 1000);
@@ -91,7 +92,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
},
{
key: "2_weeks",
label: "2 weeks",
i18n_label: "notification.snooze.2_weeks",
value: () => {
const date = new Date();
return new Date(date.getTime() + 14 * 24 * 60 * 60 * 1000);
@@ -99,7 +100,7 @@ export const NOTIFICATION_SNOOZE_OPTIONS = [
},
{
key: "custom",
label: "Custom",
i18n_label: "notification.snooze.custom",
value: undefined,
},
];
+14
View File
@@ -0,0 +1,14 @@
export enum EPageAccess {
PUBLIC = 0,
PRIVATE = 1,
}
export type TCreatePageModal = {
isOpen: boolean;
pageAccess?: EPageAccess;
};
export const DEFAULT_CREATE_PAGE_MODAL_DATA: TCreatePageModal = {
isOpen: false,
pageAccess: EPageAccess.PUBLIC,
};
@@ -1,48 +1,38 @@
import React from "react";
// icons
import { Activity, Bell, CircleUser, KeyRound, LucideProps, Settings2 } from "lucide-react";
export const PROFILE_ACTION_LINKS: {
key: string;
label: string;
i18n_label: string;
href: string;
highlight: (pathname: string) => boolean;
Icon: React.FC<LucideProps>;
}[] = [
{
key: "profile",
label: "Profile",
i18n_label: "profile.actions.profile",
href: `/profile`,
highlight: (pathname: string) => pathname === "/profile/",
Icon: CircleUser,
},
{
key: "security",
label: "Security",
i18n_label: "profile.actions.security",
href: `/profile/security`,
highlight: (pathname: string) => pathname === "/profile/security/",
Icon: KeyRound,
},
{
key: "activity",
label: "Activity",
i18n_label: "profile.actions.activity",
href: `/profile/activity`,
highlight: (pathname: string) => pathname === "/profile/activity/",
Icon: Activity,
},
{
key: "appearance",
label: "Appearance",
i18n_label: "profile.actions.appearance",
href: `/profile/appearance`,
highlight: (pathname: string) => pathname.includes("/profile/appearance"),
Icon: Settings2,
},
{
key: "notifications",
label: "Notifications",
i18n_label: "profile.actions.notifications",
href: `/profile/notifications`,
highlight: (pathname: string) => pathname === "/profile/notifications/",
Icon: Bell,
},
];
@@ -50,7 +40,7 @@ export const PROFILE_VIEWER_TAB = [
{
key: "summary",
route: "",
label: "Summary",
i18n_label: "profile.tabs.summary",
selected: "/",
},
];
@@ -59,24 +49,25 @@ export const PROFILE_ADMINS_TAB = [
{
key: "assigned",
route: "assigned",
label: "Assigned",
i18n_label: "profile.tabs.assigned",
selected: "/assigned/",
},
{
key: "created",
route: "created",
label: "Created",
i18n_label: "profile.tabs.created",
selected: "/created/",
},
{
key: "subscribed",
route: "subscribed",
label: "Subscribed",
i18n_label: "profile.tabs.subscribed",
selected: "/subscribed/",
},
{
key: "activity",
route: "activity",
label: "Activity",
i18n_label: "profile.tabs.activity",
selected: "/activity/",
},
];
@@ -1,41 +1,65 @@
// icons
import { Globe2, Lock, LucideIcon } from "lucide-react";
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
import {
TProjectAppliedDisplayFilterKeys,
TProjectOrderByOptions,
} from "@plane/types";
export const NETWORK_CHOICES: {
export type TNetworkChoiceIconKey = "Lock" | "Globe2";
export type TNetworkChoice = {
key: 0 | 2;
label: string;
labelKey: string;
i18n_label: string;
description: string;
icon: LucideIcon;
}[] = [
iconKey: TNetworkChoiceIconKey;
};
export const NETWORK_CHOICES: TNetworkChoice[] = [
{
key: 0,
label: "Private",
description: "Accessible only by invite",
icon: Lock,
labelKey: "Private",
i18n_label: "workspace_projects.network.private.title",
description: "workspace_projects.network.private.description", //"Accessible only by invite",
iconKey: "Lock",
},
{
key: 2,
label: "Public",
description: "Anyone in the workspace except Guests can join",
icon: Globe2,
labelKey: "Public",
i18n_label: "workspace_projects.network.public.title",
description: "workspace_projects.network.public.description", //"Anyone in the workspace except Guests can join",
iconKey: "Globe2",
},
];
export const GROUP_CHOICES = {
backlog: "Backlog",
unstarted: "Unstarted",
started: "Started",
completed: "Completed",
cancelled: "Cancelled",
backlog: {
key: "backlog",
i18n_label: "workspace_projects.state.backlog",
},
unstarted: {
key: "unstarted",
i18n_label: "workspace_projects.state.unstarted",
},
started: {
key: "started",
i18n_label: "workspace_projects.state.started",
},
completed: {
key: "completed",
i18n_label: "workspace_projects.state.completed",
},
cancelled: {
key: "cancelled",
i18n_label: "workspace_projects.state.cancelled",
},
};
export const PROJECT_AUTOMATION_MONTHS = [
{ label: "1 month", value: 1 },
{ label: "3 months", value: 3 },
{ label: "6 months", value: 6 },
{ label: "9 months", value: 9 },
{ label: "12 months", value: 12 },
{ i18n_label: "common.months_count", value: 1 },
{ i18n_label: "common.months_count", value: 3 },
{ i18n_label: "common.months_count", value: 6 },
{ i18n_label: "common.months_count", value: 9 },
{ i18n_label: "common.months_count", value: 12 },
];
export const PROJECT_UNSPLASH_COVERS = [
@@ -59,55 +83,55 @@ export const PROJECT_UNSPLASH_COVERS = [
export const PROJECT_ORDER_BY_OPTIONS: {
key: TProjectOrderByOptions;
label: string;
i18n_label: string;
}[] = [
{
key: "sort_order",
label: "Manual",
i18n_label: "workspace_projects.sort.manual",
},
{
key: "name",
label: "Name",
i18n_label: "workspace_projects.sort.name",
},
{
key: "created_at",
label: "Created date",
i18n_label: "workspace_projects.sort.created_at",
},
{
key: "members_length",
label: "Number of members",
i18n_label: "workspace_projects.sort.members_length",
},
];
export const PROJECT_DISPLAY_FILTER_OPTIONS: {
key: TProjectAppliedDisplayFilterKeys;
label: string;
i18n_label: string;
}[] = [
{
key: "my_projects",
label: "My projects",
i18n_label: "workspace_projects.scope.my_projects",
},
{
key: "archived_projects",
label: "Archived",
i18n_label: "workspace_projects.scope.archived_projects",
},
];
export const PROJECT_ERROR_MESSAGES = {
permissionError: {
title: "You don't have permission to perform this action.",
message: undefined,
i18n_title: "workspace_projects.error.permission",
i18n_message: undefined,
},
cycleDeleteError: {
title: "Error",
message: "Failed to delete cycle",
i18n_title: "error",
i18n_message: "workspace_projects.error.cycle_delete",
},
moduleDeleteError: {
title: "Error",
message: "Failed to delete module",
i18n_title: "error",
i18n_message: "workspace_projects.error.module_delete",
},
issueDeleteError: {
title: "Error",
message: "Failed to delete issue",
i18n_title: "error",
i18n_message: "workspace_projects.error.issue_delete",
},
};
+1
View File
@@ -0,0 +1 @@
export const SPREADSHEET_SELECT_GROUP = "spreadsheet-issues";
+14
View File
@@ -5,6 +5,11 @@ export type TStateGroups =
| "completed"
| "cancelled";
export type TDraggableData = {
groupKey: TStateGroups;
id: string;
};
export const STATE_GROUPS: {
[key in TStateGroups]: {
key: TStateGroups;
@@ -43,6 +48,13 @@ export const ARCHIVABLE_STATE_GROUPS = [
STATE_GROUPS.completed.key,
STATE_GROUPS.cancelled.key,
];
export const COMPLETED_STATE_GROUPS = [STATE_GROUPS.completed.key];
export const PENDING_STATE_GROUPS = [
STATE_GROUPS.backlog.key,
STATE_GROUPS.unstarted.key,
STATE_GROUPS.started.key,
STATE_GROUPS.cancelled.key,
];
export const PROGRESS_STATE_GROUPS_DETAILS = [
{
@@ -66,3 +78,5 @@ export const PROGRESS_STATE_GROUPS_DETAILS = [
color: "#A3A3A3",
},
];
export const DISPLAY_WORKFLOW_PRO_CTA = false;
+8
View File
@@ -6,3 +6,11 @@ export const DEFAULT_SWR_CONFIG = {
refreshInterval: 600000,
errorRetryCount: 3,
};
export const WEB_SWR_CONFIG = {
refreshWhenHidden: false,
revalidateIfStale: true,
revalidateOnFocus: true,
revalidateOnMount: true,
errorRetryCount: 3,
};
@@ -54,7 +54,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 +74,21 @@ export const PROJECT_MODULE_TAB_INDICES = [
"submit",
];
export const PROJECT_VIEW_TAB_INDICES = ["name", "description", "filters", "cancel", "submit"];
export const PROJECT_VIEW_TAB_INDICES = [
"name",
"description",
"filters",
"cancel",
"submit",
];
export const PROJECT_PAGE_TAB_INDICES = ["name", "public", "private", "cancel", "submit"];
export const PROJECT_PAGE_TAB_INDICES = [
"name",
"public",
"private",
"cancel",
"submit",
];
export enum ETabIndices {
ISSUE_FORM = "issue-form",
@@ -1,9 +1,15 @@
export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"];
export const THEMES = [
"light",
"dark",
"light-contrast",
"dark-contrast",
"custom",
];
export interface I_THEME_OPTION {
key: string;
value: string;
label: string;
i18n_label: string;
type: string;
icon: {
border: string;
@@ -16,7 +22,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "system_preference",
value: "system",
label: "System preference",
i18n_label: "System preference",
type: "light",
icon: {
border: "#DEE2E6",
@@ -27,7 +33,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "light",
value: "light",
label: "Light",
i18n_label: "Light",
type: "light",
icon: {
border: "#DEE2E6",
@@ -38,7 +44,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "dark",
value: "dark",
label: "Dark",
i18n_label: "Dark",
type: "dark",
icon: {
border: "#2E3234",
@@ -49,7 +55,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "light_contrast",
value: "light-contrast",
label: "Light high contrast",
i18n_label: "Light high contrast",
type: "light",
icon: {
border: "#000000",
@@ -60,7 +66,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "dark_contrast",
value: "dark-contrast",
label: "Dark high contrast",
i18n_label: "Dark high contrast",
type: "dark",
icon: {
border: "#FFFFFF",
@@ -71,7 +77,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
{
key: "custom",
value: "custom",
label: "Custom theme",
i18n_label: "Custom theme",
type: "light",
icon: {
border: "#FFC9C9",
+37
View File
@@ -36,3 +36,40 @@ export enum EUserProjectRoles {
MEMBER = 15,
GUEST = 5,
}
export type TUserPermissionsLevel = EUserPermissionsLevel;
export enum EUserPermissions {
ADMIN = 20,
MEMBER = 15,
GUEST = 5,
}
export type TUserPermissions = EUserPermissions;
export type TUserAllowedPermissionsObject = {
create: TUserPermissions[];
update: TUserPermissions[];
delete: TUserPermissions[];
read: TUserPermissions[];
};
export type TUserAllowedPermissions = {
workspace: {
[key: string]: Partial<TUserAllowedPermissionsObject>;
};
project: {
[key: string]: Partial<TUserAllowedPermissionsObject>;
};
};
export const USER_ALLOWED_PERMISSIONS: TUserAllowedPermissions = {
workspace: {
dashboard: {
read: [
EUserPermissions.ADMIN,
EUserPermissions.MEMBER,
EUserPermissions.GUEST,
],
},
},
project: {},
};
+23
View File
@@ -0,0 +1,23 @@
export enum EViewAccess {
PRIVATE,
PUBLIC,
}
export const VIEW_ACCESS_SPECIFIERS: {
key: EViewAccess;
i18n_label: string;
}[] = [
{ key: EViewAccess.PUBLIC, i18n_label: "common.access.public" },
{ key: EViewAccess.PRIVATE, i18n_label: "common.access.private" },
];
export const VIEW_SORTING_KEY_OPTIONS = [
{ key: "name", i18n_label: "project_view.sort_by.name" },
{ key: "created_at", i18n_label: "project_view.sort_by.created_at" },
{ key: "updated_at", i18n_label: "project_view.sort_by.updated_at" },
];
export const VIEW_SORT_BY_OPTIONS = [
{ key: "asc", i18n_label: "common.order_by.asc" },
{ key: "desc", i18n_label: "common.order_by.desc" },
];
+182
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",
},
];
+1 -1
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",
@@ -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}
@@ -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>
@@ -69,6 +69,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
return (
<PageRenderer
bubbleMenuEnabled={false}
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}
@@ -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>
)}
@@ -93,6 +93,19 @@ export const CustomColorExtension = Mark.create({
};
},
addStorage() {
return {
markdown: {
serialize: {
open: "",
close: "",
mixable: true,
expelEnclosingWhitespace: true,
},
},
};
},
parseHTML() {
return [
{
@@ -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;
@@ -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;
@@ -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);
}
@@ -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);
@@ -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 = ({
+1 -1
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,
@@ -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
+2 -1
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;
+9 -7
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);
+2 -2
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%;
+171
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."
}
}
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+122 -26
View File
@@ -1,8 +1,11 @@
import IntlMessageFormat from "intl-messageformat";
import get from "lodash/get";
import { makeAutoObservable } from "mobx";
import merge from "lodash/merge";
import { makeAutoObservable, runInAction } from "mobx";
// constants
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants";
// core translations imports
import coreEn from "../locales/en/core.json";
// types
import { TLanguage, ILanguageOption, ITranslations } from "../types";
@@ -12,42 +15,35 @@ import { TLanguage, ILanguageOption, ITranslations } from "../types";
* Uses IntlMessageFormat to format the translations
*/
export class TranslationStore {
// Core translations that are always loaded
private coreTranslations: ITranslations = {
en: coreEn,
};
// List of translations for each language
private translations: ITranslations = {};
// Cache for IntlMessageFormat instances
private messageCache: Map<string, IntlMessageFormat> = new Map();
// Current language
currentLocale: TLanguage = FALLBACK_LANGUAGE;
// Loading state
isLoading: boolean = true;
isInitialized: boolean = false;
// Set of loaded languages
private loadedLanguages: Set<TLanguage> = new Set();
/**
* Constructor for the TranslationStore class
*/
constructor() {
makeAutoObservable(this);
// Initialize with core translations immediately
this.translations = this.coreTranslations;
// Initialize language
this.initializeLanguage();
// Load all the translations
this.loadTranslations();
}
/**
* Loads translations from JSON files and initializes the message cache
*/
private async loadTranslations() {
try {
// dynamic import of translations
const translations = {
en: (await import("../locales/en/translations.json")).default,
fr: (await import("../locales/fr/translations.json")).default,
es: (await import("../locales/es/translations.json")).default,
ja: (await import("../locales/ja/translations.json")).default,
"zh-CN": (await import("../locales/zh-CN/translations.json")).default,
};
this.translations = translations;
this.messageCache.clear(); // Clear cache when translations change
} catch (error) {
console.error("Failed to load translations:", error);
}
}
/** Initializes the language based on the local storage or browser language */
private initializeLanguage() {
if (typeof window === "undefined") return;
@@ -62,6 +58,100 @@ export class TranslationStore {
this.setLanguage(browserLang);
}
/** Loads the translations for the current language */
private async loadTranslations(): Promise<void> {
try {
// Set initialized to true (Core translations are already loaded)
runInAction(() => {
this.isInitialized = true;
});
// Load current and fallback languages in parallel
await this.loadPrimaryLanguages();
// Load all remaining languages in parallel
this.loadRemainingLanguages();
} catch (error) {
console.error("Failed in translation initialization:", error);
runInAction(() => {
this.isLoading = false;
});
}
}
private async loadPrimaryLanguages(): Promise<void> {
try {
// Load current and fallback languages in parallel
const languagesToLoad = new Set<TLanguage>([this.currentLocale]);
// Add fallback language only if different from current
if (this.currentLocale !== FALLBACK_LANGUAGE) {
languagesToLoad.add(FALLBACK_LANGUAGE);
}
// Load all primary languages in parallel
const loadPromises = Array.from(languagesToLoad).map((lang) => this.loadLanguageTranslations(lang));
await Promise.all(loadPromises);
// Update loading state
runInAction(() => {
this.isLoading = false;
});
} catch (error) {
console.error("Failed to load primary languages:", error);
runInAction(() => {
this.isLoading = false;
});
}
}
private loadRemainingLanguages(): void {
const remainingLanguages = SUPPORTED_LANGUAGES.map((lang) => lang.value).filter(
(lang) =>
!this.loadedLanguages.has(lang as TLanguage) && lang !== this.currentLocale && lang !== FALLBACK_LANGUAGE
);
// Load all remaining languages in parallel
Promise.all(remainingLanguages.map((lang) => this.loadLanguageTranslations(lang as TLanguage))).catch((error) => {
console.error("Failed to load some remaining languages:", error);
});
}
private async loadLanguageTranslations(language: TLanguage): Promise<void> {
// Skip if already loaded
if (this.loadedLanguages.has(language)) return;
try {
const translations = await this.importLanguageFile(language);
runInAction(() => {
// Use lodash merge for deep merging
this.translations[language] = merge({}, this.coreTranslations[language] || {}, translations.default);
// Add to loaded languages
this.loadedLanguages.add(language);
// Clear cache
this.messageCache.clear();
});
} catch (error) {
console.error(`Failed to load translations for ${language}:`, error);
}
}
/**
* Imports the translations for the given language
* @param language - The language to import the translations for
* @returns {Promise<any>}
*/
private importLanguageFile(language: TLanguage): Promise<any> {
switch (language) {
case "en":
return import("../locales/en/translations.json");
case "fr":
return import("../locales/fr/translations.json");
case "es":
return import("../locales/es/translations.json");
case "ja":
return import("../locales/ja/translations.json");
case "zh-CN":
return import("../locales/zh-CN/translations.json");
default:
throw new Error(`Unsupported language: ${language}`);
}
}
/** Checks if the language is valid based on the supported languages */
private isValidLanguage(lang: string | null): lang is TLanguage {
return lang !== null && this.availableLanguages.some((l) => l.value === lang);
@@ -173,20 +263,26 @@ export class TranslationStore {
* Sets the current language and updates the translations
* @param lng - The new language
*/
setLanguage(lng: TLanguage): void {
async setLanguage(lng: TLanguage): Promise<void> {
try {
if (!this.isValidLanguage(lng)) {
throw new Error(`Invalid language: ${lng}`);
}
// Safeguard in case background loading failed
if (!this.loadedLanguages.has(lng)) {
await this.loadLanguageTranslations(lng);
}
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, lng);
}
this.currentLocale = lng;
this.messageCache.clear(); // Clear cache when language changes
if (typeof window !== "undefined") {
document.documentElement.lang = lng;
}
runInAction(() => {
this.currentLocale = lng;
this.messageCache.clear(); // Clear cache when language changes
});
} catch (error) {
console.error("Failed to set language:", error);
}
+2
View File
@@ -24,3 +24,5 @@ export type TLogoProps = {
};
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
export type TFetchStatus = "partial" | "complete" | undefined;
+4 -4
View File
@@ -1,7 +1,7 @@
import { TLogoProps } from "./common";
import { TIssuePriorities } from "./issues";
export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project";
export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project" | "workspace_page";
export type THomeWidgetKeys = "quick_links" | "recents" | "my_stickies" | "quick_tutorial" | "new_at_plane";
export type THomeWidgetProps = {
@@ -12,9 +12,9 @@ export type TPageEntityData = {
id: string;
name: string;
logo_props: TLogoProps;
project_id: string;
project_id?: string;
owned_by: string;
project_identifier: string;
project_identifier?: string;
};
export type TProjectEntityData = {
@@ -39,7 +39,7 @@ export type TIssueEntityData = {
export type TActivityEntityData = {
id: string;
entity_name: "page" | "project" | "issue";
entity_name: "page" | "project" | "issue" | "workspace_page";
entity_identifier: string;
visited_at: string;
entity_data: TPageEntityData | TProjectEntityData | TIssueEntityData;
+1 -32
View File
@@ -2,23 +2,6 @@ import { TPaginationInfo } from "./common";
import { TIssuePriorities } from "./issues";
import { TIssue } from "./issues/base";
enum EInboxIssueCurrentTab {
OPEN = "open",
CLOSED = "closed",
}
enum EInboxIssueStatus {
PENDING = -2,
DECLINED = -1,
SNOOZED = 0,
ACCEPTED = 1,
DUPLICATE = 2,
}
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
export type TInboxIssueStatus = EInboxIssueStatus;
// filters
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
@@ -38,10 +21,7 @@ export type TInboxIssueFilter = {
// sorting filters
export type TInboxIssueSortingKeys = "order_by" | "sort_by";
export type TInboxIssueSortingOrderByKeys =
| "issue__created_at"
| "issue__updated_at"
| "issue__sequence_id";
export type TInboxIssueSortingOrderByKeys = "issue__created_at" | "issue__updated_at" | "issue__sequence_id";
export type TInboxIssueSortingSortByKeys = "asc" | "desc";
@@ -78,17 +58,6 @@ export type TInboxDuplicateIssueDetails = {
name: string;
};
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 type TInboxIssuePaginationInfo = TPaginationInfo & {
total_results: number;
};
+1
View File
@@ -26,6 +26,7 @@ export * from "./waitlist";
export * from "./webhook";
export * from "./workspace-views";
export * from "./common";
export * from "./power-k";
export * from "./pragmatic";
export * from "./publish";
export * from "./search";
+1 -1
View File
@@ -15,7 +15,7 @@ import type {
TIssueGroupByOptions,
TIssueOrderByOptions,
TIssueGroupingFilters,
TIssueExtraOptions
TIssueExtraOptions,
} from "@plane/types";
export interface IIssueCycle {
+24
View File
@@ -0,0 +1,24 @@
export type TPowerKPageKeys =
// work-item actions
| "change-work-item-assignee"
| "change-work-item-priority"
| "change-work-item-state"
// module actions
| "change-module-member"
| "change-module-status"
// configs
| "workspace-settings"
| "project-settings"
| "profile-settings"
// personalization
| "change-theme";
export type TPowerKCreateActionKeys = "cycle" | "issue" | "module" | "page" | "project" | "view" | "workspace";
export type TPowerKCreateAction = {
key: TPowerKCreateActionKeys;
icon: any;
label: string;
onClick: () => void;
shortcut?: string;
shouldRender?: boolean;
};

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