Compare commits

..
418 changed files with 6660 additions and 13790 deletions
+3
View File
@@ -2,4 +2,7 @@ module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
+2 -1
View File
@@ -35,18 +35,19 @@
"react-dom": "^18.3.1",
"react-hook-form": "7.51.5",
"swr": "^2.2.4",
"tailwindcss": "3.3.2",
"uuid": "^9.0.1",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/node": "18.16.1",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"@types/uuid": "^9.0.8",
"@types/zxcvbn": "^4.4.4",
"tailwind-config-custom": "*",
"typescript": "5.3.3"
}
}
+1 -2
View File
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const sharedConfig = require("@plane/tailwind-config/tailwind.config.js");
const sharedConfig = require("tailwind-config-custom/tailwind.config.js");
module.exports = {
presets: [sharedConfig],
-2
View File
@@ -207,7 +207,6 @@ class IssueSerializer(BaseSerializer):
for assignee_id in assignees
],
batch_size=10,
ignore_conflicts=True,
)
if labels is not None:
@@ -225,7 +224,6 @@ class IssueSerializer(BaseSerializer):
for label_id in labels
],
batch_size=10,
ignore_conflicts=True,
)
# Time updation occues even when other related models are updated
@@ -72,8 +72,6 @@ from .issue import (
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
IssueVersionDetailSerializer,
IssueDescriptionVersionDetailSerializer,
)
from .module import (
+2 -99
View File
@@ -33,8 +33,6 @@ from plane.db.models import (
IssueVote,
IssueRelation,
State,
IssueVersion,
IssueDescriptionVersion,
)
@@ -203,7 +201,6 @@ class IssueCreateSerializer(BaseSerializer):
for user in assignees
],
batch_size=10,
ignore_conflicts=True,
)
if labels is not None:
@@ -221,7 +218,6 @@ class IssueCreateSerializer(BaseSerializer):
for label in labels
],
batch_size=10,
ignore_conflicts=True,
)
# Time updation occues even when other related models are updated
@@ -285,26 +281,10 @@ 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",
"state_id",
"priority",
"assignee_ids",
]
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
read_only_fields = ["workspace", "project"]
@@ -316,26 +296,10 @@ 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",
"state_id",
"priority",
"assignee_ids",
]
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
read_only_fields = ["workspace", "project"]
@@ -703,64 +667,3 @@ class IssueSubscriberSerializer(BaseSerializer):
model = IssueSubscriber
fields = "__all__"
read_only_fields = ["workspace", "project", "issue"]
class IssueVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueVersion
fields = [
"id",
"workspace",
"project",
"issue",
"parent",
"state",
"estimate_point",
"name",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
"is_draft",
"external_source",
"external_id",
"type",
"cycle",
"modules",
"meta",
"name",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]
class IssueDescriptionVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueDescriptionVersion
fields = [
"id",
"workspace",
"project",
"issue",
"description_binary",
"description_html",
"description_stripped",
"description_json",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]
-22
View File
@@ -24,8 +24,6 @@ from plane.app.views import (
IssueDetailEndpoint,
IssueAttachmentV2Endpoint,
IssueBulkUpdateDateEndpoint,
IssueVersionEndpoint,
IssueDescriptionVersionEndpoint,
)
urlpatterns = [
@@ -258,24 +256,4 @@ urlpatterns = [
IssueBulkUpdateDateEndpoint.as_view(),
name="project-issue-dates",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
]
-2
View File
@@ -141,8 +141,6 @@ from .issue.sub_issue import SubIssuesEndpoint
from .issue.subscriber import IssueSubscriberViewSet
from .issue.version import IssueVersionEndpoint, IssueDescriptionVersionEndpoint
from .module.base import (
ModuleViewSet,
ModuleLinkViewSet,
-14
View File
@@ -132,18 +132,6 @@ class CycleViewSet(BaseViewSet):
),
)
)
.annotate(
pending_issues=Count(
"issue_cycle__issue__id",
distinct=True,
filter=Q(
issue_cycle__issue__state__group__in=["backlog", "unstarted", "started"],
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(
status=Case(
When(
@@ -226,7 +214,6 @@ class CycleViewSet(BaseViewSet):
"is_favorite",
"total_issues",
"completed_issues",
"pending_issues",
"assignee_ids",
"status",
"version",
@@ -258,7 +245,6 @@ class CycleViewSet(BaseViewSet):
# meta fields
"is_favorite",
"total_issues",
"pending_issues",
"completed_issues",
"assignee_ids",
"status",
+3 -9
View File
@@ -53,10 +53,10 @@ from .. import BaseAPIView
def dashboard_overview_stats(self, request, slug):
assigned_issues = (
Issue.issue_objects.filter(
(Q(assignees__in=[request.user]) & Q(issue_assignee__deleted_at__isnull=True)),
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
workspace__slug=slug,
assignees__in=[request.user],
)
.filter(
Q(
@@ -133,13 +133,10 @@ def dashboard_overview_stats(self, request, slug):
completed_issues_count = (
Issue.issue_objects.filter(
(
Q(assignees__in=[request.user])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
assignees__in=[request.user],
state__group="completed",
)
.filter(
@@ -179,13 +176,10 @@ def dashboard_assigned_issues(self, request, slug):
# get all the assigned issues
assigned_issues = (
Issue.issue_objects.filter(
(
Q(assignees__in=[request.user])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
assignees__in=[request.user],
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
-118
View File
@@ -1,118 +0,0 @@
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.db.models import IssueVersion, IssueDescriptionVersion
from ..base import BaseAPIView
from plane.app.serializers import (
IssueVersionDetailSerializer,
IssueDescriptionVersionDetailSerializer,
)
from plane.app.permissions import allow_permission, ROLE
from plane.utils.global_paginator import paginate
from plane.utils.timezone_converter import user_timezone_converter
class IssueVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
paginated_data, datetime_fields, timezone
)
return paginated_data
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
issue_version = IssueVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
serializer = IssueVersionDetailSerializer(issue_version)
return Response(serializer.data, status=status.HTTP_200_OK)
cursor = request.GET.get("cursor", None)
required_fields = [
"id",
"workspace",
"project",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
issue_versions_queryset = IssueVersion.objects.filter(
workspace__slug=slug, project_id=project_id, issue_id=issue_id
)
paginated_data = paginate(
base_queryset=issue_versions_queryset,
queryset=issue_versions_queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
),
)
return Response(paginated_data, status=status.HTTP_200_OK)
class IssueDescriptionVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
paginated_data, datetime_fields, timezone
)
return paginated_data
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
issue_description_version = IssueDescriptionVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
serializer = IssueDescriptionVersionDetailSerializer(
issue_description_version
)
return Response(serializer.data, status=status.HTTP_200_OK)
cursor = request.GET.get("cursor", None)
required_fields = [
"id",
"workspace",
"project",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
workspace__slug=slug, project_id=project_id, issue_id=issue_id
)
paginated_data = paginate(
base_queryset=issue_description_versions_queryset,
queryset=issue_description_versions_queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
),
)
return Response(paginated_data, status=status.HTTP_200_OK)
@@ -39,9 +39,9 @@ class WorkspaceStickyViewSet(BaseViewSet):
)
def list(self, request, slug):
query = request.query_params.get("query", False)
stickies = self.get_queryset().order_by("-sort_order")
stickies = self.get_queryset()
if query:
stickies = stickies.filter(description_stripped__icontains=query)
stickies = stickies.filter(name__icontains=query)
return self.paginate(
request=request,
@@ -49,7 +49,7 @@ class WorkspaceStickyViewSet(BaseViewSet):
on_results=lambda stickies: StickySerializer(stickies, many=True).data,
default_per_page=20,
)
@allow_permission(allowed_roles=[], creator=True, model=Sticky, level="WORKSPACE")
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
+5 -20
View File
@@ -375,11 +375,8 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
state_distribution = (
Issue.issue_objects.filter(
(
Q(assignees__in=[user_id])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
@@ -394,11 +391,8 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
priority_distribution = (
Issue.issue_objects.filter(
(
Q(assignees__in=[user_id])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
@@ -432,11 +426,8 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
assigned_issues_count = (
Issue.issue_objects.filter(
(
Q(assignees__in=[user_id])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
@@ -447,11 +438,8 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
pending_issues_count = (
Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
(
Q(assignees__in=[user_id])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
@@ -461,11 +449,8 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
completed_issues_count = (
Issue.issue_objects.filter(
(
Q(assignees__in=[user_id])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
assignees__in=[user_id],
state__group="completed",
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
-9
View File
@@ -5,9 +5,6 @@ from django.db import models
# Module imports
from .base import BaseModel
# Third party imports
from plane.utils.html_processor import strip_tags
class Sticky(BaseModel):
name = models.TextField(null=True, blank=True)
@@ -36,12 +33,6 @@ class Sticky(BaseModel):
ordering = ("-created_at",)
def save(self, *args, **kwargs):
# Strip the html tags using html parser
self.description_stripped = (
None
if (self.description_html == "" or self.description_html is None)
else strip_tags(self.description_html)
)
if self._state.adding:
# Get the maximum sequence value from the database
last_id = Sticky.objects.filter(workspace=self.workspace).aggregate(
+9 -9
View File
@@ -290,12 +290,11 @@ class InstanceAdminSignInEndpoint(View):
# Fetch the user
user = User.objects.filter(email=email).first()
# Error out if the user is not present
if not user:
# is_active
if not user.is_active:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DOES_NOT_EXIST"],
error_message="ADMIN_USER_DOES_NOT_EXIST",
payload={"email": email},
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DEACTIVATED"],
error_message="ADMIN_USER_DEACTIVATED",
)
url = urljoin(
base_host(request=request, is_admin=True),
@@ -303,11 +302,12 @@ class InstanceAdminSignInEndpoint(View):
)
return HttpResponseRedirect(url)
# is_active
if not user.is_active:
# Error out if the user is not present
if not user:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DEACTIVATED"],
error_message="ADMIN_USER_DEACTIVATED",
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DOES_NOT_EXIST"],
error_message="ADMIN_USER_DOES_NOT_EXIST",
payload={"email": email},
)
url = urljoin(
base_host(request=request, is_admin=True),
+1 -1
View File
@@ -1,7 +1,7 @@
# base requirements
# django
Django==4.2.18
Django==4.2.17
# rest framework
djangorestframework==3.15.2
# postgres
+6 -6
View File
@@ -16,10 +16,10 @@
"author": "",
"license": "ISC",
"dependencies": {
"@hocuspocus/extension-database": "^2.15.0",
"@hocuspocus/extension-logger": "^2.15.0",
"@hocuspocus/extension-redis": "^2.15.0",
"@hocuspocus/server": "^2.15.0",
"@hocuspocus/extension-database": "^2.11.3",
"@hocuspocus/extension-logger": "^2.11.3",
"@hocuspocus/extension-redis": "^2.13.5",
"@hocuspocus/server": "^2.11.3",
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/types": "*",
@@ -40,9 +40,9 @@
"pino-http": "^10.3.0",
"pino-pretty": "^11.2.2",
"uuid": "^10.0.0",
"y-prosemirror": "^1.2.15",
"y-prosemirror": "^1.2.9",
"y-protocols": "^1.0.6",
"yjs": "^13.6.20"
"yjs": "^13.6.14"
},
"devDependencies": {
"@babel/cli": "^7.25.6",
-53
View File
@@ -2,56 +2,3 @@ export enum E_SORT_ORDER {
ASC = "asc",
DESC = "desc",
}
export const DATE_AFTER_FILTER_OPTIONS = [
{
name: "1 week from now",
value: "1_weeks;after;fromnow",
},
{
name: "2 weeks from now",
value: "2_weeks;after;fromnow",
},
{
name: "1 month from now",
value: "1_months;after;fromnow",
},
{
name: "2 months from now",
value: "2_months;after;fromnow",
},
];
export const DATE_BEFORE_FILTER_OPTIONS = [
{
name: "1 week ago",
value: "1_weeks;before;fromnow",
},
{
name: "2 weeks ago",
value: "2_weeks;before;fromnow",
},
{
name: "1 month ago",
i18n_name: "date_filters.1_month_ago",
value: "1_months;before;fromnow",
},
];
export const PROJECT_CREATED_AT_FILTER_OPTIONS = [
{
name: "Today",
value: "today;custom;custom",
},
{
name: "Yesterday",
value: "yesterday;custom;custom",
},
{
name: "Last 7 days",
value: "last_7_days;custom;custom",
},
{
name: "Last 30 days",
value: "last_30_days;custom;custom",
},
];
-91
View File
@@ -1,91 +0,0 @@
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",
},
];
-7
View File
@@ -11,12 +11,5 @@ export * from "./issue";
export * from "./metadata";
export * from "./state";
export * from "./swr";
export * from "./tab-indices";
export * from "./user";
export * from "./workspace";
export * from "./stickies";
export * from "./project";
export * from "./views";
export * from "./inbox";
export * from "./profile";
export * from "./workspace-drafts";
+185
View File
@@ -0,0 +1,185 @@
import { List, Kanban } from "lucide-react";
export const ALL_ISSUES = "All Issues";
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export type TIssueFilterKeys = "priority" | "state" | "labels";
export type TIssueLayout =
| "list"
| "kanban"
| "calendar"
| "spreadsheet"
| "gantt";
export type TIssueFilterPriorityObject = {
key: TIssuePriorities;
title: string;
className: string;
icon: string;
};
export enum EIssueGroupByToServerOptions {
"state" = "state_id",
"priority" = "priority",
"labels" = "labels__id",
"state_detail.group" = "state__group",
"assignees" = "assignees__id",
"cycle" = "cycle_id",
"module" = "issue_module__module_id",
"target_date" = "target_date",
"project" = "project_id",
"created_by" = "created_by",
"team_project" = "project_id",
}
export enum EIssueGroupBYServerToProperty {
"state_id" = "state_id",
"priority" = "priority",
"labels__id" = "label_ids",
"state__group" = "state__group",
"assignees__id" = "assignee_ids",
"cycle_id" = "cycle_id",
"issue_module__module_id" = "module_ids",
"target_date" = "target_date",
"project_id" = "project_id",
"created_by" = "created_by",
}
export enum EServerGroupByToFilterOptions {
"state_id" = "state",
"priority" = "priority",
"labels__id" = "labels",
"state__group" = "state_group",
"assignees__id" = "assignees",
"cycle_id" = "cycle",
"issue_module__module_id" = "module",
"target_date" = "target_date",
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
export enum EIssueLayoutTypes {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
GANTT = "gantt_chart",
SPREADSHEET = "spreadsheet",
}
export enum EIssuesStoreType {
GLOBAL = "GLOBAL",
PROFILE = "PROFILE",
TEAM = "TEAM",
PROJECT = "PROJECT",
CYCLE = "CYCLE",
MODULE = "MODULE",
TEAM_VIEW = "TEAM_VIEW",
PROJECT_VIEW = "PROJECT_VIEW",
ARCHIVED = "ARCHIVED",
DRAFT = "DRAFT",
DEFAULT = "DEFAULT",
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
EPIC = "EPIC",
}
export enum EIssueFilterType {
FILTERS = "filters",
DISPLAY_FILTERS = "display_filters",
DISPLAY_PROPERTIES = "display_properties",
KANBAN_FILTERS = "kanban_filters",
}
export enum EIssueCommentAccessSpecifier {
EXTERNAL = "EXTERNAL",
INTERNAL = "INTERNAL",
}
export enum EIssueListRow {
HEADER = "HEADER",
ISSUE = "ISSUE",
NO_ISSUES = "NO_ISSUES",
QUICK_ADD = "QUICK_ADD",
}
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
} = {
list: {
filters: ["priority", "state", "labels"],
},
kanban: {
filters: ["priority", "state", "labels"],
},
calendar: {
filters: ["priority", "state", "labels"],
},
spreadsheet: {
filters: ["priority", "state", "labels"],
},
gantt: {
filters: ["priority", "state", "labels"],
},
};
export const ISSUE_PRIORITIES: {
key: TIssuePriorities;
title: string;
}[] = [
{ key: "urgent", title: "Urgent" },
{ key: "high", title: "High" },
{ key: "medium", title: "Medium" },
{ key: "low", title: "Low" },
{ key: "none", title: "None" },
];
export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
{
key: "urgent",
title: "Urgent",
className: "bg-red-500 border-red-500 text-white",
icon: "error",
},
{
key: "high",
title: "High",
className: "text-orange-500 border-custom-border-300",
icon: "signal_cellular_alt",
},
{
key: "medium",
title: "Medium",
className: "text-yellow-500 border-custom-border-300",
icon: "signal_cellular_alt_2_bar",
},
{
key: "low",
title: "Low",
className: "text-green-500 border-custom-border-300",
icon: "signal_cellular_alt_1_bar",
},
{
key: "none",
title: "None",
className: "text-gray-500 border-custom-border-300",
icon: "block",
},
];
export const SITES_ISSUE_LAYOUTS: {
key: TIssueLayout;
title: string;
icon: any;
}[] = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
// { key: "calendar", title: "Calendar", icon: Calendar },
// { key: "spreadsheet", title: "Spreadsheet", icon: Sheet },
// { key: "gantt", title: "Gantt chart", icon: GanttChartSquare },
];
-217
View File
@@ -1,217 +0,0 @@
import {
TIssueGroupByOptions,
TIssueOrderByOptions,
IIssueDisplayProperties,
} from "@plane/types";
export const ALL_ISSUES = "All Issues";
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export type TIssueFilterPriorityObject = {
key: TIssuePriorities;
titleTranslationKey: string;
className: string;
icon: string;
};
export enum EIssueGroupByToServerOptions {
"state" = "state_id",
"priority" = "priority",
"labels" = "labels__id",
"state_detail.group" = "state__group",
"assignees" = "assignees__id",
"cycle" = "cycle_id",
"module" = "issue_module__module_id",
"target_date" = "target_date",
"project" = "project_id",
"created_by" = "created_by",
"team_project" = "project_id",
}
export enum EIssueGroupBYServerToProperty {
"state_id" = "state_id",
"priority" = "priority",
"labels__id" = "label_ids",
"state__group" = "state__group",
"assignees__id" = "assignee_ids",
"cycle_id" = "cycle_id",
"issue_module__module_id" = "module_ids",
"target_date" = "target_date",
"project_id" = "project_id",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
export enum EIssuesStoreType {
GLOBAL = "GLOBAL",
PROFILE = "PROFILE",
TEAM = "TEAM",
PROJECT = "PROJECT",
CYCLE = "CYCLE",
MODULE = "MODULE",
TEAM_VIEW = "TEAM_VIEW",
PROJECT_VIEW = "PROJECT_VIEW",
ARCHIVED = "ARCHIVED",
DRAFT = "DRAFT",
DEFAULT = "DEFAULT",
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
EPIC = "EPIC",
}
export enum EIssueCommentAccessSpecifier {
EXTERNAL = "EXTERNAL",
INTERNAL = "INTERNAL",
}
export enum EIssueListRow {
HEADER = "HEADER",
ISSUE = "ISSUE",
NO_ISSUES = "NO_ISSUES",
QUICK_ADD = "QUICK_ADD",
}
export const ISSUE_PRIORITIES: {
key: TIssuePriorities;
title: string;
}[] = [
{
key: "urgent",
title: "Urgent",
},
{
key: "high",
title: "High",
},
{
key: "medium",
title: "Medium",
},
{
key: "low",
title: "Low",
},
{
key: "none",
title: "None",
},
];
export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
"state",
"priority",
"assignees",
"labels",
"module",
"cycle",
];
export type TCreateModalStoreTypes =
| EIssuesStoreType.TEAM
| EIssuesStoreType.PROJECT
| EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.PROFILE
| EIssuesStoreType.CYCLE
| EIssuesStoreType.MODULE
| EIssuesStoreType.EPIC;
export const ISSUE_GROUP_BY_OPTIONS: {
key: TIssueGroupByOptions;
titleTranslationKey: string;
}[] = [
{ key: "state", titleTranslationKey: "common.states" },
{ key: "state_detail.group", titleTranslationKey: "common.state_groups" },
{ key: "priority", titleTranslationKey: "common.priority" },
{ key: "team_project", titleTranslationKey: "common.team_project" }, // required this on team issues
{ key: "project", titleTranslationKey: "common.project" }, // required this on my issues
{ key: "cycle", titleTranslationKey: "common.cycle" }, // required this on my issues
{ key: "module", titleTranslationKey: "common.module" }, // required this on my issues
{ key: "labels", titleTranslationKey: "common.labels" },
{ key: "assignees", titleTranslationKey: "common.assignees" },
{ key: "created_by", titleTranslationKey: "common.created_by" },
{ key: null, titleTranslationKey: "common.none" },
];
export const ISSUE_ORDER_BY_OPTIONS: {
key: TIssueOrderByOptions;
titleTranslationKey: string;
}[] = [
{ key: "sort_order", titleTranslationKey: "common.order_by.manual" },
{ key: "-created_at", titleTranslationKey: "common.order_by.last_created" },
{ key: "-updated_at", titleTranslationKey: "common.order_by.last_updated" },
{ key: "start_date", titleTranslationKey: "common.order_by.start_date" },
{ key: "target_date", titleTranslationKey: "common.order_by.due_date" },
{ key: "-priority", titleTranslationKey: "common.priority" },
];
export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] =
[
"assignee",
"start_date",
"due_date",
"labels",
"key",
"priority",
"state",
"sub_issue_count",
"link",
"attachment_count",
"estimate",
"created_on",
"updated_on",
"modules",
"cycle",
"issue_type",
];
export const ISSUE_DISPLAY_PROPERTIES: {
key: keyof IIssueDisplayProperties;
titleTranslationKey: string;
}[] = [
{
key: "key",
titleTranslationKey: "issue.display.properties.id",
},
{
key: "issue_type",
titleTranslationKey: "issue.display.properties.issue_type",
},
{
key: "assignee",
titleTranslationKey: "common.assignee",
},
{
key: "start_date",
titleTranslationKey: "common.order_by.start_date",
},
{
key: "due_date",
titleTranslationKey: "common.order_by.due_date",
},
{ key: "labels", titleTranslationKey: "common.labels" },
{
key: "priority",
titleTranslationKey: "common.priority",
},
{ key: "state", titleTranslationKey: "common.state" },
{
key: "sub_issue_count",
titleTranslationKey: "issue.display.properties.sub_issue_count",
},
{
key: "attachment_count",
titleTranslationKey: "issue.display.properties.attachment_count",
},
{ key: "link", titleTranslationKey: "common.link" },
{
key: "estimate",
titleTranslationKey: "common.estimate",
},
{ key: "modules", titleTranslationKey: "common.module" },
{ key: "cycle", titleTranslationKey: "common.cycle" },
];
-530
View File
@@ -1,530 +0,0 @@
import {
ILayoutDisplayFiltersOptions,
TIssueActivityComment,
} from "@plane/types";
import {
TIssueFilterPriorityObject,
ISSUE_DISPLAY_PROPERTIES_KEYS,
EIssuesStoreType,
} from "./common";
import { TIssueLayout } from "./layout";
export type TIssueFilterKeys = "priority" | "state" | "labels";
export enum EServerGroupByToFilterOptions {
"state_id" = "state",
"priority" = "priority",
"labels__id" = "labels",
"state__group" = "state_group",
"assignees__id" = "assignees",
"cycle_id" = "cycle",
"issue_module__module_id" = "module",
"target_date" = "target_date",
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueFilterType {
FILTERS = "filters",
DISPLAY_FILTERS = "display_filters",
DISPLAY_PROPERTIES = "display_properties",
KANBAN_FILTERS = "kanban_filters",
}
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
} = {
list: {
filters: ["priority", "state", "labels"],
},
kanban: {
filters: ["priority", "state", "labels"],
},
calendar: {
filters: ["priority", "state", "labels"],
},
spreadsheet: {
filters: ["priority", "state", "labels"],
},
gantt: {
filters: ["priority", "state", "labels"],
},
};
export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
{
key: "urgent",
titleTranslationKey: "issue.priority.urgent",
className: "bg-red-500 border-red-500 text-white",
icon: "error",
},
{
key: "high",
titleTranslationKey: "issue.priority.high",
className: "text-orange-500 border-custom-border-300",
icon: "signal_cellular_alt",
},
{
key: "medium",
titleTranslationKey: "issue.priority.medium",
className: "text-yellow-500 border-custom-border-300",
icon: "signal_cellular_alt_2_bar",
},
{
key: "low",
titleTranslationKey: "issue.priority.low",
className: "text-green-500 border-custom-border-300",
icon: "signal_cellular_alt_1_bar",
},
{
key: "none",
titleTranslationKey: "common.none",
className: "text-gray-500 border-custom-border-300",
icon: "block",
},
];
export type TFiltersByLayout = {
[layoutType: string]: ILayoutDisplayFiltersOptions;
};
export type TIssueFiltersToDisplayByPageType = {
[pageType: string]: TFiltersByLayout;
};
export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
profile_issues: {
list: {
filters: [
"priority",
"state_group",
"labels",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels", null],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {
filters: [
"priority",
"state_group",
"labels",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels"],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
archived_issues: {
list: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"cycle",
"module",
"state_detail.group",
"priority",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
draft_issues: {
list: {
filters: [
"priority",
"state_group",
"cycle",
"module",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state_detail.group",
"cycle",
"module",
"priority",
"project",
"labels",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
kanban: {
filters: [
"priority",
"state_group",
"cycle",
"module",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state_detail.group",
"cycle",
"module",
"priority",
"project",
"labels",
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
my_issues: {
spreadsheet: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
list: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: false,
values: [],
},
},
},
issues: {
list: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
],
sub_group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
"target_date",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
calendar: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"issue_type",
],
display_properties: ["key", "issue_type"],
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
spreadsheet: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
gantt_chart: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ["key", "issue_type"],
display_filters: {
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
},
};
export const ISSUE_STORE_TO_FILTERS_MAP: Partial<
Record<EIssuesStoreType, TFiltersByLayout>
> = {
[EIssuesStoreType.PROJECT]: ISSUE_DISPLAY_FILTERS_BY_PAGE.issues,
};
export enum EActivityFilterType {
ACTIVITY = "ACTIVITY",
COMMENT = "COMMENT",
}
export type TActivityFilters = EActivityFilterType;
export const ACTIVITY_FILTER_TYPE_OPTIONS: Record<
TActivityFilters,
{ labelTranslationKey: string }
> = {
[EActivityFilterType.ACTIVITY]: {
labelTranslationKey: "common.updates",
},
[EActivityFilterType.COMMENT]: {
labelTranslationKey: "common.comments",
},
};
export type TActivityFilterOption = {
key: TActivityFilters;
labelTranslationKey: string;
isSelected: boolean;
onClick: () => void;
};
export const defaultActivityFilters: TActivityFilters[] = [
EActivityFilterType.ACTIVITY,
EActivityFilterType.COMMENT,
];
export const filterActivityOnSelectedFilters = (
activity: TIssueActivityComment[],
filters: TActivityFilters[]
): TIssueActivityComment[] =>
activity.filter((activity) =>
filters.includes(activity.activity_type as TActivityFilters)
);
export const ENABLE_ISSUE_DEPENDENCIES = false;
-3
View File
@@ -1,3 +0,0 @@
export * from "./common";
export * from "./filter";
export * from "./layout";
-76
View File
@@ -1,76 +0,0 @@
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);
-1
View File
@@ -1 +0,0 @@
export const STICKIES_PER_PAGE = 30;
-23
View File
@@ -1,23 +0,0 @@
export enum EViewAccess {
PRIVATE,
PUBLIC,
}
export const VIEW_ACCESS_SPECIFIERS: {
key: EViewAccess;
i18n_label: string;
}[] = [
{ key: EViewAccess.PUBLIC, i18n_label: "project_view.access.public" },
{ key: EViewAccess.PRIVATE, i18n_label: "project_view.access.private" },
];
export const VIEW_SORTING_KEY_OPTIONS = [
{ key: "name", i18n_label: "project_view.sort_by.name" },
{ key: "created_at", i18n_label: "project_view.sort_by.created_at" },
{ key: "updated_at", i18n_label: "project_view.sort_by.updated_at" },
];
export const VIEW_SORT_BY_OPTIONS = [
{ key: "asc", i18n_label: "common.order_by.asc" },
{ key: "desc", i18n_label: "common.order_by.desc" },
];
-182
View File
@@ -1,6 +1,3 @@
import { TStaticViewTypes } from "@plane/types";
import { EUserWorkspaceRoles } from "./user";
export const ORGANIZATION_SIZE = [
"Just myself", // TODO: translate
"2-10",
@@ -77,182 +74,3 @@ 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.webhooks.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",
},
];
+9 -7
View File
@@ -12,12 +12,14 @@
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
"import": "./dist/index.mjs",
"module": "./dist/index.mjs"
},
"./lib": {
"require": "./dist/lib.js",
"types": "./dist/lib.d.mts",
"import": "./dist/lib.mjs"
"import": "./dist/lib.mjs",
"module": "./dist/lib.mjs"
}
},
"scripts": {
@@ -34,7 +36,7 @@
},
"dependencies": {
"@floating-ui/react": "^0.26.4",
"@hocuspocus/provider": "^2.15.0",
"@hocuspocus/provider": "^2.13.5",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
@@ -65,21 +67,21 @@
"prosemirror-codemark": "^0.4.2",
"prosemirror-utils": "^1.2.2",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.10",
"tiptap-markdown": "^0.8.9",
"uuid": "^10.0.0",
"y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.15",
"y-prosemirror": "^1.2.5",
"y-protocols": "^1.0.6",
"yjs": "^13.6.20"
"yjs": "^13.6.15"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/node": "18.15.3",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"postcss": "^8.4.38",
"tailwind-config-custom": "*",
"tsup": "^7.2.0",
"typescript": "5.3.3"
},
@@ -1,6 +1,5 @@
import { HocuspocusProvider } from "@hocuspocus/provider";
import { Extensions } from "@tiptap/core";
import { AnyExtension } from "@tiptap/core";
import { SlashCommands } from "@/extensions";
// plane editor types
import { TIssueEmbedConfig } from "@/plane-editor/types";
@@ -14,24 +13,15 @@ type Props = {
userDetails: TUserDetails;
};
type ExtensionConfig = {
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
getExtension: (props: Props) => AnyExtension;
};
const extensionRegistry: ExtensionConfig[] = [
{
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
getExtension: () => SlashCommands({}),
},
];
export const DocumentEditorAdditionalExtensions = (_props: Props) => {
const { disabledExtensions = [] } = _props;
const { disabledExtensions } = _props;
const extensions: Extensions = disabledExtensions?.includes("slash-commands")
? []
: [
SlashCommands({
disabledExtensions,
}),
];
const documentExtensions = extensionRegistry
.filter((config) => config.isEnabled(disabledExtensions))
.map((config) => config.getExtension(_props));
return documentExtensions;
return extensions;
};
@@ -4,7 +4,7 @@ import { TSlashCommandAdditionalOption } from "@/extensions";
import { TExtensions } from "@/types";
type Props = {
disabledExtensions?: TExtensions[];
disabledExtensions: TExtensions[];
};
export const coreEditorAdditionalSlashCommandOptions = (props: Props): TSlashCommandAdditionalOption[] => {
@@ -202,7 +202,8 @@ export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({
key: "image",
name: "Image",
isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"),
command: () => insertImage({ editor, event: "insert", pos: editor.state.selection.from }),
command: ({ savedSelection }) =>
insertImage({ editor, event: "insert", pos: savedSelection?.from ?? editor.state.selection.from }),
icon: ImageIcon,
});
@@ -39,11 +39,11 @@ import {
setText,
} from "@/helpers/editor-commands";
// types
import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/types";
import { CommandProps, ISlashCommandItem, TExtensions, TSlashCommandSectionKeys } from "@/types";
// plane editor extensions
import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
// local types
import { TExtensionProps } from "./root";
import { TSlashCommandAdditionalOption } from "./root";
export type TSlashCommandSection = {
key: TSlashCommandSectionKeys;
@@ -51,8 +51,13 @@ export type TSlashCommandSection = {
items: ISlashCommandItem[];
};
type TArgs = {
additionalOptions?: TSlashCommandAdditionalOption[];
disabledExtensions: TExtensions[];
};
export const getSlashCommandFilteredSections =
(args: TExtensionProps) =>
(args: TArgs) =>
({ query }: { query: string }): TSlashCommandSection[] => {
const { additionalOptions, disabledExtensions } = args;
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
@@ -103,9 +103,9 @@ const renderItems = () => {
};
};
export type TExtensionProps = {
type TExtensionProps = {
additionalOptions?: TSlashCommandAdditionalOption[];
disabledExtensions?: TExtensions[];
disabledExtensions: TExtensions[];
};
export const SlashCommands = (props: TExtensionProps) =>
@@ -1,21 +1,27 @@
import { MutableRefObject } from "react";
import { Selection } from "@tiptap/pm/state";
import { Editor } from "@tiptap/react";
export const insertContentAtSavedSelection = (editor: Editor, content: string) => {
if (!editor || editor.isDestroyed) {
export const insertContentAtSavedSelection = (
editorRef: MutableRefObject<Editor | null>,
content: string,
savedSelection: Selection
) => {
if (!editorRef.current || editorRef.current.isDestroyed) {
console.error("Editor reference is not available or has been destroyed.");
return;
}
if (!editor.state.selection) {
if (!savedSelection) {
console.error("Saved selection is invalid.");
return;
}
const docSize = editor.state.doc.content.size;
const safePosition = Math.max(0, Math.min(editor.state.selection.anchor, docSize));
const docSize = editorRef.current.state.doc.content.size;
const safePosition = Math.max(0, Math.min(savedSelection.anchor, docSize));
try {
editor.chain().focus().insertContentAt(safePosition, content).run();
editorRef.current.chain().focus().insertContentAt(safePosition, content).run();
} catch (error) {
console.error("An error occurred while inserting content at saved selection:", error);
}
+76 -49
View File
@@ -1,11 +1,12 @@
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import { DOMSerializer } from "@tiptap/pm/model";
import { Selection } from "@tiptap/pm/state";
import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react";
import { useImperativeHandle, MutableRefObject, useEffect } from "react";
import { useEditor as useTiptapEditor, Editor, Extensions } from "@tiptap/react";
import * as Y from "yjs";
// components
import { getEditorMenuItems } from "@/components/menus";
import { EditorMenuItem, getEditorMenuItems } from "@/components/menus";
// extensions
import { CoreEditorExtensions } from "@/extensions";
// helpers
@@ -70,12 +71,14 @@ export const useEditor = (props: CustomEditorProps) => {
provider,
autofocus = false,
} = props;
// states
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
// refs
const editorRef: MutableRefObject<Editor | null> = useRef(null);
const savedSelectionRef = useRef(savedSelection);
const editor = useTiptapEditor(
{
editable,
immediatelyRender: false,
shouldRerenderOnTransaction: false,
autofocus,
editorProps: {
...CoreEditorProps({
@@ -97,7 +100,8 @@ export const useEditor = (props: CustomEditorProps) => {
],
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
onCreate: () => handleEditorReady?.(true),
onTransaction: () => {
onTransaction: ({ editor }) => {
setSavedSelection(editor.state.selection);
onTransaction?.();
},
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
@@ -106,17 +110,23 @@ export const useEditor = (props: CustomEditorProps) => {
[editable]
);
// Update the ref whenever savedSelection changes
useEffect(() => {
savedSelectionRef.current = savedSelection;
}, [savedSelection]);
// Effect for syncing SWR data
useEffect(() => {
// value is null when intentionally passed where syncing is not yet
// supported and value is undefined when the data from swr is not populated
if (value == null) return;
if (value === null || value === undefined) return;
if (editor && !editor.isDestroyed && !editor.storage.imageComponent.uploadInProgress) {
try {
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
if (editor.state.selection) {
const currentSavedSelection = savedSelectionRef.current;
if (currentSavedSelection) {
const docLength = editor.state.doc.content.size;
const relativePosition = Math.min(editor.state.selection.from, docLength - 1);
const relativePosition = Math.min(currentSavedSelection.from, docLength - 1);
editor.commands.setTextSelection(relativePosition);
}
} catch (error) {
@@ -128,40 +138,46 @@ export const useEditor = (props: CustomEditorProps) => {
useImperativeHandle(
forwardedRef,
() => ({
blur: () => editor.commands.blur(),
blur: () => editorRef.current?.commands.blur(),
scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) {
const resolvedPos = pos ?? editor.state.selection.from;
if (!editor || !resolvedPos) return;
scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior);
const resolvedPos = pos ?? savedSelection?.from;
if (!editorRef.current || !resolvedPos) return;
scrollToNodeViaDOMCoordinates(editorRef.current, resolvedPos, behavior);
},
getCurrentCursorPosition: () => editor.state.selection.from,
getCurrentCursorPosition: () => savedSelection?.from,
clearEditor: (emitUpdate = false) => {
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
},
setEditorValue: (content: string) => {
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
},
setEditorValueAtCursorPosition: (content: string) => {
if (editor.state.selection) {
insertContentAtSavedSelection(editor, content);
if (savedSelection) {
insertContentAtSavedSelection(editorRef, content, savedSelection);
}
},
executeMenuItemCommand: (props) => {
const { itemKey } = props;
const editorItems = getEditorMenuItems(editor);
const editorItems = getEditorMenuItems(editorRef.current);
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
const item = getEditorMenuItem(itemKey);
if (item) {
item.command(props);
if (item.key === "image") {
(item as EditorMenuItem<"image">).command({
savedSelection: savedSelectionRef.current,
});
} else {
item.command(props);
}
} else {
console.warn(`No command found for item: ${itemKey}`);
}
},
isMenuItemActive: (props) => {
const { itemKey } = props;
const editorItems = getEditorMenuItems(editor);
const editorItems = getEditorMenuItems(editorRef.current);
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
const item = getEditorMenuItem(itemKey);
@@ -171,20 +187,20 @@ export const useEditor = (props: CustomEditorProps) => {
},
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
// Subscribe to update event emitted from headers extension
editor?.on("update", () => {
callback(editor?.storage.headingList.headings);
editorRef.current?.on("update", () => {
callback(editorRef.current?.storage.headingList.headings);
});
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editor?.off("update");
editorRef.current?.off("update");
};
},
getHeadings: () => editor?.storage.headingList.headings,
getHeadings: () => editorRef?.current?.storage.headingList.headings,
onStateChange: (callback: () => void) => {
// Subscribe to editor state changes
editor?.on("transaction", () => {
editorRef.current?.on("transaction", () => {
callback();
});
@@ -192,17 +208,17 @@ export const useEditor = (props: CustomEditorProps) => {
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editor?.off("transaction");
editorRef.current?.off("transaction");
};
},
getMarkDown: (): string => {
const markdownOutput = editor?.storage.markdown.getMarkdown();
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
return markdownOutput;
},
getDocument: () => {
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
const documentHTML = editor?.getHTML() ?? "<p></p>";
const documentJSON = editor.getJSON() ?? null;
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
const documentJSON = editorRef.current?.getJSON() ?? null;
return {
binary: documentBinary,
@@ -211,19 +227,19 @@ export const useEditor = (props: CustomEditorProps) => {
};
},
scrollSummary: (marking: IMarking): void => {
if (!editor) return;
scrollSummary(editor, marking);
if (!editorRef.current) return;
scrollSummary(editorRef.current, marking);
},
isEditorReadyToDiscard: () => editor?.storage.imageComponent.uploadInProgress === false,
isEditorReadyToDiscard: () => editorRef.current?.storage.imageComponent.uploadInProgress === false,
setFocusAtPosition: (position: number) => {
if (!editor || editor.isDestroyed) {
if (!editorRef.current || editorRef.current.isDestroyed) {
console.error("Editor reference is not available or has been destroyed.");
return;
}
try {
const docSize = editor.state.doc.content.size;
const docSize = editorRef.current.state.doc.content.size;
const safePosition = Math.max(0, Math.min(position, docSize));
editor
editorRef.current
.chain()
.insertContentAt(safePosition, [{ type: "paragraph" }])
.focus()
@@ -233,17 +249,17 @@ export const useEditor = (props: CustomEditorProps) => {
}
},
getSelectedText: () => {
if (!editor) return null;
if (!editorRef.current) return null;
const { state } = editor;
const { state } = editorRef.current;
const { from, to, empty } = state.selection;
if (empty) return null;
const nodesArray: string[] = [];
state.doc.nodesBetween(from, to, (node, _pos, parent) => {
if (parent === state.doc && editor) {
const serializer = DOMSerializer.fromSchema(editor.schema);
if (parent === state.doc && editorRef.current) {
const serializer = DOMSerializer.fromSchema(editorRef.current?.schema);
const dom = serializer.serializeNode(node);
const tempDiv = document.createElement("div");
tempDiv.appendChild(dom);
@@ -254,21 +270,28 @@ export const useEditor = (props: CustomEditorProps) => {
return selection;
},
insertText: (contentHTML, insertOnNextLine) => {
if (!editor) return;
const { from, to, empty } = editor.state.selection;
if (!editorRef.current) return;
// get selection
const { from, to, empty } = editorRef.current.state.selection;
if (empty) return;
if (insertOnNextLine) {
// move cursor to the end of the selection and insert a new line
editor.chain().focus().setTextSelection(to).insertContent("<br />").insertContent(contentHTML).run();
editorRef.current
.chain()
.focus()
.setTextSelection(to)
.insertContent("<br />")
.insertContent(contentHTML)
.run();
} else {
// replace selected text with the content provided
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
editorRef.current.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
}
},
getDocumentInfo: () => ({
characters: editor?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editor?.state),
words: editor?.storage?.characterCount?.words?.() ?? 0,
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editorRef?.current?.state),
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
}),
setProviderDocument: (value) => {
const document = provider?.document;
@@ -278,12 +301,16 @@ export const useEditor = (props: CustomEditorProps) => {
emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
}),
[editor]
[editorRef, savedSelection]
);
if (!editor) {
return null;
}
// the editorRef is used to access the editor instance from outside the hook
// and should only be used after editor is initialized
editorRef.current = editor;
return editor;
};
@@ -1,7 +1,7 @@
import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react";
import { useImperativeHandle, MutableRefObject, useEffect } from "react";
import { useEditor as useCustomEditor, Editor, Extensions } from "@tiptap/react";
import * as Y from "yjs";
// extensions
import { CoreReadOnlyEditorExtensions } from "@/extensions";
@@ -11,7 +11,13 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
// props
import { CoreReadOnlyEditorProps } from "@/props";
// types
import type { EditorReadOnlyRefApi, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
import type {
EditorReadOnlyRefApi,
TExtensions,
TDocumentEventsServer,
TFileHandler,
TReadOnlyMentionHandler,
} from "@/types";
interface CustomReadOnlyEditorProps {
disabledExtensions: TExtensions[];
@@ -40,10 +46,8 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
provider,
} = props;
const editor = useTiptapEditor({
const editor = useCustomEditor({
editable: false,
immediatelyRender: true,
shouldRerenderOnTransaction: false,
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
editorProps: {
...CoreReadOnlyEditorProps({
@@ -73,21 +77,23 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: "full" });
}, [editor, initialValue]);
const editorRef: MutableRefObject<Editor | null> = useRef(null);
useImperativeHandle(forwardedRef, () => ({
clearEditor: (emitUpdate = false) => {
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
},
setEditorValue: (content: string) => {
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
},
getMarkDown: (): string => {
const markdownOutput = editor?.storage.markdown.getMarkdown();
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
return markdownOutput;
},
getDocument: () => {
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
const documentHTML = editor?.getHTML() ?? "<p></p>";
const documentJSON = editor?.getJSON() ?? null;
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
const documentJSON = editorRef.current?.getJSON() ?? null;
return {
binary: documentBinary,
@@ -96,22 +102,35 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
};
},
scrollSummary: (marking: IMarking): void => {
if (!editor) return;
scrollSummary(editor, marking);
if (!editorRef.current) return;
scrollSummary(editorRef.current, marking);
},
getDocumentInfo: () => {
if (!editor) return;
return {
characters: editor.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editor.state),
words: editor.storage?.characterCount?.words?.() ?? 0,
getDocumentInfo: () => ({
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editorRef?.current?.state),
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
}),
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
// Subscribe to update event emitted from headers extension
editorRef.current?.on("update", () => {
callback(editorRef.current?.storage.headingList.headings);
});
// Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this
// method
return () => {
editorRef.current?.off("update");
};
},
emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
getHeadings: () => editorRef?.current?.storage.headingList.headings,
}));
if (!editor) {
return null;
}
editorRef.current = editor;
return editor;
};
+4 -4
View File
@@ -86,6 +86,10 @@ export type EditorReadOnlyRefApi = {
paragraphs: number;
words: number;
};
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
};
export interface EditorRefApi extends EditorReadOnlyRefApi {
@@ -101,10 +105,6 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
getSelectedText: () => string | null;
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
setProviderDocument: (value: Uint8Array) => void;
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
}
// editor props
+6 -6
View File
@@ -25,7 +25,7 @@
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: var(--color-placeholder);
color: rgb(var(--color-text-400));
pointer-events: none;
height: 0;
}
@@ -34,7 +34,7 @@
.ProseMirror p.is-empty::before {
content: attr(data-placeholder);
float: left;
color: var(--color-placeholder);
color: rgb(var(--color-text-400));
pointer-events: none;
height: 0;
}
@@ -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-text-100), 0.2) !important;
border: 1px solid rgba(var(--color-border-300)) !important;
outline: none;
border-radius: 2px;
transform: scale(1.05);
}
.ProseMirror[contenteditable="true"] input[type="checkbox"]:hover {
background-color: rgba(var(--color-text-100), 0.1);
background-color: rgba(var(--color-background-80));
}
.ProseMirror[contenteditable="false"] input[type="checkbox"] {
@@ -145,7 +145,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
position: relative;
-webkit-appearance: none;
appearance: none;
background-color: transparent;
background-color: rgb(var(--color-background-100));
margin: 0;
cursor: pointer;
width: 0.8rem;
@@ -192,7 +192,7 @@ ul[data-type="taskList"] li > div {
ul[data-type="taskList"] li[data-checked="true"] {
& > div > p.editor-paragraph-block {
color: var(--color-placeholder);
color: rgb(var(--color-text-400));
}
[data-text-color] {
+38 -2
View File
@@ -1,6 +1,42 @@
.editor-container {
--color-placeholder: rgba(var(--color-text-100), 0.5);
:root {
/* text colors */
--editor-colors-gray-text: #5c5e63;
--editor-colors-peach-text: #ff5b59;
--editor-colors-pink-text: #f65385;
--editor-colors-orange-text: #fd9038;
--editor-colors-green-text: #0fc27b;
--editor-colors-light-blue-text: #17bee9;
--editor-colors-dark-blue-text: #266df0;
--editor-colors-purple-text: #9162f9;
/* end text colors */
}
/* text background colors */
[data-theme="light"],
[data-theme="light-contrast"] {
--editor-colors-gray-background: #d6d6d8;
--editor-colors-peach-background: #ffd5d7;
--editor-colors-pink-background: #fdd4e3;
--editor-colors-orange-background: #ffe3cd;
--editor-colors-green-background: #c3f0de;
--editor-colors-light-blue-background: #c5eff9;
--editor-colors-dark-blue-background: #c9dafb;
--editor-colors-purple-background: #e3d8fd;
}
[data-theme="dark"],
[data-theme="dark-contrast"] {
--editor-colors-gray-background: #404144;
--editor-colors-peach-background: #593032;
--editor-colors-pink-background: #562e3d;
--editor-colors-orange-background: #583e2a;
--editor-colors-green-background: #1d4a3b;
--editor-colors-light-blue-background: #1f495c;
--editor-colors-dark-blue-background: #223558;
--editor-colors-purple-background: #3d325a;
}
/* end text background colors */
.editor-container {
/* font sizes and line heights */
&.large-font {
--font-size-h1: 1.75rem;
+1 -1
View File
@@ -1,4 +1,4 @@
const sharedConfig = require("@plane/tailwind-config/tailwind.config.js");
const sharedConfig = require("tailwind-config-custom/tailwind.config.js");
module.exports = {
// prefix ui lib classes to avoid conflicting with the app
+1 -2
View File
@@ -10,8 +10,7 @@
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"@plane/utils": "*",
"intl-messageformat": "^10.7.11"
"@plane/utils": "*"
},
"devDependencies": {
"@plane/eslint-config": "*",
+29
View File
@@ -0,0 +1,29 @@
import { observer } from "mobx-react";
import React, { createContext, useEffect } from "react";
import { Language, languages } from "../config";
import { TranslationStore } from "./store";
// Create the store instance
const translationStore = new TranslationStore();
// Create Context
export const TranslationContext = createContext<TranslationStore>(translationStore);
export const TranslationProvider = observer(({ children }: { children: React.ReactNode }) => {
// Handle storage events for cross-tab synchronization
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === "userLanguage" && event.newValue) {
const newLang = event.newValue as Language;
if (languages.includes(newLang)) {
translationStore.setLanguage(newLang);
}
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, []);
return <TranslationContext.Provider value={translationStore}>{children}</TranslationContext.Provider>;
});
+42
View File
@@ -0,0 +1,42 @@
import { makeObservable, observable } from "mobx";
import { Language, fallbackLng, languages, translations } from "../config";
export class TranslationStore {
currentLocale: Language = fallbackLng;
constructor() {
makeObservable(this, {
currentLocale: observable.ref,
});
this.initializeLanguage();
}
get availableLanguages() {
return languages;
}
t(key: string) {
return translations[this.currentLocale]?.[key] || translations[fallbackLng][key] || key;
}
setLanguage(lng: Language) {
try {
localStorage.setItem("userLanguage", lng);
this.currentLocale = lng;
} catch (error) {
console.error(error);
}
}
initializeLanguage() {
if (typeof window === "undefined") return;
const savedLocale = localStorage.getItem("userLanguage") as Language;
if (savedLocale && languages.includes(savedLocale)) {
this.setLanguage(savedLocale);
} else {
const browserLang = navigator.language.split("-")[0] as Language;
const newLocale = languages.includes(browserLang as Language) ? (browserLang as Language) : fallbackLng;
this.setLanguage(newLocale);
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import en from "../locales/en/translations.json";
import fr from "../locales/fr/translations.json";
import es from "../locales/es/translations.json";
import ja from "../locales/ja/translations.json";
export type Language = (typeof languages)[number];
export type Translations = {
[key: string]: {
[key: string]: string;
};
};
export const fallbackLng = "en";
export const languages = ["en", "fr", "es", "ja"] as const;
export const translations: Translations = {
en,
fr,
es,
ja,
};
export const SUPPORTED_LANGUAGES = [
{
label: "English",
value: "en",
},
{
label: "French",
value: "fr",
},
{
label: "Spanish",
value: "es",
},
{
label: "Japanese",
value: "ja",
},
];
-1
View File
@@ -1 +0,0 @@
export * from "./language";
-13
View File
@@ -1,13 +0,0 @@
import { TLanguage, ILanguageOption } from "../types";
export const FALLBACK_LANGUAGE: TLanguage = "en";
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "English", value: "en" },
{ label: "Français", value: "fr" },
{ label: "Español", value: "es" },
{ label: "日本語", value: "ja" },
{ label: "中文", value: "zh-CN" },
];
export const STORAGE_KEY = "userLanguage";
-19
View File
@@ -1,19 +0,0 @@
import { observer } from "mobx-react";
import React, { createContext } from "react";
// store
import { TranslationStore } from "../store";
export const TranslationContext = createContext<TranslationStore | null>(null);
interface TranslationProviderProps {
children: React.ReactNode;
}
/**
* Provides the translation store to the application
*/
export const TranslationProvider: React.FC<TranslationProviderProps> = observer(({ children }) => {
const [store] = React.useState(() => new TranslationStore());
return <TranslationContext.Provider value={store}>{children}</TranslationContext.Provider>;
});
+7 -25
View File
@@ -1,35 +1,17 @@
import { useContext } from 'react';
// context
import { TranslationContext } from '../context';
// types
import { ILanguageOption, TLanguage } from '../types';
import { useContext } from "react";
import { TranslationContext } from "../components";
import { Language } from "../config";
export type TTranslationStore = {
t: (key: string, params?: Record<string, any>) => string;
currentLocale: TLanguage;
changeLanguage: (lng: TLanguage) => void;
languages: ILanguageOption[];
};
/**
* Provides the translation store to the application
* @returns {TTranslationStore}
* @returns {(key: string, params?: Record<string, any>) => string} t: method to translate the key with params
* @returns {TLanguage} currentLocale - current locale language
* @returns {(lng: TLanguage) => void} changeLanguage - method to change the language
* @returns {ILanguageOption[]} languages - available languages
* @throws {Error} if the TranslationProvider is not used
*/
export function useTranslation(): TTranslationStore {
export function useTranslation() {
const store = useContext(TranslationContext);
if (!store) {
throw new Error('useTranslation must be used within a TranslationProvider');
throw new Error("useTranslation must be used within a TranslationProvider");
}
return {
t: store.t.bind(store),
t: (key: string) => store.t(key),
currentLocale: store.currentLocale,
changeLanguage: (lng: TLanguage) => store.setLanguage(lng),
changeLanguage: (lng: Language) => store.setLanguage(lng),
languages: store.availableLanguages,
};
}
+2 -3
View File
@@ -1,4 +1,3 @@
export * from "./constants";
export * from "./context";
export * from "./config";
export * from "./components";
export * from "./hooks";
export * from "./types";
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,856 +0,0 @@
{
"submit": "提交",
"cancel": "取消",
"loading": "加载中",
"error": "错误",
"success": "成功",
"warning": "警告",
"info": "信息",
"close": "关闭",
"yes": "是",
"no": "否",
"ok": "确定",
"name": "名称",
"description": "描述",
"search": "搜索",
"add_member": "添加成员",
"remove_member": "移除成员",
"add_members": "添加成员",
"remove_members": "移除成员",
"add": "添加",
"remove": "移除",
"add_new": "新增",
"remove_selected": "移除选中项",
"first_name": "名字",
"last_name": "姓氏",
"email": "电子邮件",
"display_name": "显示名称",
"role": "角色",
"timezone": "时区",
"avatar": "头像",
"cover_image": "封面图片",
"password": "密码",
"change_cover": "更换封面",
"language": "语言",
"saving": "保存中...",
"save_changes": "保存更改",
"deactivate_account": "停用账户",
"deactivate_account_description": "停用账户后,该账户内的所有数据和资源将被永久删除,且无法恢复。",
"profile_settings": "个人资料设置",
"your_account": "账户",
"profile": "个人资料",
"security": "安全",
"activity": "活动",
"appearance": "外观",
"notifications": "通知",
"workspaces": "工作区",
"create_workspace": "创建工作区",
"invitations": "邀请",
"summary": "摘要",
"assigned": "已分配",
"created": "已创建",
"subscribed": "已订阅",
"you_do_not_have_the_permission_to_access_this_page": "您没有权限访问此页面。",
"failed_to_sign_out_please_try_again": "登出失败,请重试。",
"password_changed_successfully": "密码已成功更改。",
"something_went_wrong_please_try_again": "出错了,请重试。",
"change_password": "更改密码",
"passwords_dont_match": "密码不匹配",
"current_password": "当前密码",
"new_password": "新密码",
"confirm_password": "确认密码",
"this_field_is_required": "此字段为必填项",
"changing_password": "正在更改密码",
"please_enter_your_password": "请输入您的密码。",
"password_length_should_me_more_than_8_characters": "密码长度应大于8个字符。",
"password_is_weak": "密码强度较弱。",
"password_is_strong": "密码强度较强。",
"load_more": "加载更多",
"select_or_customize_your_interface_color_scheme": "选择或自定义界面配色方案。",
"theme": "主题",
"system_preference": "系统偏好",
"light": "浅色",
"dark": "深色",
"light_contrast": "浅色高对比",
"dark_contrast": "深色高对比",
"custom": "自定义主题",
"select_your_theme": "选择主题",
"customize_your_theme": "自定义主题",
"background_color": "背景颜色",
"text_color": "文字颜色",
"primary_color": "主色调",
"sidebar_background_color": "侧边栏背景颜色",
"sidebar_text_color": "侧边栏文字颜色",
"set_theme": "设置主题",
"enter_a_valid_hex_code_of_6_characters": "请输入6位有效的十六进制代码",
"background_color_is_required": "背景颜色为必填项",
"text_color_is_required": "文字颜色为必填项",
"primary_color_is_required": "主色调为必填项",
"sidebar_background_color_is_required": "侧边栏背景颜色为必填项",
"sidebar_text_color_is_required": "侧边栏文字颜色为必填项",
"updating_theme": "正在更新主题",
"theme_updated_successfully": "主题已成功更新",
"failed_to_update_the_theme": "主题更新失败",
"email_notifications": "电子邮件通知",
"stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "启用此功能以接收您订阅问题的通知。",
"email_notification_setting_updated_successfully": "电子邮件通知设置已成功更新",
"failed_to_update_email_notification_setting": "电子邮件通知设置更新失败",
"notify_me_when": "在以下情况下通知我",
"property_changes": "属性更改",
"property_changes_description": "当负责人、优先级、估算等属性更改时通知。",
"state_change": "状态更改",
"state_change_description": "当问题状态更改时通知",
"issue_completed": "问题完成",
"issue_completed_description": "仅在问题完成时通知",
"comments": "评论",
"comments_description": "当有人在问题中添加评论时通知",
"mentions": "提及",
"mentions_description": "仅在评论或描述中提及您时通知",
"create_your_workspace": "创建工作区",
"only_your_instance_admin_can_create_workspaces": "只有实例管理员可以创建工作区",
"only_your_instance_admin_can_create_workspaces_description": "如果您知道实例管理员的电子邮件地址,请点击以下按钮联系他们。",
"go_back": "返回",
"request_instance_admin": "请求实例管理员",
"plane_logo": "Plane 徽标",
"workspace_creation_disabled": "工作区创建已禁用",
"workspace_request_subject": "新工作区请求",
"workspace_request_body": "实例管理员:\n\n请在 URL [/workspace-name] 处创建一个新的工作区。[工作区创建目的]\n\n谢谢。\n{{firstName}} {{lastName}}\n{{email}}",
"creating_workspace": "正在创建工作区",
"workspace_created_successfully": "工作区已成功创建",
"create_workspace_page": "创建工作区页面",
"workspace_could_not_be_created_please_try_again": "无法创建工作区,请重试。",
"workspace_could_not_be_created_please_try_again_description": "创建工作区时出错,请重试。",
"this_is_a_required_field": "此字段为必填项。",
"name_your_workspace": "为工作区命名",
"workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "工作区名称只能包含空格、破折号和字母数字字符。",
"limit_your_name_to_80_characters": "名称长度限制为80个字符。",
"set_your_workspace_url": "设置工作区URL",
"limit_your_url_to_48_characters": "URL长度限制为48个字符。",
"how_many_people_will_use_this_workspace": "有多少人将使用此工作区?",
"how_many_people_will_use_this_workspace_description": "这将帮助确定购买的席位数量。",
"select_a_range": "选择一个范围",
"urls_can_contain_only_dash_and_alphanumeric_characters": "URL只能包含破折号和字母数字字符。",
"something_familiar_and_recognizable_is_always_best": "熟悉且易于识别的名称总是最佳选择。",
"workspace_url_is_already_taken": "工作区URL已被占用!",
"old_password": "旧密码",
"general_settings": "常规设置",
"sign_out": "登出",
"signing_out": "正在登出",
"active_cycles": "活跃周期",
"active_cycles_description": "监控所有项目的周期,跟踪高优先级问题,并关注需要关注的周期。",
"on_demand_snapshots_of_all_your_cycles": "所有周期的按需快照",
"upgrade": "升级",
"10000_feet_view": "所有活跃周期的概览",
"10000_feet_view_description": "放大查看所有项目的周期,而不是单独查看每个项目的周期。",
"get_snapshot_of_each_active_cycle": "获取每个活跃周期的快照",
"get_snapshot_of_each_active_cycle_description": "跟踪所有活跃周期的高级指标,查看进度并了解范围与截止日期的关系。",
"compare_burndowns": "比较燃尽图",
"compare_burndowns_description": "监控每个团队的表现,查看每个周期的燃尽报告。",
"quickly_see_make_or_break_issues": "快速查看关键问题",
"quickly_see_make_or_break_issues_description": "预览每个周期中与截止日期相关的高优先级问题。一键查看所有周期的详细信息。",
"zoom_into_cycles_that_need_attention": "关注需要关注的周期",
"zoom_into_cycles_that_need_attention_description": "一键调查未达预期的周期状态。",
"stay_ahead_of_blockers": "提前解决阻碍",
"stay_ahead_of_blockers_description": "发现项目间的挑战,查看其他视图中不明显的周期依赖关系。",
"analytics": "分析",
"workspace_invites": "工作区邀请",
"workspace_settings": {
"label": "工作区设置",
"settings": {
"general": {
"title": "常规"
},
"members": {
"title": "会员"
},
"billing-and-plans": {
"title": "账单与计划"
},
"exports": {
"title": "导出"
},
"webhooks": {
"title": "网络钩子"
},
"api-tokens": {
"title": "API 令牌"
}
},
"empty_state": {
"api_tokens": {
"title": "尚未创建 API 令牌",
"description": "Plane API 可用于将您在 Plane 中的数据与任何外部系统集成。创建令牌即可开始使用。"
},
"webhooks": {
"title": "尚未添加网络钩子",
"description": "创建网络钩子以接收实时更新并自动执行操作。"
},
"exports": {
"title": "尚无导出记录",
"description": "每次导出时,您都可以在此处找到参考副本。"
},
"imports": {
"title": "尚无导入记录",
"description": "在此处查找所有以前的导入记录并下载。"
}
}
},
"enter_god_mode": "进入上帝模式",
"workspace_logo": "工作区徽标",
"new_issue": "新建问题",
"your_work": "您的工作",
"drafts": "草稿",
"projects": "项目",
"views": "视图",
"workspace": "工作区",
"archives": "归档",
"settings": "设置",
"failed_to_move_favorite": "移动收藏失败",
"your_favorites": "您的收藏",
"no_favorites_yet": "尚无收藏",
"create_folder": "创建文件夹",
"new_folder": "新建文件夹",
"favorite_updated_successfully": "收藏已成功更新",
"favorite_created_successfully": "收藏已成功创建",
"folder_already_exists": "文件夹已存在",
"folder_name_cannot_be_empty": "文件夹名称不能为空",
"something_went_wrong": "出错了",
"failed_to_reorder_favorite": "重新排序收藏失败",
"favorite_removed_successfully": "收藏已成功移除",
"failed_to_create_favorite": "创建收藏失败",
"failed_to_rename_favorite": "重命名收藏失败",
"project_link_copied_to_clipboard": "项目链接已复制到剪贴板",
"link_copied": "链接已复制",
"your_projects": "您的项目",
"add_project": "添加项目",
"create_project": "创建项目",
"failed_to_remove_project_from_favorites": "无法从收藏中移除项目,请重试。",
"project_created_successfully": "项目已成功创建",
"project_created_successfully_description": "项目已成功创建。您现在可以开始添加问题。",
"project_cover_image_alt": "项目封面图片",
"name_is_required": "名称为必填项",
"title_should_be_less_than_255_characters": "标题应少于255个字符",
"project_name": "项目名称",
"project_id_must_be_at_least_1_character": "项目ID至少为1个字符",
"project_id_must_be_at_most_5_characters": "项目ID最多为5个字符",
"project_id": "项目ID",
"project_id_tooltip_content": "用于唯一标识项目中的问题。最多5个字符。",
"description_placeholder": "描述...",
"only_alphanumeric_non_latin_characters_allowed": "仅允许字母数字和非拉丁字符。",
"project_id_is_required": "项目ID为必填项",
"select_network": "选择网络",
"lead": "负责人",
"private": "私有",
"public": "公开",
"accessible_only_by_invite": "仅限邀请访问",
"anyone_in_the_workspace_except_guests_can_join": "除访客外,工作区中的任何人都可以加入",
"creating": "创建中",
"creating_project": "正在创建项目",
"adding_project_to_favorites": "正在将项目添加到收藏",
"project_added_to_favorites": "项目已添加到收藏",
"couldnt_add_the_project_to_favorites": "无法将项目添加到收藏,请重试。",
"removing_project_from_favorites": "正在从收藏中移除项目",
"project_removed_from_favorites": "项目已从收藏中移除",
"couldnt_remove_the_project_from_favorites": "无法从收藏中移除项目,请重试。",
"add_to_favorites": "添加到收藏",
"remove_from_favorites": "从收藏中移除",
"publish_settings": "发布设置",
"publish": "发布",
"copy_link": "复制链接",
"leave_project": "离开项目",
"join_the_project_to_rearrange": "加入项目以重新排序",
"drag_to_rearrange": "拖动以重新排序",
"congrats": "恭喜!",
"open_project": "打开项目",
"issues": "问题",
"cycles": "周期",
"modules": "模块",
"pages": "页面",
"intake": "接收",
"time_tracking": "时间跟踪",
"work_management": "工作管理",
"projects_and_issues": "项目与问题",
"projects_and_issues_description": "在此项目上启用或禁用。",
"cycles_description": "按时间段对项目工作进行时间盒管理,并调整频率。",
"modules_description": "将工作分组为类似子项目的设置,拥有自己的负责人和分配人。",
"views_description": "保存排序、过滤和显示选项以供以后使用或共享。",
"pages_description": "写下一些内容。",
"intake_description": "启用此功能以接收您订阅问题的通知。",
"time_tracking_description": "跟踪问题和项目所花费的时间。",
"work_management_description": "轻松管理工作和项目。",
"documentation": "文档",
"message_support": "联系支持",
"contact_sales": "联系销售",
"hyper_mode": "超速模式",
"keyboard_shortcuts": "键盘快捷键",
"whats_new": "新功能",
"version": "版本",
"we_are_having_trouble_fetching_the_updates": "获取更新时出现问题。",
"our_changelogs": "我们的更新日志",
"for_the_latest_updates": "获取最新更新,请访问",
"please_visit": "请访问",
"docs": "文档",
"full_changelog": "完整更新日志",
"support": "支持",
"discord": "Discord",
"powered_by_plane_pages": "由 Plane Pages 提供支持",
"please_select_at_least_one_invitation": "请至少选择一个邀请。",
"please_select_at_least_one_invitation_description": "请至少选择一个邀请以加入工作区。",
"we_see_that_someone_has_invited_you_to_join_a_workspace": "我们发现有人邀请您加入一个工作区",
"join_a_workspace": "加入工作区",
"we_see_that_someone_has_invited_you_to_join_a_workspace_description": "我们发现有人邀请您加入一个工作区",
"join_a_workspace_description": "加入工作区",
"accept_and_join": "接受并加入",
"go_home": "返回首页",
"no_pending_invites": "没有待处理的邀请",
"you_can_see_here_if_someone_invites_you_to_a_workspace": "如果有人邀请您加入工作区,您可以在此处查看",
"back_to_home": "返回首页",
"workspace_name": "工作区名称",
"deactivate_your_account": "停用您的账户",
"deactivate_your_account_description": "停用后,您将无法再分配问题,工作区的计费也将停止。要重新激活账户,您需要使用此电子邮件地址收到工作区邀请。",
"deactivating": "正在停用",
"confirm": "确认",
"draft_created": "草稿已创建",
"issue_created_successfully": "问题已成功创建",
"draft_creation_failed": "草稿创建失败",
"issue_creation_failed": "问题创建失败",
"draft_issue": "草稿问题",
"issue_updated_successfully": "问题已成功更新",
"issue_could_not_be_updated": "无法更新问题",
"create_a_draft": "创建草稿",
"save_to_drafts": "保存为草稿",
"save": "保存",
"updating": "正在更新",
"create_new_issue": "创建新问题",
"editor_is_not_ready_to_discard_changes": "编辑器尚未准备好丢弃更改",
"failed_to_move_issue_to_project": "无法将问题移动到项目",
"create_more": "创建更多",
"add_to_project": "添加到项目",
"discard": "丢弃",
"duplicate_issue_found": "发现重复问题",
"duplicate_issues_found": "发现重复问题",
"no_matching_results": "没有匹配的结果",
"title_is_required": "标题为必填项",
"title": "标题",
"state": "状态",
"priority": "优先级",
"none": "无",
"urgent": "紧急",
"high": "高",
"medium": "中",
"low": "低",
"members": "成员",
"assignee": "分配人",
"assignees": "分配人",
"you": "您",
"labels": "标签",
"create_new_label": "创建新标签",
"start_date": "开始日期",
"due_date": "截止日期",
"cycle": "周期",
"estimate": "估算",
"change_parent_issue": "更改父问题",
"remove_parent_issue": "移除父问题",
"add_parent": "添加父问题",
"loading_members": "正在加载成员...",
"inbox": "收件箱",
"show_all": "显示全部",
"show_less": "显示较少",
"no_data_yet": "暂无数据",
"project_view": {
"access": {
"public": "公开",
"private": "私密"
},
"sort_by": {
"created_at": "创建时间",
"updated_at": "更新时间",
"name": "名称"
}
},
"toast": {
"success": "成功!",
"error": "错误!"
},
"home": {
"empty": {
"create_project": {
"title": "创建项目",
"description": "在Plane中,大多数事情都从项目开始。",
"cta": "开始使用"
},
"invite_team": {
"title": "邀请团队",
"description": "与同事一起构建、部署和管理。",
"cta": "邀请成员"
},
"configure_workspace": {
"title": "设置工作空间",
"description": "开启或关闭功能,或进行更多设置。",
"cta": "配置此工作空间"
},
"personalize_account": {
"title": "让Plane更个性化",
"description": "选择您的头像、颜色等更多设置。",
"cta": "立即个性化"
}
},
"quick_links": {
"empty": "保存您想随手可得的工作相关链接。",
"add": "添加快捷链接",
"title": "快捷链接",
"title_plural": "快捷链接",
"toasts": {
"created": {
"title": "链接已创建",
"message": "链接已成功创建"
},
"not_created": {
"title": "链接未创建",
"message": "无法创建链接"
},
"updated": {
"title": "链接已更新",
"message": "链接已成功更新"
},
"not_updated": {
"title": "链接未更新",
"message": "无法更新链接"
},
"removed": {
"title": "链接已删除",
"message": "链接已成功删除"
},
"not_removed": {
"title": "链接未删除",
"message": "无法删除链接"
}
}
},
"recents": {
"title": "最近",
"empty": {
"project": "访问项目后,您的最近项目将显示在这里。",
"page": "访问页面后,您的最近页面将显示在这里。",
"issue": "访问问题后,您的最近问题将显示在这里。",
"default": "您还没有任何最近项目。"
},
"filters": {
"all": "所有项目",
"projects": "项目",
"pages": "页面",
"issues": "问题"
}
},
"my_stickies": {
"title": "您的便签"
},
"new_at_plane": {
"title": "Plane 新功能"
},
"quick_tutorial": {
"title": "快速教程"
},
"widget": {
"reordered_successfully": "小部件重新排序成功。",
"reordering_failed": "重新排序小部件时发生错误。"
},
"manage_widgets": "管理小部件",
"title": "首页",
"star_us_on_github": "在 GitHub 上为我们加星"
},
"link": {
"modal": {
"url": {
"text": "网址",
"required": "网址无效",
"placeholder": "输入或粘贴网址"
},
"title": {
"text": "显示标题",
"placeholder": "您希望将此链接显示为什么"
}
}
},
"common": {
"all": "全部",
"states": "状态",
"state": "状态",
"state_groups": "状态组",
"priority": "优先级",
"team_project": "团队项目",
"project": "项目",
"cycle": "周期",
"cycles": "周期",
"module": "模块",
"modules": "模块",
"labels": "标签",
"assignees": "分配人",
"assignee": "分配人",
"created_by": "创建者",
"none": "无",
"link": "链接",
"estimate": "估算",
"layout": "布局",
"filters": "筛选",
"display": "显示",
"load_more": "加载更多",
"no_matches_found": "未找到匹配项",
"activity": "活动",
"analytics": "分析",
"success": "成功",
"error": "错误",
"group_by": "分组方式",
"search": "搜索",
"epic": "史诗",
"issue": "问题",
"warning": "警告",
"updating": "更新中",
"update": "更新",
"creating": "创建中",
"cancel": "取消",
"description": "描述",
"title": "标题",
"order_by": {
"label": "排序方式",
"manual": "手动",
"last_created": "最近创建",
"last_updated": "最近更新",
"start_date": "开始日期",
"due_date": "截止日期",
"asc": "升序",
"desc": "降序"
},
"sort": {
"asc": "升序",
"desc": "降序"
},
"comments": "评论",
"updates": "更新"
},
"form": {
"title": {
"required": "标题为必填项",
"max_length": "标题不能超过{length}个字符"
}
},
"entity": {
"grouping_title": "{entity}分组",
"priority": "{entity}",
"all": "所有{entity}",
"drop_here_to_move": "拖放至此以移动{entity}"
},
"epic": {
"all": "所有史诗",
"label": "{count, plural, one {史诗} other {史诗}}"
},
"issue": {
"label": "{count, plural, one {问题} other {问题}}",
"all": "所有问题",
"add": "添加问题",
"priority": {
"urgent": "紧急",
"high": "高",
"medium": "中",
"low": "低"
},
"display": {
"properties": {
"label": "显示属性",
"id": "ID",
"issue_type": "问题类型",
"sub_issue_count": "子问题数量",
"attachment_count": "附件数量"
},
"extra": {
"show_sub_issues": "显示子问题",
"show_empty_groups": "显示空分组"
}
},
"layouts": {
"ordered_by_label": "当前排序方式",
"list": "列表",
"kanban": "看板",
"calendar": "日历",
"spreadsheet": "表格",
"gantt": "时间线",
"title": {
"list": "列表视图",
"kanban": "看板视图",
"calendar": "日历视图",
"spreadsheet": "表格视图",
"gantt": "时间线视图"
}
},
"states": {
"active": "进行中",
"backlog": "待办"
},
"comments": {
"create": {
"success": "评论创建成功",
"error": "评论创建失败,请稍后重试"
},
"update": {
"success": "评论更新成功",
"error": "评论更新失败,请稍后重试"
},
"remove": {
"success": "评论删除成功",
"error": "评论删除失败,请稍后重试"
},
"upload": {
"error": "资源上传失败,请稍后重试"
}
}
},
"project": {
"label": "{count, plural, one {项目} other {项目}}"
},
"view": {
"create": {
"label": "创建视图"
},
"update": {
"label": "更新视图"
}
},
"inbox_issue": {
"status": {
"pending": {
"title": "待处理",
"description": "待处理"
},
"declined": {
"title": "已拒绝",
"description": "已拒绝"
},
"snoozed": {
"title": "已暂停",
"description": "还剩{days}天"
},
"accepted": {
"title": "已接受",
"description": "已接受"
},
"duplicate": {
"title": "重复",
"description": "重复"
}
},
"source": {
"in-app": "应用内"
},
"order_by": {
"created_at": "创建时间",
"updated_at": "更新时间",
"id": "编号"
}
},
"user_profile": {
"title": "你的工作",
"work": "工作",
"details": {
"joined_on": "加入时间",
"time_zone": "时区"
},
"stats": {
"workload": "工作量",
"overview": "概述",
"created": "已创建问题",
"assigned": "已分配问题",
"subscribed": "已订阅问题",
"state_distribution": {
"title": "按状态显示的问题",
"empty": "创建问题以在图表中按状态查看它们,以便更好地分析。"
},
"priority_distribution": {
"title": "按优先级显示的问题",
"empty": "创建问题以在图表中按优先级查看它们,以便更好地分析。"
},
"recent_activity": {
"title": "最近活动",
"empty": "我们找不到数据。请查看您的输入"
}
},
"actions": {
"profile": "个人资料",
"security": "安全",
"activity": "活动",
"appearance": "外观",
"notifications": "通知"
},
"tabs": {
"summary": "摘要",
"assigned": "已分配",
"created": "已创建",
"subscribed": "已订阅",
"activity": "活动"
}
},
"role_details": {
"guest": {
"title": "访客",
"description": "组织的外部成员可以被邀请为访客。"
},
"member": {
"title": "成员",
"description": "可以在项目、周期和模块内读取、写入、编辑和删除实体"
},
"admin": {
"title": "管理员",
"description": "工作区内的所有权限都设置为允许。"
}
},
"user_roles": {
"product_or_project_manager": "产品/项目经理",
"development_or_engineering": "开发/工程",
"founder_or_executive": "创始人/高管",
"freelancer_or_consultant": "自由职业者/顾问",
"marketing_or_growth": "市场营销/增长",
"sales_or_business_development": "销售/业务发展",
"support_or_operations": "支持/运营",
"student_or_professor": "学生/教授",
"human_or_resources": "人力资源",
"other": "其他"
},
"importer": {
"github": {
"title": "Github",
"description": "从 GitHub 仓库导入问题并同步。"
},
"jira": {
"title": "Jira",
"description": "从 Jira 项目导入问题和史诗。"
}
},
"exporter": {
"csv": {
"title": "CSV",
"description": "将问题导出为 CSV 文件。"
},
"xlsx": {
"title": "Excel",
"description": "将问题导出为 Excel 文件。"
},
"json": {
"title": "JSON",
"description": "将问题导出为 JSON 文件。"
}
},
"default_global_view": {
"all_issues": "所有问题",
"assigned": "已分配",
"created": "已创建",
"subscribed": "已订阅"
},
"stickies": {
"title": "便签",
"placeholder": "点击此处输入",
"all": "所有便签",
"no-data": "记下想法,捕捉灵感,或记录突发奇想。添加便签即可开始。",
"add": "添加便签",
"search_placeholder": "按标题搜索",
"delete": "删除便签",
"delete_confirmation": "确定要删除这个便签吗?",
"empty_state": {
"simple": "记下想法,捕捉灵感,或记录突发奇想。添加便签即可开始。",
"general": {
"title": "便签是您随手记下的快速笔记和待办事项。",
"description": "通过创建便签轻松捕捉您的想法和创意,随时随地都可以访问。",
"primary_button": {
"text": "添加便签"
}
},
"search": {
"title": "没有找到匹配的便签。",
"description": "尝试使用不同的搜索词,或如果您确定搜索正确,\n请告诉我们。",
"primary_button": {
"text": "添加便签"
}
}
},
"toasts": {
"errors": {
"wrong_name": "便签名称不能超过100个字符。",
"already_exists": "已存在一个没有描述的便签"
},
"created": {
"title": "便签已创建",
"message": "便签已成功创建"
},
"not_created": {
"title": "便签未创建",
"message": "无法创建便签"
},
"updated": {
"title": "便签已更新",
"message": "便签已成功更新"
},
"not_updated": {
"title": "便签未更新",
"message": "无法更新便签"
},
"removed": {
"title": "便签已删除",
"message": "便签已成功删除"
},
"not_removed": {
"title": "便签未删除",
"message": "无法删除便签"
}
}
},
"workspace_projects": {
"network": {
"private": {
"title": "私有",
"description": "仅限邀请访问"
},
"public": {
"title": "公开",
"description": "除访客外的工作区所有成员都可加入"
}
},
"error": {
"permission": "您没有执行此操作的权限。",
"cycle_delete": "删除周期失败",
"module_delete": "删除模块失败",
"issue_delete": "删除问题失败"
},
"state": {
"backlog": "待办",
"unstarted": "未开始",
"started": "进行中",
"completed": "已完成",
"cancelled": "已取消"
},
"sort": {
"manual": "手动",
"name": "名称",
"created_at": "创建日期",
"members_length": "成员数量"
},
"scope": {
"my_projects": "我的项目",
"archived_projects": "已归档"
},
"common": {
"months_count": "{months, plural, one{#个月} other{#个月}}"
}
}
}
-202
View File
@@ -1,202 +0,0 @@
import IntlMessageFormat from "intl-messageformat";
import get from "lodash/get";
import { makeAutoObservable } from "mobx";
// constants
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants";
// types
import { TLanguage, ILanguageOption, ITranslations } from "../types";
/**
* Mobx store class for handling translations and language changes in the application
* Provides methods to translate keys with params and change the language
* Uses IntlMessageFormat to format the translations
*/
export class TranslationStore {
// List of translations for each language
private translations: ITranslations = {};
// Cache for IntlMessageFormat instances
private messageCache: Map<string, IntlMessageFormat> = new Map();
// Current language
currentLocale: TLanguage = FALLBACK_LANGUAGE;
/**
* Constructor for the TranslationStore class
*/
constructor() {
makeAutoObservable(this);
this.initializeLanguage();
this.loadTranslations();
}
/**
* Loads translations from JSON files and initializes the message cache
*/
private async loadTranslations() {
try {
// dynamic import of translations
const translations = {
en: (await import("../locales/en/translations.json")).default,
fr: (await import("../locales/fr/translations.json")).default,
es: (await import("../locales/es/translations.json")).default,
ja: (await import("../locales/ja/translations.json")).default,
"zh-CN": (await import("../locales/zh-CN/translations.json")).default,
};
this.translations = translations;
this.messageCache.clear(); // Clear cache when translations change
} catch (error) {
console.error("Failed to load translations:", error);
}
}
/** Initializes the language based on the local storage or browser language */
private initializeLanguage() {
if (typeof window === "undefined") return;
const savedLocale = localStorage.getItem(STORAGE_KEY) as TLanguage;
if (this.isValidLanguage(savedLocale)) {
this.setLanguage(savedLocale);
return;
}
const browserLang = this.getBrowserLanguage();
this.setLanguage(browserLang);
}
/** Checks if the language is valid based on the supported languages */
private isValidLanguage(lang: string | null): lang is TLanguage {
return lang !== null && this.availableLanguages.some((l) => l.value === lang);
}
/** Checks if a language code is similar to any supported language */
private findSimilarLanguage(lang: string): TLanguage | null {
// Convert to lowercase for case-insensitive comparison
const normalizedLang = lang.toLowerCase();
// Find a supported language that includes or is included in the browser language
const similarLang = this.availableLanguages.find(
(l) => normalizedLang.includes(l.value.toLowerCase()) || l.value.toLowerCase().includes(normalizedLang)
);
return similarLang ? similarLang.value : null;
}
/** Gets the browser language based on the navigator.language */
private getBrowserLanguage(): TLanguage {
const browserLang = navigator.language;
// Check exact match first
if (this.isValidLanguage(browserLang)) {
return browserLang;
}
// Check base language without region code
const baseLang = browserLang.split("-")[0];
if (this.isValidLanguage(baseLang)) {
return baseLang as TLanguage;
}
// Try to find a similar language
const similarLang = this.findSimilarLanguage(browserLang) || this.findSimilarLanguage(baseLang);
return similarLang || FALLBACK_LANGUAGE;
}
/**
* Gets the cache key for the given key and locale
* @param key - the key to get the cache key for
* @param locale - the locale to get the cache key for
* @returns the cache key for the given key and locale
*/
private getCacheKey(key: string, locale: TLanguage): string {
return `${locale}:${key}`;
}
/**
* Gets the IntlMessageFormat instance for the given key and locale
* Returns cached instance if available
* Throws an error if the key is not found in the translations
*/
private getMessageInstance(key: string, locale: TLanguage): IntlMessageFormat | null {
const cacheKey = this.getCacheKey(key, locale);
// Check if the cache already has the key
if (this.messageCache.has(cacheKey)) {
return this.messageCache.get(cacheKey) || null;
}
// Get the message from the translations
const message = get(this.translations[locale], key);
if (!message) return null;
try {
const formatter = new IntlMessageFormat(message as any, locale);
this.messageCache.set(cacheKey, formatter);
return formatter;
} catch (error) {
console.error(`Failed to create message formatter for key "${key}":`, error);
return null;
}
}
/**
* Translates a key with params using the current locale
* Falls back to the default language if the translation is not found
* Returns the key itself if the translation is not found
* @param key - The key to translate
* @param params - The params to format the translation with
* @returns The translated string
*/
t(key: string, params?: Record<string, any>): string {
try {
// Try current locale
let formatter = this.getMessageInstance(key, this.currentLocale);
// Fallback to default language if necessary
if (!formatter && this.currentLocale !== FALLBACK_LANGUAGE) {
formatter = this.getMessageInstance(key, FALLBACK_LANGUAGE);
}
// If we have a formatter, use it
if (formatter) {
return formatter.format(params || {}) as string;
}
// Last resort: return the key itself
return key;
} catch (error) {
console.error(`Translation error for key "${key}":`, error);
return key;
}
}
/**
* Sets the current language and updates the translations
* @param lng - The new language
*/
setLanguage(lng: TLanguage): void {
try {
if (!this.isValidLanguage(lng)) {
throw new Error(`Invalid language: ${lng}`);
}
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, lng);
}
this.currentLocale = lng;
this.messageCache.clear(); // Clear cache when language changes
if (typeof window !== "undefined") {
document.documentElement.lang = lng;
}
} catch (error) {
console.error("Failed to set language:", error);
}
}
/**
* Gets the available language options for the dropdown
* @returns An array of language options
*/
get availableLanguages(): ILanguageOption[] {
return SUPPORTED_LANGUAGES;
}
}
-2
View File
@@ -1,2 +0,0 @@
export * from "./language";
export * from "./translation";
-6
View File
@@ -1,6 +0,0 @@
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN";
export interface ILanguageOption {
label: string;
value: TLanguage;
}
-7
View File
@@ -1,7 +0,0 @@
export interface ITranslation {
[key: string]: string | ITranslation;
}
export interface ITranslations {
[locale: string]: ITranslation;
}
-3
View File
@@ -1,3 +0,0 @@
build/*
dist/*
out/*
-6
View File
@@ -1,6 +0,0 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
};
-27
View File
@@ -1,27 +0,0 @@
{
"name": "@plane/propel",
"version": "0.24.1",
"private": true,
"exports": {
"./globals.css": "./src/globals.css",
"./components/*": "./src/*.tsx"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
}
}
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
-59
View File
@@ -1,59 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 100% 50%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
}
.dark {
--background: 224 71% 4%;
--foreground: 213 31% 91%;
--muted: 223 47% 11%;
--muted-foreground: 215.4 16.3% 56.9%;
--accent: 216 34% 17%;
--accent-foreground: 210 40% 98%;
--popover: 224 71% 4%;
--popover-foreground: 215 20.2% 65.1%;
--border: 216 34% 17%;
--input: 216 34% 17%;
--card: 224 71% 4%;
--card-foreground: 213 31% 91%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 1.2%;
--secondary: 222.2 47.4% 11.2%;
--secondary-foreground: 210 40% 98%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--ring: 216 34% 17%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply font-sans antialiased bg-background text-foreground;
}
}
-57
View File
@@ -1,57 +0,0 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@plane/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
-7
View File
@@ -1,7 +0,0 @@
import type { Config } from "tailwindcss";
import sharedConfig from "@plane/tailwind-config/tailwind.config";
export default {
...sharedConfig,
content: ["./src/**/*.{ts,tsx}"],
} satisfies Config;
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "@plane/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
-1
View File
@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { IndexedDBService } from "./indexedDB.service";
/**
* Abstract base class for making HTTP requests using axios
@@ -1,62 +0,0 @@
export abstract class IndexedDBService {
private dbName: string;
private version: number;
private db: IDBDatabase | null = null;
constructor(dbName: string, version: number) {
this.dbName = dbName;
this.version = version;
}
async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains("workspaces")) {
db.createObjectStore("workspaces", { keyPath: "id" });
}
};
});
}
async save(workspaces: any[]): Promise<void> {
if (!this.db) throw new Error("Database not initialized");
const transaction = this.db.transaction("workspaces", "readwrite");
const store = transaction.objectStore("workspaces");
return new Promise((resolve, reject) => {
// Clear existing data
store.clear();
// Add new workspaces
workspaces.forEach((workspace) => {
store.add(workspace);
});
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}
async query(): Promise<any[]> {
if (!this.db) throw new Error("Database not initialized");
const transaction = this.db.transaction("workspaces", "readonly");
const store = transaction.objectStore("workspaces");
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
-3
View File
@@ -1,3 +0,0 @@
build/*
dist/*
out/*
-9
View File
@@ -1,9 +0,0 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
-5
View File
@@ -1,5 +0,0 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
-20
View File
@@ -1,20 +0,0 @@
{
"name": "@plane/shared-state",
"version": "0.24.1",
"description": "Shared state shared across multiple apps internally",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"lint": "eslint src --ext .ts,.tsx",
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"zod": "^3.22.2"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"typescript": "^5.3.3"
}
}
View File
@@ -1,136 +0,0 @@
import { makeObservable, observable } from "mobx";
import { IWorkspaceStore } from "./workspace.store";
export interface IUserStore {
user: any;
workspaces: Map<string, IWorkspaceStore>;
isLoading: boolean;
error: any;
}
export class UserStore implements IUserStore {
user: any = null;
workspaces: Map<string, IWorkspaceStore> = new Map();
isLoading: boolean = false;
error: any = null;
constructor() {
makeObservable(this, {
user: observable.ref,
workspaces: observable,
isLoading: observable.ref,
error: observable.ref,
});
}
}
// userStore.ts
// class UserStore {
// user: User | null = null;
// workspaces: Workspace[] = [];
// isLoading = false;
// error: string | null = null;
// private indexedDBService: IndexedDBService;
// constructor() {
// makeAutoObservable(this);
// this.indexedDBService = new IndexedDBService();
// this.init();
// }
// private async init() {
// try {
// await this.indexedDBService.init();
// await this.loadWorkspacesFromIndexedDB();
// } catch (error) {
// runInAction(() => {
// this.error = "Failed to initialize store";
// console.error("Store initialization error:", error);
// });
// }
// }
// setUser(user: User | null) {
// this.user = user;
// }
// async loadWorkspacesFromIndexedDB() {
// try {
// const workspaces = await this.indexedDBService.getWorkspaces();
// runInAction(() => {
// this.workspaces = workspaces;
// });
// } catch (error) {
// runInAction(() => {
// this.error = "Failed to load workspaces from IndexedDB";
// console.error("Load workspaces error:", error);
// });
// }
// }
// async fetchAndSyncWorkspaces() {
// this.isLoading = true;
// this.error = null;
// try {
// // Simulate API call to fetch workspaces
// const response = await fetch("/api/workspaces");
// const workspaces = await response.json();
// // Save to IndexedDB
// await this.indexedDBService.saveWorkspaces(workspaces);
// // Update MobX store
// runInAction(() => {
// this.workspaces = workspaces;
// this.isLoading = false;
// });
// } catch (error) {
// runInAction(() => {
// this.error = "Failed to fetch workspaces";
// this.isLoading = false;
// console.error("Fetch workspaces error:", error);
// });
// }
// }
// // Additional methods for workspace management
// async addWorkspace(workspace: Omit<Workspace, "id" | "createdAt" | "updatedAt">) {
// this.isLoading = true;
// this.error = null;
// try {
// // Simulate API call to create workspace
// const response = await fetch("/api/workspaces", {
// method: "POST",
// body: JSON.stringify(workspace),
// });
// const newWorkspace = await response.json();
// // Update local storage and state
// const updatedWorkspaces = [...this.workspaces, newWorkspace];
// await this.indexedDBService.saveWorkspaces(updatedWorkspaces);
// runInAction(() => {
// this.workspaces.push(newWorkspace);
// this.isLoading = false;
// });
// } catch (error) {
// runInAction(() => {
// this.error = "Failed to add workspace";
// this.isLoading = false;
// console.error("Add workspace error:", error);
// });
// }
// }
// logout() {
// this.user = null;
// this.workspaces = [];
// // Optionally clear IndexedDB data
// this.indexedDBService.init().then(() => {
// this.indexedDBService.saveWorkspaces([]);
// });
// }
// }
@@ -1,28 +0,0 @@
import { makeObservable, observable } from "mobx";
export interface IWorkspaceStore {
id: string;
name: string;
createdAt: string;
updatedAt: string;
}
export class WorkspaceStore implements IWorkspaceStore {
id: string;
name: string;
createdAt: string;
updatedAt: string;
constructor(data: IWorkspaceStore) {
makeObservable(this, {
id: observable.ref,
name: observable.ref,
createdAt: observable.ref,
updatedAt: observable.ref,
});
this.id = data.id;
this.name = data.name;
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"extends": "@plane/typescript-config/react-library.json",
"compilerOptions": {
"jsx": "react",
"lib": ["esnext", "dom"],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["./src"],
"exclude": ["dist", "build", "node_modules"]
}
@@ -1,8 +1,8 @@
{
"name": "@plane/tailwind-config",
"name": "tailwind-config-custom",
"version": "0.24.1",
"description": "common tailwind configuration across monorepo",
"main": "tailwind.config.js",
"main": "index.js",
"private": true,
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
@@ -10,7 +10,7 @@
"postcss": "^8.4.38",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.3.0",
"tailwindcss": "^3.4.17",
"tailwindcss": "^3.2.7",
"tailwindcss-animate": "^1.0.6"
}
}
+3 -2
View File
@@ -104,7 +104,6 @@ export interface ICycle extends TProgressSnapshot {
project_detail: IProjectDetails;
progress: any[];
version: number;
pending_issues: number;
}
export interface CycleIssueResponse {
@@ -121,7 +120,9 @@ export interface CycleIssueResponse {
sub_issues_count: number;
}
export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined;
export type SelectCycleType =
| (ICycle & { actionType: "edit" | "delete" | "create-issue" })
| undefined;
export type CycleDateCheckData = {
start_date: string;
+4 -4
View File
@@ -1,7 +1,7 @@
import { TLogoProps } from "./common";
import { TIssuePriorities } from "./issues";
export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project" | "workspace_page";
export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project";
export type THomeWidgetKeys = "quick_links" | "recents" | "my_stickies" | "quick_tutorial" | "new_at_plane";
export type THomeWidgetProps = {
@@ -12,9 +12,9 @@ export type TPageEntityData = {
id: string;
name: string;
logo_props: TLogoProps;
project_id?: string;
project_id: string;
owned_by: string;
project_identifier?: string;
project_identifier: string;
};
export type TProjectEntityData = {
@@ -39,7 +39,7 @@ export type TIssueEntityData = {
export type TActivityEntityData = {
id: string;
entity_name: "page" | "project" | "issue" | "workspace_page";
entity_name: "page" | "project" | "issue";
entity_identifier: string;
visited_at: string;
entity_data: TPageEntityData | TProjectEntityData | TIssueEntityData;
+32 -1
View File
@@ -2,6 +2,23 @@ import { TPaginationInfo } from "./common";
import { TIssuePriorities } from "./issues";
import { TIssue } from "./issues/base";
enum EInboxIssueCurrentTab {
OPEN = "open",
CLOSED = "closed",
}
enum EInboxIssueStatus {
PENDING = -2,
DECLINED = -1,
SNOOZED = 0,
ACCEPTED = 1,
DUPLICATE = 2,
}
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
export type TInboxIssueStatus = EInboxIssueStatus;
// filters
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
@@ -21,7 +38,10 @@ export type TInboxIssueFilter = {
// sorting filters
export type TInboxIssueSortingKeys = "order_by" | "sort_by";
export type TInboxIssueSortingOrderByKeys = "issue__created_at" | "issue__updated_at" | "issue__sequence_id";
export type TInboxIssueSortingOrderByKeys =
| "issue__created_at"
| "issue__updated_at"
| "issue__sequence_id";
export type TInboxIssueSortingSortByKeys = "asc" | "desc";
@@ -58,6 +78,17 @@ export type TInboxDuplicateIssueDetails = {
name: string;
};
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
duplicate_to: string | undefined;
source: string;
issue: TIssue;
created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
};
export type TInboxIssuePaginationInfo = TPaginationInfo & {
total_results: number;
};
+1 -1
View File
@@ -15,7 +15,7 @@ import type {
TIssueGroupByOptions,
TIssueOrderByOptions,
TIssueGroupingFilters,
TIssueExtraOptions,
TIssueExtraOptions
} from "@plane/types";
export interface IIssueCycle {
+4 -12
View File
@@ -1,16 +1,8 @@
import { TLogoProps } from "./common";
export type TSticky = {
created_at?: string | undefined;
created_by?: string | undefined;
background_color?: string | null | undefined;
description?: object | undefined;
description_html?: string | undefined;
id: string;
logo_props: TLogoProps | undefined;
name?: string;
sort_order: number | undefined;
updated_at?: string | undefined;
updated_by?: string | undefined;
workspace: string | undefined;
description_html?: string;
color?: string;
createdAt?: Date;
updatedAt?: Date;
};
+2 -1
View File
@@ -47,7 +47,6 @@
"devDependencies": {
"@chromatic-com/storybook": "^1.4.0",
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@storybook/addon-essentials": "^8.1.1",
"@storybook/addon-interactions": "^8.1.1",
@@ -69,6 +68,8 @@
"postcss-cli": "^11.0.0",
"postcss-nested": "^6.0.1",
"storybook": "^8.1.1",
"tailwind-config-custom": "*",
"tailwindcss": "^3.4.3",
"tsup": "^7.2.0",
"typescript": "5.3.3"
},
+1 -2
View File
@@ -1,2 +1 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
module.exports = require("@plane/tailwind-config/postcss.config.js");
module.exports = require("tailwind-config-custom/postcss.config");
+1 -2
View File
@@ -1,5 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
const config = require("@plane/tailwind-config/tailwind.config.js");
const config = require("tailwind-config-custom/tailwind.config");
config.content.files = ["./src/**/*.{js,ts,jsx,tsx}"];
+4
View File
@@ -3,4 +3,8 @@ module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
rules: {},
};
+1 -3
View File
@@ -32,9 +32,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} />
</head>
<body>
<AppProvider>
<>{children}</>
</AppProvider>
<AppProvider>{children}</AppProvider>
</body>
</html>
);
@@ -2,9 +2,8 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
import { PriorityIcon } from "@plane/ui";
// components
import { FilterHeader, FilterOption } from "./helpers";
@@ -19,9 +18,6 @@ type Props = {
export const FilterPriority: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
// hooks
const { t } = useTranslation();
const [previewEnabled, setPreviewEnabled] = useState(true);
const appliedFiltersCount = appliedFilters?.length ?? 0;
@@ -44,11 +40,11 @@ export const FilterPriority: React.FC<Props> = observer((props) => {
isChecked={appliedFilters?.includes(priority.key) ? true : false}
onClick={() => handleUpdate(priority.key)}
icon={<PriorityIcon priority={priority.key} className="h-3.5 w-3.5" />}
title={t(priority.titleTranslationKey)}
title={priority.title}
/>
))
) : (
<p className="text-xs italic text-custom-text-400">{t("common.no_matches_found")}</p>
<p className="text-xs italic text-custom-text-400">No matches found</p>
)}
</div>
)}
@@ -94,7 +94,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
<HeaderGroupByCard
groupBy={groupBy}
icon={subList.icon as any}
icon={subList.icon}
title={subList.name}
count={getGroupIssueCount(subList.id, undefined, false) ?? 0}
/>
@@ -131,14 +131,10 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
if (subGroupByVisibilityToggle === false) return <></>;
return (
<div key={`${subGroupBy}_${group.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
groupBy={groupBy}
icon={group.icon}
title={group.name}
count={groupCount}
/>
<HeaderGroupByCard groupBy={groupBy} icon={group.icon} title={group.name} count={groupCount} />
</div>
);
})}
@@ -266,7 +262,7 @@ const SubGroup: React.FC<ISubGroup> = observer((props) => {
<div className="sticky top-[50px] z-[3] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
<div className="sticky left-0 flex-shrink-0">
<HeaderSubGroupByCard
icon={group.icon as any}
icon={group.icon}
title={group.name || ""}
count={issueCount}
isExpanded={isExpanded}
@@ -2,7 +2,6 @@
import { Fragment, MutableRefObject, forwardRef, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
// plane types
import { IGroupByColumn, TIssueGroupByOptions, IIssueDisplayProperties, TPaginationData, TLoader } from "@plane/types";
// plane utils
@@ -63,8 +62,6 @@ export const ListGroup = observer((props: Props) => {
} = props;
const [isExpanded, setIsExpanded] = useState(true);
const groupRef = useRef<HTMLDivElement | null>(null);
// hooks
const { t } = useTranslation();
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
@@ -88,7 +85,7 @@ export const ListGroup = observer((props: Props) => {
}
onClick={() => loadMoreIssues(group.id)}
>
{t("common.load_more")} &darr;
Load More &darr;
</div>
);
@@ -1,6 +1,5 @@
"use client";
import { useTranslation } from "@plane/i18n";
// types
import { TIssuePriorities } from "@plane/types";
import { Tooltip } from "@plane/ui";
@@ -14,19 +13,17 @@ export const IssueBlockPriority = ({
priority: TIssuePriorities | null;
shouldShowName?: boolean;
}) => {
// hooks
const { t } = useTranslation();
const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null;
if (priority_detail === null) return <></>;
return (
<Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}>
<Tooltip tooltipHeading="Priority" tooltipContent={priority_detail?.title}>
<div className="flex items-center relative w-full h-full">
<div className={`grid h-5 w-5 place-items-center rounded border-[0.5px] gap-2 ${priority_detail?.className}`}>
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
</div>
{shouldShowName && <span className="pl-2 text-sm">{t(priority_detail?.titleTranslationKey || "")}</span>}
{shouldShowName && <span className="pl-2 text-sm">{priority_detail?.title}</span>}
</div>
</Tooltip>
);
@@ -5,8 +5,6 @@ import { observer } from "mobx-react";
import { useRouter, useSearchParams } from "next/navigation";
// ui
import { SITES_ISSUE_LAYOUTS } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
import { Tooltip } from "@plane/ui";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
@@ -21,8 +19,6 @@ type Props = {
export const IssuesLayoutSelection: FC<Props> = observer((props) => {
const { anchor } = props;
// hooks
const { t } = useTranslation();
// router
const router = useRouter();
const searchParams = useSearchParams();
@@ -49,7 +45,7 @@ export const IssuesLayoutSelection: FC<Props> = observer((props) => {
if (!layoutOptions[layout.key]) return;
return (
<Tooltip key={layout.key} tooltipContent={t(layout.titleTranslationKey)}>
<Tooltip key={layout.key} tooltipContent={layout.title}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
@@ -3,7 +3,6 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { CalendarCheck2, Signal } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// ui
import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
import { getIssuePriorityFilters } from "@plane/utils";
@@ -25,8 +24,6 @@ type Props = {
};
export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDetails, mode }) => {
// hooks
const { t } = useTranslation();
const { getStateById } = useStates();
const state = getStateById(issueDetails?.state_id ?? undefined);
@@ -98,7 +95,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDet
<Icon iconName={priority?.icon} />
</span>
)}
<span>{t(priority?.titleTranslationKey || "common.none")}</span>
<span>{priority?.title ?? "None"}</span>
</div>
</div>
</div>
+1 -2
View File
@@ -20,7 +20,6 @@
"@mui/material": "^5.14.1",
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/i18n": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/services": "*",
@@ -51,7 +50,6 @@
},
"devDependencies": {
"@plane/eslint-config": "*",
"@plane/tailwind-config": "*",
"@plane/typescript-config": "*",
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.17.1",
@@ -62,6 +60,7 @@
"@types/uuid": "^9.0.1",
"@types/zxcvbn": "^4.4.4",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"tailwind-config-custom": "*",
"typescript": "5.3.3"
}
}
+1 -2
View File
@@ -1,2 +1 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
module.exports = require("@plane/tailwind-config/postcss.config.js");
module.exports = require("tailwind-config-custom/postcss.config");
+1 -47
View File
@@ -300,53 +300,6 @@
--color-sidebar-border-300: var(--color-border-300); /* strong sidebar border- 1 */
--color-sidebar-border-400: var(--color-border-400); /* strong sidebar border- 2 */
}
/* stickies and editor colors */
:root {
/* text colors */
--editor-colors-gray-text: #5c5e63;
--editor-colors-peach-text: #ff5b59;
--editor-colors-pink-text: #f65385;
--editor-colors-orange-text: #fd9038;
--editor-colors-green-text: #0fc27b;
--editor-colors-light-blue-text: #17bee9;
--editor-colors-dark-blue-text: #266df0;
--editor-colors-purple-text: #9162f9;
/* end text colors */
/* background colors */
--editor-colors-gray-background: #d6d6d8;
--editor-colors-peach-background: #ffd5d7;
--editor-colors-pink-background: #fdd4e3;
--editor-colors-orange-background: #ffe3cd;
--editor-colors-green-background: #c3f0de;
--editor-colors-light-blue-background: #c5eff9;
--editor-colors-dark-blue-background: #c9dafb;
--editor-colors-purple-background: #e3d8fd;
/* end background colors */
}
/* background colors */
[data-theme*="light"] {
--editor-colors-gray-background: #d6d6d8;
--editor-colors-peach-background: #ffd5d7;
--editor-colors-pink-background: #fdd4e3;
--editor-colors-orange-background: #ffe3cd;
--editor-colors-green-background: #c3f0de;
--editor-colors-light-blue-background: #c5eff9;
--editor-colors-dark-blue-background: #c9dafb;
--editor-colors-purple-background: #e3d8fd;
}
[data-theme*="dark"] {
--editor-colors-gray-background: #404144;
--editor-colors-peach-background: #593032;
--editor-colors-pink-background: #562e3d;
--editor-colors-orange-background: #583e2a;
--editor-colors-green-background: #1d4a3b;
--editor-colors-light-blue-background: #1f495c;
--editor-colors-dark-blue-background: #223558;
--editor-colors-purple-background: #3d325a;
}
/* end background colors */
}
* {
@@ -402,6 +355,7 @@ body {
-webkit-background-clip: text;
}
@-moz-document url-prefix() {
* {
scrollbar-width: none;

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