Compare commits
81
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
409b2b78f1 | ||
|
|
ebc2bdcd3a | ||
|
|
11b222ece8 | ||
|
|
c1a078ef3f | ||
|
|
ad11a34efc | ||
|
|
9c28db8b7b | ||
|
|
32d5fea3d3 | ||
|
|
6adc721b34 | ||
|
|
531748dcc3 | ||
|
|
9965f48ba7 | ||
|
|
d15d7549f7 | ||
|
|
8fcffd2338 | ||
|
|
07e937cd8e | ||
|
|
1f1b421735 | ||
|
|
5a43ec8411 | ||
|
|
c86e7e02bc | ||
|
|
d91d7a2f60 | ||
|
|
b3b285b1e5 | ||
|
|
11debee402 | ||
|
|
1608e4f122 | ||
|
|
edeeee1227 | ||
|
|
9ff238816b | ||
|
|
6bd5caf008 | ||
|
|
c021aff58f | ||
|
|
683be55883 | ||
|
|
970ce8cf26 | ||
|
|
cbbe1a4e4d | ||
|
|
6a74677cc9 | ||
|
|
f6ea4f931d | ||
|
|
950fcfdb40 | ||
|
|
053c895120 | ||
|
|
245167e8aa | ||
|
|
6be3f0ea73 | ||
|
|
14d2d69120 | ||
|
|
570a9e319e | ||
|
|
469a027bb6 | ||
|
|
8c99a7df88 | ||
|
|
f34f078bd2 | ||
|
|
0fe2549bc6 | ||
|
|
118964de01 | ||
|
|
9f37f1ef0e | ||
|
|
986f29d1f2 | ||
|
|
1113f9fc19 | ||
|
|
ef3ec7274c | ||
|
|
a0a45b7916 | ||
|
|
2792d48288 | ||
|
|
b2ccca0567 | ||
|
|
2e822b38e4 | ||
|
|
e570fe404f | ||
|
|
48b613ae66 | ||
|
|
e70105235b | ||
|
|
7766e8b5cf | ||
|
|
16d63abcdc | ||
|
|
0568b8d583 | ||
|
|
64da29b0d9 | ||
|
|
7c336a65c4 | ||
|
|
2242a85e5c | ||
|
|
323920a358 | ||
|
|
151fc8389e | ||
|
|
0f828fd5e0 | ||
|
|
67cbe94d4a | ||
|
|
322af8c436 | ||
|
|
41c2aefad4 | ||
|
|
445c819fbd | ||
|
|
046a8a1bcf | ||
|
|
099a1cc12b | ||
|
|
a0a697401b | ||
|
|
cb92108bf4 | ||
|
|
01b685ea57 | ||
|
|
b16a585102 | ||
|
|
4a97d7c28c | ||
|
|
141cb17e8a | ||
|
|
26b62c4a70 | ||
|
|
e388a9a279 | ||
|
|
a3a580923c | ||
|
|
b4bc49971c | ||
|
|
461e099bbc | ||
|
|
45e25ce18b | ||
|
|
4d88dbaf49 | ||
|
+4 |
e61ff879c4 | ||
|
|
adeb7d977d |
+2
-1
@@ -2,6 +2,7 @@
|
||||
*.pyc
|
||||
.env
|
||||
venv
|
||||
.venv
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
npm-debug.log
|
||||
@@ -14,4 +15,4 @@ build/
|
||||
out/
|
||||
**/out/
|
||||
dist/
|
||||
**/dist/
|
||||
**/dist/
|
||||
|
||||
@@ -290,5 +290,6 @@ jobs:
|
||||
${{ github.workspace }}/deploy/selfhost/setup.sh
|
||||
${{ github.workspace }}/deploy/selfhost/swarm.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore-airgapped.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
|
||||
+3
-3
@@ -69,14 +69,14 @@ chmod +x setup.sh
|
||||
docker compose -f docker-compose-local.yml up
|
||||
```
|
||||
|
||||
5. Start web apps:
|
||||
4. Start web apps:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
6. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin
|
||||
7. Open up your browser to http://localhost:3000 then log in using the same credentials from the previous step
|
||||
5. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin
|
||||
6. Open up your browser to http://localhost:3000 then log in using the same credentials from the previous step
|
||||
|
||||
That’s it! You’re all set to begin coding. Remember to refresh your browser if changes don’t auto-reload. Happy contributing! 🎉
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ type Props = {
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
enum ESendEmailSteps {
|
||||
SEND_EMAIL = "SEND_EMAIL",
|
||||
SUCCESS = "SUCCESS",
|
||||
FAILED = "FAILED",
|
||||
}
|
||||
const ESendEmailSteps = {
|
||||
SEND_EMAIL: "SEND_EMAIL",
|
||||
SUCCESS: "SUCCESS",
|
||||
FAILED: "FAILED",
|
||||
} as const;
|
||||
|
||||
type ESendEmailSteps = typeof ESendEmailSteps[keyof typeof ESendEmailSteps];
|
||||
|
||||
const instanceService = new InstanceService();
|
||||
|
||||
|
||||
@@ -16,14 +16,16 @@ import { Banner, PasswordStrengthMeter } from "@/components/common";
|
||||
const authService = new AuthService();
|
||||
|
||||
// error codes
|
||||
enum EErrorCodes {
|
||||
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
||||
ADMIN_ALREADY_EXIST = "ADMIN_ALREADY_EXIST",
|
||||
REQUIRED_EMAIL_PASSWORD_FIRST_NAME = "REQUIRED_EMAIL_PASSWORD_FIRST_NAME",
|
||||
INVALID_EMAIL = "INVALID_EMAIL",
|
||||
INVALID_PASSWORD = "INVALID_PASSWORD",
|
||||
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS",
|
||||
}
|
||||
const EErrorCodes = {
|
||||
INSTANCE_NOT_CONFIGURED: "INSTANCE_NOT_CONFIGURED",
|
||||
ADMIN_ALREADY_EXIST: "ADMIN_ALREADY_EXIST",
|
||||
REQUIRED_EMAIL_PASSWORD_FIRST_NAME: "REQUIRED_EMAIL_PASSWORD_FIRST_NAME",
|
||||
INVALID_EMAIL: "INVALID_EMAIL",
|
||||
INVALID_PASSWORD: "INVALID_PASSWORD",
|
||||
USER_ALREADY_EXISTS: "USER_ALREADY_EXISTS",
|
||||
} as const;
|
||||
|
||||
type EErrorCodes = typeof EErrorCodes[keyof typeof EErrorCodes];
|
||||
|
||||
type TError = {
|
||||
type: EErrorCodes | undefined;
|
||||
@@ -144,7 +146,7 @@ export const InstanceSetupForm: FC = (props) => {
|
||||
|
||||
{errorData.type &&
|
||||
errorData?.message &&
|
||||
![EErrorCodes.INVALID_EMAIL, EErrorCodes.INVALID_PASSWORD].includes(errorData.type) && (
|
||||
!([EErrorCodes.INVALID_EMAIL, EErrorCodes.INVALID_PASSWORD] as EErrorCodes[]).includes(errorData.type) && (
|
||||
<Banner type="error" message={errorData?.message} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -18,13 +18,15 @@ import { AuthBanner } from "../authentication";
|
||||
const authService = new AuthService();
|
||||
|
||||
// error codes
|
||||
enum EErrorCodes {
|
||||
INSTANCE_NOT_CONFIGURED = "INSTANCE_NOT_CONFIGURED",
|
||||
REQUIRED_EMAIL_PASSWORD = "REQUIRED_EMAIL_PASSWORD",
|
||||
INVALID_EMAIL = "INVALID_EMAIL",
|
||||
USER_DOES_NOT_EXIST = "USER_DOES_NOT_EXIST",
|
||||
AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED",
|
||||
}
|
||||
const EErrorCodes = {
|
||||
INSTANCE_NOT_CONFIGURED: "INSTANCE_NOT_CONFIGURED",
|
||||
REQUIRED_EMAIL_PASSWORD: "REQUIRED_EMAIL_PASSWORD",
|
||||
INVALID_EMAIL: "INVALID_EMAIL",
|
||||
USER_DOES_NOT_EXIST: "USER_DOES_NOT_EXIST",
|
||||
AUTHENTICATION_FAILED: "AUTHENTICATION_FAILED",
|
||||
} as const;
|
||||
|
||||
type EErrorCodes = typeof EErrorCodes[keyof typeof EErrorCodes];
|
||||
|
||||
type TError = {
|
||||
type: EErrorCodes | undefined;
|
||||
|
||||
@@ -20,13 +20,15 @@ import githubDarkModeImage from "@/public/logos/github-white.png";
|
||||
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
|
||||
import GoogleLogo from "@/public/logos/google-logo.svg";
|
||||
|
||||
export enum EErrorAlertType {
|
||||
BANNER_ALERT = "BANNER_ALERT",
|
||||
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL = "INLINE_EMAIL",
|
||||
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||
}
|
||||
export const EErrorAlertType = {
|
||||
BANNER_ALERT: "BANNER_ALERT",
|
||||
INLINE_FIRST_NAME: "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL: "INLINE_EMAIL",
|
||||
INLINE_PASSWORD: "INLINE_PASSWORD",
|
||||
INLINE_EMAIL_CODE: "INLINE_EMAIL_CODE",
|
||||
} as const;
|
||||
|
||||
export type EErrorAlertType = typeof EErrorAlertType[keyof typeof EErrorAlertType];
|
||||
|
||||
const errorCodeMessages: {
|
||||
[key in EAdminAuthErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Admin UI for Plane",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -31,7 +31,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "^14.2.28",
|
||||
"next": "^14.2.29",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18.3.1",
|
||||
@@ -50,6 +50,6 @@
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/zxcvbn": "^4.4.4",
|
||||
"typescript": "5.3.3"
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
|
||||
@@ -58,7 +58,7 @@ from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.host import base_host
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
|
||||
from plane.bgtasks.work_item_link_task import crawl_work_item_link_title
|
||||
|
||||
class WorkspaceIssueAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
@@ -692,6 +692,9 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
serializer = IssueLinkSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, issue_id=issue_id)
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
|
||||
link = IssueLink.objects.get(pk=serializer.data["id"])
|
||||
link.created_by_id = request.data.get("created_by", request.user.id)
|
||||
@@ -719,6 +722,9 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="link.activity.updated",
|
||||
requested_data=requested_data,
|
||||
|
||||
@@ -148,10 +148,13 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||
original_role = serializers.IntegerField(source='role', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = ("id", "role", "member", "project")
|
||||
fields = ("id", "role", "member", "project", "original_role", "created_at")
|
||||
read_only_fields = ["original_role", "created_at"]
|
||||
|
||||
|
||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||
|
||||
@@ -3,11 +3,22 @@ from rest_framework import serializers
|
||||
|
||||
# Module import
|
||||
from plane.db.models import Account, Profile, User, Workspace, WorkspaceMemberInvite
|
||||
from plane.utils.url import contains_url
|
||||
|
||||
from .base import BaseSerializer
|
||||
|
||||
|
||||
class UserSerializer(BaseSerializer):
|
||||
def validate_first_name(self, value):
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("First name cannot contain a URL.")
|
||||
return value
|
||||
|
||||
def validate_last_name(self, value):
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("Last name cannot contain a URL.")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
# Exclude password field from the serializer
|
||||
@@ -99,11 +110,16 @@ class UserMeSettingsSerializer(BaseSerializer):
|
||||
workspace_member__member=obj.id,
|
||||
workspace_member__is_active=True,
|
||||
).first()
|
||||
logo_asset_url = workspace.logo_asset.asset_url if workspace.logo_asset is not None else ""
|
||||
return {
|
||||
"last_workspace_id": profile.last_workspace_id,
|
||||
"last_workspace_slug": (
|
||||
workspace.slug if workspace is not None else ""
|
||||
),
|
||||
"last_workspace_name": (
|
||||
workspace.name if workspace is not None else ""
|
||||
),
|
||||
"last_workspace_logo": (logo_asset_url),
|
||||
"fallback_workspace_id": profile.last_workspace_id,
|
||||
"fallback_workspace_slug": (
|
||||
workspace.slug if workspace is not None else ""
|
||||
|
||||
@@ -25,10 +25,12 @@ from plane.db.models import (
|
||||
WorkspaceUserPreference,
|
||||
)
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
from plane.utils.url import contains_url
|
||||
|
||||
# Django imports
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
import re
|
||||
|
||||
|
||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
@@ -36,10 +38,21 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
logo_url = serializers.CharField(read_only=True)
|
||||
role = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validate_name(self, value):
|
||||
# Check if the name contains a URL
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("Name must not contain URLs")
|
||||
return value
|
||||
|
||||
def validate_slug(self, value):
|
||||
# Check if the slug is restricted
|
||||
if value in RESTRICTED_WORKSPACE_SLUGS:
|
||||
raise serializers.ValidationError("Slug is not valid")
|
||||
# Slug should only contain alphanumeric characters, hyphens, and underscores
|
||||
if not re.match(r"^[a-zA-Z0-9_-]+$", value):
|
||||
raise serializers.ValidationError(
|
||||
"Slug can only contain letters, numbers, hyphens (-), and underscores (_)"
|
||||
)
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -12,6 +12,7 @@ from plane.app.views import (
|
||||
AssetRestoreEndpoint,
|
||||
ProjectAssetEndpoint,
|
||||
ProjectBulkAssetEndpoint,
|
||||
AssetCheckEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,5 +82,11 @@ urlpatterns = [
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/projects/<uuid:project_id>/<uuid:entity_id>/bulk/",
|
||||
ProjectBulkAssetEndpoint.as_view(),
|
||||
name="bulk-asset-update",
|
||||
),
|
||||
path(
|
||||
"assets/v2/workspaces/<str:slug>/check/<uuid:asset_id>/",
|
||||
AssetCheckEndpoint.as_view(),
|
||||
name="asset-check",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -106,6 +106,7 @@ from .asset.v2 import (
|
||||
AssetRestoreEndpoint,
|
||||
ProjectAssetEndpoint,
|
||||
ProjectBulkAssetEndpoint,
|
||||
AssetCheckEndpoint,
|
||||
)
|
||||
from .issue.base import (
|
||||
IssueListEndpoint,
|
||||
|
||||
@@ -707,3 +707,14 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
|
||||
pass
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class AssetCheckEndpoint(BaseAPIView):
|
||||
"""Endpoint to check if an asset exists."""
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def get(self, request, slug, asset_id):
|
||||
asset = FileAsset.all_objects.filter(
|
||||
id=asset_id, workspace__slug=slug, deleted_at__isnull=True
|
||||
).exists()
|
||||
return Response({"exists": asset}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -15,6 +15,7 @@ from plane.app.serializers import IssueLinkSerializer
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import IssueLink
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.bgtasks.work_item_link_task import crawl_work_item_link_title
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
@@ -44,6 +45,9 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
serializer = IssueLinkSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, issue_id=issue_id)
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="link.activity.created",
|
||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
@@ -55,6 +59,10 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
|
||||
issue_link = self.get_queryset().get(id=serializer.data.get("id"))
|
||||
serializer = IssueLinkSerializer(issue_link)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -66,9 +74,14 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
current_instance = json.dumps(
|
||||
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
|
||||
issue_activity.delay(
|
||||
type="link.activity.updated",
|
||||
requested_data=requested_data,
|
||||
@@ -80,6 +93,9 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
issue_link = self.get_queryset().get(id=serializer.data.get("id"))
|
||||
serializer = IssueLinkSerializer(issue_link)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@@ -168,6 +168,8 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
member__is_bot=False,
|
||||
is_active=True,
|
||||
member__member_workspace__workspace__slug=slug,
|
||||
member__member_workspace__is_active=True,
|
||||
).select_related("project", "member", "workspace")
|
||||
|
||||
serializer = ProjectMemberRoleSerializer(
|
||||
@@ -313,7 +315,11 @@ class UserProjectRolesEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug):
|
||||
project_members = ProjectMember.objects.filter(
|
||||
workspace__slug=slug, member_id=request.user.id, is_active=True
|
||||
workspace__slug=slug,
|
||||
member_id=request.user.id,
|
||||
is_active=True,
|
||||
member__member_workspace__workspace__slug=slug,
|
||||
member__member_workspace__is_active=True,
|
||||
).values("project_id", "role")
|
||||
|
||||
project_members = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import csv
|
||||
import io
|
||||
import os
|
||||
from datetime import date
|
||||
import uuid
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.db import IntegrityError
|
||||
@@ -35,6 +36,7 @@ from plane.db.models import (
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
WorkspaceTheme,
|
||||
Profile,
|
||||
)
|
||||
from plane.app.permissions import ROLE, allow_permission
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -43,6 +45,7 @@ from django.views.decorators.vary import vary_on_cookie
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.workspace_seed_task import workspace_seed
|
||||
from plane.utils.url import contains_url
|
||||
|
||||
|
||||
class WorkSpaceViewSet(BaseViewSet):
|
||||
@@ -109,6 +112,12 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if contains_url(name):
|
||||
return Response(
|
||||
{"error": "Name cannot contain a URL"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save(owner=request.user)
|
||||
# Create Workspace member
|
||||
@@ -150,8 +159,18 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
def remove_last_workspace_ids_from_user_settings(self, id: uuid.UUID) -> None:
|
||||
"""
|
||||
Remove the last workspace id from the user settings
|
||||
"""
|
||||
Profile.objects.filter(last_workspace_id=id).update(last_workspace_id=None)
|
||||
return
|
||||
|
||||
@allow_permission([ROLE.ADMIN], level="WORKSPACE")
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
# Get the workspace
|
||||
workspace = self.get_object()
|
||||
self.remove_last_workspace_ids_from_user_settings(workspace.id)
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -159,8 +178,6 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
search_fields = ["name"]
|
||||
filterset_fields = ["owner"]
|
||||
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def get(self, request):
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
member_count = (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Django imports
|
||||
from django.db.models import Count, Q, OuterRef, Subquery, IntegerField
|
||||
from django.utils import timezone
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
# Third party modules
|
||||
@@ -133,7 +134,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
# Deactivate the users from the projects where the user is part of
|
||||
_ = ProjectMember.objects.filter(
|
||||
workspace__slug=slug, member_id=workspace_member.member_id, is_active=True
|
||||
).update(is_active=False)
|
||||
).update(is_active=False, updated_at=timezone.now())
|
||||
|
||||
workspace_member.is_active = False
|
||||
workspace_member.save()
|
||||
@@ -194,7 +195,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
# # Deactivate the users from the projects where the user is part of
|
||||
_ = ProjectMember.objects.filter(
|
||||
workspace__slug=slug, member_id=workspace_member.member_id, is_active=True
|
||||
).update(is_active=False)
|
||||
).update(is_active=False, updated_at=timezone.now())
|
||||
|
||||
# # Deactivate the user
|
||||
workspace_member.is_active = False
|
||||
|
||||
@@ -284,6 +284,7 @@ def send_email_notification(
|
||||
"project": str(issue.project.name),
|
||||
"user_preference": f"{base_api}/profile/preferences/email",
|
||||
"comments": comments,
|
||||
"entity_type": "issue",
|
||||
}
|
||||
html_content = render_to_string(
|
||||
"emails/notifications/issue-updates.html", context
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# Python imports
|
||||
import logging
|
||||
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urlparse, urljoin
|
||||
import base64
|
||||
import ipaddress
|
||||
from typing import Dict, Any
|
||||
from typing import Optional
|
||||
from plane.db.models import IssueLink
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
logger = logging.getLogger("plane.worker")
|
||||
|
||||
|
||||
DEFAULT_FAVICON = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpbmstaWNvbiBsdWNpZGUtbGluayI+PHBhdGggZD0iTTEwIDEzYTUgNSAwIDAgMCA3LjU0LjU0bDMtM2E1IDUgMCAwIDAtNy4wNy03LjA3bC0xLjcyIDEuNzEiLz48cGF0aCBkPSJNMTQgMTFhNSA1IDAgMCAwLTcuNTQtLjU0bC0zIDNhNSA1IDAgMCAwIDcuMDcgNy4wN2wxLjcxLTEuNzEiLz48L3N2Zz4=" # noqa: E501
|
||||
|
||||
def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Crawls a URL to extract the title and favicon.
|
||||
|
||||
Args:
|
||||
url (str): The URL to crawl
|
||||
|
||||
Returns:
|
||||
str: JSON string containing title and base64-encoded favicon
|
||||
"""
|
||||
try:
|
||||
# Prevent access to private IP ranges
|
||||
parsed = urlparse(url)
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(parsed.hostname)
|
||||
if ip.is_private or ip.is_loopback or ip.is_reserved:
|
||||
raise ValueError("Access to private/internal networks is not allowed")
|
||||
except ValueError:
|
||||
# Not an IP address, continue with domain validation
|
||||
pass
|
||||
|
||||
# Set up headers to mimic a real browser
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa: E501
|
||||
}
|
||||
|
||||
soup = None
|
||||
title = None
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=1)
|
||||
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
title_tag = soup.find("title")
|
||||
title = title_tag.get_text().strip() if title_tag else None
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.warning(f"Failed to fetch HTML for title: {str(e)}")
|
||||
|
||||
# Fetch and encode favicon
|
||||
favicon_base64 = fetch_and_encode_favicon(headers, soup, url)
|
||||
|
||||
# Prepare result
|
||||
result = {
|
||||
"title": title,
|
||||
"favicon": favicon_base64["favicon_base64"],
|
||||
"url": url,
|
||||
"favicon_url": favicon_base64["favicon_url"],
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return {
|
||||
"error": f"Unexpected error: {str(e)}",
|
||||
"title": None,
|
||||
"favicon": None,
|
||||
"url": url,
|
||||
}
|
||||
|
||||
|
||||
def find_favicon_url(soup: Optional[BeautifulSoup], base_url: str) -> Optional[str]:
|
||||
"""
|
||||
Find the favicon URL from HTML soup.
|
||||
|
||||
Args:
|
||||
soup: BeautifulSoup object
|
||||
base_url: Base URL for resolving relative paths
|
||||
|
||||
Returns:
|
||||
str: Absolute URL to favicon or None
|
||||
"""
|
||||
|
||||
if soup is not None:
|
||||
# Look for various favicon link tags
|
||||
favicon_selectors = [
|
||||
'link[rel="icon"]',
|
||||
'link[rel="shortcut icon"]',
|
||||
'link[rel="apple-touch-icon"]',
|
||||
'link[rel="apple-touch-icon-precomposed"]',
|
||||
]
|
||||
|
||||
for selector in favicon_selectors:
|
||||
favicon_tag = soup.select_one(selector)
|
||||
if favicon_tag and favicon_tag.get("href"):
|
||||
return urljoin(base_url, favicon_tag["href"])
|
||||
|
||||
# Fallback to /favicon.ico
|
||||
parsed_url = urlparse(base_url)
|
||||
fallback_url = f"{parsed_url.scheme}://{parsed_url.netloc}/favicon.ico"
|
||||
|
||||
# Check if fallback exists
|
||||
try:
|
||||
response = requests.head(fallback_url, timeout=2)
|
||||
if response.status_code == 200:
|
||||
return fallback_url
|
||||
except requests.RequestException as e:
|
||||
log_exception(e)
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def fetch_and_encode_favicon(
|
||||
headers: Dict[str, str], soup: Optional[BeautifulSoup], url: str
|
||||
) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
Fetch favicon and encode it as base64.
|
||||
|
||||
Args:
|
||||
favicon_url: URL to the favicon
|
||||
headers: Request headers
|
||||
|
||||
Returns:
|
||||
str: Base64 encoded favicon with data URI prefix or None
|
||||
"""
|
||||
try:
|
||||
favicon_url = find_favicon_url(soup, url)
|
||||
if favicon_url is None:
|
||||
return {
|
||||
"favicon_url": None,
|
||||
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
|
||||
}
|
||||
|
||||
response = requests.get(favicon_url, headers=headers, timeout=1)
|
||||
|
||||
# Get content type
|
||||
content_type = response.headers.get("content-type", "image/x-icon")
|
||||
|
||||
# Convert to base64
|
||||
favicon_base64 = base64.b64encode(response.content).decode("utf-8")
|
||||
|
||||
# Return as data URI
|
||||
return {
|
||||
"favicon_url": favicon_url,
|
||||
"favicon_base64": f"data:{content_type};base64,{favicon_base64}",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch favicon: {e}")
|
||||
return {
|
||||
"favicon_url": None,
|
||||
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
|
||||
}
|
||||
|
||||
|
||||
@shared_task
|
||||
def crawl_work_item_link_title(id: str, url: str) -> None:
|
||||
meta_data = crawl_work_item_link_title_and_favicon(url)
|
||||
issue_link = IssueLink.objects.get(id=id)
|
||||
|
||||
issue_link.metadata = meta_data
|
||||
|
||||
issue_link.save()
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.21 on 2025-06-06 12:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0096_user_is_email_valid_user_masked_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='external_source',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -122,6 +122,9 @@ class Project(BaseModel):
|
||||
# timezone
|
||||
TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
|
||||
timezone = models.CharField(max_length=255, default="UTC", choices=TIMEZONE_CHOICES)
|
||||
# external_id for imports
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
@property
|
||||
def cover_image_url(self):
|
||||
|
||||
@@ -4,6 +4,14 @@ from typing import Optional
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
|
||||
def contains_url(value: str) -> bool:
|
||||
"""
|
||||
Check if the value contains a URL.
|
||||
"""
|
||||
url_pattern = re.compile(r"https?://|www\\.")
|
||||
return bool(url_pattern.search(value))
|
||||
|
||||
|
||||
def is_valid_url(url: str) -> bool:
|
||||
"""
|
||||
Validates whether the given string is a well-formed URL.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# base requirements
|
||||
|
||||
# django
|
||||
Django==4.2.21
|
||||
Django==4.2.22
|
||||
# rest framework
|
||||
djangorestframework==3.15.2
|
||||
# postgres
|
||||
|
||||
@@ -9,4 +9,4 @@ factory-boy==3.3.0
|
||||
freezegun==1.2.2
|
||||
coverage==7.2.7
|
||||
httpx==0.24.1
|
||||
requests==2.32.2
|
||||
requests==2.32.4
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Updates on issue</title>
|
||||
<title>Updates on {{entity_type}}</title>
|
||||
<style type="text/css" emogrify="no"> html { font-family: system-ui; } p, h1, h2, h3, h4, ol, ul { margin: 0; } h-full { height: 100%; } a:hover { color: #3358d4 !important; } </style>
|
||||
<style> *[class="gmail-fix"] { display: none !important; } </style>
|
||||
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
|
||||
@@ -37,7 +37,7 @@
|
||||
{% else %}
|
||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> {{summary}} <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {% if data|length > 0 %} {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name}} {% else %} {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name}} {% endif %} </span>and others. </p>
|
||||
{% endif %} <!-- {% if actors_involved == 1 %} {% if data|length > 0 and comments|length == 0 %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name }} </span> made {{total_updates}} {% if total_updates > 1 %}updates{% else %}update{% endif %} to the issue. </p> {% elif data|length == 0 and comments|length > 0 %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name }} </span> added {{total_comments}} new {% if total_comments > 1 %}comments{% else %}comment{% endif %}. </p> {% elif data|length > 0 and comments|length > 0 %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name }} </span> made {{total_updates}} {% if total_updates > 1 %}updates{% else %}update{% endif %} and added {{total_comments}} new {% if total_comments > 1 %}comments{% else %}comment{% endif %} on the issue. </p> {% endif %} {% else %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> There are {{ total_updates }} new updates and {{total_comments}} new comments on the issue. </p> {% endif %} --> {% for update in data %} {% if update.changes.name %} <!-- Issue title updated -->
|
||||
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c"> The issue title has been updated to {{ issue.name}} </p>
|
||||
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c"> The {{entity_type}} title has been updated to {{ issue.name}} </p>
|
||||
{% endif %} <!-- Outer update Box start --> {% if data %}
|
||||
<div style=" background-color: #f7f9ff; border-radius: 8px; border-style: solid; border-width: 1px; border-color: #c1d0ff; padding: 20px; margin-top: 15px; max-width: 100%; " >
|
||||
<!-- Block Heading -->
|
||||
@@ -224,7 +224,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ issue_url }}" style="text-decoration: none;">
|
||||
<div style=" max-width: min-content; white-space: nowrap; background-color: #3e63dd; padding: 10px 15px; border: 1px solid #2f4ba8; border-radius: 4px; margin-top: 15px; cursor: pointer; font-size: 0.8rem; color: white; " > View issue </div>
|
||||
<div style=" max-width: min-content; white-space: nowrap; background-color: #3e63dd; padding: 10px 15px; border: 1px solid #2f4ba8; border-radius: 4px; margin-top: 15px; cursor: pointer; font-size: 0.8rem; color: white; " > View {{entity_type}} </div>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
@@ -232,7 +232,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-size: 0.8rem; color: #1c2024">
|
||||
This email was sent to <a href="mailto:{{receiver.email}}" style="color: #3a5bc7; font-weight: 500; text-decoration: none" >{{ receiver.email }}.</a > If you'd rather not receive this kind of email, <a href="{{ issue_url }}" style="color: #3a5bc7; text-decoration: none" >you can unsubscribe to the issue</a > or <a href="{{ user_preference }}" style="color: #3a5bc7; text-decoration: none" >manage your email preferences</a >. <!-- Github | LinkedIn | Twitter -->
|
||||
This email was sent to <a href="mailto:{{receiver.email}}" style="color: #3a5bc7; font-weight: 500; text-decoration: none" >{{ receiver.email }}.</a > If you'd rather not receive this kind of email, <a href="{{ issue_url }}" style="color: #3a5bc7; text-decoration: none" >you can unsubscribe to the {{entity_type}}</a > or <a href="{{ user_preference }}" style="color: #3a5bc7; text-decoration: none" >manage your email preferences</a >. <!-- Github | LinkedIn | Twitter -->
|
||||
<div style="margin-top: 60px; float: right"> <a href="https://github.com/makeplane" target="_blank" style="margin-left: 10px; text-decoration: none" > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="25" height="25" border="0" style="display: inline-block" /> </a> <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style="margin-left: 10px; text-decoration: none" > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="25" height="25" border="0" style="display: inline-block" /> </a> <a href="https://twitter.com/planepowers" target="_blank" style="margin-left: 10px; text-decoration: none" > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="25" height="25" border="0" style="display: inline-block" /> </a> </div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -486,7 +486,7 @@ When you want to restore the previously backed-up data, follow the instructions
|
||||
1. Download the restore script using the command below. We suggest downloading it in the same folder as `setup.sh`.
|
||||
|
||||
```bash
|
||||
curl -fsSL -o restore.sh https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/restore.sh
|
||||
curl -fsSL -o restore.sh https://github.com/makeplane/plane/releases/latest/download/restore.sh
|
||||
chmod +x restore.sh
|
||||
```
|
||||
|
||||
@@ -529,6 +529,31 @@ When you want to restore the previously backed-up data, follow the instructions
|
||||
|
||||
---
|
||||
|
||||
### Restore for Commercial Air-Gapped (Docker Compose)
|
||||
|
||||
When you want to restore the previously backed-up data on Plane Commercial Air-Gapped version, follow the instructions below.
|
||||
|
||||
1. Download the restore script using the command below
|
||||
|
||||
```bash
|
||||
curl -fsSL -o restore-airgapped.sh https://github.com/makeplane/plane/releases/latest/download/restore-airgapped.sh
|
||||
chmod +x restore-airgapped.sh
|
||||
```
|
||||
|
||||
1. Copy the backup folder and the `restore-airgapped.sh` to `Commercial Airgapped Edition` server
|
||||
|
||||
1. Make sure that Plane Commercial (Airgapped) is extracted and ready to get started. In case it is running, you would need to stop that.
|
||||
|
||||
1. Execute the command below to restore your data.
|
||||
|
||||
```bash
|
||||
./restore-airgapped.sh <path to backup folder containing *.tar.gz files>
|
||||
```
|
||||
|
||||
1. After restoration, you are ready to start Plane Commercial (Airgapped) will all your previously saved data.
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary><h2>Upgrading from v0.13.2 to v0.14.x</h2></summary>
|
||||
|
||||
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/bin/bash
|
||||
+set -euo pipefail
|
||||
|
||||
function print_header() {
|
||||
clear
|
||||
|
||||
cat <<"EOF"
|
||||
--------------------------------------------
|
||||
____ _ /////////
|
||||
| _ \| | __ _ _ __ ___ /////////
|
||||
| |_) | |/ _` | '_ \ / _ \ ///// /////
|
||||
| __/| | (_| | | | | __/ ///// /////
|
||||
|_| |_|\__,_|_| |_|\___| ////
|
||||
////
|
||||
--------------------------------------------
|
||||
Project management tool from the future
|
||||
--------------------------------------------
|
||||
EOF
|
||||
}
|
||||
|
||||
function restoreData() {
|
||||
|
||||
echo ""
|
||||
echo "****************************************************"
|
||||
echo "We are about to restore your data from the backup files."
|
||||
echo "****************************************************"
|
||||
echo ""
|
||||
|
||||
# set the backup folder path
|
||||
BACKUP_FOLDER=${1}
|
||||
|
||||
if [ -z "$BACKUP_FOLDER" ]; then
|
||||
BACKUP_FOLDER="$PWD/backup"
|
||||
read -p "Enter the backup folder path [$BACKUP_FOLDER]: " BACKUP_FOLDER
|
||||
if [ -z "$BACKUP_FOLDER" ]; then
|
||||
BACKUP_FOLDER="$PWD/backup"
|
||||
fi
|
||||
fi
|
||||
|
||||
# check if the backup folder exists
|
||||
if [ ! -d "$BACKUP_FOLDER" ]; then
|
||||
echo "Error: Backup folder not found at $BACKUP_FOLDER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if there are any .tar.gz files in the backup folder
|
||||
if ! ls "$BACKUP_FOLDER"/*.tar.gz 1> /dev/null 2>&1; then
|
||||
echo "Error: Backup folder does not contain .tar.gz files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Using backup folder: $BACKUP_FOLDER"
|
||||
echo ""
|
||||
|
||||
# ask for current install path
|
||||
AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped"
|
||||
read -p "Enter the airgapped instance install path [$AIRGAPPED_INSTALL_PATH]: " AIRGAPPED_INSTALL_PATH
|
||||
if [ -z "$AIRGAPPED_INSTALL_PATH" ]; then
|
||||
AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped"
|
||||
fi
|
||||
|
||||
# check if the airgapped instance install path exists
|
||||
if [ ! -d "$AIRGAPPED_INSTALL_PATH" ]; then
|
||||
echo "Error: Airgapped instance install path not found at $AIRGAPPED_INSTALL_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Using airgapped instance install path: $AIRGAPPED_INSTALL_PATH"
|
||||
echo ""
|
||||
|
||||
# check if the docker-compose.yaml exists
|
||||
if [ ! -f "$AIRGAPPED_INSTALL_PATH/docker-compose.yml" ]; then
|
||||
echo "Error: docker-compose.yml not found at $AIRGAPPED_INSTALL_PATH/docker-compose.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local dockerServiceStatus
|
||||
if command -v jq &> /dev/null; then
|
||||
dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped --format=json | jq -r .[0].Status)
|
||||
else
|
||||
dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped | grep -o "running" | head -n 1)
|
||||
fi
|
||||
|
||||
if [[ $dockerServiceStatus == "running" ]]; then
|
||||
echo "Plane Airgapped is running. Please STOP the Plane Airgapped before restoring data."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_USER_ID=$(id -u)
|
||||
CURRENT_GROUP_ID=$(id -g)
|
||||
|
||||
# if the data folder not exists, create it
|
||||
if [ ! -d "$AIRGAPPED_INSTALL_PATH/data" ]; then
|
||||
mkdir -p "$AIRGAPPED_INSTALL_PATH/data"
|
||||
chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data"
|
||||
fi
|
||||
|
||||
for BACKUP_FILE in "$BACKUP_FOLDER/*.tar.gz"; do
|
||||
if [ -e "$BACKUP_FILE" ]; then
|
||||
|
||||
# get the basefilename without the extension
|
||||
BASE_FILE_NAME=$(basename "$BACKUP_FILE" ".tar.gz")
|
||||
|
||||
# extract the restoreFile to the airgapped instance install path
|
||||
echo "Restoring $BASE_FILE_NAME"
|
||||
rm -rf "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" || true
|
||||
|
||||
tar -xvzf "$BACKUP_FILE" -C "$AIRGAPPED_INSTALL_PATH/data/"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to extract $BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to change ownership of $AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No .tar.gz files found in the current directory."
|
||||
echo ""
|
||||
echo "Please provide the path to the backup file."
|
||||
echo ""
|
||||
echo "Usage: $0 /path/to/backup"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Restore completed successfully."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# if docker-compose is installed
|
||||
if command -v docker-compose &> /dev/null
|
||||
then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
COMPOSE_CMD="docker compose"
|
||||
fi
|
||||
|
||||
print_header
|
||||
restoreData "$@"
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A realtime collaborative server powers Plane's rich text editor",
|
||||
"main": "./src/server.ts",
|
||||
@@ -57,7 +57,7 @@
|
||||
"concurrently": "^9.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "5.3.3"
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -2,7 +2,7 @@
|
||||
"name": "plane",
|
||||
"description": "Open-source project management that unlocks customer value",
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
@@ -24,13 +24,15 @@
|
||||
"devDependencies": {
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"turbo": "^2.5.3"
|
||||
"turbo": "^2.5.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"nanoid": "3.3.8",
|
||||
"esbuild": "0.25.0",
|
||||
"@babel/helpers": "7.26.10",
|
||||
"@babel/runtime": "7.26.10"
|
||||
"@babel/runtime": "7.26.10",
|
||||
"chokidar": "3.6.0",
|
||||
"tar-fs": "3.0.9"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"license": "AGPL-3.0"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export enum AI_EDITOR_TASKS {
|
||||
ASK_ANYTHING = "ASK_ANYTHING",
|
||||
}
|
||||
export const AI_EDITOR_TASKS = {
|
||||
ASK_ANYTHING: "ASK_ANYTHING",
|
||||
} as const;
|
||||
|
||||
export type AI_EDITOR_TASKS = typeof AI_EDITOR_TASKS[keyof typeof AI_EDITOR_TASKS];
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { TAnalyticsTabsV2Base } from "@plane/types";
|
||||
import { ChartXAxisProperty, ChartYAxisMetric } from "../chart";
|
||||
|
||||
export const insightsFields: Record<TAnalyticsTabsV2Base, string[]> = {
|
||||
overview: [
|
||||
"total_users",
|
||||
"total_admins",
|
||||
"total_members",
|
||||
"total_guests",
|
||||
"total_projects",
|
||||
"total_work_items",
|
||||
"total_cycles",
|
||||
"total_intake",
|
||||
],
|
||||
"work-items": [
|
||||
"total_work_items",
|
||||
"started_work_items",
|
||||
"backlog_work_items",
|
||||
"un_started_work_items",
|
||||
"completed_work_items",
|
||||
],
|
||||
};
|
||||
|
||||
export const ANALYTICS_V2_DURATION_FILTER_OPTIONS = [
|
||||
{
|
||||
name: "Yesterday",
|
||||
value: "yesterday",
|
||||
},
|
||||
{
|
||||
name: "Last 7 days",
|
||||
value: "last_7_days",
|
||||
},
|
||||
{
|
||||
name: "Last 30 days",
|
||||
value: "last_30_days",
|
||||
},
|
||||
{
|
||||
name: "Last 3 months",
|
||||
value: "last_3_months",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_V2_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = [
|
||||
{
|
||||
value: ChartXAxisProperty.STATES,
|
||||
label: "State name",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.STATE_GROUPS,
|
||||
label: "State group",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.PRIORITY,
|
||||
label: "Priority",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.LABELS,
|
||||
label: "Label",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.ASSIGNEES,
|
||||
label: "Assignee",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.ESTIMATE_POINTS,
|
||||
label: "Estimate point",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.CYCLES,
|
||||
label: "Cycle",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.MODULES,
|
||||
label: "Module",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.COMPLETED_AT,
|
||||
label: "Completed date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.TARGET_DATE,
|
||||
label: "Due date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.START_DATE,
|
||||
label: "Start date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.CREATED_AT,
|
||||
label: "Created date",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_V2_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = [
|
||||
{
|
||||
value: ChartYAxisMetric.WORK_ITEM_COUNT,
|
||||
label: "Work item",
|
||||
},
|
||||
{
|
||||
value: ChartYAxisMetric.ESTIMATE_POINT_COUNT,
|
||||
label: "Estimate",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_V2_DATE_KEYS = ["completed_at", "target_date", "start_date", "created_at"];
|
||||
@@ -1,81 +0,0 @@
|
||||
// types
|
||||
import { TXAxisValues, TYAxisValues } from "@plane/types";
|
||||
|
||||
export const ANALYTICS_TABS = [
|
||||
{
|
||||
key: "scope_and_demand",
|
||||
i18n_title: "workspace_analytics.tabs.scope_and_demand",
|
||||
},
|
||||
{ key: "custom", i18n_title: "workspace_analytics.tabs.custom" },
|
||||
];
|
||||
|
||||
export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
|
||||
[
|
||||
{
|
||||
value: "state_id",
|
||||
label: "State name",
|
||||
},
|
||||
{
|
||||
value: "state__group",
|
||||
label: "State group",
|
||||
},
|
||||
{
|
||||
value: "priority",
|
||||
label: "Priority",
|
||||
},
|
||||
{
|
||||
value: "labels__id",
|
||||
label: "Label",
|
||||
},
|
||||
{
|
||||
value: "assignees__id",
|
||||
label: "Assignee",
|
||||
},
|
||||
{
|
||||
value: "estimate_point__value",
|
||||
label: "Estimate point",
|
||||
},
|
||||
{
|
||||
value: "issue_cycle__cycle_id",
|
||||
label: "Cycle",
|
||||
},
|
||||
{
|
||||
value: "issue_module__module_id",
|
||||
label: "Module",
|
||||
},
|
||||
{
|
||||
value: "completed_at",
|
||||
label: "Completed date",
|
||||
},
|
||||
{
|
||||
value: "target_date",
|
||||
label: "Due date",
|
||||
},
|
||||
{
|
||||
value: "start_date",
|
||||
label: "Start date",
|
||||
},
|
||||
{
|
||||
value: "created_at",
|
||||
label: "Created date",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] =
|
||||
[
|
||||
{
|
||||
value: "issue_count",
|
||||
label: "Work item Count",
|
||||
},
|
||||
{
|
||||
value: "estimate",
|
||||
label: "Estimate",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_DATE_KEYS = [
|
||||
"completed_at",
|
||||
"target_date",
|
||||
"start_date",
|
||||
"created_at",
|
||||
];
|
||||
@@ -0,0 +1,178 @@
|
||||
import { TAnalyticsTabsBase } from "@plane/types";
|
||||
import { ChartXAxisProperty, ChartYAxisMetric } from "../chart";
|
||||
|
||||
export interface IInsightField {
|
||||
key: string;
|
||||
i18nKey: string;
|
||||
i18nProps?: {
|
||||
entity?: string;
|
||||
entityPlural?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export const insightsFields: Record<TAnalyticsTabsBase, IInsightField[]> = {
|
||||
overview: [
|
||||
{
|
||||
key: "total_users",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.users",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_admins",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.admins",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_members",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.members",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_guests",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.guests",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_projects",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.projects",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_work_items",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.work_items",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_cycles",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "common.cycles",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "total_intake",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
i18nProps: {
|
||||
entity: "sidebar.intake",
|
||||
},
|
||||
},
|
||||
],
|
||||
"work-items": [
|
||||
{
|
||||
key: "total_work_items",
|
||||
i18nKey: "workspace_analytics.total",
|
||||
},
|
||||
{
|
||||
key: "started_work_items",
|
||||
i18nKey: "workspace_analytics.started_work_items",
|
||||
},
|
||||
{
|
||||
key: "backlog_work_items",
|
||||
i18nKey: "workspace_analytics.backlog_work_items",
|
||||
},
|
||||
{
|
||||
key: "un_started_work_items",
|
||||
i18nKey: "workspace_analytics.un_started_work_items",
|
||||
},
|
||||
{
|
||||
key: "completed_work_items",
|
||||
i18nKey: "workspace_analytics.completed_work_items",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ANALYTICS_DURATION_FILTER_OPTIONS = [
|
||||
{
|
||||
name: "Yesterday",
|
||||
value: "yesterday",
|
||||
},
|
||||
{
|
||||
name: "Last 7 days",
|
||||
value: "last_7_days",
|
||||
},
|
||||
{
|
||||
name: "Last 30 days",
|
||||
value: "last_30_days",
|
||||
},
|
||||
{
|
||||
name: "Last 3 months",
|
||||
value: "last_3_months",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = [
|
||||
{
|
||||
value: ChartXAxisProperty.STATES,
|
||||
label: "State name",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.STATE_GROUPS,
|
||||
label: "State group",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.PRIORITY,
|
||||
label: "Priority",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.LABELS,
|
||||
label: "Label",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.ASSIGNEES,
|
||||
label: "Assignee",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.ESTIMATE_POINTS,
|
||||
label: "Estimate point",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.CYCLES,
|
||||
label: "Cycle",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.MODULES,
|
||||
label: "Module",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.COMPLETED_AT,
|
||||
label: "Completed date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.TARGET_DATE,
|
||||
label: "Due date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.START_DATE,
|
||||
label: "Start date",
|
||||
},
|
||||
{
|
||||
value: ChartXAxisProperty.CREATED_AT,
|
||||
label: "Created date",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = [
|
||||
{
|
||||
value: ChartYAxisMetric.WORK_ITEM_COUNT,
|
||||
label: "Work item",
|
||||
},
|
||||
{
|
||||
value: ChartYAxisMetric.ESTIMATE_POINT_COUNT,
|
||||
label: "Estimate",
|
||||
},
|
||||
];
|
||||
|
||||
export const ANALYTICS_V2_DATE_KEYS = ["completed_at", "target_date", "start_date", "created_at"];
|
||||
+116
-100
@@ -1,9 +1,11 @@
|
||||
export enum E_PASSWORD_STRENGTH {
|
||||
EMPTY = "empty",
|
||||
LENGTH_NOT_VALID = "length_not_valid",
|
||||
STRENGTH_NOT_VALID = "strength_not_valid",
|
||||
STRENGTH_VALID = "strength_valid",
|
||||
}
|
||||
export const E_PASSWORD_STRENGTH = {
|
||||
EMPTY: "empty",
|
||||
LENGTH_NOT_VALID: "length_not_valid",
|
||||
STRENGTH_NOT_VALID: "strength_not_valid",
|
||||
STRENGTH_VALID: "strength_valid",
|
||||
} as const;
|
||||
|
||||
export type E_PASSWORD_STRENGTH = typeof E_PASSWORD_STRENGTH[keyof typeof E_PASSWORD_STRENGTH];
|
||||
|
||||
export const PASSWORD_MIN_LENGTH = 8;
|
||||
|
||||
@@ -31,41 +33,51 @@ export const SPACE_PASSWORD_CRITERIA = [
|
||||
// },
|
||||
];
|
||||
|
||||
export enum EAuthPageTypes {
|
||||
PUBLIC = "PUBLIC",
|
||||
NON_AUTHENTICATED = "NON_AUTHENTICATED",
|
||||
SET_PASSWORD = "SET_PASSWORD",
|
||||
ONBOARDING = "ONBOARDING",
|
||||
AUTHENTICATED = "AUTHENTICATED",
|
||||
}
|
||||
export const EAuthPageTypes = {
|
||||
PUBLIC: "PUBLIC",
|
||||
NON_AUTHENTICATED: "NON_AUTHENTICATED",
|
||||
SET_PASSWORD: "SET_PASSWORD",
|
||||
ONBOARDING: "ONBOARDING",
|
||||
AUTHENTICATED: "AUTHENTICATED",
|
||||
} as const;
|
||||
|
||||
export enum EPageTypes {
|
||||
INIT = "INIT",
|
||||
PUBLIC = "PUBLIC",
|
||||
NON_AUTHENTICATED = "NON_AUTHENTICATED",
|
||||
ONBOARDING = "ONBOARDING",
|
||||
AUTHENTICATED = "AUTHENTICATED",
|
||||
}
|
||||
export type EAuthPageTypes = typeof EAuthPageTypes[keyof typeof EAuthPageTypes];
|
||||
|
||||
export enum EAuthModes {
|
||||
SIGN_IN = "SIGN_IN",
|
||||
SIGN_UP = "SIGN_UP",
|
||||
}
|
||||
export const EPageTypes = {
|
||||
INIT: "INIT",
|
||||
PUBLIC: "PUBLIC",
|
||||
NON_AUTHENTICATED: "NON_AUTHENTICATED",
|
||||
ONBOARDING: "ONBOARDING",
|
||||
AUTHENTICATED: "AUTHENTICATED",
|
||||
} as const;
|
||||
|
||||
export enum EAuthSteps {
|
||||
EMAIL = "EMAIL",
|
||||
PASSWORD = "PASSWORD",
|
||||
UNIQUE_CODE = "UNIQUE_CODE",
|
||||
}
|
||||
export type EPageTypes = typeof EPageTypes[keyof typeof EPageTypes];
|
||||
|
||||
export enum EErrorAlertType {
|
||||
BANNER_ALERT = "BANNER_ALERT",
|
||||
TOAST_ALERT = "TOAST_ALERT",
|
||||
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL = "INLINE_EMAIL",
|
||||
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||
}
|
||||
export const EAuthModes = {
|
||||
SIGN_IN: "SIGN_IN",
|
||||
SIGN_UP: "SIGN_UP",
|
||||
} as const;
|
||||
|
||||
export type EAuthModes = typeof EAuthModes[keyof typeof EAuthModes];
|
||||
|
||||
export const EAuthSteps = {
|
||||
EMAIL: "EMAIL",
|
||||
PASSWORD: "PASSWORD",
|
||||
UNIQUE_CODE: "UNIQUE_CODE",
|
||||
} as const;
|
||||
|
||||
export type EAuthSteps = typeof EAuthSteps[keyof typeof EAuthSteps];
|
||||
|
||||
export const EErrorAlertType = {
|
||||
BANNER_ALERT: "BANNER_ALERT",
|
||||
TOAST_ALERT: "TOAST_ALERT",
|
||||
INLINE_FIRST_NAME: "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL: "INLINE_EMAIL",
|
||||
INLINE_PASSWORD: "INLINE_PASSWORD",
|
||||
INLINE_EMAIL_CODE: "INLINE_EMAIL_CODE",
|
||||
} as const;
|
||||
|
||||
export type EErrorAlertType = typeof EErrorAlertType[keyof typeof EErrorAlertType];
|
||||
|
||||
export type TAuthErrorInfo = {
|
||||
type: EErrorAlertType;
|
||||
@@ -74,79 +86,83 @@ export type TAuthErrorInfo = {
|
||||
message: any;
|
||||
};
|
||||
|
||||
export enum EAdminAuthErrorCodes {
|
||||
export const EAdminAuthErrorCodes = {
|
||||
// Admin
|
||||
ADMIN_ALREADY_EXIST = "5150",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155",
|
||||
INVALID_ADMIN_EMAIL = "5160",
|
||||
INVALID_ADMIN_PASSWORD = "5165",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD = "5170",
|
||||
ADMIN_AUTHENTICATION_FAILED = "5175",
|
||||
ADMIN_USER_ALREADY_EXIST = "5180",
|
||||
ADMIN_USER_DOES_NOT_EXIST = "5185",
|
||||
ADMIN_USER_DEACTIVATED = "5190",
|
||||
}
|
||||
ADMIN_ALREADY_EXIST: "5150",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME: "5155",
|
||||
INVALID_ADMIN_EMAIL: "5160",
|
||||
INVALID_ADMIN_PASSWORD: "5165",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD: "5170",
|
||||
ADMIN_AUTHENTICATION_FAILED: "5175",
|
||||
ADMIN_USER_ALREADY_EXIST: "5180",
|
||||
ADMIN_USER_DOES_NOT_EXIST: "5185",
|
||||
ADMIN_USER_DEACTIVATED: "5190",
|
||||
} as const;
|
||||
|
||||
export enum EAuthErrorCodes {
|
||||
export type EAdminAuthErrorCodes = typeof EAdminAuthErrorCodes[keyof typeof EAdminAuthErrorCodes];
|
||||
|
||||
export const EAuthErrorCodes = {
|
||||
// Global
|
||||
INSTANCE_NOT_CONFIGURED = "5000",
|
||||
INVALID_EMAIL = "5005",
|
||||
EMAIL_REQUIRED = "5010",
|
||||
SIGNUP_DISABLED = "5015",
|
||||
MAGIC_LINK_LOGIN_DISABLED = "5016",
|
||||
PASSWORD_LOGIN_DISABLED = "5018",
|
||||
USER_ACCOUNT_DEACTIVATED = "5019",
|
||||
INSTANCE_NOT_CONFIGURED: "5000",
|
||||
INVALID_EMAIL: "5005",
|
||||
EMAIL_REQUIRED: "5010",
|
||||
SIGNUP_DISABLED: "5015",
|
||||
MAGIC_LINK_LOGIN_DISABLED: "5016",
|
||||
PASSWORD_LOGIN_DISABLED: "5018",
|
||||
USER_ACCOUNT_DEACTIVATED: "5019",
|
||||
// Password strength
|
||||
INVALID_PASSWORD = "5020",
|
||||
SMTP_NOT_CONFIGURED = "5025",
|
||||
INVALID_PASSWORD: "5020",
|
||||
SMTP_NOT_CONFIGURED: "5025",
|
||||
// Sign Up
|
||||
USER_ALREADY_EXIST = "5030",
|
||||
AUTHENTICATION_FAILED_SIGN_UP = "5035",
|
||||
REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5040",
|
||||
INVALID_EMAIL_SIGN_UP = "5045",
|
||||
INVALID_EMAIL_MAGIC_SIGN_UP = "5050",
|
||||
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055",
|
||||
USER_ALREADY_EXIST: "5030",
|
||||
AUTHENTICATION_FAILED_SIGN_UP: "5035",
|
||||
REQUIRED_EMAIL_PASSWORD_SIGN_UP: "5040",
|
||||
INVALID_EMAIL_SIGN_UP: "5045",
|
||||
INVALID_EMAIL_MAGIC_SIGN_UP: "5050",
|
||||
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED: "5055",
|
||||
// Sign In
|
||||
USER_DOES_NOT_EXIST = "5060",
|
||||
AUTHENTICATION_FAILED_SIGN_IN = "5065",
|
||||
REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070",
|
||||
INVALID_EMAIL_SIGN_IN = "5075",
|
||||
INVALID_EMAIL_MAGIC_SIGN_IN = "5080",
|
||||
MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED = "5085",
|
||||
USER_DOES_NOT_EXIST: "5060",
|
||||
AUTHENTICATION_FAILED_SIGN_IN: "5065",
|
||||
REQUIRED_EMAIL_PASSWORD_SIGN_IN: "5070",
|
||||
INVALID_EMAIL_SIGN_IN: "5075",
|
||||
INVALID_EMAIL_MAGIC_SIGN_IN: "5080",
|
||||
MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED: "5085",
|
||||
// Both Sign in and Sign up for magic
|
||||
INVALID_MAGIC_CODE_SIGN_IN = "5090",
|
||||
INVALID_MAGIC_CODE_SIGN_UP = "5092",
|
||||
EXPIRED_MAGIC_CODE_SIGN_IN = "5095",
|
||||
EXPIRED_MAGIC_CODE_SIGN_UP = "5097",
|
||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100",
|
||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102",
|
||||
INVALID_MAGIC_CODE_SIGN_IN: "5090",
|
||||
INVALID_MAGIC_CODE_SIGN_UP: "5092",
|
||||
EXPIRED_MAGIC_CODE_SIGN_IN: "5095",
|
||||
EXPIRED_MAGIC_CODE_SIGN_UP: "5097",
|
||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN: "5100",
|
||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP: "5102",
|
||||
// Oauth
|
||||
OAUTH_NOT_CONFIGURED = "5104",
|
||||
GOOGLE_NOT_CONFIGURED = "5105",
|
||||
GITHUB_NOT_CONFIGURED = "5110",
|
||||
GITLAB_NOT_CONFIGURED = "5111",
|
||||
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
|
||||
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
|
||||
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
|
||||
OAUTH_NOT_CONFIGURED: "5104",
|
||||
GOOGLE_NOT_CONFIGURED: "5105",
|
||||
GITHUB_NOT_CONFIGURED: "5110",
|
||||
GITLAB_NOT_CONFIGURED: "5111",
|
||||
GOOGLE_OAUTH_PROVIDER_ERROR: "5115",
|
||||
GITHUB_OAUTH_PROVIDER_ERROR: "5120",
|
||||
GITLAB_OAUTH_PROVIDER_ERROR: "5121",
|
||||
// Reset Password
|
||||
INVALID_PASSWORD_TOKEN = "5125",
|
||||
EXPIRED_PASSWORD_TOKEN = "5130",
|
||||
INVALID_PASSWORD_TOKEN: "5125",
|
||||
EXPIRED_PASSWORD_TOKEN: "5130",
|
||||
// Change password
|
||||
INCORRECT_OLD_PASSWORD = "5135",
|
||||
MISSING_PASSWORD = "5138",
|
||||
INVALID_NEW_PASSWORD = "5140",
|
||||
INCORRECT_OLD_PASSWORD: "5135",
|
||||
MISSING_PASSWORD: "5138",
|
||||
INVALID_NEW_PASSWORD: "5140",
|
||||
// set password
|
||||
PASSWORD_ALREADY_SET = "5145",
|
||||
PASSWORD_ALREADY_SET: "5145",
|
||||
// Admin
|
||||
ADMIN_ALREADY_EXIST = "5150",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155",
|
||||
INVALID_ADMIN_EMAIL = "5160",
|
||||
INVALID_ADMIN_PASSWORD = "5165",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD = "5170",
|
||||
ADMIN_AUTHENTICATION_FAILED = "5175",
|
||||
ADMIN_USER_ALREADY_EXIST = "5180",
|
||||
ADMIN_USER_DOES_NOT_EXIST = "5185",
|
||||
ADMIN_USER_DEACTIVATED = "5190",
|
||||
ADMIN_ALREADY_EXIST: "5150",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME: "5155",
|
||||
INVALID_ADMIN_EMAIL: "5160",
|
||||
INVALID_ADMIN_PASSWORD: "5165",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD: "5170",
|
||||
ADMIN_AUTHENTICATION_FAILED: "5175",
|
||||
ADMIN_USER_ALREADY_EXIST: "5180",
|
||||
ADMIN_USER_DOES_NOT_EXIST: "5185",
|
||||
ADMIN_USER_DEACTIVATED: "5190",
|
||||
// Rate limit
|
||||
RATE_LIMIT_EXCEEDED = "5900",
|
||||
}
|
||||
RATE_LIMIT_EXCEEDED: "5900",
|
||||
} as const;
|
||||
|
||||
export type EAuthErrorCodes = typeof EAuthErrorCodes[keyof typeof EAuthErrorCodes];
|
||||
|
||||
@@ -4,43 +4,49 @@ export const LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracki
|
||||
export const AXIS_LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracking-wide";
|
||||
|
||||
|
||||
export enum ChartXAxisProperty {
|
||||
STATES = "STATES",
|
||||
STATE_GROUPS = "STATE_GROUPS",
|
||||
LABELS = "LABELS",
|
||||
ASSIGNEES = "ASSIGNEES",
|
||||
ESTIMATE_POINTS = "ESTIMATE_POINTS",
|
||||
CYCLES = "CYCLES",
|
||||
MODULES = "MODULES",
|
||||
PRIORITY = "PRIORITY",
|
||||
START_DATE = "START_DATE",
|
||||
TARGET_DATE = "TARGET_DATE",
|
||||
CREATED_AT = "CREATED_AT",
|
||||
COMPLETED_AT = "COMPLETED_AT",
|
||||
CREATED_BY = "CREATED_BY",
|
||||
WORK_ITEM_TYPES = "WORK_ITEM_TYPES",
|
||||
PROJECTS = "PROJECTS",
|
||||
EPICS = "EPICS",
|
||||
}
|
||||
export const ChartXAxisProperty = {
|
||||
STATES: "STATES",
|
||||
STATE_GROUPS: "STATE_GROUPS",
|
||||
LABELS: "LABELS",
|
||||
ASSIGNEES: "ASSIGNEES",
|
||||
ESTIMATE_POINTS: "ESTIMATE_POINTS",
|
||||
CYCLES: "CYCLES",
|
||||
MODULES: "MODULES",
|
||||
PRIORITY: "PRIORITY",
|
||||
START_DATE: "START_DATE",
|
||||
TARGET_DATE: "TARGET_DATE",
|
||||
CREATED_AT: "CREATED_AT",
|
||||
COMPLETED_AT: "COMPLETED_AT",
|
||||
CREATED_BY: "CREATED_BY",
|
||||
WORK_ITEM_TYPES: "WORK_ITEM_TYPES",
|
||||
PROJECTS: "PROJECTS",
|
||||
EPICS: "EPICS",
|
||||
} as const;
|
||||
|
||||
export enum ChartYAxisMetric {
|
||||
WORK_ITEM_COUNT = "WORK_ITEM_COUNT",
|
||||
ESTIMATE_POINT_COUNT = "ESTIMATE_POINT_COUNT",
|
||||
PENDING_WORK_ITEM_COUNT = "PENDING_WORK_ITEM_COUNT",
|
||||
COMPLETED_WORK_ITEM_COUNT = "COMPLETED_WORK_ITEM_COUNT",
|
||||
IN_PROGRESS_WORK_ITEM_COUNT = "IN_PROGRESS_WORK_ITEM_COUNT",
|
||||
WORK_ITEM_DUE_THIS_WEEK_COUNT = "WORK_ITEM_DUE_THIS_WEEK_COUNT",
|
||||
WORK_ITEM_DUE_TODAY_COUNT = "WORK_ITEM_DUE_TODAY_COUNT",
|
||||
BLOCKED_WORK_ITEM_COUNT = "BLOCKED_WORK_ITEM_COUNT",
|
||||
}
|
||||
export type ChartXAxisProperty = typeof ChartXAxisProperty[keyof typeof ChartXAxisProperty];
|
||||
|
||||
export const ChartYAxisMetric = {
|
||||
WORK_ITEM_COUNT: "WORK_ITEM_COUNT",
|
||||
ESTIMATE_POINT_COUNT: "ESTIMATE_POINT_COUNT",
|
||||
PENDING_WORK_ITEM_COUNT: "PENDING_WORK_ITEM_COUNT",
|
||||
COMPLETED_WORK_ITEM_COUNT: "COMPLETED_WORK_ITEM_COUNT",
|
||||
IN_PROGRESS_WORK_ITEM_COUNT: "IN_PROGRESS_WORK_ITEM_COUNT",
|
||||
WORK_ITEM_DUE_THIS_WEEK_COUNT: "WORK_ITEM_DUE_THIS_WEEK_COUNT",
|
||||
WORK_ITEM_DUE_TODAY_COUNT: "WORK_ITEM_DUE_TODAY_COUNT",
|
||||
BLOCKED_WORK_ITEM_COUNT: "BLOCKED_WORK_ITEM_COUNT",
|
||||
} as const;
|
||||
|
||||
export type ChartYAxisMetric = typeof ChartYAxisMetric[keyof typeof ChartYAxisMetric];
|
||||
|
||||
|
||||
export enum ChartXAxisDateGrouping {
|
||||
DAY = "DAY",
|
||||
WEEK = "WEEK",
|
||||
MONTH = "MONTH",
|
||||
YEAR = "YEAR",
|
||||
}
|
||||
export const ChartXAxisDateGrouping = {
|
||||
DAY: "DAY",
|
||||
WEEK: "WEEK",
|
||||
MONTH: "MONTH",
|
||||
YEAR: "YEAR",
|
||||
} as const;
|
||||
|
||||
export type ChartXAxisDateGrouping = typeof ChartXAxisDateGrouping[keyof typeof ChartXAxisDateGrouping];
|
||||
|
||||
export const TO_CAPITALIZE_PROPERTIES: ChartXAxisProperty[] = [
|
||||
ChartXAxisProperty.PRIORITY,
|
||||
@@ -55,14 +61,16 @@ export const CHART_X_AXIS_DATE_PROPERTIES: ChartXAxisProperty[] = [
|
||||
];
|
||||
|
||||
|
||||
export enum EChartModels {
|
||||
BASIC = "BASIC",
|
||||
STACKED = "STACKED",
|
||||
GROUPED = "GROUPED",
|
||||
MULTI_LINE = "MULTI_LINE",
|
||||
COMPARISON = "COMPARISON",
|
||||
PROGRESS = "PROGRESS",
|
||||
}
|
||||
export const EChartModels = {
|
||||
BASIC: "BASIC",
|
||||
STACKED: "STACKED",
|
||||
GROUPED: "GROUPED",
|
||||
MULTI_LINE: "MULTI_LINE",
|
||||
COMPARISON: "COMPARISON",
|
||||
PROGRESS: "PROGRESS",
|
||||
} as const;
|
||||
|
||||
export type EChartModels = typeof EChartModels[keyof typeof EChartModels];
|
||||
|
||||
export const CHART_COLOR_PALETTES: {
|
||||
key: TChartColorScheme;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// types
|
||||
import { TIssuesListTypes } from "@plane/types";
|
||||
|
||||
export enum EDurationFilters {
|
||||
NONE = "none",
|
||||
TODAY = "today",
|
||||
THIS_WEEK = "this_week",
|
||||
THIS_MONTH = "this_month",
|
||||
THIS_YEAR = "this_year",
|
||||
CUSTOM = "custom",
|
||||
}
|
||||
export const EDurationFilters = {
|
||||
NONE: "none",
|
||||
TODAY: "today",
|
||||
THIS_WEEK: "this_week",
|
||||
THIS_MONTH: "this_month",
|
||||
THIS_YEAR: "this_year",
|
||||
CUSTOM: "custom",
|
||||
} as const;
|
||||
|
||||
export type EDurationFilters = typeof EDurationFilters[keyof typeof EDurationFilters];
|
||||
|
||||
// filter duration options
|
||||
export const DURATION_FILTER_OPTIONS: {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export enum E_SORT_ORDER {
|
||||
ASC = "asc",
|
||||
DESC = "desc",
|
||||
}
|
||||
export const E_SORT_ORDER = {
|
||||
ASC: "asc",
|
||||
DESC: "desc",
|
||||
} as const;
|
||||
|
||||
export type E_SORT_ORDER = typeof E_SORT_ORDER[keyof typeof E_SORT_ORDER];
|
||||
export const DATE_AFTER_FILTER_OPTIONS = [
|
||||
{
|
||||
name: "1 week from now",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export enum EIconSize {
|
||||
XS = "xs",
|
||||
SM = "sm",
|
||||
MD = "md",
|
||||
LG = "lg",
|
||||
XL = "xl",
|
||||
}
|
||||
export const EIconSize = {
|
||||
XS: "xs",
|
||||
SM: "sm",
|
||||
MD: "md",
|
||||
LG: "lg",
|
||||
XL: "xl",
|
||||
} as const;
|
||||
|
||||
export type EIconSize = typeof EIconSize[keyof typeof EIconSize];
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
|
||||
|
||||
export enum EInboxIssueCurrentTab {
|
||||
OPEN = "open",
|
||||
CLOSED = "closed",
|
||||
}
|
||||
export const EInboxIssueCurrentTab = {
|
||||
OPEN: "open",
|
||||
CLOSED: "closed",
|
||||
} as const;
|
||||
|
||||
export enum EInboxIssueStatus {
|
||||
PENDING = -2,
|
||||
DECLINED = -1,
|
||||
SNOOZED = 0,
|
||||
ACCEPTED = 1,
|
||||
DUPLICATE = 2,
|
||||
}
|
||||
export type EInboxIssueCurrentTab = typeof EInboxIssueCurrentTab[keyof typeof EInboxIssueCurrentTab];
|
||||
|
||||
export enum EInboxIssueSource {
|
||||
IN_APP = "IN_APP",
|
||||
FORMS = "FORMS",
|
||||
EMAIL = "EMAIL",
|
||||
}
|
||||
export const EInboxIssueStatus = {
|
||||
PENDING: -2,
|
||||
DECLINED: -1,
|
||||
SNOOZED: 0,
|
||||
ACCEPTED: 1,
|
||||
DUPLICATE: 2,
|
||||
} as const;
|
||||
|
||||
export type EInboxIssueStatus = typeof EInboxIssueStatus[keyof typeof EInboxIssueStatus];
|
||||
|
||||
export const EInboxIssueSource = {
|
||||
IN_APP: "IN_APP",
|
||||
FORMS: "FORMS",
|
||||
EMAIL: "EMAIL",
|
||||
} as const;
|
||||
|
||||
export type EInboxIssueSource = typeof EInboxIssueSource[keyof typeof EInboxIssueSource];
|
||||
|
||||
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
|
||||
export type TInboxIssueStatus = EInboxIssueStatus;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./ai";
|
||||
export * from "./analytics";
|
||||
export * from "./auth";
|
||||
export * from "./chart";
|
||||
export * from "./endpoints";
|
||||
@@ -32,5 +31,6 @@ export * from "./dashboard";
|
||||
export * from "./page";
|
||||
export * from "./emoji";
|
||||
export * from "./subscription";
|
||||
export * from "./settings";
|
||||
export * from "./icon";
|
||||
export * from "./analytics-v2";
|
||||
export * from "./analytics";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export enum EInstanceStatus {
|
||||
ERROR = "ERROR",
|
||||
NOT_YET_READY = "NOT_YET_READY",
|
||||
}
|
||||
export const EInstanceStatus = {
|
||||
ERROR: "ERROR",
|
||||
NOT_YET_READY: "NOT_YET_READY",
|
||||
} as const;
|
||||
|
||||
export type EInstanceStatus = typeof EInstanceStatus[keyof typeof EInstanceStatus];
|
||||
|
||||
export type TInstanceStatus = {
|
||||
status: EInstanceStatus | undefined;
|
||||
|
||||
@@ -17,66 +17,78 @@ export type TIssueFilterPriorityObject = {
|
||||
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 const 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",
|
||||
} as const;
|
||||
|
||||
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 type EIssueGroupByToServerOptions = typeof EIssueGroupByToServerOptions[keyof typeof EIssueGroupByToServerOptions];
|
||||
|
||||
export enum EIssueServiceType {
|
||||
ISSUES = "issues",
|
||||
EPICS = "epics",
|
||||
WORK_ITEMS = "work-items",
|
||||
}
|
||||
export const 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",
|
||||
} as const;
|
||||
|
||||
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 type EIssueGroupBYServerToProperty = typeof EIssueGroupBYServerToProperty[keyof typeof EIssueGroupBYServerToProperty];
|
||||
|
||||
export enum EIssueCommentAccessSpecifier {
|
||||
EXTERNAL = "EXTERNAL",
|
||||
INTERNAL = "INTERNAL",
|
||||
}
|
||||
export const EIssueServiceType = {
|
||||
ISSUES: "issues",
|
||||
EPICS: "epics",
|
||||
WORK_ITEMS: "work-items",
|
||||
} as const;
|
||||
|
||||
export enum EIssueListRow {
|
||||
HEADER = "HEADER",
|
||||
ISSUE = "ISSUE",
|
||||
NO_ISSUES = "NO_ISSUES",
|
||||
QUICK_ADD = "QUICK_ADD",
|
||||
}
|
||||
export type EIssueServiceType = typeof EIssueServiceType[keyof typeof EIssueServiceType];
|
||||
|
||||
export const 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",
|
||||
} as const;
|
||||
|
||||
export type EIssuesStoreType = typeof EIssuesStoreType[keyof typeof EIssuesStoreType];
|
||||
|
||||
export const EIssueCommentAccessSpecifier = {
|
||||
EXTERNAL: "EXTERNAL",
|
||||
INTERNAL: "INTERNAL",
|
||||
} as const;
|
||||
|
||||
export type EIssueCommentAccessSpecifier = typeof EIssueCommentAccessSpecifier[keyof typeof EIssueCommentAccessSpecifier];
|
||||
|
||||
export const EIssueListRow = {
|
||||
HEADER: "HEADER",
|
||||
ISSUE: "ISSUE",
|
||||
NO_ISSUES: "NO_ISSUES",
|
||||
QUICK_ADD: "QUICK_ADD",
|
||||
} as const;
|
||||
|
||||
export type EIssueListRow = typeof EIssueListRow[keyof typeof EIssueListRow];
|
||||
|
||||
export const ISSUE_PRIORITIES: {
|
||||
key: TIssuePriorities;
|
||||
@@ -114,14 +126,14 @@ export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [
|
||||
];
|
||||
|
||||
export type TCreateModalStoreTypes =
|
||||
| EIssuesStoreType.TEAM
|
||||
| EIssuesStoreType.PROJECT
|
||||
| EIssuesStoreType.TEAM_VIEW
|
||||
| EIssuesStoreType.PROJECT_VIEW
|
||||
| EIssuesStoreType.PROFILE
|
||||
| EIssuesStoreType.CYCLE
|
||||
| EIssuesStoreType.MODULE
|
||||
| EIssuesStoreType.EPIC;
|
||||
| typeof EIssuesStoreType.TEAM
|
||||
| typeof EIssuesStoreType.PROJECT
|
||||
| typeof EIssuesStoreType.TEAM_VIEW
|
||||
| typeof EIssuesStoreType.PROJECT_VIEW
|
||||
| typeof EIssuesStoreType.PROFILE
|
||||
| typeof EIssuesStoreType.CYCLE
|
||||
| typeof EIssuesStoreType.MODULE
|
||||
| typeof EIssuesStoreType.EPIC;
|
||||
|
||||
export const ISSUE_GROUP_BY_OPTIONS: {
|
||||
key: TIssueGroupByOptions;
|
||||
|
||||
@@ -10,25 +10,29 @@ 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 const 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",
|
||||
} as const;
|
||||
|
||||
export enum EIssueFilterType {
|
||||
FILTERS = "filters",
|
||||
DISPLAY_FILTERS = "display_filters",
|
||||
DISPLAY_PROPERTIES = "display_properties",
|
||||
KANBAN_FILTERS = "kanban_filters",
|
||||
}
|
||||
export type EServerGroupByToFilterOptions = typeof EServerGroupByToFilterOptions[keyof typeof EServerGroupByToFilterOptions];
|
||||
|
||||
export const EIssueFilterType = {
|
||||
FILTERS: "filters",
|
||||
DISPLAY_FILTERS: "display_filters",
|
||||
DISPLAY_PROPERTIES: "display_properties",
|
||||
KANBAN_FILTERS: "kanban_filters",
|
||||
} as const;
|
||||
|
||||
export type EIssueFilterType = typeof EIssueFilterType[keyof typeof EIssueFilterType];
|
||||
|
||||
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
|
||||
@@ -136,45 +140,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
||||
],
|
||||
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"],
|
||||
group_by: ["state", "cycle", "module", "priority", "labels", "assignees", "created_by", null],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
@@ -372,10 +338,12 @@ export const ISSUE_STORE_TO_FILTERS_MAP: Partial<Record<EIssuesStoreType, TFilte
|
||||
[EIssuesStoreType.PROJECT]: ISSUE_DISPLAY_FILTERS_BY_PAGE.issues,
|
||||
};
|
||||
|
||||
export enum EActivityFilterType {
|
||||
ACTIVITY = "ACTIVITY",
|
||||
COMMENT = "COMMENT",
|
||||
}
|
||||
export const EActivityFilterType = {
|
||||
ACTIVITY: "ACTIVITY",
|
||||
COMMENT: "COMMENT",
|
||||
} as const;
|
||||
|
||||
export type EActivityFilterType = typeof EActivityFilterType[keyof typeof EActivityFilterType];
|
||||
|
||||
export type TActivityFilters = EActivityFilterType;
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ export type TIssueLayout =
|
||||
| "spreadsheet"
|
||||
| "gantt";
|
||||
|
||||
export enum EIssueLayoutTypes {
|
||||
LIST = "list",
|
||||
KANBAN = "kanban",
|
||||
CALENDAR = "calendar",
|
||||
GANTT = "gantt_chart",
|
||||
SPREADSHEET = "spreadsheet",
|
||||
}
|
||||
export const EIssueLayoutTypes = {
|
||||
LIST: "list",
|
||||
KANBAN: "kanban",
|
||||
CALENDAR: "calendar",
|
||||
GANTT: "gantt_chart",
|
||||
SPREADSHEET: "spreadsheet",
|
||||
} as const;
|
||||
|
||||
export type EIssueLayoutTypes = typeof EIssueLayoutTypes[keyof typeof EIssueLayoutTypes];
|
||||
|
||||
export type TIssueLayoutMap = Record<
|
||||
EIssueLayoutTypes,
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
// types
|
||||
import {
|
||||
TModuleLayoutOptions,
|
||||
TModuleOrderByOptions,
|
||||
TModuleStatus,
|
||||
} from "@plane/types";
|
||||
import { TModuleLayoutOptions, TModuleOrderByOptions, TModuleStatus } from "@plane/types";
|
||||
|
||||
export const MODULE_STATUS_COLORS: {
|
||||
[key in TModuleStatus]: string;
|
||||
} = {
|
||||
backlog: "#a3a3a2",
|
||||
planned: "#3f76ff",
|
||||
paused: "#525252",
|
||||
completed: "#16a34a",
|
||||
cancelled: "#ef4444",
|
||||
"in-progress": "#f39e1f",
|
||||
};
|
||||
|
||||
export const MODULE_STATUS: {
|
||||
i18n_label: string;
|
||||
@@ -15,42 +22,42 @@ export const MODULE_STATUS: {
|
||||
{
|
||||
i18n_label: "project_modules.status.backlog",
|
||||
value: "backlog",
|
||||
color: "#a3a3a2",
|
||||
color: MODULE_STATUS_COLORS.backlog,
|
||||
textColor: "text-custom-text-400",
|
||||
bgColor: "bg-custom-background-80",
|
||||
},
|
||||
{
|
||||
i18n_label: "project_modules.status.planned",
|
||||
value: "planned",
|
||||
color: "#3f76ff",
|
||||
color: MODULE_STATUS_COLORS.planned,
|
||||
textColor: "text-blue-500",
|
||||
bgColor: "bg-indigo-50",
|
||||
},
|
||||
{
|
||||
i18n_label: "project_modules.status.in_progress",
|
||||
value: "in-progress",
|
||||
color: "#f39e1f",
|
||||
color: MODULE_STATUS_COLORS["in-progress"],
|
||||
textColor: "text-amber-500",
|
||||
bgColor: "bg-amber-50",
|
||||
},
|
||||
{
|
||||
i18n_label: "project_modules.status.paused",
|
||||
value: "paused",
|
||||
color: "#525252",
|
||||
color: MODULE_STATUS_COLORS.paused,
|
||||
textColor: "text-custom-text-300",
|
||||
bgColor: "bg-custom-background-90",
|
||||
},
|
||||
{
|
||||
i18n_label: "project_modules.status.completed",
|
||||
value: "completed",
|
||||
color: "#16a34a",
|
||||
color: MODULE_STATUS_COLORS.completed,
|
||||
textColor: "text-green-600",
|
||||
bgColor: "bg-green-100",
|
||||
},
|
||||
{
|
||||
i18n_label: "project_modules.status.cancelled",
|
||||
value: "cancelled",
|
||||
color: "#ef4444",
|
||||
color: MODULE_STATUS_COLORS.cancelled,
|
||||
textColor: "text-red-500",
|
||||
bgColor: "bg-red-50",
|
||||
},
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
import { TUnreadNotificationsCount } from "@plane/types";
|
||||
|
||||
export enum ENotificationTab {
|
||||
ALL = "all",
|
||||
MENTIONS = "mentions",
|
||||
}
|
||||
export const ENotificationTab = {
|
||||
ALL: "all",
|
||||
MENTIONS: "mentions",
|
||||
} as const;
|
||||
|
||||
export enum ENotificationFilterType {
|
||||
CREATED = "created",
|
||||
ASSIGNED = "assigned",
|
||||
SUBSCRIBED = "subscribed",
|
||||
}
|
||||
export type ENotificationTab = typeof ENotificationTab[keyof typeof ENotificationTab];
|
||||
|
||||
export enum ENotificationLoader {
|
||||
INIT_LOADER = "init-loader",
|
||||
MUTATION_LOADER = "mutation-loader",
|
||||
PAGINATION_LOADER = "pagination-loader",
|
||||
REFRESH = "refresh",
|
||||
MARK_ALL_AS_READY = "mark-all-as-read",
|
||||
}
|
||||
export const ENotificationFilterType = {
|
||||
CREATED: "created",
|
||||
ASSIGNED: "assigned",
|
||||
SUBSCRIBED: "subscribed",
|
||||
} as const;
|
||||
|
||||
export enum ENotificationQueryParamType {
|
||||
INIT = "init",
|
||||
CURRENT = "current",
|
||||
NEXT = "next",
|
||||
}
|
||||
export type ENotificationFilterType = typeof ENotificationFilterType[keyof typeof ENotificationFilterType];
|
||||
|
||||
export type TNotificationTab = ENotificationTab.ALL | ENotificationTab.MENTIONS;
|
||||
export const ENotificationLoader = {
|
||||
INIT_LOADER: "init-loader",
|
||||
MUTATION_LOADER: "mutation-loader",
|
||||
PAGINATION_LOADER: "pagination-loader",
|
||||
REFRESH: "refresh",
|
||||
MARK_ALL_AS_READY: "mark-all-as-read",
|
||||
} as const;
|
||||
|
||||
export type ENotificationLoader = typeof ENotificationLoader[keyof typeof ENotificationLoader];
|
||||
|
||||
export const ENotificationQueryParamType = {
|
||||
INIT: "init",
|
||||
CURRENT: "current",
|
||||
NEXT: "next",
|
||||
} as const;
|
||||
|
||||
export type ENotificationQueryParamType = typeof ENotificationQueryParamType[keyof typeof ENotificationQueryParamType];
|
||||
|
||||
export type TNotificationTab = typeof ENotificationTab.ALL | typeof ENotificationTab.MENTIONS;
|
||||
|
||||
export const NOTIFICATION_TABS = [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export enum EPageAccess {
|
||||
PUBLIC = 0,
|
||||
PRIVATE = 1,
|
||||
}
|
||||
export const EPageAccess = {
|
||||
PUBLIC: 0,
|
||||
PRIVATE: 1,
|
||||
} as const;
|
||||
|
||||
export type EPageAccess = typeof EPageAccess[keyof typeof EPageAccess];
|
||||
|
||||
export type TCreatePageModal = {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -3,13 +3,15 @@ import { IPaymentProduct, TBillingFrequency, TProductBillingFrequency } from "@p
|
||||
/**
|
||||
* Enum representing different product subscription types
|
||||
*/
|
||||
export enum EProductSubscriptionEnum {
|
||||
FREE = "FREE",
|
||||
ONE = "ONE",
|
||||
PRO = "PRO",
|
||||
BUSINESS = "BUSINESS",
|
||||
ENTERPRISE = "ENTERPRISE",
|
||||
}
|
||||
export const EProductSubscriptionEnum = {
|
||||
FREE: "FREE",
|
||||
ONE: "ONE",
|
||||
PRO: "PRO",
|
||||
BUSINESS: "BUSINESS",
|
||||
ENTERPRISE: "ENTERPRISE",
|
||||
} as const;
|
||||
|
||||
export type EProductSubscriptionEnum = typeof EProductSubscriptionEnum[keyof typeof EProductSubscriptionEnum];
|
||||
|
||||
/**
|
||||
* Default billing frequency for each product subscription type
|
||||
@@ -29,7 +31,7 @@ export const SUBSCRIPTION_WITH_BILLING_FREQUENCY = [
|
||||
EProductSubscriptionEnum.PRO,
|
||||
EProductSubscriptionEnum.BUSINESS,
|
||||
EProductSubscriptionEnum.ENTERPRISE,
|
||||
];
|
||||
] as EProductSubscriptionEnum[];
|
||||
|
||||
/**
|
||||
* Mapping of product subscription types to their respective payment product details
|
||||
@@ -72,23 +74,23 @@ export const PLANE_COMMUNITY_PRODUCTS: Record<string, IPaymentProduct> = {
|
||||
prices: [
|
||||
{
|
||||
id: `price_yearly_${EProductSubscriptionEnum.BUSINESS}`,
|
||||
unit_amount: 0,
|
||||
unit_amount: 15600,
|
||||
recurring: "year",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
workspace_amount: 15600,
|
||||
product: EProductSubscriptionEnum.BUSINESS,
|
||||
},
|
||||
{
|
||||
id: `price_monthly_${EProductSubscriptionEnum.BUSINESS}`,
|
||||
unit_amount: 0,
|
||||
unit_amount: 1500,
|
||||
recurring: "month",
|
||||
currency: "usd",
|
||||
workspace_amount: 0,
|
||||
workspace_amount: 1500,
|
||||
product: EProductSubscriptionEnum.BUSINESS,
|
||||
},
|
||||
],
|
||||
payment_quantity: 1,
|
||||
is_active: false,
|
||||
is_active: true,
|
||||
},
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: {
|
||||
id: EProductSubscriptionEnum.ENTERPRISE,
|
||||
@@ -141,8 +143,8 @@ export const SUBSCRIPTION_REDIRECTION_URLS: Record<EProductSubscriptionEnum, Rec
|
||||
year: "https://app.plane.so/upgrade/pro/self-hosted?plan=year",
|
||||
},
|
||||
[EProductSubscriptionEnum.BUSINESS]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
year: TALK_TO_SALES_URL,
|
||||
month: "https://app.plane.so/upgrade/business/self-hosted?plan=month",
|
||||
year: "https://app.plane.so/upgrade/business/self-hosted?plan=year",
|
||||
},
|
||||
[EProductSubscriptionEnum.ENTERPRISE]: {
|
||||
month: TALK_TO_SALES_URL,
|
||||
|
||||
@@ -1,39 +1,53 @@
|
||||
export const PROFILE_SETTINGS = {
|
||||
profile: {
|
||||
key: "profile",
|
||||
i18n_label: "profile.actions.profile",
|
||||
href: `/settings/account`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/",
|
||||
},
|
||||
security: {
|
||||
key: "security",
|
||||
i18n_label: "profile.actions.security",
|
||||
href: `/settings/account/security`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/security/",
|
||||
},
|
||||
activity: {
|
||||
key: "activity",
|
||||
i18n_label: "profile.actions.activity",
|
||||
href: `/settings/account/activity`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/activity/",
|
||||
},
|
||||
preferences: {
|
||||
key: "preferences",
|
||||
i18n_label: "profile.actions.preferences",
|
||||
href: `/settings/account/preferences`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/preferences",
|
||||
},
|
||||
notifications: {
|
||||
key: "notifications",
|
||||
i18n_label: "profile.actions.notifications",
|
||||
href: `/settings/account/notifications`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/notifications/",
|
||||
},
|
||||
"api-tokens": {
|
||||
key: "api-tokens",
|
||||
i18n_label: "profile.actions.api-tokens",
|
||||
href: `/settings/account/api-tokens`,
|
||||
highlight: (pathname: string) => pathname === "/settings/account/api-tokens/",
|
||||
},
|
||||
};
|
||||
export const PROFILE_ACTION_LINKS: {
|
||||
key: string;
|
||||
i18n_label: string;
|
||||
href: string;
|
||||
highlight: (pathname: string) => boolean;
|
||||
}[] = [
|
||||
{
|
||||
key: "profile",
|
||||
i18n_label: "profile.actions.profile",
|
||||
href: `/profile`,
|
||||
highlight: (pathname: string) => pathname === "/profile/",
|
||||
},
|
||||
{
|
||||
key: "security",
|
||||
i18n_label: "profile.actions.security",
|
||||
href: `/profile/security`,
|
||||
highlight: (pathname: string) => pathname === "/profile/security/",
|
||||
},
|
||||
{
|
||||
key: "activity",
|
||||
i18n_label: "profile.actions.activity",
|
||||
href: `/profile/activity`,
|
||||
highlight: (pathname: string) => pathname === "/profile/activity/",
|
||||
},
|
||||
{
|
||||
key: "appearance",
|
||||
i18n_label: "profile.actions.appearance",
|
||||
href: `/profile/appearance`,
|
||||
highlight: (pathname: string) => pathname.includes("/profile/appearance"),
|
||||
},
|
||||
{
|
||||
key: "notifications",
|
||||
i18n_label: "profile.actions.notifications",
|
||||
href: `/profile/notifications`,
|
||||
highlight: (pathname: string) => pathname === "/profile/notifications/",
|
||||
},
|
||||
PROFILE_SETTINGS["profile"],
|
||||
PROFILE_SETTINGS["security"],
|
||||
PROFILE_SETTINGS["activity"],
|
||||
PROFILE_SETTINGS["preferences"],
|
||||
PROFILE_SETTINGS["notifications"],
|
||||
PROFILE_SETTINGS["api-tokens"],
|
||||
];
|
||||
|
||||
export const PROFILE_VIEWER_TAB = [
|
||||
@@ -72,19 +86,38 @@ export const PROFILE_ADMINS_TAB = [
|
||||
},
|
||||
];
|
||||
|
||||
export const PREFERENCE_OPTIONS: {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[] = [
|
||||
{
|
||||
id: "theme",
|
||||
title: "theme",
|
||||
description: "select_or_customize_your_interface_color_scheme",
|
||||
},
|
||||
{
|
||||
id: "start_of_week",
|
||||
title: "First day of the week",
|
||||
description: "This will change how all calendars in your app look.",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description The start of the week for the user
|
||||
* @enum {number}
|
||||
*/
|
||||
export enum EStartOfTheWeek {
|
||||
SUNDAY = 0,
|
||||
MONDAY = 1,
|
||||
TUESDAY = 2,
|
||||
WEDNESDAY = 3,
|
||||
THURSDAY = 4,
|
||||
FRIDAY = 5,
|
||||
SATURDAY = 6,
|
||||
}
|
||||
export const EStartOfTheWeek = {
|
||||
SUNDAY: 0,
|
||||
MONDAY: 1,
|
||||
TUESDAY: 2,
|
||||
WEDNESDAY: 3,
|
||||
THURSDAY: 4,
|
||||
FRIDAY: 5,
|
||||
SATURDAY: 6,
|
||||
} as const;
|
||||
|
||||
export type EStartOfTheWeek = typeof EStartOfTheWeek[keyof typeof EStartOfTheWeek];
|
||||
|
||||
/**
|
||||
* @description The options for the start of the week
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { PROFILE_SETTINGS } from ".";
|
||||
import { WORKSPACE_SETTINGS } from "./workspace";
|
||||
|
||||
export const WORKSPACE_SETTINGS_CATEGORY = {
|
||||
ADMINISTRATION: "administration",
|
||||
FEATURES: "features",
|
||||
DEVELOPER: "developer",
|
||||
} as const;
|
||||
|
||||
export type WORKSPACE_SETTINGS_CATEGORY = typeof WORKSPACE_SETTINGS_CATEGORY[keyof typeof WORKSPACE_SETTINGS_CATEGORY];
|
||||
|
||||
export const PROFILE_SETTINGS_CATEGORY = {
|
||||
YOUR_PROFILE: "your profile",
|
||||
DEVELOPER: "developer",
|
||||
} as const;
|
||||
|
||||
export type PROFILE_SETTINGS_CATEGORY = typeof PROFILE_SETTINGS_CATEGORY[keyof typeof PROFILE_SETTINGS_CATEGORY];
|
||||
|
||||
export const PROJECT_SETTINGS_CATEGORY = {
|
||||
PROJECTS: "projects",
|
||||
} as const;
|
||||
|
||||
export type PROJECT_SETTINGS_CATEGORY = typeof PROJECT_SETTINGS_CATEGORY[keyof typeof PROJECT_SETTINGS_CATEGORY];
|
||||
|
||||
export const WORKSPACE_SETTINGS_CATEGORIES = [
|
||||
WORKSPACE_SETTINGS_CATEGORY.ADMINISTRATION,
|
||||
WORKSPACE_SETTINGS_CATEGORY.FEATURES,
|
||||
WORKSPACE_SETTINGS_CATEGORY.DEVELOPER,
|
||||
];
|
||||
|
||||
export const PROFILE_SETTINGS_CATEGORIES = [
|
||||
PROFILE_SETTINGS_CATEGORY.YOUR_PROFILE,
|
||||
PROFILE_SETTINGS_CATEGORY.DEVELOPER,
|
||||
];
|
||||
|
||||
export const PROJECT_SETTINGS_CATEGORIES = [PROJECT_SETTINGS_CATEGORY.PROJECTS];
|
||||
|
||||
export const GROUPED_WORKSPACE_SETTINGS = {
|
||||
[WORKSPACE_SETTINGS_CATEGORY.ADMINISTRATION]: [
|
||||
WORKSPACE_SETTINGS["general"],
|
||||
WORKSPACE_SETTINGS["members"],
|
||||
WORKSPACE_SETTINGS["billing-and-plans"],
|
||||
WORKSPACE_SETTINGS["export"],
|
||||
],
|
||||
[WORKSPACE_SETTINGS_CATEGORY.FEATURES]: [],
|
||||
[WORKSPACE_SETTINGS_CATEGORY.DEVELOPER]: [WORKSPACE_SETTINGS["webhooks"]],
|
||||
};
|
||||
|
||||
export const GROUPED_PROFILE_SETTINGS = {
|
||||
[PROFILE_SETTINGS_CATEGORY.YOUR_PROFILE]: [
|
||||
PROFILE_SETTINGS["profile"],
|
||||
PROFILE_SETTINGS["preferences"],
|
||||
PROFILE_SETTINGS["notifications"],
|
||||
PROFILE_SETTINGS["security"],
|
||||
PROFILE_SETTINGS["activity"],
|
||||
],
|
||||
[PROFILE_SETTINGS_CATEGORY.DEVELOPER]: [PROFILE_SETTINGS["api-tokens"]],
|
||||
};
|
||||
@@ -89,16 +89,18 @@ export const PROJECT_PAGE_TAB_INDICES = [
|
||||
"submit",
|
||||
];
|
||||
|
||||
export enum ETabIndices {
|
||||
ISSUE_FORM = "issue-form",
|
||||
INTAKE_ISSUE_FORM = "intake-issue-form",
|
||||
CREATE_LABEL = "create-label",
|
||||
PROJECT_CREATE = "project-create",
|
||||
PROJECT_CYCLE = "project-cycle",
|
||||
PROJECT_MODULE = "project-module",
|
||||
PROJECT_VIEW = "project-view",
|
||||
PROJECT_PAGE = "project-page",
|
||||
}
|
||||
export const ETabIndices = {
|
||||
ISSUE_FORM: "issue-form",
|
||||
INTAKE_ISSUE_FORM: "intake-issue-form",
|
||||
CREATE_LABEL: "create-label",
|
||||
PROJECT_CREATE: "project-create",
|
||||
PROJECT_CYCLE: "project-cycle",
|
||||
PROJECT_MODULE: "project-module",
|
||||
PROJECT_VIEW: "project-view",
|
||||
PROJECT_PAGE: "project-page",
|
||||
} as const;
|
||||
|
||||
export type ETabIndices = typeof ETabIndices[keyof typeof ETabIndices];
|
||||
|
||||
export const TAB_INDEX_MAP: Record<ETabIndices, string[]> = {
|
||||
[ETabIndices.ISSUE_FORM]: ISSUE_FORM_TAB_INDICES,
|
||||
|
||||
@@ -1,49 +1,63 @@
|
||||
export enum EAuthenticationPageType {
|
||||
STATIC = "STATIC",
|
||||
NOT_AUTHENTICATED = "NOT_AUTHENTICATED",
|
||||
AUTHENTICATED = "AUTHENTICATED",
|
||||
}
|
||||
export const EAuthenticationPageType = {
|
||||
STATIC: "STATIC",
|
||||
NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
|
||||
AUTHENTICATED: "AUTHENTICATED",
|
||||
} as const;
|
||||
|
||||
export enum EInstancePageType {
|
||||
PRE_SETUP = "PRE_SETUP",
|
||||
POST_SETUP = "POST_SETUP",
|
||||
}
|
||||
export type EAuthenticationPageType = typeof EAuthenticationPageType[keyof typeof EAuthenticationPageType];
|
||||
|
||||
export enum EUserStatus {
|
||||
ERROR = "ERROR",
|
||||
AUTHENTICATION_NOT_DONE = "AUTHENTICATION_NOT_DONE",
|
||||
NOT_YET_READY = "NOT_YET_READY",
|
||||
}
|
||||
export const EInstancePageType = {
|
||||
PRE_SETUP: "PRE_SETUP",
|
||||
POST_SETUP: "POST_SETUP",
|
||||
} as const;
|
||||
|
||||
export type EInstancePageType = typeof EInstancePageType[keyof typeof EInstancePageType];
|
||||
|
||||
export const EUserStatus = {
|
||||
ERROR: "ERROR",
|
||||
AUTHENTICATION_NOT_DONE: "AUTHENTICATION_NOT_DONE",
|
||||
NOT_YET_READY: "NOT_YET_READY",
|
||||
} as const;
|
||||
|
||||
export type EUserStatus = typeof EUserStatus[keyof typeof EUserStatus];
|
||||
|
||||
export type TUserStatus = {
|
||||
status: EUserStatus | undefined;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export enum EUserPermissionsLevel {
|
||||
WORKSPACE = "WORKSPACE",
|
||||
PROJECT = "PROJECT",
|
||||
}
|
||||
export const EUserPermissionsLevel = {
|
||||
WORKSPACE: "WORKSPACE",
|
||||
PROJECT: "PROJECT",
|
||||
} as const;
|
||||
|
||||
export enum EUserWorkspaceRoles {
|
||||
ADMIN = 20,
|
||||
MEMBER = 15,
|
||||
GUEST = 5,
|
||||
}
|
||||
export type EUserPermissionsLevel = typeof EUserPermissionsLevel[keyof typeof EUserPermissionsLevel];
|
||||
|
||||
export enum EUserProjectRoles {
|
||||
ADMIN = 20,
|
||||
MEMBER = 15,
|
||||
GUEST = 5,
|
||||
}
|
||||
export const EUserWorkspaceRoles = {
|
||||
ADMIN: 20,
|
||||
MEMBER: 15,
|
||||
GUEST: 5,
|
||||
} as const;
|
||||
|
||||
export type EUserWorkspaceRoles = typeof EUserWorkspaceRoles[keyof typeof EUserWorkspaceRoles];
|
||||
|
||||
export const EUserProjectRoles = {
|
||||
ADMIN: 20,
|
||||
MEMBER: 15,
|
||||
GUEST: 5,
|
||||
} as const;
|
||||
|
||||
export type EUserProjectRoles = typeof EUserProjectRoles[keyof typeof EUserProjectRoles];
|
||||
|
||||
export type TUserPermissionsLevel = EUserPermissionsLevel;
|
||||
|
||||
export enum EUserPermissions {
|
||||
ADMIN = 20,
|
||||
MEMBER = 15,
|
||||
GUEST = 5,
|
||||
}
|
||||
export const EUserPermissions = {
|
||||
ADMIN: 20,
|
||||
MEMBER: 15,
|
||||
GUEST: 5,
|
||||
} as const;
|
||||
|
||||
export type EUserPermissions = typeof EUserPermissions[keyof typeof EUserPermissions];
|
||||
export type TUserPermissions = EUserPermissions;
|
||||
|
||||
export type TUserAllowedPermissionsObject = {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export enum EViewAccess {
|
||||
PRIVATE,
|
||||
PUBLIC,
|
||||
}
|
||||
export const EViewAccess = {
|
||||
PRIVATE: 0,
|
||||
PUBLIC: 1,
|
||||
} as const;
|
||||
|
||||
export type EViewAccess = typeof EViewAccess[keyof typeof EViewAccess];
|
||||
|
||||
export const VIEW_ACCESS_SPECIFIERS: {
|
||||
key: EViewAccess;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export enum EDraftIssuePaginationType {
|
||||
INIT = "INIT",
|
||||
NEXT = "NEXT",
|
||||
PREV = "PREV",
|
||||
CURRENT = "CURRENT",
|
||||
}
|
||||
export const EDraftIssuePaginationType = {
|
||||
INIT: "INIT",
|
||||
NEXT: "NEXT",
|
||||
PREV: "PREV",
|
||||
CURRENT: "CURRENT",
|
||||
} as const;
|
||||
|
||||
export type EDraftIssuePaginationType = typeof EDraftIssuePaginationType[keyof typeof EDraftIssuePaginationType];
|
||||
|
||||
@@ -114,18 +114,11 @@ export const WORKSPACE_SETTINGS = {
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`,
|
||||
},
|
||||
"api-tokens": {
|
||||
key: "api-tokens",
|
||||
i18n_label: "workspace_settings.settings.api_tokens.title",
|
||||
href: `/settings/api-tokens`,
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`,
|
||||
},
|
||||
};
|
||||
|
||||
export const WORKSPACE_SETTINGS_ACCESS = Object.fromEntries(
|
||||
Object.entries(WORKSPACE_SETTINGS).map(([_, { href, access }]) => [href, access])
|
||||
);
|
||||
) as Record<string, EUserWorkspaceRoles[]>;
|
||||
|
||||
export const WORKSPACE_SETTINGS_LINKS: {
|
||||
key: string;
|
||||
@@ -139,7 +132,6 @@ export const WORKSPACE_SETTINGS_LINKS: {
|
||||
WORKSPACE_SETTINGS["billing-and-plans"],
|
||||
WORKSPACE_SETTINGS["export"],
|
||||
WORKSPACE_SETTINGS["webhooks"],
|
||||
WORKSPACE_SETTINGS["api-tokens"],
|
||||
];
|
||||
|
||||
export const ROLE = {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@types/reflect-metadata": "^0.1.0",
|
||||
"@types/ws": "^8.5.10",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">=4.21.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.26.0",
|
||||
"version": "0.26.1",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
@@ -81,8 +81,8 @@
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"postcss": "^8.4.38",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "5.3.3"
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"keywords": [
|
||||
"editor",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ExtensionFileSetStorageKey } from "@/plane-editor/types/storage";
|
||||
|
||||
export const NODE_FILE_MAP: {
|
||||
[key: string]: {
|
||||
fileSetName: ExtensionFileSetStorageKey;
|
||||
};
|
||||
} = {
|
||||
image: {
|
||||
fileSetName: "deletedImageSet",
|
||||
},
|
||||
imageComponent: {
|
||||
fileSetName: "deletedImageSet",
|
||||
},
|
||||
};
|
||||
@@ -1,32 +1,32 @@
|
||||
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";
|
||||
import { TEmbedConfig } from "@/plane-editor/types";
|
||||
// types
|
||||
import { TExtensions, TUserDetails } from "@/types";
|
||||
import { TExtensions, TFileHandler, TUserDetails } from "@/types";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions?: TExtensions[];
|
||||
issueEmbedConfig: TIssueEmbedConfig | undefined;
|
||||
provider: HocuspocusProvider;
|
||||
export type TDocumentEditorAdditionalExtensionsProps = {
|
||||
disabledExtensions: TExtensions[];
|
||||
embedConfig: TEmbedConfig | undefined;
|
||||
fileHandler: TFileHandler;
|
||||
provider?: HocuspocusProvider;
|
||||
userDetails: TUserDetails;
|
||||
};
|
||||
|
||||
type ExtensionConfig = {
|
||||
export type TDocumentEditorAdditionalExtensionsRegistry = {
|
||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||
getExtension: (props: Props) => AnyExtension;
|
||||
getExtension: (props: TDocumentEditorAdditionalExtensionsProps) => AnyExtension;
|
||||
};
|
||||
|
||||
const extensionRegistry: ExtensionConfig[] = [
|
||||
const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [
|
||||
{
|
||||
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
|
||||
getExtension: () => SlashCommands({}),
|
||||
getExtension: ({ disabledExtensions }) => SlashCommands({ disabledExtensions }),
|
||||
},
|
||||
];
|
||||
|
||||
export const DocumentEditorAdditionalExtensions = (_props: Props) => {
|
||||
export const DocumentEditorAdditionalExtensions = (_props: TDocumentEditorAdditionalExtensionsProps) => {
|
||||
const { disabledExtensions = [] } = _props;
|
||||
|
||||
const documentExtensions = extensionRegistry
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { AnyExtension, Extensions } from "@tiptap/core";
|
||||
// extensions
|
||||
import { SlashCommands } from "@/extensions/slash-commands/root";
|
||||
// types
|
||||
import { TExtensions, TFileHandler } from "@/types";
|
||||
|
||||
export type TRichTextEditorAdditionalExtensionsProps = {
|
||||
disabledExtensions: TExtensions[];
|
||||
fileHandler: TFileHandler;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registry entry configuration for extensions
|
||||
*/
|
||||
export type TRichTextEditorAdditionalExtensionsRegistry = {
|
||||
/** Determines if the extension should be enabled based on disabled extensions */
|
||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||
/** Returns the extension instance(s) when enabled */
|
||||
getExtension: (props: TRichTextEditorAdditionalExtensionsProps) => AnyExtension | undefined;
|
||||
};
|
||||
|
||||
const extensionRegistry: TRichTextEditorAdditionalExtensionsRegistry[] = [
|
||||
{
|
||||
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
|
||||
getExtension: ({ disabledExtensions }) =>
|
||||
SlashCommands({
|
||||
disabledExtensions,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export const RichTextEditorAdditionalExtensions = (props: TRichTextEditorAdditionalExtensionsProps) => {
|
||||
const { disabledExtensions } = props;
|
||||
|
||||
const extensions: Extensions = extensionRegistry
|
||||
.filter((config) => config.isEnabled(disabledExtensions))
|
||||
.map((config) => config.getExtension(props))
|
||||
.filter((extension): extension is AnyExtension => extension !== undefined);
|
||||
|
||||
return extensions;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AnyExtension, Extensions } from "@tiptap/core";
|
||||
// types
|
||||
import { TExtensions, TReadOnlyFileHandler } from "@/types";
|
||||
|
||||
export type TRichTextReadOnlyEditorAdditionalExtensionsProps = {
|
||||
disabledExtensions: TExtensions[];
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registry entry configuration for extensions
|
||||
*/
|
||||
export type TRichTextReadOnlyEditorAdditionalExtensionsRegistry = {
|
||||
/** Determines if the extension should be enabled based on disabled extensions */
|
||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||
/** Returns the extension instance(s) when enabled */
|
||||
getExtension: (props: TRichTextReadOnlyEditorAdditionalExtensionsProps) => AnyExtension | undefined;
|
||||
};
|
||||
|
||||
const extensionRegistry: TRichTextReadOnlyEditorAdditionalExtensionsRegistry[] = [];
|
||||
|
||||
export const RichTextReadOnlyEditorAdditionalExtensions = (props: TRichTextReadOnlyEditorAdditionalExtensionsProps) => {
|
||||
const { disabledExtensions } = props;
|
||||
|
||||
const extensions: Extensions = extensionRegistry
|
||||
.filter((config) => config.isEnabled(disabledExtensions))
|
||||
.map((config) => config.getExtension(props))
|
||||
.filter((extension): extension is AnyExtension => extension !== undefined);
|
||||
|
||||
return extensions;
|
||||
};
|
||||
@@ -1,13 +1,20 @@
|
||||
import { HeadingExtensionStorage } from "@/extensions";
|
||||
import { CustomImageExtensionStorage } from "@/extensions/custom-image";
|
||||
import { CustomLinkStorage } from "@/extensions/custom-link";
|
||||
import { MentionExtensionStorage } from "@/extensions/mentions";
|
||||
import { ImageExtensionStorage } from "@/plugins/image";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
import { type HeadingExtensionStorage } from "@/extensions";
|
||||
import { type CustomImageExtensionStorage } from "@/extensions/custom-image";
|
||||
import { type CustomLinkStorage } from "@/extensions/custom-link";
|
||||
import { type ImageExtensionStorage } from "@/extensions/image";
|
||||
import { type MentionExtensionStorage } from "@/extensions/mentions";
|
||||
import { type UtilityExtensionStorage } from "@/extensions/utility";
|
||||
|
||||
export type ExtensionStorageMap = {
|
||||
imageComponent: CustomImageExtensionStorage;
|
||||
image: ImageExtensionStorage;
|
||||
link: CustomLinkStorage;
|
||||
headingList: HeadingExtensionStorage;
|
||||
mention: MentionExtensionStorage;
|
||||
[CORE_EXTENSIONS.CUSTOM_IMAGE]: CustomImageExtensionStorage;
|
||||
[CORE_EXTENSIONS.IMAGE]: ImageExtensionStorage;
|
||||
[CORE_EXTENSIONS.CUSTOM_LINK]: CustomLinkStorage;
|
||||
[CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage;
|
||||
[CORE_EXTENSIONS.MENTION]: MentionExtensionStorage;
|
||||
[CORE_EXTENSIONS.UTILITY]: UtilityExtensionStorage;
|
||||
};
|
||||
|
||||
export type ExtensionFileSetStorageKey = Extract<keyof ImageExtensionStorage, "deletedImageSet">;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { DocumentContentLoader, PageRenderer } from "@/components/editors";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// extensions
|
||||
import { IssueWidget } from "@/extensions";
|
||||
import { WorkItemEmbedExtension } from "@/extensions";
|
||||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
@@ -39,9 +39,10 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
||||
} = props;
|
||||
|
||||
const extensions: Extensions = [];
|
||||
|
||||
if (embedHandler?.issue) {
|
||||
extensions.push(
|
||||
IssueWidget({
|
||||
WorkItemEmbedExtension({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PageRenderer } from "@/components/editors";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// extensions
|
||||
import { IssueWidget } from "@/extensions";
|
||||
import { WorkItemEmbedExtension } from "@/extensions";
|
||||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
@@ -53,7 +53,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||
const extensions: Extensions = [];
|
||||
if (embedHandler?.issue) {
|
||||
extensions.push(
|
||||
IssueWidget({
|
||||
WorkItemEmbedExtension({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FC, ReactNode, useRef } from "react";
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// types
|
||||
import { TDisplayConfig } from "@/types";
|
||||
// components
|
||||
@@ -36,12 +37,12 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
if (
|
||||
currentNode.content.size === 0 && // Check if the current node is empty
|
||||
!(
|
||||
editor.isActive("orderedList") ||
|
||||
editor.isActive("bulletList") ||
|
||||
editor.isActive("taskItem") ||
|
||||
editor.isActive("table") ||
|
||||
editor.isActive("blockquote") ||
|
||||
editor.isActive("codeBlock")
|
||||
editor.isActive(CORE_EXTENSIONS.ORDERED_LIST) ||
|
||||
editor.isActive(CORE_EXTENSIONS.BULLET_LIST) ||
|
||||
editor.isActive(CORE_EXTENSIONS.TASK_ITEM) ||
|
||||
editor.isActive(CORE_EXTENSIONS.TABLE) ||
|
||||
editor.isActive(CORE_EXTENSIONS.BLOCKQUOTE) ||
|
||||
editor.isActive(CORE_EXTENSIONS.CODE_BLOCK)
|
||||
) // Check if it's an empty node within an orderedList, bulletList, taskItem, table, quote or code block
|
||||
) {
|
||||
return;
|
||||
@@ -53,10 +54,10 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
const lastNode = lastNodePos.node();
|
||||
|
||||
// Check if the last node is a not paragraph
|
||||
if (lastNode && lastNode.type.name !== "paragraph") {
|
||||
if (lastNode && lastNode.type.name !== CORE_EXTENSIONS.PARAGRAPH) {
|
||||
// If last node is not a paragraph, insert a new paragraph at the end
|
||||
const endPosition = editor?.state.doc.content.size;
|
||||
editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run();
|
||||
editor?.chain().insertContentAt(endPosition, { type: CORE_EXTENSIONS.PARAGRAPH }).run();
|
||||
|
||||
// Focus the newly added paragraph for immediate editing
|
||||
editor
|
||||
|
||||
@@ -12,7 +12,7 @@ interface LinkViewContainerProps {
|
||||
export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containerRef }) => {
|
||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [virtualElement, setVirtualElement] = useState<any>(null);
|
||||
const [virtualElement, setVirtualElement] = useState<Element | null>(null);
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
|
||||
@@ -15,6 +15,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName = "",
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
id,
|
||||
@@ -25,6 +26,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||
const editor = useReadOnlyEditor({
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
initialValue,
|
||||
|
||||
@@ -3,12 +3,20 @@ import { forwardRef, useCallback } from "react";
|
||||
import { EditorWrapper } from "@/components/editors";
|
||||
import { EditorBubbleMenu } from "@/components/menus";
|
||||
// extensions
|
||||
import { SideMenuExtension, SlashCommands } from "@/extensions";
|
||||
import { SideMenuExtension } from "@/extensions";
|
||||
// plane editor imports
|
||||
import { RichTextEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/extensions";
|
||||
// types
|
||||
import { EditorRefApi, IRichTextEditor } from "@/types";
|
||||
|
||||
const RichTextEditor = (props: IRichTextEditor) => {
|
||||
const { disabledExtensions, dragDropEnabled, bubbleMenuEnabled = true, extensions: externalExtensions = [] } = props;
|
||||
const {
|
||||
disabledExtensions,
|
||||
dragDropEnabled,
|
||||
fileHandler,
|
||||
bubbleMenuEnabled = true,
|
||||
extensions: externalExtensions = [],
|
||||
} = props;
|
||||
|
||||
const getExtensions = useCallback(() => {
|
||||
const extensions = [
|
||||
@@ -17,17 +25,14 @@ const RichTextEditor = (props: IRichTextEditor) => {
|
||||
aiEnabled: false,
|
||||
dragDropEnabled: !!dragDropEnabled,
|
||||
}),
|
||||
...RichTextEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
fileHandler,
|
||||
}),
|
||||
];
|
||||
if (!disabledExtensions?.includes("slash-commands")) {
|
||||
extensions.push(
|
||||
SlashCommands({
|
||||
disabledExtensions,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}, [dragDropEnabled, disabledExtensions, externalExtensions]);
|
||||
}, [dragDropEnabled, disabledExtensions, externalExtensions, fileHandler]);
|
||||
|
||||
return (
|
||||
<EditorWrapper {...props} extensions={getExtensions()}>
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
import { forwardRef } from "react";
|
||||
import { forwardRef, useCallback } from "react";
|
||||
// plane editor extensions
|
||||
import { RichTextReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/read-only-extensions";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor } from "@/types";
|
||||
// local imports
|
||||
import { ReadOnlyEditorWrapper } from "../read-only-editor-wrapper";
|
||||
|
||||
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => (
|
||||
<ReadOnlyEditorWrapper {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
|
||||
));
|
||||
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => {
|
||||
const { disabledExtensions, fileHandler } = props;
|
||||
|
||||
const getExtensions = useCallback(() => {
|
||||
const extensions = [
|
||||
...RichTextReadOnlyEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
fileHandler,
|
||||
}),
|
||||
];
|
||||
|
||||
return extensions;
|
||||
}, [disabledExtensions, fileHandler]);
|
||||
|
||||
return (
|
||||
<ReadOnlyEditorWrapper
|
||||
{...props}
|
||||
extensions={getExtensions()}
|
||||
forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
RichTextReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef";
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@ export const LinkEditView = ({ viewProps }: LinkEditViewProps) => {
|
||||
if (!hasSubmitted.current && !linkRemoved && initialUrl === "") {
|
||||
try {
|
||||
removeLink();
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
console.error("Error removing link", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
[linkRemoved, initialUrl]
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import tippy, { Instance } from "tippy.js";
|
||||
import { Copy, LucideIcon, Trash2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import tippy, { Instance } from "tippy.js";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
|
||||
interface BlockMenuProps {
|
||||
editor: Editor;
|
||||
@@ -102,7 +104,8 @@ export const BlockMenu = (props: BlockMenuProps) => {
|
||||
key: "duplicate",
|
||||
label: "Duplicate",
|
||||
isDisabled:
|
||||
editor.state.selection.content().content.firstChild?.type.name === "image" || editor.isActive("imageComponent"),
|
||||
editor.state.selection.content().content.firstChild?.type.name === CORE_EXTENSIONS.IMAGE ||
|
||||
editor.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE),
|
||||
onClick: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Check, Link, Trash2 } from "lucide-react";
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useRef, useState } from "react";
|
||||
// plane utils
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
import { setLinkEditor, unsetLinkEditor } from "@/helpers/editor-commands";
|
||||
@@ -43,7 +45,7 @@ export const BubbleMenuLinkSelector: FC<Props> = (props) => {
|
||||
"h-full flex items-center gap-1 px-3 text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 rounded transition-colors",
|
||||
{
|
||||
"bg-custom-background-80": isOpen,
|
||||
"text-custom-text-100": editor.isActive("link"),
|
||||
"text-custom-text-100": editor.isActive(CORE_EXTENSIONS.CUSTOM_LINK),
|
||||
}
|
||||
)}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/menus";
|
||||
// constants
|
||||
import { COLORS_LIST } from "@/constants/common";
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
|
||||
// local components
|
||||
@@ -90,8 +91,8 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Edi
|
||||
if (
|
||||
empty ||
|
||||
!editor.isEditable ||
|
||||
editor.isActive("image") ||
|
||||
editor.isActive("imageComponent") ||
|
||||
editor.isActive(CORE_EXTENSIONS.IMAGE) ||
|
||||
editor.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE) ||
|
||||
isNodeSelection(selection) ||
|
||||
isCellSelection(selection) ||
|
||||
isSelecting
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
Palette,
|
||||
AlignCenter,
|
||||
} from "lucide-react";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// helpers
|
||||
import {
|
||||
insertHorizontalRule,
|
||||
@@ -35,12 +37,7 @@ import {
|
||||
toggleBold,
|
||||
toggleBulletList,
|
||||
toggleCodeBlock,
|
||||
toggleHeadingFive,
|
||||
toggleHeadingFour,
|
||||
toggleHeadingOne,
|
||||
toggleHeadingSix,
|
||||
toggleHeadingThree,
|
||||
toggleHeadingTwo,
|
||||
toggleHeading,
|
||||
toggleItalic,
|
||||
toggleOrderedList,
|
||||
toggleStrike,
|
||||
@@ -65,63 +62,49 @@ export type EditorMenuItem<T extends TEditorCommands> = {
|
||||
export const TextItem = (editor: Editor): EditorMenuItem<"text"> => ({
|
||||
key: "text",
|
||||
name: "Text",
|
||||
isActive: () => editor.isActive("paragraph"),
|
||||
isActive: () => editor.isActive(CORE_EXTENSIONS.PARAGRAPH),
|
||||
command: () => setText(editor),
|
||||
icon: CaseSensitive,
|
||||
});
|
||||
|
||||
export const HeadingOneItem = (editor: Editor): EditorMenuItem<"h1"> => ({
|
||||
key: "h1",
|
||||
name: "Heading 1",
|
||||
isActive: () => editor.isActive("heading", { level: 1 }),
|
||||
command: () => toggleHeadingOne(editor),
|
||||
icon: Heading1,
|
||||
type SupportedHeadingLevels = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
||||
|
||||
const HeadingItem = <T extends SupportedHeadingLevels>(
|
||||
editor: Editor,
|
||||
level: 1 | 2 | 3 | 4 | 5 | 6,
|
||||
key: T,
|
||||
name: string,
|
||||
icon: LucideIcon
|
||||
): EditorMenuItem<T> => ({
|
||||
key,
|
||||
name,
|
||||
isActive: () => editor.isActive(CORE_EXTENSIONS.HEADING, { level }),
|
||||
command: () => toggleHeading(editor, level),
|
||||
icon,
|
||||
});
|
||||
|
||||
export const HeadingTwoItem = (editor: Editor): EditorMenuItem<"h2"> => ({
|
||||
key: "h2",
|
||||
name: "Heading 2",
|
||||
isActive: () => editor.isActive("heading", { level: 2 }),
|
||||
command: () => toggleHeadingTwo(editor),
|
||||
icon: Heading2,
|
||||
});
|
||||
export const HeadingOneItem = (editor: Editor): EditorMenuItem<"h1"> =>
|
||||
HeadingItem(editor, 1, "h1", "Heading 1", Heading1);
|
||||
|
||||
export const HeadingThreeItem = (editor: Editor): EditorMenuItem<"h3"> => ({
|
||||
key: "h3",
|
||||
name: "Heading 3",
|
||||
isActive: () => editor.isActive("heading", { level: 3 }),
|
||||
command: () => toggleHeadingThree(editor),
|
||||
icon: Heading3,
|
||||
});
|
||||
export const HeadingTwoItem = (editor: Editor): EditorMenuItem<"h2"> =>
|
||||
HeadingItem(editor, 2, "h2", "Heading 2", Heading2);
|
||||
|
||||
export const HeadingFourItem = (editor: Editor): EditorMenuItem<"h4"> => ({
|
||||
key: "h4",
|
||||
name: "Heading 4",
|
||||
isActive: () => editor.isActive("heading", { level: 4 }),
|
||||
command: () => toggleHeadingFour(editor),
|
||||
icon: Heading4,
|
||||
});
|
||||
export const HeadingThreeItem = (editor: Editor): EditorMenuItem<"h3"> =>
|
||||
HeadingItem(editor, 3, "h3", "Heading 3", Heading3);
|
||||
|
||||
export const HeadingFiveItem = (editor: Editor): EditorMenuItem<"h5"> => ({
|
||||
key: "h5",
|
||||
name: "Heading 5",
|
||||
isActive: () => editor.isActive("heading", { level: 5 }),
|
||||
command: () => toggleHeadingFive(editor),
|
||||
icon: Heading5,
|
||||
});
|
||||
export const HeadingFourItem = (editor: Editor): EditorMenuItem<"h4"> =>
|
||||
HeadingItem(editor, 4, "h4", "Heading 4", Heading4);
|
||||
|
||||
export const HeadingSixItem = (editor: Editor): EditorMenuItem<"h6"> => ({
|
||||
key: "h6",
|
||||
name: "Heading 6",
|
||||
isActive: () => editor.isActive("heading", { level: 6 }),
|
||||
command: () => toggleHeadingSix(editor),
|
||||
icon: Heading6,
|
||||
});
|
||||
export const HeadingFiveItem = (editor: Editor): EditorMenuItem<"h5"> =>
|
||||
HeadingItem(editor, 5, "h5", "Heading 5", Heading5);
|
||||
|
||||
export const HeadingSixItem = (editor: Editor): EditorMenuItem<"h6"> =>
|
||||
HeadingItem(editor, 6, "h6", "Heading 6", Heading6);
|
||||
|
||||
export const BoldItem = (editor: Editor): EditorMenuItem<"bold"> => ({
|
||||
key: "bold",
|
||||
name: "Bold",
|
||||
isActive: () => editor?.isActive("bold"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.BOLD),
|
||||
command: () => toggleBold(editor),
|
||||
icon: BoldIcon,
|
||||
});
|
||||
@@ -129,7 +112,7 @@ export const BoldItem = (editor: Editor): EditorMenuItem<"bold"> => ({
|
||||
export const ItalicItem = (editor: Editor): EditorMenuItem<"italic"> => ({
|
||||
key: "italic",
|
||||
name: "Italic",
|
||||
isActive: () => editor?.isActive("italic"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.ITALIC),
|
||||
command: () => toggleItalic(editor),
|
||||
icon: ItalicIcon,
|
||||
});
|
||||
@@ -137,7 +120,7 @@ export const ItalicItem = (editor: Editor): EditorMenuItem<"italic"> => ({
|
||||
export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({
|
||||
key: "underline",
|
||||
name: "Underline",
|
||||
isActive: () => editor?.isActive("underline"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.UNDERLINE),
|
||||
command: () => toggleUnderline(editor),
|
||||
icon: UnderlineIcon,
|
||||
});
|
||||
@@ -145,7 +128,7 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({
|
||||
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({
|
||||
key: "strikethrough",
|
||||
name: "Strikethrough",
|
||||
isActive: () => editor?.isActive("strike"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.STRIKETHROUGH),
|
||||
command: () => toggleStrike(editor),
|
||||
icon: StrikethroughIcon,
|
||||
});
|
||||
@@ -153,7 +136,7 @@ export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough
|
||||
export const BulletListItem = (editor: Editor): EditorMenuItem<"bulleted-list"> => ({
|
||||
key: "bulleted-list",
|
||||
name: "Bulleted list",
|
||||
isActive: () => editor?.isActive("bulletList"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.BULLET_LIST),
|
||||
command: () => toggleBulletList(editor),
|
||||
icon: ListIcon,
|
||||
});
|
||||
@@ -161,7 +144,7 @@ export const BulletListItem = (editor: Editor): EditorMenuItem<"bulleted-list">
|
||||
export const NumberedListItem = (editor: Editor): EditorMenuItem<"numbered-list"> => ({
|
||||
key: "numbered-list",
|
||||
name: "Numbered list",
|
||||
isActive: () => editor?.isActive("orderedList"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.ORDERED_LIST),
|
||||
command: () => toggleOrderedList(editor),
|
||||
icon: ListOrderedIcon,
|
||||
});
|
||||
@@ -169,7 +152,7 @@ export const NumberedListItem = (editor: Editor): EditorMenuItem<"numbered-list"
|
||||
export const TodoListItem = (editor: Editor): EditorMenuItem<"to-do-list"> => ({
|
||||
key: "to-do-list",
|
||||
name: "To-do list",
|
||||
isActive: () => editor.isActive("taskItem"),
|
||||
isActive: () => editor.isActive(CORE_EXTENSIONS.TASK_ITEM),
|
||||
command: () => toggleTaskList(editor),
|
||||
icon: CheckSquare,
|
||||
});
|
||||
@@ -177,7 +160,7 @@ export const TodoListItem = (editor: Editor): EditorMenuItem<"to-do-list"> => ({
|
||||
export const QuoteItem = (editor: Editor): EditorMenuItem<"quote"> => ({
|
||||
key: "quote",
|
||||
name: "Quote",
|
||||
isActive: () => editor?.isActive("blockquote"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.BLOCKQUOTE),
|
||||
command: () => toggleBlockquote(editor),
|
||||
icon: TextQuote,
|
||||
});
|
||||
@@ -185,7 +168,7 @@ export const QuoteItem = (editor: Editor): EditorMenuItem<"quote"> => ({
|
||||
export const CodeItem = (editor: Editor): EditorMenuItem<"code"> => ({
|
||||
key: "code",
|
||||
name: "Code",
|
||||
isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.CODE_INLINE) || editor?.isActive(CORE_EXTENSIONS.CODE_BLOCK),
|
||||
command: () => toggleCodeBlock(editor),
|
||||
icon: CodeIcon,
|
||||
});
|
||||
@@ -193,7 +176,7 @@ export const CodeItem = (editor: Editor): EditorMenuItem<"code"> => ({
|
||||
export const TableItem = (editor: Editor): EditorMenuItem<"table"> => ({
|
||||
key: "table",
|
||||
name: "Table",
|
||||
isActive: () => editor?.isActive("table"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.TABLE),
|
||||
command: () => insertTableCommand(editor),
|
||||
icon: TableIcon,
|
||||
});
|
||||
@@ -201,7 +184,7 @@ export const TableItem = (editor: Editor): EditorMenuItem<"table"> => ({
|
||||
export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({
|
||||
key: "image",
|
||||
name: "Image",
|
||||
isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.IMAGE) || editor?.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE),
|
||||
command: () => insertImage({ editor, event: "insert", pos: editor.state.selection.from }),
|
||||
icon: ImageIcon,
|
||||
});
|
||||
@@ -210,7 +193,7 @@ export const HorizontalRuleItem = (editor: Editor) =>
|
||||
({
|
||||
key: "divider",
|
||||
name: "Divider",
|
||||
isActive: () => editor?.isActive("horizontalRule"),
|
||||
isActive: () => editor?.isActive(CORE_EXTENSIONS.HORIZONTAL_RULE),
|
||||
command: () => insertHorizontalRule(editor),
|
||||
icon: MinusSquare,
|
||||
}) as const;
|
||||
@@ -218,7 +201,7 @@ export const HorizontalRuleItem = (editor: Editor) =>
|
||||
export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ({
|
||||
key: "text-color",
|
||||
name: "Color",
|
||||
isActive: (props) => editor.isActive("customColor", { color: props?.color }),
|
||||
isActive: (props) => editor.isActive(CORE_EXTENSIONS.CUSTOM_COLOR, { color: props?.color }),
|
||||
command: (props) => {
|
||||
if (!props) return;
|
||||
toggleTextColor(props.color, editor);
|
||||
@@ -229,7 +212,7 @@ export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => (
|
||||
export const BackgroundColorItem = (editor: Editor): EditorMenuItem<"background-color"> => ({
|
||||
key: "background-color",
|
||||
name: "Background color",
|
||||
isActive: (props) => editor.isActive("customColor", { backgroundColor: props?.color }),
|
||||
isActive: (props) => editor.isActive(CORE_EXTENSIONS.CUSTOM_COLOR, { backgroundColor: props?.color }),
|
||||
command: (props) => {
|
||||
if (!props) return;
|
||||
toggleBackgroundColor(props.color, editor);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
export const CORE_EXTENSIONS = {
|
||||
BLOCKQUOTE: "blockquote",
|
||||
BOLD: "bold",
|
||||
BULLET_LIST: "bulletList",
|
||||
CALLOUT: "calloutComponent",
|
||||
CHARACTER_COUNT: "characterCount",
|
||||
CODE_BLOCK: "codeBlock",
|
||||
CODE_INLINE: "code",
|
||||
CUSTOM_COLOR: "customColor",
|
||||
CUSTOM_IMAGE: "imageComponent",
|
||||
CUSTOM_LINK: "link",
|
||||
DOCUMENT: "doc",
|
||||
DROP_CURSOR: "dropCursor",
|
||||
ENTER_KEY: "enterKey",
|
||||
GAP_CURSOR: "gapCursor",
|
||||
HARD_BREAK: "hardBreak",
|
||||
HEADING: "heading",
|
||||
HEADINGS_LIST: "headingsList",
|
||||
HISTORY: "history",
|
||||
HORIZONTAL_RULE: "horizontalRule",
|
||||
IMAGE: "image",
|
||||
ITALIC: "italic",
|
||||
LIST_ITEM: "listItem",
|
||||
MARKDOWN_CLIPBOARD: "markdownClipboard",
|
||||
MENTION: "mention",
|
||||
ORDERED_LIST: "orderedList",
|
||||
PARAGRAPH: "paragraph",
|
||||
PLACEHOLDER: "placeholder",
|
||||
SIDE_MENU: "editorSideMenu",
|
||||
SLASH_COMMANDS: "slash-command",
|
||||
STRIKETHROUGH: "strike",
|
||||
TABLE: "table",
|
||||
TABLE_CELL: "tableCell",
|
||||
TABLE_HEADER: "tableHeader",
|
||||
TABLE_ROW: "tableRow",
|
||||
TASK_ITEM: "taskItem",
|
||||
TASK_LIST: "taskList",
|
||||
TEXT_ALIGN: "textAlign",
|
||||
TEXT_STYLE: "textStyle",
|
||||
TYPOGRAPHY: "typography",
|
||||
UNDERLINE: "underline",
|
||||
UTILITY: "utility",
|
||||
WORK_ITEM_EMBED: "issue-embed-component",
|
||||
} as const;
|
||||
|
||||
export type CORE_EXTENSIONS = typeof CORE_EXTENSIONS[keyof typeof CORE_EXTENSIONS];
|
||||
@@ -0,0 +1,5 @@
|
||||
export const CORE_EDITOR_META = {
|
||||
SKIP_FILE_DELETION: "skipFileDeletion",
|
||||
} as const;
|
||||
|
||||
export type CORE_EDITOR_META = typeof CORE_EDITOR_META[keyof typeof CORE_EDITOR_META];
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import React, { useState } from "react";
|
||||
// constants
|
||||
import { COLORS_LIST } from "@/constants/common";
|
||||
// local components
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { Node as NodeType } from "@tiptap/pm/model";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
import { Node as NodeType } from "@tiptap/pm/model";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// types
|
||||
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
|
||||
// utils
|
||||
@@ -9,14 +11,14 @@ import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES } from "./utils";
|
||||
// Extend Tiptap's Commands interface
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
calloutComponent: {
|
||||
[CORE_EXTENSIONS.CALLOUT]: {
|
||||
insertCallout: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomCalloutExtensionConfig = Node.create({
|
||||
name: "calloutComponent",
|
||||
name: CORE_EXTENSIONS.CALLOUT,
|
||||
group: "block",
|
||||
content: "block+",
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// plane helpers
|
||||
import { convertHexEmojiToDecimal } from "@plane/utils";
|
||||
// plane ui
|
||||
// plane imports
|
||||
import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
import { cn, convertHexEmojiToDecimal } from "@plane/utils";
|
||||
// types
|
||||
import { TCalloutBlockAttributes } from "./types";
|
||||
// utils
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
export enum EAttributeNames {
|
||||
ICON_COLOR = "data-icon-color",
|
||||
ICON_NAME = "data-icon-name",
|
||||
EMOJI_UNICODE = "data-emoji-unicode",
|
||||
EMOJI_URL = "data-emoji-url",
|
||||
LOGO_IN_USE = "data-logo-in-use",
|
||||
BACKGROUND = "data-background",
|
||||
BLOCK_TYPE = "data-block-type",
|
||||
}
|
||||
export const EAttributeNames = {
|
||||
ICON_COLOR: "data-icon-color",
|
||||
ICON_NAME: "data-icon-name",
|
||||
EMOJI_UNICODE: "data-emoji-unicode",
|
||||
EMOJI_URL: "data-emoji-url",
|
||||
LOGO_IN_USE: "data-logo-in-use",
|
||||
BACKGROUND: "data-background",
|
||||
BLOCK_TYPE: "data-block-type",
|
||||
} as const;
|
||||
|
||||
export type EAttributeNames = typeof EAttributeNames[keyof typeof EAttributeNames];
|
||||
|
||||
export type TCalloutBlockIconAttributes = {
|
||||
[EAttributeNames.ICON_COLOR]: string | undefined;
|
||||
@@ -20,7 +22,7 @@ export type TCalloutBlockEmojiAttributes = {
|
||||
|
||||
export type TCalloutBlockAttributes = {
|
||||
[EAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
|
||||
[EAttributeNames.BACKGROUND]: string;
|
||||
[EAttributeNames.BACKGROUND]: string | undefined;
|
||||
[EAttributeNames.BLOCK_TYPE]: "callout-component";
|
||||
} & TCalloutBlockIconAttributes &
|
||||
TCalloutBlockEmojiAttributes;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// plane helpers
|
||||
import { sanitizeHTML } from "@plane/utils";
|
||||
// plane ui
|
||||
// plane imports
|
||||
import { TEmojiLogoProps } from "@plane/ui";
|
||||
import { sanitizeHTML } from "@plane/utils";
|
||||
// types
|
||||
import {
|
||||
EAttributeNames,
|
||||
@@ -12,11 +11,11 @@ import {
|
||||
|
||||
export const DEFAULT_CALLOUT_BLOCK_ATTRIBUTES: TCalloutBlockAttributes = {
|
||||
"data-logo-in-use": "emoji",
|
||||
"data-icon-color": null,
|
||||
"data-icon-name": null,
|
||||
"data-icon-color": undefined,
|
||||
"data-icon-name": undefined,
|
||||
"data-emoji-unicode": "128161",
|
||||
"data-emoji-url": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png",
|
||||
"data-background": null,
|
||||
"data-background": undefined,
|
||||
"data-block-type": "callout-component",
|
||||
};
|
||||
|
||||
@@ -32,7 +31,7 @@ export const getStoredLogo = (): TStoredLogoValue => {
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo"));
|
||||
const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo") ?? "");
|
||||
if (storedData) {
|
||||
let parsedData: TEmojiLogoProps;
|
||||
try {
|
||||
@@ -69,7 +68,7 @@ export const updateStoredLogo = (value: TEmojiLogoProps): void => {
|
||||
// function to get the stored background color from local storage
|
||||
export const getStoredBackgroundColor = (): string | null => {
|
||||
if (typeof window !== "undefined") {
|
||||
return sanitizeHTML(localStorage.getItem("editor-calloutComponent-background"));
|
||||
return sanitizeHTML(localStorage.getItem("editor-calloutComponent-background") ?? "");
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { Fragment, Node } from "@tiptap/pm/model";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
|
||||
export const MarkdownClipboard = Extension.create({
|
||||
name: "markdownClipboard",
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("markdownClipboard"),
|
||||
props: {
|
||||
clipboardTextSerializer: (slice) => {
|
||||
const markdownSerializer = this.editor.storage.markdown.serializer;
|
||||
const isTableRow = slice.content.firstChild?.type?.name === "tableRow";
|
||||
const nodeSelect = slice.openStart === 0 && slice.openEnd === 0;
|
||||
|
||||
if (nodeSelect) {
|
||||
return markdownSerializer.serialize(slice.content);
|
||||
}
|
||||
|
||||
const processTableContent = (tableNode: Node | Fragment) => {
|
||||
let result = "";
|
||||
tableNode.content?.forEach?.((tableRowNode: Node | Fragment) => {
|
||||
tableRowNode.content?.forEach?.((cell: Node) => {
|
||||
const cellContent = cell.content ? markdownSerializer.serialize(cell.content) : "";
|
||||
result += cellContent + "\n";
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
if (isTableRow) {
|
||||
const rowsCount = slice.content?.childCount || 0;
|
||||
const cellsCount = slice.content?.firstChild?.content?.childCount || 0;
|
||||
if (rowsCount === 1 || cellsCount === 1) {
|
||||
return processTableContent(slice.content);
|
||||
} else {
|
||||
return markdownSerializer.serialize(slice.content);
|
||||
}
|
||||
}
|
||||
|
||||
const traverseToParentOfLeaf = (
|
||||
node: Node | null,
|
||||
parent: Fragment | Node,
|
||||
depth: number
|
||||
): Node | Fragment => {
|
||||
let currentNode = node;
|
||||
let currentParent = parent;
|
||||
let currentDepth = depth;
|
||||
|
||||
while (currentNode && currentDepth > 1 && currentNode.content?.firstChild) {
|
||||
if (currentNode.content?.childCount > 1) {
|
||||
if (currentNode.content.firstChild?.type?.name === "listItem") {
|
||||
return currentParent;
|
||||
} else {
|
||||
return currentNode.content;
|
||||
}
|
||||
}
|
||||
|
||||
currentParent = currentNode;
|
||||
currentNode = currentNode.content?.firstChild || null;
|
||||
currentDepth--;
|
||||
}
|
||||
|
||||
return currentParent;
|
||||
};
|
||||
|
||||
if (slice.content.childCount > 1) {
|
||||
return markdownSerializer.serialize(slice.content);
|
||||
} else {
|
||||
const targetNode = traverseToParentOfLeaf(slice.content.firstChild, slice.content, slice.openStart);
|
||||
|
||||
let currentNode = targetNode;
|
||||
while (currentNode && currentNode.content && currentNode.childCount === 1 && currentNode.firstChild) {
|
||||
currentNode = currentNode.firstChild;
|
||||
}
|
||||
if (currentNode instanceof Node && currentNode.isText) {
|
||||
return currentNode.text;
|
||||
}
|
||||
|
||||
return markdownSerializer.serialize(targetNode);
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Mark, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
|
||||
export interface CodeOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
@@ -6,7 +8,7 @@ export interface CodeOptions {
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
code: {
|
||||
[CORE_EXTENSIONS.CODE_INLINE]: {
|
||||
/**
|
||||
* Set a code mark
|
||||
*/
|
||||
@@ -27,7 +29,7 @@ export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/;
|
||||
const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/g;
|
||||
|
||||
export const CustomCodeInlineExtension = Mark.create<CodeOptions>({
|
||||
name: "code",
|
||||
name: CORE_EXTENSIONS.CODE_INLINE,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { NodeViewWrapper, NodeViewContent } from "@tiptap/react";
|
||||
import ts from "highlight.js/lib/languages/typescript";
|
||||
import { common, createLowlight } from "lowlight";
|
||||
import { CopyIcon, CheckIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// plane utils
|
||||
@@ -27,7 +27,7 @@ export const CodeBlockComponent: React.FC<CodeBlockComponentProps> = ({ node })
|
||||
await navigator.clipboard.writeText(node.textContent);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1000);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setCopied(false);
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { mergeAttributes, Node, textblockTypeInputRule } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
|
||||
export interface CodeBlockOptions {
|
||||
/**
|
||||
@@ -25,7 +27,7 @@ export interface CodeBlockOptions {
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
codeBlock: {
|
||||
[CORE_EXTENSIONS.CODE_BLOCK]: {
|
||||
/**
|
||||
* Set a code block
|
||||
*/
|
||||
@@ -42,7 +44,7 @@ export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
|
||||
export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
|
||||
|
||||
export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
name: "codeBlock",
|
||||
name: CORE_EXTENSIONS.CODE_BLOCK,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
@@ -118,7 +120,7 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
toggleCodeBlock:
|
||||
(attributes) =>
|
||||
({ commands }) =>
|
||||
commands.toggleNode(this.name, "paragraph", attributes),
|
||||
commands.toggleNode(this.name, CORE_EXTENSIONS.PARAGRAPH, attributes),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -126,7 +128,7 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
return {
|
||||
"Mod-Alt-c": () => this.editor.commands.toggleCodeBlock(),
|
||||
|
||||
// remove code block when at start of document or code block is empty
|
||||
// remove codeBlock when at start of document or codeBlock is empty
|
||||
Backspace: () => {
|
||||
try {
|
||||
const { empty, $anchor } = this.editor.state.selection;
|
||||
@@ -259,7 +261,7 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.editor.isActive("code")) {
|
||||
if (this.editor.isActive(CORE_EXTENSIONS.CODE_INLINE)) {
|
||||
// Check if it's an inline code block
|
||||
event.preventDefault();
|
||||
const text = event.clipboardData.getData("text/plain");
|
||||
|
||||
@@ -88,7 +88,7 @@ export function LowlightPlugin({
|
||||
throw Error("You should provide an instance of lowlight to use the code-block-lowlight extension");
|
||||
}
|
||||
|
||||
const lowlightPlugin: Plugin<any> = new Plugin({
|
||||
const lowlightPlugin: Plugin = new Plugin({
|
||||
key: new PluginKey("lowlight"),
|
||||
|
||||
state: {
|
||||
|
||||
@@ -3,24 +3,24 @@ import TaskList from "@tiptap/extension-task-list";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import TiptapUnderline from "@tiptap/extension-underline";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
// extensions
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
// plane editor imports
|
||||
import { CoreEditorAdditionalExtensionsWithoutProps } from "@/plane-editor/extensions/core/without-props";
|
||||
// extensions
|
||||
import { CustomCalloutExtensionConfig } from "./callout/extension-config";
|
||||
import { CustomCodeBlockExtensionWithoutProps } from "./code/without-props";
|
||||
import { CustomCodeInlineExtension } from "./code-inline";
|
||||
import { CustomColorExtension } from "./custom-color";
|
||||
import { CustomLinkExtension } from "./custom-link";
|
||||
import { CustomHorizontalRule } from "./horizontal-rule";
|
||||
import { ImageExtensionWithoutProps } from "./image";
|
||||
import { CustomImageComponentWithoutProps } from "./image/image-component-without-props";
|
||||
import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props";
|
||||
import { CustomMentionExtensionConfig } from "./mentions/extension-config";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
import { TableHeader, TableCell, TableRow, Table } from "./table";
|
||||
import { CustomTextAlignExtension } from "./text-align";
|
||||
import { CustomCalloutExtensionConfig } from "./callout/extension-config";
|
||||
import { CustomColorExtension } from "./custom-color";
|
||||
// plane editor extensions
|
||||
import { CoreEditorAdditionalExtensionsWithoutProps } from "@/plane-editor/extensions/core/without-props";
|
||||
import { WorkItemEmbedExtensionConfig } from "./work-item-embed/extension-config";
|
||||
|
||||
export const CoreEditorExtensionsWithoutProps = [
|
||||
StarterKit.configure({
|
||||
@@ -72,12 +72,12 @@ export const CoreEditorExtensionsWithoutProps = [
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
ImageExtensionWithoutProps().configure({
|
||||
ImageExtensionWithoutProps.configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomImageComponentWithoutProps(),
|
||||
CustomImageComponentWithoutProps,
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
@@ -104,4 +104,4 @@ export const CoreEditorExtensionsWithoutProps = [
|
||||
...CoreEditorAdditionalExtensionsWithoutProps,
|
||||
];
|
||||
|
||||
export const DocumentEditorExtensionsWithoutProps = [IssueWidgetWithoutProps()];
|
||||
export const DocumentEditorExtensionsWithoutProps = [WorkItemEmbedExtensionConfig];
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
import codemark from "prosemirror-codemark";
|
||||
|
||||
export const CustomCodeMarkPlugin = Extension.create({
|
||||
name: "codemarkPlugin",
|
||||
addProseMirrorPlugins() {
|
||||
return codemark({ markType: this.editor.schema.marks.code });
|
||||
},
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Mark, mergeAttributes } from "@tiptap/core";
|
||||
// constants
|
||||
import { COLORS_LIST } from "@/constants/common";
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
color: {
|
||||
[CORE_EXTENSIONS.CUSTOM_COLOR]: {
|
||||
/**
|
||||
* Set the text color
|
||||
* @param {string} color The color to set
|
||||
@@ -34,7 +35,7 @@ declare module "@tiptap/core" {
|
||||
}
|
||||
|
||||
export const CustomColorExtension = Mark.create({
|
||||
name: "customColor",
|
||||
name: CORE_EXTENSIONS.CUSTOM_COLOR,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// extensions
|
||||
import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image";
|
||||
import { CustomBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image";
|
||||
import { ImageUploadStatus } from "./upload-status";
|
||||
|
||||
const MIN_SIZE = 100;
|
||||
@@ -38,7 +38,7 @@ const ensurePixelString = <TDefault,>(value: Pixel | TDefault | number | undefin
|
||||
return value;
|
||||
};
|
||||
|
||||
type CustomImageBlockProps = CustoBaseImageNodeViewProps & {
|
||||
type CustomImageBlockProps = CustomBaseImageNodeViewProps & {
|
||||
imageFromFileSystem: string | undefined;
|
||||
setFailedToLoadImage: (isError: boolean) => void;
|
||||
editorContainer: HTMLDivElement | null;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image";
|
||||
// helpers
|
||||
import { getExtensionStorage } from "@/helpers/get-extension-storage";
|
||||
|
||||
export type CustoBaseImageNodeViewProps = {
|
||||
export type CustomBaseImageNodeViewProps = {
|
||||
getPos: () => number;
|
||||
editor: Editor;
|
||||
node: NodeViewProps["node"] & {
|
||||
@@ -14,7 +17,7 @@ export type CustoBaseImageNodeViewProps = {
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type CustomImageNodeProps = NodeViewProps & CustoBaseImageNodeViewProps;
|
||||
export type CustomImageNodeProps = NodeViewProps & CustomBaseImageNodeViewProps;
|
||||
|
||||
export const CustomImageNode = (props: CustomImageNodeProps) => {
|
||||
const { getPos, editor, node, updateAttributes, selected } = props;
|
||||
@@ -77,7 +80,7 @@ export const CustomImageNode = (props: CustomImageNodeProps) => {
|
||||
failedToLoadImage={failedToLoadImage}
|
||||
getPos={getPos}
|
||||
loadImageFromFileSystem={setImageFromFileSystem}
|
||||
maxFileSize={getExtensionStorage(editor, "imageComponent").maxFileSize}
|
||||
maxFileSize={getExtensionStorage(editor, CORE_EXTENSIONS.CUSTOM_IMAGE).maxFileSize}
|
||||
node={node}
|
||||
setIsUploaded={setIsUploaded}
|
||||
selected={selected}
|
||||
|
||||
@@ -4,12 +4,16 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
import { CustoBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image";
|
||||
import { CustomBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image";
|
||||
// helpers
|
||||
import { EFileError } from "@/helpers/file";
|
||||
import { getExtensionStorage } from "@/helpers/get-extension-storage";
|
||||
// hooks
|
||||
import { useUploader, useDropZone, uploadFirstFileAndInsertRemaining } from "@/hooks/use-file-upload";
|
||||
|
||||
type CustomImageUploaderProps = CustoBaseImageNodeViewProps & {
|
||||
type CustomImageUploaderProps = CustomBaseImageNodeViewProps & {
|
||||
maxFileSize: number;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
failedToLoadImage: boolean;
|
||||
@@ -57,7 +61,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
// control cursor position after upload
|
||||
const nextNode = editor.state.doc.nodeAt(pos + 1);
|
||||
|
||||
if (nextNode && nextNode.type.name === "paragraph") {
|
||||
if (nextNode && nextNode.type.name === CORE_EXTENSIONS.PARAGRAPH) {
|
||||
// If there is a paragraph node after the image component, move the focus to the next node
|
||||
editor.commands.setTextSelection(pos + 1);
|
||||
} else {
|
||||
@@ -69,22 +73,37 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
},
|
||||
[imageComponentImageFileMap, imageEntityId, updateAttributes, getPos]
|
||||
);
|
||||
|
||||
const uploadImageEditorCommand = useCallback(
|
||||
async (file: File) => await editor?.commands.uploadImage(imageEntityId ?? "", file),
|
||||
[editor, imageEntityId]
|
||||
);
|
||||
|
||||
const handleProgressStatus = useCallback(
|
||||
(isUploading: boolean) => {
|
||||
getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY).uploadInProgress = isUploading;
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const handleInvalidFile = useCallback((_error: EFileError, _file: File, message: string) => {
|
||||
alert(message);
|
||||
}, []);
|
||||
|
||||
// hooks
|
||||
const { isUploading: isImageBeingUploaded, uploadFile } = useUploader({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
// @ts-expect-error - TODO: fix typings, and don't remove await from here for now
|
||||
editorCommand: async (file) => await editor?.commands.uploadImage(imageEntityId, file),
|
||||
handleProgressStatus: (isUploading) => {
|
||||
editor.storage.imageComponent.uploadInProgress = isUploading;
|
||||
},
|
||||
editorCommand: uploadImageEditorCommand,
|
||||
handleProgressStatus,
|
||||
loadFileFromFileSystem: loadImageFromFileSystem,
|
||||
maxFileSize,
|
||||
onInvalidFile: handleInvalidFile,
|
||||
onUpload,
|
||||
});
|
||||
|
||||
const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
editor,
|
||||
maxFileSize,
|
||||
pos: getPos(),
|
||||
type: "image",
|
||||
uploader: uploadFile,
|
||||
@@ -119,10 +138,8 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
return;
|
||||
}
|
||||
await uploadFirstFileAndInsertRemaining({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
editor,
|
||||
filesList,
|
||||
maxFileSize,
|
||||
pos: getPos(),
|
||||
type: "image",
|
||||
uploader: uploadFile,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { useEditorState } from "@tiptap/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// helpers
|
||||
import { getExtensionStorage } from "@/helpers/get-extension-storage";
|
||||
|
||||
type Props = {
|
||||
editor: Editor;
|
||||
@@ -16,7 +20,7 @@ export const ImageUploadStatus: React.FC<Props> = (props) => {
|
||||
// subscribe to image upload status
|
||||
const uploadStatus: number | undefined = useEditorState({
|
||||
editor,
|
||||
selector: ({ editor }) => editor.storage.imageComponent?.assetsUploadStatus[nodeId],
|
||||
selector: ({ editor }) => getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.assetsUploadStatus?.[nodeId],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Editor, mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
import { Image as BaseImageExtension } from "@tiptap/extension-image";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// constants
|
||||
import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
import { CustomImageNode } from "@/extensions/custom-image";
|
||||
// helpers
|
||||
import { isFileValid } from "@/helpers/file";
|
||||
import { getExtensionStorage } from "@/helpers/get-extension-storage";
|
||||
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
||||
// plugins
|
||||
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image";
|
||||
// types
|
||||
import { TFileHandler } from "@/types";
|
||||
|
||||
@@ -23,23 +22,21 @@ export type InsertImageComponentProps = {
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
imageComponent: {
|
||||
[CORE_EXTENSIONS.CUSTOM_IMAGE]: {
|
||||
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
|
||||
uploadImage: (blockId: string, file: File) => () => Promise<string> | undefined;
|
||||
updateAssetsUploadStatus?: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
|
||||
getImageSource?: (path: string) => () => Promise<string>;
|
||||
restoreImage: (src: string) => () => Promise<void>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const getImageComponentImageFileMap = (editor: Editor) => getExtensionStorage(editor, "imageComponent")?.fileMap;
|
||||
export const getImageComponentImageFileMap = (editor: Editor) =>
|
||||
getExtensionStorage(editor, CORE_EXTENSIONS.CUSTOM_IMAGE)?.fileMap;
|
||||
|
||||
export interface CustomImageExtensionStorage {
|
||||
assetsUploadStatus: TFileHandler["assetsUploadStatus"];
|
||||
fileMap: Map<string, UploadEntity>;
|
||||
deletedImageSet: Map<string, boolean>;
|
||||
uploadInProgress: boolean;
|
||||
maxFileSize: number;
|
||||
}
|
||||
|
||||
@@ -47,16 +44,14 @@ export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File })
|
||||
|
||||
export const CustomImageExtension = (props: TFileHandler) => {
|
||||
const {
|
||||
assetsUploadStatus,
|
||||
getAssetSrc,
|
||||
upload,
|
||||
delete: deleteImageFn,
|
||||
restore: restoreImageFn,
|
||||
validation: { maxFileSize },
|
||||
} = props;
|
||||
|
||||
return Image.extend<Record<string, unknown>, CustomImageExtensionStorage>({
|
||||
name: "imageComponent",
|
||||
return BaseImageExtension.extend<Record<string, unknown>, CustomImageExtensionStorage>({
|
||||
name: CORE_EXTENSIONS.CUSTOM_IMAGE,
|
||||
selectable: true,
|
||||
group: "block",
|
||||
atom: true,
|
||||
@@ -102,41 +97,15 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name),
|
||||
];
|
||||
},
|
||||
|
||||
onCreate(this) {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImageFn(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
deletedImageSet: new Map<string, boolean>(),
|
||||
uploadInProgress: false,
|
||||
maxFileSize,
|
||||
// escape markdown for images
|
||||
markdown: {
|
||||
serialize() {},
|
||||
},
|
||||
assetsUploadStatus,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -152,6 +121,7 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
file: props.file,
|
||||
maxFileSize,
|
||||
onError: (_error, message) => alert(message),
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
@@ -196,9 +166,6 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
const fileUrl = await upload(blockId, file);
|
||||
return fileUrl;
|
||||
},
|
||||
updateAssetsUploadStatus: (updatedStatus) => () => {
|
||||
this.storage.assetsUploadStatus = updatedStatus;
|
||||
},
|
||||
getImageSource: (path) => async () => await getAssetSrc(path),
|
||||
restoreImage: (src) => async () => {
|
||||
await restoreImageFn(src);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user