mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
92 Commits
feat-date-
...
fix-sub-wo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73d5a80c38 | ||
|
|
92cdc4ecbb | ||
|
|
b88ae112f9 | ||
|
|
2d20278c9b | ||
|
|
8cff059868 | ||
|
|
6a3ccafe35 | ||
|
|
cc9b448a9b | ||
|
|
e071bf4861 | ||
|
|
b9da7df6b7 | ||
|
|
03cc819601 | ||
|
|
e1943ee11e | ||
|
|
b47d2b8825 | ||
|
|
300b47f9a1 | ||
|
|
03a4a97375 | ||
|
|
6157d5771d | ||
|
|
eee43be99a | ||
|
|
4db95cc941 | ||
|
|
6aa139a851 | ||
|
|
ac74cd9e92 | ||
|
|
7ae841d525 | ||
|
|
7aa5b6aa91 | ||
|
|
28c3f9d0cc | ||
|
|
9d01a6d5d7 | ||
|
|
4fd8b4a3a9 | ||
|
|
49cc73b6ed | ||
|
|
363507f987 | ||
|
|
30453d1c79 | ||
|
|
dff12729c0 | ||
|
|
8efe692c80 | ||
|
|
ce57c1423c | ||
|
|
1eb1e82fe4 | ||
|
|
a2328d0cbe | ||
|
|
5096a15051 | ||
|
|
55c2511ab5 | ||
|
|
16bc64e2fa | ||
|
|
14083ea7da | ||
|
|
feb88e64a4 | ||
|
|
a00bb35e54 | ||
|
|
20ba91b98c | ||
|
|
456c7f55a9 | ||
|
|
c2da3ea4c8 | ||
|
|
2b595cfe62 | ||
|
|
7a6b50a6e1 | ||
|
|
a5c2acb5f1 | ||
|
|
4cf0c702ce | ||
|
|
d36c3acbf7 | ||
|
|
e244f48776 | ||
|
|
89d1926727 | ||
|
|
9bd70cdb4e | ||
|
|
99f3d5810d | ||
|
|
10b5c625ef | ||
|
|
c14fb814c4 | ||
|
|
c82dd6901e | ||
|
|
a03a41ea5f | ||
|
|
9f4dd771fc | ||
|
|
0deec92d91 | ||
|
|
d2a6307bb0 | ||
|
|
66be0b1862 | ||
|
|
ddad1767a2 | ||
|
|
6a37a2ce21 | ||
|
|
01bd1bde64 | ||
|
|
9268180aec | ||
|
|
ff778b98f5 | ||
|
|
8f5ce6b232 | ||
|
|
58a4ca9f36 | ||
|
|
312b077657 | ||
|
|
c65e42f807 | ||
|
|
f4af78c0fc | ||
|
|
c0b6abc3d5 | ||
|
|
2f2e6626c6 | ||
|
|
6a8d3202b7 | ||
|
|
51b52a7fc3 | ||
|
|
23ede81737 | ||
|
|
b698f44500 | ||
|
|
421839ec51 | ||
|
|
940b5e4e44 | ||
|
|
6003c88d62 | ||
|
|
74913a6659 | ||
|
|
97578684c6 | ||
|
|
88b4d32220 | ||
|
|
f32635a6a8 | ||
|
|
7fe58e0ea9 | ||
|
|
7f22cd1ac1 | ||
|
|
e2550e0b2d | ||
|
|
b016ed78cf | ||
|
|
c429ca7b36 | ||
|
|
ee22dbba1b | ||
|
|
f4a208bd44 | ||
|
|
8edff26ccd | ||
|
|
d08c03f557 | ||
|
|
0b53912295 | ||
|
|
586a320d86 |
4
.github/workflows/build-branch.yml
vendored
4
.github/workflows/build-branch.yml
vendored
@@ -25,6 +25,10 @@ on:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
push:
|
||||
branches:
|
||||
- preview
|
||||
- canary
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
|
||||
12
.github/workflows/build-test-pull-request.yml
vendored
12
.github/workflows/build-test-pull-request.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- run: yarn install
|
||||
- run: yarn lint --filter=admin
|
||||
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- run: yarn install
|
||||
- run: yarn lint --filter=space
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- run: yarn install
|
||||
- run: yarn lint --filter=web
|
||||
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- run: yarn install
|
||||
- run: yarn build --filter=admin
|
||||
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- run: yarn install
|
||||
- run: yarn build --filter=space
|
||||
|
||||
@@ -133,6 +133,6 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- run: yarn install
|
||||
- run: yarn build --filter=web
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
10
apiserver/plane/api/serializers/estimate.py
Normal file
10
apiserver/plane/api/serializers/estimate.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Module imports
|
||||
from plane.db.models import EstimatePoint
|
||||
from .base import BaseSerializer
|
||||
|
||||
|
||||
class EstimatePointSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = EstimatePoint
|
||||
fields = ["id", "value"]
|
||||
read_only_fields = fields
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
for user in assignees
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
@@ -220,6 +221,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
for label in labels
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Time updation occues even when other related models are updated
|
||||
@@ -283,10 +285,26 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
)
|
||||
name = serializers.CharField(source="related_issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="related_issue.priority", read_only=True)
|
||||
assignee_ids = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
@@ -298,10 +316,26 @@ class RelatedIssueSerializer(BaseSerializer):
|
||||
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
|
||||
name = serializers.CharField(source="issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
state_id = serializers.UUIDField(source="issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="issue.priority", read_only=True)
|
||||
assignee_ids = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,10 +32,9 @@ from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
owner = UserLiteSerializer(read_only=True)
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
logo_url = serializers.CharField(read_only=True)
|
||||
role = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validate_slug(self, value):
|
||||
# Check if the slug is restricted
|
||||
@@ -146,6 +146,42 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
|
||||
return value
|
||||
|
||||
|
||||
def create(self, validated_data):
|
||||
# Filtering the WorkspaceUserLink with the given url to check if the link already exists.
|
||||
|
||||
url = validated_data.get("url")
|
||||
|
||||
workspace_user_link = WorkspaceUserLink.objects.filter(
|
||||
url=url,
|
||||
workspace_id=validated_data.get("workspace_id"),
|
||||
owner_id=validated_data.get("owner_id")
|
||||
)
|
||||
|
||||
if workspace_user_link.exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this workspace and owner"}
|
||||
)
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Filtering the WorkspaceUserLink with the given url to check if the link already exists.
|
||||
|
||||
url = validated_data.get("url")
|
||||
|
||||
workspace_user_link = WorkspaceUserLink.objects.filter(
|
||||
url=url,
|
||||
workspace_id=instance.workspace_id,
|
||||
owner=instance.owner
|
||||
)
|
||||
|
||||
if workspace_user_link.exclude(pk=instance.id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this workspace and owner"}
|
||||
)
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
class IssueRecentVisitSerializer(serializers.ModelSerializer):
|
||||
project_identifier = serializers.SerializerMethodField()
|
||||
|
||||
@@ -258,3 +294,10 @@ class StickySerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "owner"]
|
||||
extra_kwargs = {"name": {"required": False}}
|
||||
|
||||
|
||||
class WorkspaceUserPreferenceSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = WorkspaceUserPreference
|
||||
fields = ["key", "is_pinned", "sort_order"]
|
||||
read_only_fields = ["workspace", "created_by", "updated_by"]
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -48,6 +48,7 @@ from .workspace.favorite import (
|
||||
WorkspaceFavoriteGroupEndpoint,
|
||||
)
|
||||
from .workspace.recent_visit import UserRecentVisitViewSet
|
||||
from .workspace.user_preference import WorkspaceUserPreferenceViewSet
|
||||
|
||||
from .workspace.member import (
|
||||
WorkSpaceMemberViewSet,
|
||||
@@ -189,6 +190,7 @@ from .analytic.base import (
|
||||
SavedAnalyticEndpoint,
|
||||
ExportAnalyticsEndpoint,
|
||||
DefaultAnalyticsEndpoint,
|
||||
ProjectStatsEndpoint,
|
||||
)
|
||||
|
||||
from .notification.base import (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -47,6 +47,7 @@ from plane.db.models import (
|
||||
User,
|
||||
Project,
|
||||
ProjectMember,
|
||||
UserRecentVisit,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
@@ -133,11 +134,11 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
cancelled_issues=Count(
|
||||
"issue_cycle__issue__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group__in=["backlog", "unstarted", "started"],
|
||||
issue_cycle__issue__state__group__in=["cancelled"],
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
@@ -226,7 +227,7 @@ class CycleViewSet(BaseViewSet):
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
"completed_issues",
|
||||
"pending_issues",
|
||||
"cancelled_issues",
|
||||
"assignee_ids",
|
||||
"status",
|
||||
"version",
|
||||
@@ -258,7 +259,7 @@ class CycleViewSet(BaseViewSet):
|
||||
# meta fields
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
"pending_issues",
|
||||
"cancelled_issues",
|
||||
"completed_issues",
|
||||
"assignee_ids",
|
||||
"status",
|
||||
@@ -543,6 +544,13 @@ class CycleViewSet(BaseViewSet):
|
||||
entity_identifier=pk,
|
||||
project_id=project_id,
|
||||
).delete()
|
||||
# Delete the cycle from recent visits
|
||||
UserRecentVisit.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
entity_identifier=pk,
|
||||
entity_name="cycle",
|
||||
).delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
||||
@@ -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)}),
|
||||
|
||||
@@ -35,7 +35,9 @@ class LabelViewSet(BaseViewSet):
|
||||
.order_by("sort_order")
|
||||
)
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False, multiple=True
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
|
||||
@@ -272,10 +272,9 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
|
||||
issue_relations = IssueRelation.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
).filter(
|
||||
Q(issue_id=related_issue, related_issue_id=issue_id) |
|
||||
Q(issue_id=issue_id, related_issue_id=related_issue)
|
||||
Q(issue_id=related_issue, related_issue_id=issue_id)
|
||||
| Q(issue_id=issue_id, related_issue_id=related_issue)
|
||||
)
|
||||
issue_relations = issue_relations.first()
|
||||
current_instance = json.dumps(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -33,13 +33,14 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
ProjectPage,
|
||||
Project,
|
||||
UserRecentVisit,
|
||||
)
|
||||
from plane.utils.error_codes import ERROR_CODES
|
||||
from ..base import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.page_transaction_task import page_transaction
|
||||
from plane.bgtasks.page_version_task import page_version
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
|
||||
from plane.bgtasks.copy_s3_object import copy_s3_objects
|
||||
|
||||
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
||||
# Your SQL query
|
||||
@@ -387,6 +388,13 @@ class PageViewSet(BaseViewSet):
|
||||
entity_identifier=pk,
|
||||
entity_type="page",
|
||||
).delete()
|
||||
# Delete the page from recent visit
|
||||
UserRecentVisit.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
entity_identifier=pk,
|
||||
entity_name="page",
|
||||
).delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -589,6 +597,16 @@ class PageDuplicateEndpoint(BaseAPIView):
|
||||
page_transaction.delay(
|
||||
{"description_html": page.description_html}, None, page.id
|
||||
)
|
||||
|
||||
# Copy the s3 objects uploaded in the page
|
||||
copy_s3_objects.delay(
|
||||
entity_name="PAGE",
|
||||
entity_identifier=page.id,
|
||||
project_id=project_id,
|
||||
slug=slug,
|
||||
user_id=request.user.id,
|
||||
)
|
||||
|
||||
page = (
|
||||
Page.objects.filter(pk=page.id)
|
||||
.annotate(
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,9 +7,11 @@ from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
|
||||
|
||||
from django.db.models.fields import DateField
|
||||
from django.db.models.functions import Cast, ExtractDay, ExtractWeek
|
||||
|
||||
|
||||
# Django imports
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
@@ -62,12 +64,6 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
|
||||
issue_count = (
|
||||
Issue.issue_objects.filter(workspace=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
return (
|
||||
self.filter_queryset(super().get_queryset().select_related("owner"))
|
||||
.order_by("name")
|
||||
@@ -76,8 +72,6 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
workspace_member__is_active=True,
|
||||
)
|
||||
.annotate(total_members=member_count)
|
||||
.annotate(total_issues=issue_count)
|
||||
.select_related("owner")
|
||||
)
|
||||
|
||||
def create(self, request):
|
||||
@@ -123,7 +117,14 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
role=20,
|
||||
company_role=request.data.get("company_role", ""),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
# Get total members and role
|
||||
total_members=WorkspaceMember.objects.filter(workspace_id=serializer.data["id"]).count()
|
||||
data = serializer.data
|
||||
data["total_members"] = total_members
|
||||
data["role"] = 20
|
||||
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
[serializer.errors[error][0] for error in serializer.errors],
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -166,11 +167,9 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
|
||||
issue_count = (
|
||||
Issue.issue_objects.filter(workspace=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
role = (
|
||||
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member=request.user, is_active=True)
|
||||
.values("role")
|
||||
)
|
||||
|
||||
workspace = (
|
||||
@@ -182,19 +181,19 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
),
|
||||
)
|
||||
)
|
||||
.select_related("owner")
|
||||
.annotate(total_members=member_count)
|
||||
.annotate(total_issues=issue_count)
|
||||
.annotate(role=role, total_members=member_count)
|
||||
.filter(
|
||||
workspace_member__member=request.user, workspace_member__is_active=True
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
workspaces = WorkSpaceSerializer(
|
||||
self.filter_queryset(workspace),
|
||||
fields=fields if fields else None,
|
||||
many=True,
|
||||
).data
|
||||
|
||||
return Response(workspaces, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class QuickLinkViewSet(BaseViewSet):
|
||||
serializer = WorkspaceUserLinkSerializer(data=request.data)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save(workspace_id=workspace.id, owner=request.user)
|
||||
serializer.save(workspace_id=workspace.id, owner_id=request.user.id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
78
apiserver/plane/app/views/workspace/user_preference.py
Normal file
78
apiserver/plane/app/views/workspace/user_preference.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# Module imports
|
||||
from ..base import BaseAPIView
|
||||
from plane.db.models.workspace import WorkspaceUserPreference
|
||||
from plane.app.serializers.workspace import WorkspaceUserPreferenceSerializer
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.db.models import Workspace
|
||||
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class WorkspaceUserPreferenceViewSet(BaseAPIView):
|
||||
model = WorkspaceUserPreference
|
||||
|
||||
def get_serializer_class(self):
|
||||
return WorkspaceUserPreferenceSerializer
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
get_preference = WorkspaceUserPreference.objects.filter(
|
||||
user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
|
||||
create_preference_keys = []
|
||||
|
||||
keys = [
|
||||
key
|
||||
for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices
|
||||
if key not in ["projects"]
|
||||
]
|
||||
|
||||
for preference in keys:
|
||||
if preference not in get_preference.values_list("key", flat=True):
|
||||
create_preference_keys.append(preference)
|
||||
|
||||
preference = WorkspaceUserPreference.objects.bulk_create(
|
||||
[
|
||||
WorkspaceUserPreference(
|
||||
key=key, user=request.user, workspace=workspace
|
||||
)
|
||||
for key in create_preference_keys
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
preference = WorkspaceUserPreference.objects.filter(
|
||||
user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
|
||||
return Response(
|
||||
preference.values("key", "is_pinned", "sort_order"),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def patch(self, request, slug, key):
|
||||
preference = WorkspaceUserPreference.objects.filter(
|
||||
key=key, workspace__slug=slug, user=request.user
|
||||
).first()
|
||||
|
||||
if preference:
|
||||
serializer = WorkspaceUserPreferenceSerializer(
|
||||
preference, data=request.data, partial=True
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
@@ -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(),
|
||||
|
||||
150
apiserver/plane/bgtasks/copy_s3_object.py
Normal file
150
apiserver/plane/bgtasks/copy_s3_object.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# Python imports
|
||||
import uuid
|
||||
import base64
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import FileAsset, Page, Issue
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.settings.storage import S3Storage
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
def get_entity_id_field(entity_type, entity_id):
|
||||
entity_mapping = {
|
||||
FileAsset.EntityTypeContext.WORKSPACE_LOGO: {"workspace_id": entity_id},
|
||||
FileAsset.EntityTypeContext.PROJECT_COVER: {"project_id": entity_id},
|
||||
FileAsset.EntityTypeContext.USER_AVATAR: {"user_id": entity_id},
|
||||
FileAsset.EntityTypeContext.USER_COVER: {"user_id": entity_id},
|
||||
FileAsset.EntityTypeContext.ISSUE_ATTACHMENT: {"issue_id": entity_id},
|
||||
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION: {"issue_id": entity_id},
|
||||
FileAsset.EntityTypeContext.PAGE_DESCRIPTION: {"page_id": entity_id},
|
||||
FileAsset.EntityTypeContext.COMMENT_DESCRIPTION: {"comment_id": entity_id},
|
||||
FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION: {
|
||||
"draft_issue_id": entity_id
|
||||
},
|
||||
}
|
||||
return entity_mapping.get(entity_type, {})
|
||||
|
||||
|
||||
def extract_asset_ids(html, tag):
|
||||
try:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
return [tag.get("src") for tag in soup.find_all(tag) if tag.get("src")]
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return []
|
||||
|
||||
|
||||
def replace_asset_ids(html, tag, duplicated_assets):
|
||||
try:
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
for mention_tag in soup.find_all(tag):
|
||||
for asset in duplicated_assets:
|
||||
if mention_tag.get("src") == asset["old_asset_id"]:
|
||||
mention_tag["src"] = asset["new_asset_id"]
|
||||
return str(soup)
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return html
|
||||
|
||||
|
||||
def update_description(entity, duplicated_assets, tag):
|
||||
updated_html = replace_asset_ids(entity.description_html, tag, duplicated_assets)
|
||||
entity.description_html = updated_html
|
||||
entity.save()
|
||||
return updated_html
|
||||
|
||||
|
||||
# Get the description binary and description from the live server
|
||||
def sync_with_external_service(entity_name, description_html):
|
||||
try:
|
||||
data = {
|
||||
"description_html": description_html,
|
||||
"variant": "rich" if entity_name == "PAGE" else "document",
|
||||
}
|
||||
response = requests.post(
|
||||
f"{settings.LIVE_BASE_URL}/convert-document/",
|
||||
json=data,
|
||||
headers=None,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
log_exception(e)
|
||||
return {}
|
||||
|
||||
|
||||
@shared_task
|
||||
def copy_s3_objects(entity_name, entity_identifier, project_id, slug, user_id):
|
||||
"""
|
||||
Step 1: Extract asset ids from the description_html of the entity
|
||||
Step 2: Duplicate the assets
|
||||
Step 3: Update the description_html of the entity with the new asset ids (change the src of img tag)
|
||||
Step 4: Request the live server to generate the description_binary and description for the entity
|
||||
|
||||
"""
|
||||
try:
|
||||
model_class = {"PAGE": Page, "ISSUE": Issue}.get(entity_name)
|
||||
if not model_class:
|
||||
raise ValueError(f"Unsupported entity_name: {entity_name}")
|
||||
|
||||
entity = model_class.objects.get(id=entity_identifier)
|
||||
asset_ids = extract_asset_ids(entity.description_html, "image-component")
|
||||
|
||||
duplicated_assets = []
|
||||
workspace = entity.workspace
|
||||
storage = S3Storage()
|
||||
original_assets = FileAsset.objects.filter(
|
||||
workspace=workspace, project_id=project_id, id__in=asset_ids
|
||||
)
|
||||
|
||||
for original_asset in original_assets:
|
||||
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
|
||||
duplicated_asset = FileAsset.objects.create(
|
||||
attributes={
|
||||
"name": original_asset.attributes.get("name"),
|
||||
"type": original_asset.attributes.get("type"),
|
||||
"size": original_asset.attributes.get("size"),
|
||||
},
|
||||
asset=destination_key,
|
||||
size=original_asset.size,
|
||||
workspace=workspace,
|
||||
created_by_id=user_id,
|
||||
entity_type=original_asset.entity_type,
|
||||
project_id=project_id,
|
||||
storage_metadata=original_asset.storage_metadata,
|
||||
**get_entity_id_field(original_asset.entity_type, entity_identifier),
|
||||
)
|
||||
storage.copy_object(original_asset.asset, destination_key)
|
||||
duplicated_assets.append(
|
||||
{
|
||||
"new_asset_id": str(duplicated_asset.id),
|
||||
"old_asset_id": str(original_asset.id),
|
||||
}
|
||||
)
|
||||
|
||||
if duplicated_assets:
|
||||
FileAsset.objects.filter(
|
||||
pk__in=[item["new_asset_id"] for item in duplicated_assets]
|
||||
).update(is_uploaded=True)
|
||||
updated_html = update_description(
|
||||
entity, duplicated_assets, "image-component"
|
||||
)
|
||||
external_data = sync_with_external_service(entity_name, updated_html)
|
||||
|
||||
if external_data:
|
||||
entity.description = external_data.get("description")
|
||||
entity.description_binary = base64.b64decode(
|
||||
external_data.get("description_binary")
|
||||
)
|
||||
entity.save()
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return []
|
||||
@@ -82,7 +82,10 @@ def soft_delete_related_objects(app_label, model_name, instance_pk, using=None):
|
||||
)
|
||||
else:
|
||||
# Handle other relationships
|
||||
related_queryset = getattr(instance, related_name).all()
|
||||
related_queryset = getattr(instance, related_name)(
|
||||
manager="objects"
|
||||
).all()
|
||||
|
||||
for related_obj in related_queryset:
|
||||
if hasattr(related_obj, "deleted_at"):
|
||||
if not related_obj.deleted_at:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
]
|
||||
@@ -69,7 +69,8 @@ from .workspace import (
|
||||
WorkspaceTheme,
|
||||
WorkspaceUserProperties,
|
||||
WorkspaceUserLink,
|
||||
WorkspaceHomePreference
|
||||
WorkspaceHomePreference,
|
||||
WorkspaceUserPreference,
|
||||
)
|
||||
|
||||
from .favorite import UserFavorite
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)}"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -336,6 +336,8 @@ CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure"
|
||||
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
|
||||
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
|
||||
APP_BASE_URL = os.environ.get("APP_BASE_URL")
|
||||
LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL")
|
||||
|
||||
|
||||
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
|
||||
|
||||
|
||||
@@ -151,3 +151,17 @@ class S3Storage(S3Boto3Storage):
|
||||
"ETag": response.get("ETag"),
|
||||
"Metadata": response.get("Metadata", {}),
|
||||
}
|
||||
|
||||
def copy_object(self, object_name, new_object_name):
|
||||
"""Copy an S3 object to a new location"""
|
||||
try:
|
||||
response = self.s3_client.copy_object(
|
||||
Bucket=self.aws_storage_bucket_name,
|
||||
CopySource={"Bucket": self.aws_storage_bucket_name, "Key": object_name},
|
||||
Key=new_object_name,
|
||||
)
|
||||
except ClientError as e:
|
||||
log_exception(e)
|
||||
return None
|
||||
|
||||
return response
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -51,7 +51,7 @@ beautifulsoup4==4.12.3
|
||||
# analytics
|
||||
posthog==3.5.0
|
||||
# crypto
|
||||
cryptography==43.0.1
|
||||
cryptography==44.0.1
|
||||
# html validator
|
||||
lxml==5.2.1
|
||||
# s3
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@plane/constants": "*",
|
||||
"@plane/editor": "*",
|
||||
"@plane/types": "*",
|
||||
"@sentry/node": "^8.28.0",
|
||||
"@sentry/node": "^9.0.1",
|
||||
"@sentry/profiling-node": "^8.28.0",
|
||||
"@tiptap/core": "2.10.4",
|
||||
"@tiptap/html": "2.11.0",
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
"devDependencies": {
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"turbo": "^2.3.3"
|
||||
"turbo": "^2.4.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"nanoid": "3.3.8",
|
||||
"esbuild": "0.25.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"name": "plane"
|
||||
|
||||
5
packages/constants/.prettierignore
Normal file
5
packages/constants/.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.next
|
||||
.turbo
|
||||
out/
|
||||
dist/
|
||||
build/
|
||||
5
packages/constants/.prettierrc
Normal file
5
packages/constants/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
import { TXAxisValues, TYAxisValues } from "@plane/types";
|
||||
|
||||
export const ANALYTICS_TABS = [
|
||||
{ key: "scope_and_demand", title: "Scope and Demand" },
|
||||
{ key: "custom", title: "Custom Analytics" },
|
||||
{
|
||||
key: "scope_and_demand",
|
||||
i18n_title: "workspace_analytics.tabs.scope_and_demand",
|
||||
},
|
||||
{ key: "custom", i18n_title: "workspace_analytics.tabs.custom" },
|
||||
];
|
||||
|
||||
export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
|
||||
@@ -62,7 +65,7 @@ export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] =
|
||||
[
|
||||
{
|
||||
value: "issue_count",
|
||||
label: "Issue Count",
|
||||
label: "Work item Count",
|
||||
},
|
||||
{
|
||||
value: "estimate",
|
||||
|
||||
2
packages/constants/src/chart.ts
Normal file
2
packages/constants/src/chart.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracking-wide";
|
||||
export const AXIS_LINE_CLASSNAME = "text-custom-text-400/70";
|
||||
@@ -1,56 +1,40 @@
|
||||
// types
|
||||
import { TCycleLayoutOptions, TCycleTabOptions } from "@plane/types";
|
||||
|
||||
export const CYCLE_TABS_LIST: {
|
||||
key: TCycleTabOptions;
|
||||
name: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "active",
|
||||
name: "Active",
|
||||
},
|
||||
{
|
||||
key: "all",
|
||||
name: "All",
|
||||
},
|
||||
];
|
||||
|
||||
export const CYCLE_STATUS: {
|
||||
label: string;
|
||||
i18n_label: string;
|
||||
value: "current" | "upcoming" | "completed" | "draft";
|
||||
title: string;
|
||||
i18n_title: string;
|
||||
color: string;
|
||||
textColor: string;
|
||||
bgColor: string;
|
||||
}[] = [
|
||||
{
|
||||
label: "day left",
|
||||
i18n_label: "project_cycles.status.days_left",
|
||||
value: "current",
|
||||
title: "In progress",
|
||||
i18n_title: "project_cycles.status.in_progress",
|
||||
color: "#F59E0B",
|
||||
textColor: "text-amber-500",
|
||||
bgColor: "bg-amber-50",
|
||||
},
|
||||
{
|
||||
label: "Yet to start",
|
||||
i18n_label: "project_cycles.status.yet_to_start",
|
||||
value: "upcoming",
|
||||
title: "Yet to start",
|
||||
i18n_title: "project_cycles.status.yet_to_start",
|
||||
color: "#3F76FF",
|
||||
textColor: "text-blue-500",
|
||||
bgColor: "bg-indigo-50",
|
||||
},
|
||||
{
|
||||
label: "Completed",
|
||||
i18n_label: "project_cycles.status.completed",
|
||||
value: "completed",
|
||||
title: "Completed",
|
||||
i18n_title: "project_cycles.status.completed",
|
||||
color: "#16A34A",
|
||||
textColor: "text-green-600",
|
||||
bgColor: "bg-green-50",
|
||||
},
|
||||
{
|
||||
label: "Draft",
|
||||
i18n_label: "project_cycles.status.draft",
|
||||
value: "draft",
|
||||
title: "Draft",
|
||||
i18n_title: "project_cycles.status.draft",
|
||||
color: "#525252",
|
||||
textColor: "text-custom-text-300",
|
||||
bgColor: "bg-custom-background-90",
|
||||
92
packages/constants/src/dashboard.ts
Normal file
92
packages/constants/src/dashboard.ts
Normal 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;
|
||||
};
|
||||
@@ -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 +0,0 @@
|
||||
export const SIDEBAR_CLICKED = "Sidenav clicked";
|
||||
@@ -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
packages/constants/src/inbox.ts
Normal file
91
packages/constants/src/inbox.ts
Normal 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",
|
||||
},
|
||||
];
|
||||
@@ -1,16 +1,31 @@
|
||||
export * from "./ai";
|
||||
export * from "./analytics";
|
||||
export * from "./auth";
|
||||
export * from "./chart";
|
||||
export * from "./endpoints";
|
||||
export * from "./event";
|
||||
export * from "./file";
|
||||
export * from "./filter";
|
||||
export * from "./graph";
|
||||
export * from "./instance";
|
||||
export * from "./issue";
|
||||
export * from "./metadata";
|
||||
export * from "./notification";
|
||||
export * from "./state";
|
||||
export * from "./swr";
|
||||
export * from "./tab-indices";
|
||||
export * from "./user";
|
||||
export * from "./workspace";
|
||||
export * from "./stickies";
|
||||
export * from "./cycle";
|
||||
export * from "./module";
|
||||
export * from "./project";
|
||||
export * from "./views";
|
||||
export * from "./themes";
|
||||
export * from "./inbox";
|
||||
export * from "./profile";
|
||||
export * from "./workspace-drafts";
|
||||
export * from "./label";
|
||||
export * from "./event-tracker";
|
||||
export * from "./spreadsheet";
|
||||
export * from "./dashboard";
|
||||
export * from "./page";
|
||||
|
||||
@@ -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
packages/constants/src/issue/common.ts
Normal file
217
packages/constants/src/issue/common.ts
Normal 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
packages/constants/src/issue/filter.ts
Normal file
530
packages/constants/src/issue/filter.ts
Normal 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
packages/constants/src/issue/index.ts
Normal file
3
packages/constants/src/issue/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./common";
|
||||
export * from "./filter";
|
||||
export * from "./layout";
|
||||
76
packages/constants/src/issue/layout.ts
Normal file
76
packages/constants/src/issue/layout.ts
Normal 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,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
packages/constants/src/page.ts
Normal file
14
packages/constants/src/page.ts
Normal 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
packages/constants/src/spreadsheet.ts
Normal file
1
packages/constants/src/spreadsheet.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const SPREADSHEET_SELECT_GROUP = "spreadsheet-issues";
|
||||
@@ -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;
|
||||
|
||||
@@ -6,3 +6,11 @@ export const DEFAULT_SWR_CONFIG = {
|
||||
refreshInterval: 600000,
|
||||
errorRetryCount: 3,
|
||||
};
|
||||
|
||||
export const WEB_SWR_CONFIG = {
|
||||
refreshWhenHidden: false,
|
||||
revalidateIfStale: true,
|
||||
revalidateOnFocus: true,
|
||||
revalidateOnMount: true,
|
||||
errorRetryCount: 3,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ export const ISSUE_FORM_TAB_INDICES = [
|
||||
"name",
|
||||
"description_html",
|
||||
"feeling_lucky",
|
||||
"ai_assistant",
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
@@ -54,7 +53,14 @@ export const PROJECT_CREATE_TAB_INDICES = [
|
||||
"logo_props",
|
||||
];
|
||||
|
||||
export const PROJECT_CYCLE_TAB_INDICES = ["name", "description", "date_range", "cancel", "submit", "project_id"];
|
||||
export const PROJECT_CYCLE_TAB_INDICES = [
|
||||
"name",
|
||||
"description",
|
||||
"date_range",
|
||||
"cancel",
|
||||
"submit",
|
||||
"project_id",
|
||||
];
|
||||
|
||||
export const PROJECT_MODULE_TAB_INDICES = [
|
||||
"name",
|
||||
@@ -67,9 +73,21 @@ export const PROJECT_MODULE_TAB_INDICES = [
|
||||
"submit",
|
||||
];
|
||||
|
||||
export const PROJECT_VIEW_TAB_INDICES = ["name", "description", "filters", "cancel", "submit"];
|
||||
export const PROJECT_VIEW_TAB_INDICES = [
|
||||
"name",
|
||||
"description",
|
||||
"filters",
|
||||
"cancel",
|
||||
"submit",
|
||||
];
|
||||
|
||||
export const PROJECT_PAGE_TAB_INDICES = ["name", "public", "private", "cancel", "submit"];
|
||||
export const PROJECT_PAGE_TAB_INDICES = [
|
||||
"name",
|
||||
"public",
|
||||
"private",
|
||||
"cancel",
|
||||
"submit",
|
||||
];
|
||||
|
||||
export enum ETabIndices {
|
||||
ISSUE_FORM = "issue-form",
|
||||
@@ -1,9 +1,15 @@
|
||||
export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"];
|
||||
export const THEMES = [
|
||||
"light",
|
||||
"dark",
|
||||
"light-contrast",
|
||||
"dark-contrast",
|
||||
"custom",
|
||||
];
|
||||
|
||||
export interface I_THEME_OPTION {
|
||||
key: string;
|
||||
value: string;
|
||||
label: string;
|
||||
i18n_label: string;
|
||||
type: string;
|
||||
icon: {
|
||||
border: string;
|
||||
@@ -16,7 +22,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
||||
{
|
||||
key: "system_preference",
|
||||
value: "system",
|
||||
label: "System preference",
|
||||
i18n_label: "System preference",
|
||||
type: "light",
|
||||
icon: {
|
||||
border: "#DEE2E6",
|
||||
@@ -27,7 +33,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
||||
{
|
||||
key: "light",
|
||||
value: "light",
|
||||
label: "Light",
|
||||
i18n_label: "Light",
|
||||
type: "light",
|
||||
icon: {
|
||||
border: "#DEE2E6",
|
||||
@@ -38,7 +44,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
||||
{
|
||||
key: "dark",
|
||||
value: "dark",
|
||||
label: "Dark",
|
||||
i18n_label: "Dark",
|
||||
type: "dark",
|
||||
icon: {
|
||||
border: "#2E3234",
|
||||
@@ -49,7 +55,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
||||
{
|
||||
key: "light_contrast",
|
||||
value: "light-contrast",
|
||||
label: "Light high contrast",
|
||||
i18n_label: "Light high contrast",
|
||||
type: "light",
|
||||
icon: {
|
||||
border: "#000000",
|
||||
@@ -60,7 +66,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
||||
{
|
||||
key: "dark_contrast",
|
||||
value: "dark-contrast",
|
||||
label: "Dark high contrast",
|
||||
i18n_label: "Dark high contrast",
|
||||
type: "dark",
|
||||
icon: {
|
||||
border: "#FFFFFF",
|
||||
@@ -71,7 +77,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
||||
{
|
||||
key: "custom",
|
||||
value: "custom",
|
||||
label: "Custom theme",
|
||||
i18n_label: "Custom theme",
|
||||
type: "light",
|
||||
icon: {
|
||||
border: "#FFC9C9",
|
||||
@@ -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
packages/constants/src/views.ts
Normal file
23
packages/constants/src/views.ts
Normal 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" },
|
||||
];
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -51,6 +51,10 @@ export const ListKeymap = ({ tabIndex }: { tabIndex?: number }) =>
|
||||
} else if (this.editor.commands.liftListItem("taskItem")) {
|
||||
return true;
|
||||
}
|
||||
// if tabIndex is set, we don't want to handle Tab key
|
||||
if (tabIndex !== undefined && tabIndex !== null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
Delete: ({ editor }) => {
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
packages/i18n/src/locales/en/core.json
Normal file
171
packages/i18n/src/locales/en/core.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"sidebar": {
|
||||
"projects": "Projects",
|
||||
"pages": "Pages",
|
||||
"new_work_item": "New work item",
|
||||
"home": "Home",
|
||||
"your_work": "Your work",
|
||||
"inbox": "Inbox",
|
||||
"workspace": "Workspace",
|
||||
"views": "Views",
|
||||
"analytics": "Analytics",
|
||||
"work_items": "Work items",
|
||||
"cycles": "Cycles",
|
||||
"modules": "Modules",
|
||||
"intake": "Intake",
|
||||
"drafts": "Drafts",
|
||||
"favorites": "Favorites",
|
||||
"pro": "Pro",
|
||||
"upgrade": "Upgrade"
|
||||
},
|
||||
|
||||
"auth": {
|
||||
"common": {
|
||||
"email": {
|
||||
"label": "Email",
|
||||
"placeholder": "name@company.com",
|
||||
"errors": {
|
||||
"required": "Email is required",
|
||||
"invalid": "Email is invalid"
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"set_password": "Set a password",
|
||||
"placeholder": "Enter password",
|
||||
"confirm_password": {
|
||||
"label": "Confirm password",
|
||||
"placeholder": "Confirm password"
|
||||
},
|
||||
"current_password": {
|
||||
"label": "Current password"
|
||||
},
|
||||
"new_password": {
|
||||
"label": "New password",
|
||||
"placeholder": "Enter new password"
|
||||
},
|
||||
"change_password": {
|
||||
"label": {
|
||||
"default": "Change password",
|
||||
"submitting": "Changing password"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"match": "Passwords don't match",
|
||||
"empty": "Please enter your password",
|
||||
"length": "Password length should me more than 8 characters",
|
||||
"strength": {
|
||||
"weak": "Password is weak",
|
||||
"strong": "Password is strong"
|
||||
}
|
||||
},
|
||||
"submit": "Set password",
|
||||
"toast": {
|
||||
"change_password": {
|
||||
"success": {
|
||||
"title": "Success!",
|
||||
"message": "Password changed successfully."
|
||||
},
|
||||
"error": {
|
||||
"title": "Error!",
|
||||
"message": "Something went wrong. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"unique_code": {
|
||||
"label": "Unique code",
|
||||
"placeholder": "gets-sets-flys",
|
||||
"paste_code": "Paste the code sent to your email",
|
||||
"requesting_new_code": "Requesting new code",
|
||||
"sending_code": "Sending code"
|
||||
},
|
||||
"already_have_an_account": "Already have an account?",
|
||||
"login": "Log in",
|
||||
"create_account": "Create an account",
|
||||
"new_to_plane": "New to Plane?",
|
||||
"back_to_sign_in": "Back to sign in",
|
||||
"resend_in": "Resend in {seconds} seconds",
|
||||
"sign_in_with_unique_code": "Sign in with unique code",
|
||||
"forgot_password": "Forgot your password?"
|
||||
},
|
||||
"sign_up": {
|
||||
"header": {
|
||||
"label": "Create an account to start managing work with your team.",
|
||||
"step": {
|
||||
"email": {
|
||||
"header": "Sign up",
|
||||
"sub_header": ""
|
||||
},
|
||||
"password": {
|
||||
"header": "Sign up",
|
||||
"sub_header": "Sign up using an email-password combination."
|
||||
},
|
||||
"unique_code": {
|
||||
"header": "Sign up",
|
||||
"sub_header": "Sign up using a unique code sent to the email address above."
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"password": {
|
||||
"strength": "Try setting-up a strong password to proceed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sign_in": {
|
||||
"header": {
|
||||
"label": "Log in to start managing work with your team.",
|
||||
"step": {
|
||||
"email": {
|
||||
"header": "Log in or sign up",
|
||||
"sub_header": ""
|
||||
},
|
||||
"password": {
|
||||
"header": "Log in or sign up",
|
||||
"sub_header": "Use your email-password combination to log in."
|
||||
},
|
||||
"unique_code": {
|
||||
"header": "Log in or sign up",
|
||||
"sub_header": "Log in using a unique code sent to the email address above."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"forgot_password": {
|
||||
"title": "Reset your password",
|
||||
"description": "Enter your user account's verified email address and we will send you a password reset link.",
|
||||
"email_sent": "We sent the reset link to your email address",
|
||||
"send_reset_link": "Send reset link",
|
||||
"errors": {
|
||||
"smtp_not_enabled": "We see that your god hasn't enabled SMTP, we will not be able to send a password reset link"
|
||||
},
|
||||
"toast": {
|
||||
"success": {
|
||||
"title": "Email sent",
|
||||
"message": "Check your inbox for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder."
|
||||
},
|
||||
"error": {
|
||||
"title": "Error!",
|
||||
"message": "Something went wrong. Please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"reset_password": {
|
||||
"title": "Set new password",
|
||||
"description": "Secure your account with a strong password"
|
||||
},
|
||||
"set_password": {
|
||||
"title": "Secure your account",
|
||||
"description": "Setting password helps you login securely"
|
||||
},
|
||||
"sign_out": {
|
||||
"toast": {
|
||||
"error": {
|
||||
"title": "Error!",
|
||||
"message": "Failed to sign out. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user