diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..fa445360f
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,20 @@
+### Description
+
+
+### Type of Change
+
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] Feature (non-breaking change which adds functionality)
+- [ ] Improvement (change that would cause existing functionality to not work as expected)
+- [ ] Code refactoring
+- [ ] Performance improvements
+- [ ] Documentation update
+
+### Screenshots and Media (if applicable)
+
+
+### Test Scenarios
+
+
+### References
+
\ No newline at end of file
diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml
index 1e06c1bd3..627c782f9 100644
--- a/.github/workflows/build-branch.yml
+++ b/.github/workflows/build-branch.yml
@@ -314,8 +314,8 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
attach_assets_to_build:
- if: ${{ needs.branch_build_setup.outputs.build_type == 'Build' }}
- name: Attach Assets to Build
+ if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
+ name: Attach Assets to Release
runs-on: ubuntu-20.04
needs: [branch_build_setup]
steps:
diff --git a/admin/core/components/admin-sidebar/root.tsx b/admin/core/components/admin-sidebar/root.tsx
index 9ef6b92bd..05dde0d8a 100644
--- a/admin/core/components/admin-sidebar/root.tsx
+++ b/admin/core/components/admin-sidebar/root.tsx
@@ -3,7 +3,7 @@
import { FC, useEffect, useRef } from "react";
import { observer } from "mobx-react";
// plane helpers
-import { useOutsideClickDetector } from "@plane/helpers";
+import { useOutsideClickDetector } from "@plane/hooks";
// components
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
// hooks
diff --git a/admin/package.json b/admin/package.json
index e4026da02..e2fe4cf33 100644
--- a/admin/package.json
+++ b/admin/package.json
@@ -1,6 +1,6 @@
{
"name": "admin",
- "version": "0.24.0",
+ "version": "0.24.1",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -14,9 +14,10 @@
"dependencies": {
"@headlessui/react": "^1.7.19",
"@plane/constants": "*",
- "@plane/helpers": "*",
+ "@plane/hooks": "*",
"@plane/types": "*",
"@plane/ui": "*",
+ "@plane/utils": "*",
"@sentry/nextjs": "^8.32.0",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
@@ -26,7 +27,7 @@
"lucide-react": "^0.356.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
- "next": "^14.2.12",
+ "next": "^14.2.20",
"next-themes": "^0.2.1",
"postcss": "^8.4.38",
"react": "^18.3.1",
diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api
index 97a2b2d41..b0fa44788 100644
--- a/apiserver/Dockerfile.api
+++ b/apiserver/Dockerfile.api
@@ -4,7 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
-ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
+ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
WORKDIR /code
diff --git a/apiserver/Dockerfile.dev b/apiserver/Dockerfile.dev
index c81966de4..3ec8c6340 100644
--- a/apiserver/Dockerfile.dev
+++ b/apiserver/Dockerfile.dev
@@ -4,7 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
-ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
+ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
RUN apk --no-cache add \
"bash~=5.2" \
diff --git a/apiserver/package.json b/apiserver/package.json
index 4b44b3898..6d350d83c 100644
--- a/apiserver/package.json
+++ b/apiserver/package.json
@@ -1,4 +1,4 @@
{
"name": "plane-api",
- "version": "0.24.0"
+ "version": "0.24.1"
}
diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py
index 26e623864..d91fdb60b 100644
--- a/apiserver/plane/app/urls/workspace.py
+++ b/apiserver/plane/app/urls/workspace.py
@@ -68,9 +68,7 @@ urlpatterns = [
# user workspace invitations
path(
"users/me/workspaces/invitations/",
- UserWorkspaceInvitationsViewSet.as_view(
- {"get": "list", "post": "create"}
- ),
+ UserWorkspaceInvitationsViewSet.as_view({"get": "list", "post": "create"}),
name="user-workspace-invitations",
),
path(
diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py
index 3d548aeac..d0c614368 100644
--- a/apiserver/plane/app/views/issue/base.py
+++ b/apiserver/plane/app/views/issue/base.py
@@ -15,8 +15,6 @@ from django.db.models import (
UUIDField,
Value,
Subquery,
- Case,
- When,
)
from django.db.models.functions import Coalesce
from django.utils import timezone
@@ -445,12 +443,10 @@ class IssueViewSet(BaseViewSet):
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
- cycle_id=Case(
- When(
- issue_cycle__cycle__deleted_at__isnull=True,
- then=F("issue_cycle__cycle_id"),
- ),
- default=None,
+ cycle_id=Subquery(
+ CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[
+ :1
+ ]
)
)
.annotate(
diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py
index 24ceb2d3f..46ce81ce1 100644
--- a/apiserver/plane/app/views/page/base.py
+++ b/apiserver/plane/app/views/page/base.py
@@ -114,7 +114,7 @@ class PageViewSet(BaseViewSet):
.distinct()
)
- @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
+ @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def create(self, request, slug, project_id):
serializer = PageSerializer(
data=request.data,
@@ -134,7 +134,7 @@ class PageViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
+ @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def partial_update(self, request, slug, project_id, pk):
try:
page = Page.objects.get(
@@ -234,7 +234,7 @@ class PageViewSet(BaseViewSet):
)
return Response(data, status=status.HTTP_200_OK)
- @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
+ @allow_permission([ROLE.ADMIN], model=Page, creator=True)
def lock(self, request, slug, project_id, pk):
page = Page.objects.filter(
pk=pk, workspace__slug=slug, projects__id=project_id
@@ -244,7 +244,7 @@ class PageViewSet(BaseViewSet):
page.save()
return Response(status=status.HTTP_204_NO_CONTENT)
- @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
+ @allow_permission([ROLE.ADMIN], model=Page, creator=True)
def unlock(self, request, slug, project_id, pk):
page = Page.objects.filter(
pk=pk, workspace__slug=slug, projects__id=project_id
@@ -255,7 +255,7 @@ class PageViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
- @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
+ @allow_permission([ROLE.ADMIN], model=Page, creator=True)
def access(self, request, slug, project_id, pk):
access = request.data.get("access", 0)
page = Page.objects.filter(
@@ -296,7 +296,7 @@ class PageViewSet(BaseViewSet):
pages = PageSerializer(queryset, many=True).data
return Response(pages, status=status.HTTP_200_OK)
- @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
+ @allow_permission([ROLE.ADMIN], model=Page, creator=True)
def archive(self, request, slug, project_id, pk):
page = Page.objects.get(pk=pk, workspace__slug=slug, projects__id=project_id)
@@ -323,7 +323,7 @@ class PageViewSet(BaseViewSet):
return Response({"archived_at": str(datetime.now())}, status=status.HTTP_200_OK)
- @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
+ @allow_permission([ROLE.ADMIN], model=Page, creator=True)
def unarchive(self, request, slug, project_id, pk):
page = Page.objects.get(pk=pk, workspace__slug=slug, projects__id=project_id)
@@ -348,7 +348,7 @@ class PageViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
- @allow_permission([ROLE.ADMIN], creator=True, model=Page)
+ @allow_permission([ROLE.ADMIN], model=Page, creator=True)
def destroy(self, request, slug, project_id, pk):
page = Page.objects.get(pk=pk, workspace__slug=slug, projects__id=project_id)
diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py
index c274d87c5..55d2d4a58 100644
--- a/apiserver/plane/app/views/project/member.py
+++ b/apiserver/plane/app/views/project/member.py
@@ -16,12 +16,7 @@ from plane.app.permissions import (
WorkspaceUserPermission,
)
-from plane.db.models import (
- Project,
- ProjectMember,
- IssueUserProperty,
- WorkspaceMember,
-)
+from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember
from plane.bgtasks.project_add_user_email_task import project_add_user_email
from plane.utils.host import base_host
from plane.app.permissions.base import allow_permission, ROLE
@@ -83,10 +78,7 @@ class ProjectMemberViewSet(BaseViewSet):
workspace_member_role = WorkspaceMember.objects.get(
workspace__slug=slug, member=member, is_active=True
).role
- if workspace_member_role in [20] and member_roles.get(member) in [
- 5,
- 15,
- ]:
+ if workspace_member_role in [20] and member_roles.get(member) in [5, 15]:
return Response(
{
"error": "You cannot add a user with role lower than the workspace role"
@@ -94,10 +86,7 @@ class ProjectMemberViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
- if workspace_member_role in [5] and member_roles.get(member) in [
- 15,
- 20,
- ]:
+ if workspace_member_role in [5] and member_roles.get(member) in [15, 20]:
return Response(
{
"error": "You cannot add a user with role higher than the workspace role"
@@ -135,8 +124,7 @@ class ProjectMemberViewSet(BaseViewSet):
sort_order = [
project_member.get("sort_order")
for project_member in project_members
- if str(project_member.get("member_id"))
- == str(member.get("member_id"))
+ if str(project_member.get("member_id")) == str(member.get("member_id"))
]
# Create a new project member
bulk_project_members.append(
@@ -145,9 +133,7 @@ class ProjectMemberViewSet(BaseViewSet):
role=member.get("role", 5),
project_id=project_id,
workspace_id=project.workspace_id,
- sort_order=(
- sort_order[0] - 10000 if len(sort_order) else 65535
- ),
+ sort_order=(sort_order[0] - 10000 if len(sort_order) else 65535),
)
)
# Create a new issue property
@@ -238,9 +224,7 @@ class ProjectMemberViewSet(BaseViewSet):
> requested_project_member.role
):
return Response(
- {
- "error": "You cannot update a role that is higher than your own role"
- },
+ {"error": "You cannot update a role that is higher than your own role"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -280,9 +264,7 @@ class ProjectMemberViewSet(BaseViewSet):
# User cannot deactivate higher role
if requesting_project_member.role < project_member.role:
return Response(
- {
- "error": "You cannot remove a user having role higher than you"
- },
+ {"error": "You cannot remove a user having role higher than you"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -303,10 +285,7 @@ class ProjectMemberViewSet(BaseViewSet):
if (
project_member.role == 20
and not ProjectMember.objects.filter(
- workspace__slug=slug,
- project_id=project_id,
- role=20,
- is_active=True,
+ workspace__slug=slug, project_id=project_id, role=20, is_active=True
).count()
> 1
):
@@ -344,7 +323,6 @@ class UserProjectRolesEndpoint(BaseAPIView):
).values("project_id", "role")
project_members = {
- str(member["project_id"]): member["role"]
- for member in project_members
+ str(member["project_id"]): member["role"] for member in project_members
}
return Response(project_members, status=status.HTTP_200_OK)
diff --git a/apiserver/plane/app/views/workspace/base.py b/apiserver/plane/app/views/workspace/base.py
index 515a3479b..058f7702a 100644
--- a/apiserver/plane/app/views/workspace/base.py
+++ b/apiserver/plane/app/views/workspace/base.py
@@ -41,6 +41,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
+
class WorkSpaceViewSet(BaseViewSet):
model = Workspace
serializer_class = WorkSpaceSerializer
@@ -81,12 +82,12 @@ class WorkSpaceViewSet(BaseViewSet):
def create(self, request):
try:
- DISABLE_WORKSPACE_CREATION, = get_configuration_value(
+ (DISABLE_WORKSPACE_CREATION,) = get_configuration_value(
[
{
"key": "DISABLE_WORKSPACE_CREATION",
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
- },
+ }
]
)
diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py
index 91a89ad07..9541f9980 100644
--- a/apiserver/plane/app/views/workspace/member.py
+++ b/apiserver/plane/app/views/workspace/member.py
@@ -1,22 +1,12 @@
# Django imports
-from django.db.models import (
- Count,
- Q,
- OuterRef,
- Subquery,
- IntegerField,
-)
+from django.db.models import Count, Q, OuterRef, Subquery, IntegerField
from django.db.models.functions import Coalesce
# Third party modules
from rest_framework import status
from rest_framework.response import Response
-from plane.app.permissions import (
- WorkspaceEntityPermission,
- allow_permission,
- ROLE,
-)
+from plane.app.permissions import WorkspaceEntityPermission, allow_permission, ROLE
# Module imports
from plane.app.serializers import (
@@ -26,12 +16,7 @@ from plane.app.serializers import (
WorkSpaceMemberSerializer,
)
from plane.app.views.base import BaseAPIView
-from plane.db.models import (
- Project,
- ProjectMember,
- WorkspaceMember,
- DraftIssue,
-)
+from plane.db.models import Project, ProjectMember, WorkspaceMember, DraftIssue
from plane.utils.cache import invalidate_cache
from .. import BaseViewSet
@@ -119,9 +104,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
if requesting_workspace_member.role < workspace_member.role:
return Response(
- {
- "error": "You cannot remove a user having role higher than you"
- },
+ {"error": "You cannot remove a user having role higher than you"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -148,9 +131,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,
+ workspace__slug=slug, member_id=workspace_member.member_id, is_active=True
).update(is_active=False)
workspace_member.is_active = False
@@ -164,9 +145,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
multiple=True,
)
@invalidate_cache(path="/api/users/me/settings/")
- @invalidate_cache(
- path="api/users/me/workspaces/", user=False, multiple=True
- )
+ @invalidate_cache(path="api/users/me/workspaces/", user=False, multiple=True)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
@@ -213,9 +192,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,
+ workspace__slug=slug, member_id=workspace_member.member_id, is_active=True
).update(is_active=False)
# # Deactivate the user
@@ -279,9 +256,7 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView):
project_members = ProjectMember.objects.filter(
workspace__slug=slug, project_id__in=project_ids, is_active=True
).select_related("project", "member", "workspace")
- project_members = ProjectMemberRoleSerializer(
- project_members, many=True
- ).data
+ project_members = ProjectMemberRoleSerializer(project_members, many=True).data
project_members_dict = dict()
diff --git a/apiserver/plane/authentication/views/app/check.py b/apiserver/plane/authentication/views/app/check.py
index c7e4b8a5e..0ad1db61f 100644
--- a/apiserver/plane/authentication/views/app/check.py
+++ b/apiserver/plane/authentication/views/app/check.py
@@ -60,6 +60,9 @@ class EmailCheckEndpoint(APIView):
)
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
+ # Lower the email
+ email = str(email).lower().strip()
+
# Validate email
try:
validate_email(email)
diff --git a/apiserver/plane/authentication/views/space/check.py b/apiserver/plane/authentication/views/space/check.py
index 9b4d8aa56..c8a4539b7 100644
--- a/apiserver/plane/authentication/views/space/check.py
+++ b/apiserver/plane/authentication/views/space/check.py
@@ -60,6 +60,7 @@ class EmailCheckSpaceEndpoint(APIView):
)
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
+ email = str(email).lower().strip()
# Validate email
try:
validate_email(email)
diff --git a/apiserver/plane/bgtasks/deletion_task.py b/apiserver/plane/bgtasks/deletion_task.py
index 0752272e3..30ff7e8bd 100644
--- a/apiserver/plane/bgtasks/deletion_task.py
+++ b/apiserver/plane/bgtasks/deletion_task.py
@@ -3,7 +3,8 @@ from django.utils import timezone
from django.apps import apps
from django.conf import settings
from django.db import models
-from django.core.exceptions import ObjectDoesNotExist
+from django.db.models.fields.related import OneToOneRel
+
# Third party imports
from celery import shared_task
@@ -11,31 +12,98 @@ from celery import shared_task
@shared_task
def soft_delete_related_objects(app_label, model_name, instance_pk, using=None):
+ """
+ Soft delete related objects for a given model instance
+ """
+ # Get the model class using app registry
model_class = apps.get_model(app_label, model_name)
- instance = model_class.all_objects.get(pk=instance_pk)
- related_fields = instance._meta.get_fields()
- for field in related_fields:
- if field.one_to_many or field.one_to_one:
- try:
- # Check if the field has CASCADE on delete
- if (
- not hasattr(field.remote_field, "on_delete")
- or field.remote_field.on_delete == models.CASCADE
- ):
- if field.one_to_many:
- related_objects = getattr(instance, field.name).all()
- elif field.one_to_one:
- related_object = getattr(instance, field.name)
- related_objects = (
- [related_object] if related_object is not None else []
- )
- for obj in related_objects:
- if obj:
- obj.deleted_at = timezone.now()
- obj.save(using=using)
- except ObjectDoesNotExist:
- pass
+ # Get the instance using all_objects to ensure we can get even if it's already soft deleted
+ try:
+ instance = model_class.all_objects.get(pk=instance_pk)
+ except model_class.DoesNotExist:
+ return
+
+ # Get all related fields that are reverse relationships
+ all_related = [
+ f
+ for f in instance._meta.get_fields()
+ if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete
+ ]
+
+ # Handle each related field
+ for relation in all_related:
+ related_name = relation.get_accessor_name()
+
+ # Skip if the relation doesn't exist
+ if not hasattr(instance, related_name):
+ continue
+
+ # Get the on_delete behavior name
+ on_delete_name = (
+ relation.on_delete.__name__
+ if hasattr(relation.on_delete, "__name__")
+ else ""
+ )
+
+ if on_delete_name == "DO_NOTHING":
+ continue
+
+ elif on_delete_name == "SET_NULL":
+ # Handle SET_NULL relationships
+ if isinstance(relation, OneToOneRel):
+ # For OneToOne relationships
+ related_obj = getattr(instance, related_name, None)
+ if related_obj and isinstance(related_obj, models.Model):
+ setattr(related_obj, relation.remote_field.name, None)
+ related_obj.save(update_fields=[relation.remote_field.name])
+ else:
+ # For other relationships
+ related_queryset = getattr(instance, related_name).all()
+ related_queryset.update(**{relation.remote_field.name: None})
+
+ else:
+ # Handle CASCADE and other delete behaviors
+ try:
+ if relation.one_to_one:
+ # Handle OneToOne relationships
+ related_obj = getattr(instance, related_name, None)
+ if related_obj:
+ if hasattr(related_obj, "deleted_at"):
+ if not related_obj.deleted_at:
+ related_obj.deleted_at = timezone.now()
+ related_obj.save()
+ # Recursively handle related objects
+ soft_delete_related_objects(
+ related_obj._meta.app_label,
+ related_obj._meta.model_name,
+ related_obj.pk,
+ using,
+ )
+ else:
+ # Handle other relationships
+ related_queryset = getattr(instance, related_name).all()
+ for related_obj in related_queryset:
+ if hasattr(related_obj, "deleted_at"):
+ if not related_obj.deleted_at:
+ related_obj.deleted_at = timezone.now()
+ related_obj.save()
+ # Recursively handle related objects
+ soft_delete_related_objects(
+ related_obj._meta.app_label,
+ related_obj._meta.model_name,
+ related_obj.pk,
+ using,
+ )
+ except Exception as e:
+ # Log the error or handle as needed
+ print(f"Error handling relation {related_name}: {str(e)}")
+ continue
+
+ # Finally, soft delete the instance itself if it hasn't been deleted yet
+ if hasattr(instance, "deleted_at") and not instance.deleted_at:
+ instance.deleted_at = timezone.now()
+ instance.save()
# @shared_task
diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py
index f7b19f00a..33e382f44 100644
--- a/apiserver/plane/bgtasks/export_task.py
+++ b/apiserver/plane/bgtasks/export_task.py
@@ -162,8 +162,7 @@ def generate_table_row(issue):
issue["priority"],
(
f"{issue['created_by__first_name']} {issue['created_by__last_name']}"
- if issue["created_by__first_name"]
- and issue["created_by__last_name"]
+ if issue["created_by__first_name"] and issue["created_by__last_name"]
else ""
),
(
@@ -197,8 +196,7 @@ def generate_json_row(issue):
"Priority": issue["priority"],
"Created By": (
f"{issue['created_by__first_name']} {issue['created_by__last_name']}"
- if issue["created_by__first_name"]
- and issue["created_by__last_name"]
+ if issue["created_by__first_name"] and issue["created_by__last_name"]
else ""
),
"Assignee": (
@@ -208,17 +206,11 @@ def generate_json_row(issue):
),
"Labels": issue["labels__name"] if issue["labels__name"] else "",
"Cycle Name": issue["issue_cycle__cycle__name"],
- "Cycle Start Date": dateConverter(
- issue["issue_cycle__cycle__start_date"]
- ),
+ "Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]),
"Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]),
"Module Name": issue["issue_module__module__name"],
- "Module Start Date": dateConverter(
- issue["issue_module__module__start_date"]
- ),
- "Module Target Date": dateConverter(
- issue["issue_module__module__target_date"]
- ),
+ "Module Start Date": dateConverter(issue["issue_module__module__start_date"]),
+ "Module Target Date": dateConverter(issue["issue_module__module__target_date"]),
"Created At": dateTimeConverter(issue["created_at"]),
"Updated At": dateTimeConverter(issue["updated_at"]),
"Completed At": dateTimeConverter(issue["completed_at"]),
diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py
index ade247909..f61c2e3c3 100644
--- a/apiserver/plane/bgtasks/notification_task.py
+++ b/apiserver/plane/bgtasks/notification_task.py
@@ -257,7 +257,9 @@ def notifications(
)
new_mentions = [
- str(mention) for mention in new_mentions if mention in set(project_members)
+ str(mention)
+ for mention in new_mentions
+ if mention in set(project_members)
]
removed_mention = get_removed_mentions(
requested_instance=requested_data, current_instance=current_instance
diff --git a/apiserver/plane/db/management/commands/create_project_member.py b/apiserver/plane/db/management/commands/create_project_member.py
index a2a5c669e..927f97e9d 100644
--- a/apiserver/plane/db/management/commands/create_project_member.py
+++ b/apiserver/plane/db/management/commands/create_project_member.py
@@ -13,28 +13,14 @@ from plane.db.models import (
class Command(BaseCommand):
-
help = "Add a member to a project. If present in the workspace"
def add_arguments(self, parser):
# Positional argument
+ parser.add_argument("--project_id", type=str, nargs="?", help="Project ID")
+ parser.add_argument("--user_email", type=str, nargs="?", help="User Email")
parser.add_argument(
- "--project_id",
- type=str,
- nargs="?",
- help="Project ID",
- )
- parser.add_argument(
- "--user_email",
- type=str,
- nargs="?",
- help="User Email",
- )
- parser.add_argument(
- "--role",
- type=int,
- nargs="?",
- help="Role of the user in the project",
+ "--role", type=int, nargs="?", help="Role of the user in the project"
)
def handle(self, *args: Any, **options: Any):
@@ -67,9 +53,7 @@ class Command(BaseCommand):
# Get the smallest sort order
smallest_sort_order = (
- ProjectMember.objects.filter(
- workspace_id=project.workspace_id,
- )
+ ProjectMember.objects.filter(workspace_id=project.workspace_id)
.order_by("sort_order")
.first()
)
@@ -79,22 +63,15 @@ class Command(BaseCommand):
else:
sort_order = 65535
- if ProjectMember.objects.filter(
- project=project,
- member=user,
- ).exists():
+ if ProjectMember.objects.filter(project=project, member=user).exists():
# Update the project member
- ProjectMember.objects.filter(
- project=project,
- member=user,
- ).update(is_active=True, sort_order=sort_order, role=role)
+ ProjectMember.objects.filter(project=project, member=user).update(
+ is_active=True, sort_order=sort_order, role=role
+ )
else:
# Create the project member
ProjectMember.objects.create(
- project=project,
- member=user,
- role=role,
- sort_order=sort_order,
+ project=project, member=user, role=role, sort_order=sort_order
)
# Issue Property
@@ -102,9 +79,7 @@ class Command(BaseCommand):
# Success message
self.stdout.write(
- self.style.SUCCESS(
- f"User {user_email} added to project {project_id}"
- )
+ self.style.SUCCESS(f"User {user_email} added to project {project_id}")
)
return
except CommandError as e:
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index 36810956c..e3a9df254 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -53,7 +53,6 @@ from .project import (
ProjectMemberInvite,
ProjectPublicMember,
)
-from .deploy_board import DeployBoard
from .session import Session
from .social_connection import SocialLoginConnection
from .state import State
@@ -69,23 +68,14 @@ from .workspace import (
WorkspaceUserProperties,
)
-from .importer import Importer
-from .page import Page, PageLog, PageLabel
-from .estimate import Estimate, EstimatePoint
-from .intake import Intake, IntakeIssue
-from .analytic import AnalyticView
-from .notification import Notification, UserNotificationPreference, EmailNotificationLog
-from .exporter import ExporterHistory
-from .webhook import Webhook, WebhookLog
-from .dashboard import Dashboard, DashboardWidget, Widget
from .favorite import UserFavorite
diff --git a/apiserver/plane/db/models/asset.py b/apiserver/plane/db/models/asset.py
index 9f99a8144..9973d122f 100644
--- a/apiserver/plane/db/models/asset.py
+++ b/apiserver/plane/db/models/asset.py
@@ -44,45 +44,25 @@ class FileAsset(BaseModel):
"db.User", on_delete=models.CASCADE, null=True, related_name="assets"
)
workspace = models.ForeignKey(
- "db.Workspace",
- on_delete=models.CASCADE,
- null=True,
- related_name="assets",
+ "db.Workspace", on_delete=models.CASCADE, null=True, related_name="assets"
)
draft_issue = models.ForeignKey(
- "db.DraftIssue",
- on_delete=models.CASCADE,
- null=True,
- related_name="assets",
+ "db.DraftIssue", on_delete=models.CASCADE, null=True, related_name="assets"
)
project = models.ForeignKey(
- "db.Project",
- on_delete=models.CASCADE,
- null=True,
- related_name="assets",
+ "db.Project", on_delete=models.CASCADE, null=True, related_name="assets"
)
issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, null=True, related_name="assets"
)
comment = models.ForeignKey(
- "db.IssueComment",
- on_delete=models.CASCADE,
- null=True,
- related_name="assets",
+ "db.IssueComment", on_delete=models.CASCADE, null=True, related_name="assets"
)
page = models.ForeignKey(
"db.Page", on_delete=models.CASCADE, null=True, related_name="assets"
)
- entity_type = models.CharField(
- max_length=255,
- null=True,
- blank=True,
- )
- entity_identifier = models.CharField(
- max_length=255,
- null=True,
- blank=True,
- )
+ entity_type = models.CharField(max_length=255, null=True, blank=True)
+ entity_identifier = models.CharField(max_length=255, null=True, blank=True)
is_deleted = models.BooleanField(default=False)
is_archived = models.BooleanField(default=False)
external_id = models.CharField(max_length=255, null=True, blank=True)
diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py
index e50dbe7ce..9ea1d3b26 100644
--- a/apiserver/plane/db/models/issue.py
+++ b/apiserver/plane/db/models/issue.py
@@ -661,9 +661,7 @@ class IssueVote(ProjectBaseModel):
class IssueVersion(ProjectBaseModel):
issue = models.ForeignKey(
- "db.Issue",
- on_delete=models.CASCADE,
- related_name="versions",
+ "db.Issue", on_delete=models.CASCADE, related_name="versions"
)
PRIORITY_CHOICES = (
("urgent", "Urgent"),
@@ -688,9 +686,7 @@ class IssueVersion(ProjectBaseModel):
)
start_date = models.DateField(null=True, blank=True)
target_date = models.DateField(null=True, blank=True)
- sequence_id = models.IntegerField(
- default=1, verbose_name="Issue Sequence ID"
- )
+ sequence_id = models.IntegerField(default=1, verbose_name="Issue Sequence ID")
sort_order = models.FloatField(default=65535)
completed_at = models.DateTimeField(null=True)
archived_at = models.DateField(null=True)
@@ -700,25 +696,10 @@ class IssueVersion(ProjectBaseModel):
type = models.UUIDField(blank=True, null=True)
last_saved_at = models.DateTimeField(default=timezone.now)
owned_by = models.UUIDField()
- assignees = ArrayField(
- models.UUIDField(),
- blank=True,
- default=list,
- )
- labels = ArrayField(
- models.UUIDField(),
- blank=True,
- default=list,
- )
- cycle = models.UUIDField(
- null=True,
- blank=True,
- )
- modules = ArrayField(
- models.UUIDField(),
- blank=True,
- default=list,
- )
+ assignees = ArrayField(models.UUIDField(), blank=True, default=list)
+ labels = ArrayField(models.UUIDField(), blank=True, default=list)
+ cycle = models.UUIDField(null=True, blank=True)
+ modules = ArrayField(models.UUIDField(), blank=True, default=list)
properties = models.JSONField(default=dict)
meta = models.JSONField(default=dict)
@@ -741,9 +722,7 @@ class IssueVersion(ProjectBaseModel):
Module = apps.get_model("db.Module")
CycleIssue = apps.get_model("db.CycleIssue")
- cycle_issue = CycleIssue.objects.filter(
- issue=issue,
- ).first()
+ cycle_issue = CycleIssue.objects.filter(issue=issue).first()
cls.objects.create(
issue=issue,
@@ -771,9 +750,7 @@ class IssueVersion(ProjectBaseModel):
assignees=issue.assignees,
labels=issue.labels,
cycle=cycle_issue.cycle if cycle_issue else None,
- modules=Module.objects.filter(issue=issue).values_list(
- "id", flat=True
- ),
+ modules=Module.objects.filter(issue=issue).values_list("id", flat=True),
owned_by=user,
)
return True
diff --git a/apiserver/plane/db/models/webhook.py b/apiserver/plane/db/models/webhook.py
index 92d45a058..ec8fcda3a 100644
--- a/apiserver/plane/db/models/webhook.py
+++ b/apiserver/plane/db/models/webhook.py
@@ -29,9 +29,7 @@ def validate_domain(value):
class Webhook(BaseModel):
workspace = models.ForeignKey(
- "db.Workspace",
- on_delete=models.CASCADE,
- related_name="workspace_webhooks",
+ "db.Workspace", on_delete=models.CASCADE, related_name="workspace_webhooks"
)
url = models.URLField(
validators=[validate_schema, validate_domain], max_length=1024
diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py
index df1f26d3f..f8082e492 100644
--- a/apiserver/plane/db/models/workspace.py
+++ b/apiserver/plane/db/models/workspace.py
@@ -102,12 +102,7 @@ def get_default_display_properties():
def get_issue_props():
- return {
- "subscribed": True,
- "assigned": True,
- "created": True,
- "all_issues": True,
- }
+ return {"subscribed": True, "assigned": True, "created": True, "all_issues": True}
def slug_validator(value):
@@ -136,9 +131,7 @@ class Workspace(BaseModel):
max_length=48, db_index=True, unique=True, validators=[slug_validator]
)
organization_size = models.CharField(max_length=20, blank=True, null=True)
- timezone = models.CharField(
- max_length=255, default="UTC", choices=TIMEZONE_CHOICES
- )
+ timezone = models.CharField(max_length=255, default="UTC", choices=TIMEZONE_CHOICES)
def __str__(self):
"""Return name of the Workspace"""
@@ -167,10 +160,7 @@ class WorkspaceBaseModel(BaseModel):
"db.Workspace", models.CASCADE, related_name="workspace_%(class)s"
)
project = models.ForeignKey(
- "db.Project",
- models.CASCADE,
- related_name="project_%(class)s",
- null=True,
+ "db.Project", models.CASCADE, related_name="project_%(class)s", null=True
)
class Meta:
@@ -184,9 +174,7 @@ class WorkspaceBaseModel(BaseModel):
class WorkspaceMember(BaseModel):
workspace = models.ForeignKey(
- "db.Workspace",
- on_delete=models.CASCADE,
- related_name="workspace_member",
+ "db.Workspace", on_delete=models.CASCADE, related_name="workspace_member"
)
member = models.ForeignKey(
settings.AUTH_USER_MODEL,
@@ -221,9 +209,7 @@ class WorkspaceMember(BaseModel):
class WorkspaceMemberInvite(BaseModel):
workspace = models.ForeignKey(
- "db.Workspace",
- on_delete=models.CASCADE,
- related_name="workspace_member_invite",
+ "db.Workspace", on_delete=models.CASCADE, related_name="workspace_member_invite"
)
email = models.CharField(max_length=255)
accepted = models.BooleanField(default=False)
@@ -283,9 +269,7 @@ class WorkspaceTheme(BaseModel):
)
name = models.CharField(max_length=300)
actor = models.ForeignKey(
- settings.AUTH_USER_MODEL,
- on_delete=models.CASCADE,
- related_name="themes",
+ settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="themes"
)
colors = models.JSONField(default=dict)
@@ -320,9 +304,7 @@ class WorkspaceUserProperties(BaseModel):
)
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
- display_properties = models.JSONField(
- default=get_default_display_properties
- )
+ display_properties = models.JSONField(default=get_default_display_properties)
class Meta:
unique_together = ["workspace", "user", "deleted_at"]
diff --git a/apiserver/plane/license/api/serializers/__init__.py b/apiserver/plane/license/api/serializers/__init__.py
index 48ecd4536..6e0a5941c 100644
--- a/apiserver/plane/license/api/serializers/__init__.py
+++ b/apiserver/plane/license/api/serializers/__init__.py
@@ -2,4 +2,4 @@ from .instance import InstanceSerializer
from .configuration import InstanceConfigurationSerializer
from .admin import InstanceAdminSerializer, InstanceAdminMeSerializer
-from .workspace import WorkspaceSerializer
\ No newline at end of file
+from .workspace import WorkspaceSerializer
diff --git a/apiserver/plane/license/api/serializers/user.py b/apiserver/plane/license/api/serializers/user.py
index 8935a882f..c53b4a484 100644
--- a/apiserver/plane/license/api/serializers/user.py
+++ b/apiserver/plane/license/api/serializers/user.py
@@ -1,6 +1,8 @@
from .base import BaseSerializer
from plane.db.models import User
+
+
class UserLiteSerializer(BaseSerializer):
class Meta:
model = User
- fields = ["id", "email", "first_name", "last_name",]
+ fields = ["id", "email", "first_name", "last_name"]
diff --git a/apiserver/plane/license/api/views/__init__.py b/apiserver/plane/license/api/views/__init__.py
index d57ebf52c..a2ef90fac 100644
--- a/apiserver/plane/license/api/views/__init__.py
+++ b/apiserver/plane/license/api/views/__init__.py
@@ -13,6 +13,8 @@ from .admin import (
InstanceAdminUserSessionEndpoint,
)
-from .changelog import ChangeLogEndpoint
-from .workspace import InstanceWorkSpaceAvailabilityCheckEndpoint, InstanceWorkSpaceEndpoint
+from .workspace import (
+ InstanceWorkSpaceAvailabilityCheckEndpoint,
+ InstanceWorkSpaceEndpoint,
+)
diff --git a/apiserver/plane/license/api/views/changelog.py b/apiserver/plane/license/api/views/changelog.py
deleted file mode 100644
index 52583a35f..000000000
--- a/apiserver/plane/license/api/views/changelog.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Python imports
-import requests
-
-# Django imports
-from django.conf import settings
-
-# Third party imports
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.permissions import AllowAny
-
-# plane imports
-from .base import BaseAPIView
-
-
-class ChangeLogEndpoint(BaseAPIView):
- permission_classes = [AllowAny]
-
- def fetch_change_logs(self):
- response = requests.get(settings.INSTANCE_CHANGELOG_URL)
- response.raise_for_status()
- return response.json()
-
- def get(self, request):
- # Fetch the changelog
- if settings.INSTANCE_CHANGELOG_URL:
- data = self.fetch_change_logs()
- return Response(data, status=status.HTTP_200_OK)
- else:
- return Response(
- {"error": "could not fetch changelog please try again later"},
- status=status.HTTP_400_BAD_REQUEST,
- )
diff --git a/apiserver/plane/license/api/views/workspace.py b/apiserver/plane/license/api/views/workspace.py
index 14118d85b..607016cc3 100644
--- a/apiserver/plane/license/api/views/workspace.py
+++ b/apiserver/plane/license/api/views/workspace.py
@@ -43,19 +43,19 @@ class InstanceWorkSpaceEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
-
+
member_count = (
WorkspaceMember.objects.filter(
workspace=OuterRef("id"), member__is_bot=False, is_active=True
- ).select_related("owner")
+ )
+ .select_related("owner")
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
workspaces = Workspace.objects.annotate(
- total_projects=project_count,
- total_members=member_count,
+ total_projects=project_count, total_members=member_count
)
# Add search functionality
@@ -66,16 +66,14 @@ class InstanceWorkSpaceEndpoint(BaseAPIView):
return self.paginate(
request=request,
queryset=workspaces,
- on_results=lambda results: WorkspaceSerializer(
- results, many=True,
- ).data,
+ on_results=lambda results: WorkspaceSerializer(results, many=True).data,
max_per_page=10,
default_per_page=10,
)
def post(self, request):
try:
- serializer = WorkspaceSerializer (data=request.data)
+ serializer = WorkspaceSerializer(data=request.data)
slug = request.data.get("slug", False)
name = request.data.get("name", False)
diff --git a/apiserver/plane/license/urls.py b/apiserver/plane/license/urls.py
index 842af0959..9c3adbf98 100644
--- a/apiserver/plane/license/urls.py
+++ b/apiserver/plane/license/urls.py
@@ -11,14 +11,12 @@ from plane.license.api.views import (
InstanceAdminUserMeEndpoint,
InstanceAdminSignOutEndpoint,
InstanceAdminUserSessionEndpoint,
- ChangeLogEndpoint,
InstanceWorkSpaceAvailabilityCheckEndpoint,
InstanceWorkSpaceEndpoint,
)
urlpatterns = [
path("", InstanceEndpoint.as_view(), name="instance"),
- path("changelog/", ChangeLogEndpoint.as_view(), name="instance-changelog"),
path("admins/", InstanceAdminEndpoint.as_view(), name="instance-admins"),
path("admins/me/", InstanceAdminUserMeEndpoint.as_view(), name="instance-admins"),
path(
@@ -62,9 +60,5 @@ urlpatterns = [
InstanceWorkSpaceAvailabilityCheckEndpoint.as_view(),
name="instance-workspace-availability",
),
- path(
- "workspaces/",
- InstanceWorkSpaceEndpoint.as_view(),
- name="instance-workspace",
- ),
+ path("workspaces/", InstanceWorkSpaceEndpoint.as_view(), name="instance-workspace"),
]
diff --git a/apiserver/plane/space/urls/project.py b/apiserver/plane/space/urls/project.py
index 7676c9599..068b8c5c1 100644
--- a/apiserver/plane/space/urls/project.py
+++ b/apiserver/plane/space/urls/project.py
@@ -10,9 +10,15 @@ from plane.space.views import (
ProjectStatesEndpoint,
ProjectLabelsEndpoint,
ProjectMembersEndpoint,
+ ProjectMetaDataEndpoint,
)
urlpatterns = [
+ path(
+ "anchor//meta/",
+ ProjectMetaDataEndpoint.as_view(),
+ name="project-meta",
+ ),
path(
"anchor//settings/",
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
diff --git a/apiserver/plane/space/views/__init__.py b/apiserver/plane/space/views/__init__.py
index afdc1d337..22acfd15b 100644
--- a/apiserver/plane/space/views/__init__.py
+++ b/apiserver/plane/space/views/__init__.py
@@ -25,3 +25,5 @@ from .state import ProjectStatesEndpoint
from .label import ProjectLabelsEndpoint
from .asset import EntityAssetEndpoint, AssetRestoreEndpoint, EntityBulkAssetEndpoint
+
+from .meta import ProjectMetaDataEndpoint
diff --git a/apiserver/plane/space/views/meta.py b/apiserver/plane/space/views/meta.py
new file mode 100644
index 000000000..fa4413599
--- /dev/null
+++ b/apiserver/plane/space/views/meta.py
@@ -0,0 +1,34 @@
+# third party
+from rest_framework.permissions import AllowAny
+from rest_framework import status
+from rest_framework.response import Response
+
+from plane.db.models import DeployBoard, Project
+
+from .base import BaseAPIView
+from plane.space.serializer.project import ProjectLiteSerializer
+
+
+class ProjectMetaDataEndpoint(BaseAPIView):
+ permission_classes = [AllowAny]
+
+ def get(self, request, anchor):
+ try:
+ deploy_board = DeployBoard.objects.filter(
+ anchor=anchor, entity_name="project"
+ ).first()
+ except DeployBoard.DoesNotExist:
+ return Response(
+ {"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND
+ )
+
+ try:
+ project_id = deploy_board.entity_identifier
+ project = Project.objects.get(id=project_id)
+ except Project.DoesNotExist:
+ return Response(
+ {"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND
+ )
+
+ serializer = ProjectLiteSerializer(project)
+ return Response(serializer.data, status=status.HTTP_200_OK)
diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt
index 854ab95f9..40e90aedf 100644
--- a/apiserver/requirements/base.txt
+++ b/apiserver/requirements/base.txt
@@ -1,7 +1,7 @@
# base requirements
# django
-Django==4.2.16
+Django==4.2.17
# rest framework
djangorestframework==3.15.2
# postgres
diff --git a/live/package.json b/live/package.json
index a4fed4434..e098564e1 100644
--- a/live/package.json
+++ b/live/package.json
@@ -1,16 +1,16 @@
{
"name": "live",
- "version": "0.24.0",
+ "version": "0.24.1",
"description": "",
"main": "./src/server.ts",
"private": true,
"type": "module",
"scripts": {
+ "dev": "concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
"build": "babel src --out-dir dist --extensions \".ts,.js\"",
"start": "node dist/server.js",
- "lint": "eslint . --ext .ts,.tsx",
- "dev": "concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
- "lint:errors": "eslint . --ext .ts,.tsx --quiet"
+ "lint": "eslint src --ext .ts,.tsx",
+ "lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"keywords": [],
"author": "",
@@ -30,7 +30,7 @@
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
- "express": "^4.20.0",
+ "express": "^4.21.2",
"express-ws": "^5.0.2",
"helmet": "^7.1.0",
"ioredis": "^5.4.1",
diff --git a/live/src/core/hocuspocus-server.ts b/live/src/core/hocuspocus-server.ts
index b34a8fbb2..51896c23b 100644
--- a/live/src/core/hocuspocus-server.ts
+++ b/live/src/core/hocuspocus-server.ts
@@ -4,6 +4,10 @@ import { v4 as uuidv4 } from "uuid";
import { handleAuthentication } from "@/core/lib/authentication.js";
// extensions
import { getExtensions } from "@/core/extensions/index.js";
+import {
+ DocumentCollaborativeEvents,
+ TDocumentEventsServer,
+} from "@plane/editor/lib";
// editor types
import { TUserDetails } from "@plane/editor";
// types
@@ -55,6 +59,14 @@ export const getHocusPocusServer = async () => {
throw Error("Authentication unsuccessful!");
}
},
+ async onStateless({ payload, document }) {
+ // broadcast the client event (derived from the server event) to all the clients so that they can update their state
+ const response =
+ DocumentCollaborativeEvents[payload as TDocumentEventsServer].client;
+ if (response) {
+ document.broadcastStateless(response);
+ }
+ },
extensions,
debounce: 10000,
});
diff --git a/package.json b/package.json
index 070063609..f14aa4ac7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
- "version": "0.24.0",
+ "version": "0.24.1",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
diff --git a/packages/constants/package.json b/packages/constants/package.json
index 5271ee3b4..c1fe71a30 100644
--- a/packages/constants/package.json
+++ b/packages/constants/package.json
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
- "version": "0.24.0",
+ "version": "0.24.1",
"private": true,
- "main": "./index.ts"
+ "main": "./src/index.ts"
}
diff --git a/packages/constants/auth.ts b/packages/constants/src/auth.ts
similarity index 100%
rename from packages/constants/auth.ts
rename to packages/constants/src/auth.ts
diff --git a/packages/constants/src/endpoints.ts b/packages/constants/src/endpoints.ts
new file mode 100644
index 000000000..fa6db6ec7
--- /dev/null
+++ b/packages/constants/src/endpoints.ts
@@ -0,0 +1,18 @@
+export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
+// PI Base Url
+export const PI_BASE_URL = process.env.NEXT_PUBLIC_PI_BASE_URL || "";
+// God Mode Admin App Base Url
+export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || "";
+export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
+export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}/`);
+// Publish App Base Url
+export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || "";
+export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
+export const SITES_URL = encodeURI(`${SPACE_BASE_URL}${SPACE_BASE_PATH}/`);
+// Live App Base Url
+export const LIVE_BASE_URL = process.env.NEXT_PUBLIC_LIVE_BASE_URL || "";
+export const LIVE_BASE_PATH = process.env.NEXT_PUBLIC_LIVE_BASE_PATH || "";
+export const LIVE_URL = encodeURI(`${LIVE_BASE_URL}${LIVE_BASE_PATH}/`);
+// plane website url
+export const WEBSITE_URL =
+ process.env.NEXT_PUBLIC_WEBSITE_URL || "https://plane.so";
diff --git a/packages/constants/index.ts b/packages/constants/src/index.ts
similarity index 72%
rename from packages/constants/index.ts
rename to packages/constants/src/index.ts
index 85e95bf4e..418908622 100644
--- a/packages/constants/index.ts
+++ b/packages/constants/src/index.ts
@@ -1,3 +1,4 @@
export * from "./auth";
+export * from "./endpoints";
export * from "./issue";
export * from "./workspace";
diff --git a/packages/constants/issue.ts b/packages/constants/src/issue.ts
similarity index 100%
rename from packages/constants/issue.ts
rename to packages/constants/src/issue.ts
diff --git a/packages/constants/src/workspace.ts b/packages/constants/src/workspace.ts
new file mode 100644
index 000000000..c17b5432e
--- /dev/null
+++ b/packages/constants/src/workspace.ts
@@ -0,0 +1,76 @@
+export const ORGANIZATION_SIZE = [
+ "Just myself",
+ "2-10",
+ "11-50",
+ "51-200",
+ "201-500",
+ "500+",
+];
+
+export const RESTRICTED_URLS = [
+ "404",
+ "accounts",
+ "api",
+ "create-workspace",
+ "god-mode",
+ "installations",
+ "invitations",
+ "onboarding",
+ "profile",
+ "spaces",
+ "workspace-invitations",
+ "password",
+ "flags",
+ "monitor",
+ "monitoring",
+ "ingest",
+ "plane-pro",
+ "plane-ultimate",
+ "enterprise",
+ "plane-enterprise",
+ "disco",
+ "silo",
+ "chat",
+ "calendar",
+ "drive",
+ "channels",
+ "upgrade",
+ "billing",
+ "sign-in",
+ "sign-up",
+ "signin",
+ "signup",
+ "config",
+ "live",
+ "admin",
+ "m",
+ "import",
+ "importers",
+ "integrations",
+ "integration",
+ "configuration",
+ "initiatives",
+ "initiative",
+ "config",
+ "workflow",
+ "workflows",
+ "epics",
+ "epic",
+ "story",
+ "mobile",
+ "dashboard",
+ "desktop",
+ "onload",
+ "real-time",
+ "one",
+ "pages",
+ "mobile",
+ "business",
+ "pro",
+ "settings",
+ "monitor",
+ "license",
+ "licenses",
+ "instances",
+ "instance",
+];
diff --git a/packages/constants/workspace.ts b/packages/constants/workspace.ts
deleted file mode 100644
index 32f36de1b..000000000
--- a/packages/constants/workspace.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-export const ORGANIZATION_SIZE = [
- "Just myself",
- "2-10",
- "11-50",
- "51-200",
- "201-500",
- "500+",
-];
-
-export const RESTRICTED_URLS = [
- "404",
- "accounts",
- "api",
- "create-workspace",
- "error",
- "god-mode",
- "installations",
- "invitations",
- "onboarding",
- "profile",
- "spaces",
- "workspace-invitations",
-];
diff --git a/packages/editor/package.json b/packages/editor/package.json
index 8471513c9..8f7295a0e 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
- "version": "0.24.0",
+ "version": "0.24.1",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
@@ -27,6 +27,7 @@
"dev": "tsup --watch",
"check-types": "tsc --noEmit",
"lint": "eslint src --ext .ts,.tsx",
+ "lint:errors": "eslint src --ext .ts,.tsx --quiet",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"peerDependencies": {
@@ -36,8 +37,8 @@
"dependencies": {
"@floating-ui/react": "^0.26.4",
"@hocuspocus/provider": "^2.13.5",
- "@plane/helpers": "*",
"@plane/ui": "*",
+ "@plane/utils": "*",
"@tiptap/core": "^2.1.13",
"@tiptap/extension-blockquote": "^2.1.13",
"@tiptap/extension-character-count": "^2.6.5",
diff --git a/packages/editor/src/ce/extensions/core/extensions.ts b/packages/editor/src/ce/extensions/core/extensions.ts
new file mode 100644
index 000000000..d03229133
--- /dev/null
+++ b/packages/editor/src/ce/extensions/core/extensions.ts
@@ -0,0 +1,12 @@
+import { Extensions } from "@tiptap/core";
+// types
+import { TExtensions } from "@/types";
+
+type Props = {
+ disabledExtensions: TExtensions[];
+};
+
+export const CoreEditorAdditionalExtensions = (props: Props): Extensions => {
+ const {} = props;
+ return [];
+};
diff --git a/packages/editor/src/ce/extensions/core/index.ts b/packages/editor/src/ce/extensions/core/index.ts
new file mode 100644
index 000000000..9ffc978c3
--- /dev/null
+++ b/packages/editor/src/ce/extensions/core/index.ts
@@ -0,0 +1,2 @@
+export * from "./extensions";
+export * from "./read-only-extensions";
diff --git a/packages/editor/src/ce/extensions/core/read-only-extensions.ts b/packages/editor/src/ce/extensions/core/read-only-extensions.ts
new file mode 100644
index 000000000..398848e31
--- /dev/null
+++ b/packages/editor/src/ce/extensions/core/read-only-extensions.ts
@@ -0,0 +1,12 @@
+import { Extensions } from "@tiptap/core";
+// types
+import { TExtensions } from "@/types";
+
+type Props = {
+ disabledExtensions: TExtensions[];
+};
+
+export const CoreReadOnlyEditorAdditionalExtensions = (props: Props): Extensions => {
+ const {} = props;
+ return [];
+};
diff --git a/packages/editor/src/ce/extensions/core/without-props.ts b/packages/editor/src/ce/extensions/core/without-props.ts
new file mode 100644
index 000000000..0debff0ea
--- /dev/null
+++ b/packages/editor/src/ce/extensions/core/without-props.ts
@@ -0,0 +1,3 @@
+import { Extensions } from "@tiptap/core";
+
+export const CoreEditorAdditionalExtensionsWithoutProps: Extensions = [];
diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx
index e3c94fa0e..35d7c0f3d 100644
--- a/packages/editor/src/ce/extensions/document-extensions.tsx
+++ b/packages/editor/src/ce/extensions/document-extensions.tsx
@@ -15,7 +15,13 @@ type Props = {
export const DocumentEditorAdditionalExtensions = (_props: Props) => {
const { disabledExtensions } = _props;
- const extensions: Extensions = disabledExtensions?.includes("slash-commands") ? [] : [SlashCommands()];
+ const extensions: Extensions = disabledExtensions?.includes("slash-commands")
+ ? []
+ : [
+ SlashCommands({
+ disabledExtensions,
+ }),
+ ];
return extensions;
};
diff --git a/packages/editor/src/ce/extensions/index.ts b/packages/editor/src/ce/extensions/index.ts
index 4a975b8c5..c9f58a936 100644
--- a/packages/editor/src/ce/extensions/index.ts
+++ b/packages/editor/src/ce/extensions/index.ts
@@ -1 +1,3 @@
+export * from "./core";
export * from "./document-extensions";
+export * from "./slash-commands";
diff --git a/packages/editor/src/ce/extensions/slash-commands.tsx b/packages/editor/src/ce/extensions/slash-commands.tsx
new file mode 100644
index 000000000..6eabee082
--- /dev/null
+++ b/packages/editor/src/ce/extensions/slash-commands.tsx
@@ -0,0 +1,14 @@
+// extensions
+import { TSlashCommandAdditionalOption } from "@/extensions";
+// types
+import { TExtensions } from "@/types";
+
+type Props = {
+ disabledExtensions: TExtensions[];
+};
+
+export const coreEditorAdditionalSlashCommandOptions = (props: Props): TSlashCommandAdditionalOption[] => {
+ const {} = props;
+ const options: TSlashCommandAdditionalOption[] = [];
+ return options;
+};
diff --git a/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx
index aa925abec..89acace7b 100644
--- a/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx
+++ b/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx
@@ -15,6 +15,7 @@ import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/ty
const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOnlyEditor) => {
const {
containerClassName,
+ disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
embedHandler,
@@ -37,6 +38,7 @@ const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOn
}
const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({
+ disabledExtensions,
editorClassName,
extensions,
fileHandler,
diff --git a/packages/editor/src/core/components/editors/document/read-only-editor.tsx b/packages/editor/src/core/components/editors/document/read-only-editor.tsx
index 8544157aa..b36fb44a7 100644
--- a/packages/editor/src/core/components/editors/document/read-only-editor.tsx
+++ b/packages/editor/src/core/components/editors/document/read-only-editor.tsx
@@ -10,9 +10,10 @@ import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types
-import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig, TFileHandler } from "@/types";
+import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig, TExtensions, TFileHandler } from "@/types";
interface IDocumentReadOnlyEditor {
+ disabledExtensions: TExtensions[];
id: string;
initialValue: string;
containerClassName: string;
@@ -31,6 +32,7 @@ interface IDocumentReadOnlyEditor {
const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
const {
containerClassName,
+ disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
embedHandler,
@@ -51,6 +53,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
}
const editor = useReadOnlyEditor({
+ disabledExtensions,
editorClassName,
extensions,
fileHandler,
diff --git a/packages/editor/src/core/components/editors/editor-wrapper.tsx b/packages/editor/src/core/components/editors/editor-wrapper.tsx
index 33f011535..075420ed7 100644
--- a/packages/editor/src/core/components/editors/editor-wrapper.tsx
+++ b/packages/editor/src/core/components/editors/editor-wrapper.tsx
@@ -19,6 +19,7 @@ export const EditorWrapper: React.FC = (props) => {
const {
children,
containerClassName,
+ disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
extensions,
@@ -37,6 +38,7 @@ export const EditorWrapper: React.FC = (props) => {
} = props;
const editor = useEditor({
+ disabledExtensions,
editorClassName,
enableHistory: true,
extensions,
diff --git a/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx b/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx
index e06826a28..6cd360ac0 100644
--- a/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx
+++ b/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx
@@ -12,6 +12,7 @@ import { IReadOnlyEditorProps } from "@/types";
export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
const {
containerClassName,
+ disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
fileHandler,
@@ -22,6 +23,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
} = props;
const editor = useReadOnlyEditor({
+ disabledExtensions,
editorClassName,
fileHandler,
forwardedRef,
diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx
index 87dba8b4d..ffcc21da6 100644
--- a/packages/editor/src/core/components/editors/rich-text/editor.tsx
+++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx
@@ -8,12 +8,7 @@ import { SideMenuExtension, SlashCommands } from "@/extensions";
import { EditorRefApi, IRichTextEditor } from "@/types";
const RichTextEditor = (props: IRichTextEditor) => {
- const {
- disabledExtensions,
- dragDropEnabled,
- bubbleMenuEnabled = true,
- extensions: externalExtensions = [],
- } = props;
+ const { disabledExtensions, dragDropEnabled, bubbleMenuEnabled = true, extensions: externalExtensions = [] } = props;
const getExtensions = useCallback(() => {
const extensions = [
@@ -24,7 +19,11 @@ const RichTextEditor = (props: IRichTextEditor) => {
}),
];
if (!disabledExtensions?.includes("slash-commands")) {
- extensions.push(SlashCommands());
+ extensions.push(
+ SlashCommands({
+ disabledExtensions,
+ })
+ );
}
return extensions;
diff --git a/packages/editor/src/core/constants/document-collaborative-events.ts b/packages/editor/src/core/constants/document-collaborative-events.ts
new file mode 100644
index 000000000..5e79efc7a
--- /dev/null
+++ b/packages/editor/src/core/constants/document-collaborative-events.ts
@@ -0,0 +1,6 @@
+export const DocumentCollaborativeEvents = {
+ lock: { client: "locked", server: "lock" },
+ unlock: { client: "unlocked", server: "unlock" },
+ archive: { client: "archived", server: "archive" },
+ unarchive: { client: "unarchived", server: "unarchive" },
+} as const;
diff --git a/packages/editor/src/core/extensions/callout/logo-selector.tsx b/packages/editor/src/core/extensions/callout/logo-selector.tsx
index 4e9f966af..4c78a2c04 100644
--- a/packages/editor/src/core/extensions/callout/logo-selector.tsx
+++ b/packages/editor/src/core/extensions/callout/logo-selector.tsx
@@ -1,5 +1,5 @@
// plane helpers
-import { convertHexEmojiToDecimal } from "@plane/helpers";
+import { convertHexEmojiToDecimal } from "@plane/utils";
// plane ui
import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui";
// helpers
diff --git a/packages/editor/src/core/extensions/callout/utils.ts b/packages/editor/src/core/extensions/callout/utils.ts
index c450cbdd2..6568a40e3 100644
--- a/packages/editor/src/core/extensions/callout/utils.ts
+++ b/packages/editor/src/core/extensions/callout/utils.ts
@@ -1,5 +1,5 @@
// plane helpers
-import { sanitizeHTML } from "@plane/helpers";
+import { sanitizeHTML } from "@plane/utils";
// plane ui
import { TEmojiLogoProps } from "@plane/ui";
// types
diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts
index 075d90f2d..212e1c241 100644
--- a/packages/editor/src/core/extensions/core-without-props.ts
+++ b/packages/editor/src/core/extensions/core-without-props.ts
@@ -19,6 +19,8 @@ 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";
export const CoreEditorExtensionsWithoutProps = [
StarterKit.configure({
@@ -41,6 +43,16 @@ export const CoreEditorExtensionsWithoutProps = [
codeBlock: false,
horizontalRule: false,
blockquote: false,
+ paragraph: {
+ HTMLAttributes: {
+ class: "editor-paragraph-block",
+ },
+ },
+ heading: {
+ HTMLAttributes: {
+ class: "editor-heading-block",
+ },
+ },
dropcursor: false,
}),
CustomQuoteExtension,
@@ -89,6 +101,7 @@ export const CoreEditorExtensionsWithoutProps = [
CustomTextAlignExtension,
CustomCalloutExtensionConfig,
CustomColorExtension,
+ ...CoreEditorAdditionalExtensionsWithoutProps,
];
export const DocumentEditorExtensionsWithoutProps = [IssueWidgetWithoutProps()];
diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx
index f1a85ab1b..b5b27e271 100644
--- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx
+++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx
@@ -118,7 +118,6 @@ export const CustomImageBlock: React.FC = (props) => {
height: `${Math.round(initialHeight)}px` satisfies Pixel,
aspectRatio: aspectRatioCalculated,
};
-
setSize(initialComputedSize);
updateAttributesSafely(
initialComputedSize,
diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx
index 78caa87b3..58b60b306 100644
--- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx
+++ b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx
@@ -29,12 +29,9 @@ export const CustomImageNode = (props: CustomImageNodeProps) => {
useEffect(() => {
const closestEditorContainer = imageComponentRef.current?.closest(".editor-container");
- if (!closestEditorContainer) {
- console.error("Editor container not found");
- return;
+ if (closestEditorContainer) {
+ setEditorContainer(closestEditorContainer as HTMLDivElement);
}
-
- setEditorContainer(closestEditorContainer as HTMLDivElement);
}, []);
// the image is already uploaded if the image-component node has src attribute
@@ -55,7 +52,7 @@ export const CustomImageNode = (props: CustomImageNodeProps) => {
setResolvedSrc(url as string);
};
getImageSource();
- }, [imageFromFileSystem, node.attrs.src]);
+ }, [imgNodeSrc]);
return (
diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts
index a232bb258..3b64db8d0 100644
--- a/packages/editor/src/core/extensions/custom-image/custom-image.ts
+++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts
@@ -1,11 +1,9 @@
import { Editor, mergeAttributes } from "@tiptap/core";
import { Image } from "@tiptap/extension-image";
-import { MarkdownSerializerState } from "@tiptap/pm/markdown";
-import { Node } from "@tiptap/pm/model";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { v4 as uuidv4 } from "uuid";
// extensions
-import { CustomImageNode, ImageAttributes } from "@/extensions/custom-image";
+import { CustomImageNode } from "@/extensions/custom-image";
// plugins
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image";
// types
@@ -126,14 +124,9 @@ export const CustomImageExtension = (props: TFileHandler) => {
deletedImageSet: new Map(),
uploadInProgress: false,
maxFileSize,
+ // escape markdown for images
markdown: {
- serialize(state: MarkdownSerializerState, node: Node) {
- const attrs = node.attrs as ImageAttributes;
- const imageSource = state.esc(this?.editor?.commands?.getImageSource?.(attrs.src) || attrs.src);
- const imageWidth = state.esc(attrs.width?.toString());
- state.write(`
`);
- state.closeBlock(node);
- },
+ serialize() {},
},
};
},
diff --git a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts
index 3248329f0..c27970d92 100644
--- a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts
+++ b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts
@@ -1,10 +1,8 @@
import { mergeAttributes } from "@tiptap/core";
import { Image } from "@tiptap/extension-image";
-import { MarkdownSerializerState } from "@tiptap/pm/markdown";
-import { Node } from "@tiptap/pm/model";
import { ReactNodeViewRenderer } from "@tiptap/react";
// components
-import { CustomImageNode, ImageAttributes, UploadImageExtensionStorage } from "@/extensions/custom-image";
+import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
// types
import { TFileHandler } from "@/types";
@@ -54,14 +52,9 @@ export const CustomReadOnlyImageExtension = (props: Pick`);
- state.closeBlock(node);
- },
+ serialize() {},
},
};
},
diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx
index 959d20e2b..0e06f774b 100644
--- a/packages/editor/src/core/extensions/extensions.tsx
+++ b/packages/editor/src/core/extensions/extensions.tsx
@@ -1,3 +1,4 @@
+import { Extensions } from "@tiptap/core";
import CharacterCount from "@tiptap/extension-character-count";
import Placeholder from "@tiptap/extension-placeholder";
import TaskItem from "@tiptap/extension-task-item";
@@ -32,9 +33,12 @@ import {
// helpers
import { isValidHttpUrl } from "@/helpers/common";
// types
-import { IMentionHighlight, IMentionSuggestion, TFileHandler } from "@/types";
+import { IMentionHighlight, IMentionSuggestion, TExtensions, TFileHandler } from "@/types";
+// plane editor extensions
+import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";
type TArguments = {
+ disabledExtensions: TExtensions[];
enableHistory: boolean;
fileHandler: TFileHandler;
mentionConfig: {
@@ -45,8 +49,8 @@ type TArguments = {
tabIndex?: number;
};
-export const CoreEditorExtensions = (args: TArguments) => {
- const { enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args;
+export const CoreEditorExtensions = (args: TArguments): Extensions => {
+ const { disabledExtensions, enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args;
return [
StarterKit.configure({
@@ -69,6 +73,16 @@ export const CoreEditorExtensions = (args: TArguments) => {
codeBlock: false,
horizontalRule: false,
blockquote: false,
+ paragraph: {
+ HTMLAttributes: {
+ class: "editor-paragraph-block",
+ },
+ },
+ heading: {
+ HTMLAttributes: {
+ class: "editor-heading-block",
+ },
+ },
dropcursor: {
class: "text-custom-text-300",
},
@@ -162,5 +176,8 @@ export const CoreEditorExtensions = (args: TArguments) => {
CustomTextAlignExtension,
CustomCalloutExtension,
CustomColorExtension,
+ ...CoreEditorAdditionalExtensions({
+ disabledExtensions,
+ }),
];
};
diff --git a/packages/editor/src/core/extensions/read-only-extensions.tsx b/packages/editor/src/core/extensions/read-only-extensions.tsx
index 2d90592d6..4debda019 100644
--- a/packages/editor/src/core/extensions/read-only-extensions.tsx
+++ b/packages/editor/src/core/extensions/read-only-extensions.tsx
@@ -1,3 +1,4 @@
+import { Extensions } from "@tiptap/core";
import CharacterCount from "@tiptap/extension-character-count";
import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
@@ -28,17 +29,20 @@ import {
// helpers
import { isValidHttpUrl } from "@/helpers/common";
// types
-import { IMentionHighlight, TFileHandler } from "@/types";
+import { IMentionHighlight, TExtensions, TFileHandler } from "@/types";
+// plane editor extensions
+import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
type Props = {
+ disabledExtensions: TExtensions[];
fileHandler: Pick;
mentionConfig: {
mentionHighlights?: () => Promise;
};
};
-export const CoreReadOnlyEditorExtensions = (props: Props) => {
- const { fileHandler, mentionConfig } = props;
+export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
+ const { disabledExtensions, fileHandler, mentionConfig } = props;
return [
StarterKit.configure({
@@ -61,6 +65,16 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
codeBlock: false,
horizontalRule: false,
blockquote: false,
+ paragraph: {
+ HTMLAttributes: {
+ class: "editor-paragraph-block",
+ },
+ },
+ heading: {
+ HTMLAttributes: {
+ class: "editor-heading-block",
+ },
+ },
dropcursor: false,
gapcursor: false,
}),
@@ -128,5 +142,8 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
HeadingListExtension,
CustomTextAlignExtension,
CustomCalloutReadOnlyExtension,
+ ...CoreReadOnlyEditorAdditionalExtensions({
+ disabledExtensions,
+ }),
];
};
diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx
index c19bda306..1efb72901 100644
--- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx
+++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx
@@ -39,17 +39,27 @@ import {
setText,
} from "@/helpers/editor-commands";
// types
-import { CommandProps, ISlashCommandItem } from "@/types";
+import { CommandProps, ISlashCommandItem, TExtensions, TSlashCommandSectionKeys } from "@/types";
+// plane editor extensions
+import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
+// local types
+import { TSlashCommandAdditionalOption } from "./root";
export type TSlashCommandSection = {
- key: string;
+ key: TSlashCommandSectionKeys;
title?: string;
items: ISlashCommandItem[];
};
+type TArgs = {
+ additionalOptions?: TSlashCommandAdditionalOption[];
+ disabledExtensions: TExtensions[];
+};
+
export const getSlashCommandFilteredSections =
- (additionalOptions?: ISlashCommandItem[]) =>
+ (args: TArgs) =>
({ query }: { query: string }): TSlashCommandSection[] => {
+ const { additionalOptions, disabledExtensions } = args;
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
{
key: "general",
@@ -201,7 +211,7 @@ export const getSlashCommandFilteredSections =
],
},
{
- key: "text-color",
+ key: "text-colors",
title: "Colors",
items: [
{
@@ -242,7 +252,7 @@ export const getSlashCommandFilteredSections =
],
},
{
- key: "background-color",
+ key: "background-colors",
title: "Background colors",
items: [
{
@@ -279,8 +289,19 @@ export const getSlashCommandFilteredSections =
},
];
- additionalOptions?.map((item) => {
- SLASH_COMMAND_SECTIONS?.[0]?.items.push(item);
+ [
+ ...(additionalOptions ?? []),
+ ...coreEditorAdditionalSlashCommandOptions({
+ disabledExtensions,
+ }),
+ ]?.forEach((item) => {
+ const sectionToPushTo = SLASH_COMMAND_SECTIONS.find((s) => s.key === item.section) ?? SLASH_COMMAND_SECTIONS[0];
+ const itemIndexToPushAfter = sectionToPushTo.items.findIndex((i) => i.commandKey === item.pushAfter);
+ if (itemIndexToPushAfter !== -1) {
+ sectionToPushTo.items.splice(itemIndexToPushAfter + 1, 0, item);
+ } else {
+ sectionToPushTo.items.push(item);
+ }
});
const filteredSlashSections = SLASH_COMMAND_SECTIONS.map((section) => ({
diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx
index d6148b69a..93b0ce2ea 100644
--- a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx
+++ b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx
@@ -41,7 +41,7 @@ export const SlashCommandsMenu = (props: SlashCommandsMenuProps) => {
if (nextItem < 0) {
nextSection = currentSection - 1;
if (nextSection < 0) nextSection = sections.length - 1;
- nextItem = sections[nextSection].items.length - 1;
+ nextItem = sections[nextSection]?.items.length - 1;
}
}
if (e.key === "ArrowDown") {
diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx
index a99cbc5f9..62c353f92 100644
--- a/packages/editor/src/core/extensions/slash-commands/root.tsx
+++ b/packages/editor/src/core/extensions/slash-commands/root.tsx
@@ -3,7 +3,7 @@ import { ReactRenderer } from "@tiptap/react";
import Suggestion, { SuggestionOptions } from "@tiptap/suggestion";
import tippy from "tippy.js";
// types
-import { ISlashCommandItem } from "@/types";
+import { ISlashCommandItem, TEditorCommands, TExtensions, TSlashCommandSectionKeys } from "@/types";
// components
import { getSlashCommandFilteredSections } from "./command-items-list";
import { SlashCommandsMenu, SlashCommandsMenuProps } from "./command-menu";
@@ -12,6 +12,11 @@ export type SlashCommandOptions = {
suggestion: Omit;
};
+export type TSlashCommandAdditionalOption = ISlashCommandItem & {
+ section: TSlashCommandSectionKeys;
+ pushAfter: TEditorCommands;
+};
+
const Command = Extension.create({
name: "slash-command",
addOptions() {
@@ -102,10 +107,15 @@ const renderItems = () => {
};
};
-export const SlashCommands = (additionalOptions?: ISlashCommandItem[]) =>
+type TExtensionProps = {
+ additionalOptions?: TSlashCommandAdditionalOption[];
+ disabledExtensions: TExtensions[];
+};
+
+export const SlashCommands = (props: TExtensionProps) =>
Command.configure({
suggestion: {
- items: getSlashCommandFilteredSections(additionalOptions),
+ items: getSlashCommandFilteredSections(props),
render: renderItems,
},
});
diff --git a/packages/editor/src/core/helpers/get-document-server-event.ts b/packages/editor/src/core/helpers/get-document-server-event.ts
new file mode 100644
index 000000000..1ba7646b2
--- /dev/null
+++ b/packages/editor/src/core/helpers/get-document-server-event.ts
@@ -0,0 +1,11 @@
+import { DocumentCollaborativeEvents } from "@/constants/document-collaborative-events";
+import { TDocumentEventKey, TDocumentEventsClient, TDocumentEventsServer } from "@/types/document-collaborative-events";
+
+export const getServerEventName = (clientEvent: TDocumentEventsClient): TDocumentEventsServer | undefined => {
+ for (const key in DocumentCollaborativeEvents) {
+ if (DocumentCollaborativeEvents[key as TDocumentEventKey].client === clientEvent) {
+ return DocumentCollaborativeEvents[key as TDocumentEventKey].server;
+ }
+ }
+ return undefined;
+};
diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts
index 5bee8c0c3..b3c7d6cfc 100644
--- a/packages/editor/src/core/hooks/use-collaborative-editor.ts
+++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts
@@ -1,4 +1,4 @@
-import { useEffect, useLayoutEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import Collaboration from "@tiptap/extension-collaboration";
import { IndexeddbPersistence } from "y-indexeddb";
@@ -58,23 +58,22 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
[id, realtimeConfig, serverHandler, user]
);
- // destroy and disconnect connection on unmount
+ const localProvider = useMemo(
+ () => (id ? new IndexeddbPersistence(id, provider.document) : undefined),
+ [id, provider]
+ );
+
+ // destroy and disconnect all providers connection on unmount
useEffect(
() => () => {
- provider.destroy();
- provider.disconnect();
- },
- [provider]
- );
- // indexed db integration for offline support
- useLayoutEffect(() => {
- const localProvider = new IndexeddbPersistence(id, provider.document);
- return () => {
+ provider?.destroy();
localProvider?.destroy();
- };
- }, [provider, id]);
+ },
+ [provider, localProvider]
+ );
const editor = useEditor({
+ disabledExtensions,
id,
onTransaction,
editorProps,
diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts
index eef72797c..15fbd19d5 100644
--- a/packages/editor/src/core/hooks/use-editor.ts
+++ b/packages/editor/src/core/hooks/use-editor.ts
@@ -16,12 +16,21 @@ import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helper
// props
import { CoreEditorProps } from "@/props";
// types
-import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types";
+import type {
+ TDocumentEventsServer,
+ EditorRefApi,
+ IMentionHighlight,
+ IMentionSuggestion,
+ TEditorCommands,
+ TFileHandler,
+ TExtensions,
+} from "@/types";
export interface CustomEditorProps {
editorClassName: string;
editorProps?: EditorProps;
enableHistory: boolean;
+ disabledExtensions: TExtensions[];
extensions?: any;
fileHandler: TFileHandler;
forwardedRef?: MutableRefObject;
@@ -45,6 +54,7 @@ export interface CustomEditorProps {
export const useEditor = (props: CustomEditorProps) => {
const {
+ disabledExtensions,
editorClassName,
editorProps = {},
enableHistory,
@@ -58,9 +68,9 @@ export const useEditor = (props: CustomEditorProps) => {
onChange,
onTransaction,
placeholder,
- provider,
tabIndex,
value,
+ provider,
autofocus = false,
} = props;
// states
@@ -79,6 +89,7 @@ export const useEditor = (props: CustomEditorProps) => {
},
extensions: [
...CoreEditorExtensions({
+ disabledExtensions,
enableHistory,
fileHandler,
mentionConfig: {
@@ -247,7 +258,7 @@ export const useEditor = (props: CustomEditorProps) => {
if (empty) return null;
const nodesArray: string[] = [];
- state.doc.nodesBetween(from, to, (node, pos, parent) => {
+ state.doc.nodesBetween(from, to, (node, _pos, parent) => {
if (parent === state.doc && editorRef.current) {
const serializer = DOMSerializer.fromSchema(editorRef.current?.schema);
const dom = serializer.serializeNode(node);
@@ -288,6 +299,8 @@ export const useEditor = (props: CustomEditorProps) => {
if (!document) return;
Y.applyUpdate(document, value);
},
+ emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
+ listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
}),
[editorRef, savedSelection]
);
diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts
index d40819229..01ca19b81 100644
--- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts
+++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts
@@ -1,4 +1,4 @@
-import { useEffect, useLayoutEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import Collaboration from "@tiptap/extension-collaboration";
import { IndexeddbPersistence } from "y-indexeddb";
@@ -11,6 +11,7 @@ import { TReadOnlyCollaborativeEditorProps } from "@/types";
export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEditorProps) => {
const {
+ disabledExtensions,
editorClassName,
editorProps = {},
extensions,
@@ -30,8 +31,8 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
const provider = useMemo(
() =>
new HocuspocusProvider({
- url: realtimeConfig.url,
name: id,
+ url: realtimeConfig.url,
token: JSON.stringify(user),
parameters: realtimeConfig.queryParams,
onAuthenticationFailed: () => {
@@ -47,25 +48,26 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
},
onSynced: () => setHasServerSynced(true),
}),
- [id, realtimeConfig, user]
+ [id, realtimeConfig, serverHandler, user]
);
+
+ // indexed db integration for offline support
+ const localProvider = useMemo(
+ () => (id ? new IndexeddbPersistence(id, provider.document) : undefined),
+ [id, provider]
+ );
+
// destroy and disconnect connection on unmount
useEffect(
() => () => {
provider.destroy();
- provider.disconnect();
- },
- [provider]
- );
- // indexed db integration for offline support
- useLayoutEffect(() => {
- const localProvider = new IndexeddbPersistence(id, provider.document);
- return () => {
localProvider?.destroy();
- };
- }, [provider, id]);
+ },
+ [provider, localProvider]
+ );
const editor = useReadOnlyEditor({
+ disabledExtensions,
editorProps,
editorClassName,
extensions: [
diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts
index 23ce023ad..5fb49be5f 100644
--- a/packages/editor/src/core/hooks/use-read-only-editor.ts
+++ b/packages/editor/src/core/hooks/use-read-only-editor.ts
@@ -11,14 +11,21 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
// props
import { CoreReadOnlyEditorProps } from "@/props";
// types
-import { EditorReadOnlyRefApi, IMentionHighlight, TFileHandler } from "@/types";
+import type {
+ EditorReadOnlyRefApi,
+ IMentionHighlight,
+ TExtensions,
+ TDocumentEventsServer,
+ TFileHandler,
+} from "@/types";
interface CustomReadOnlyEditorProps {
- initialValue?: string;
+ disabledExtensions: TExtensions[];
editorClassName: string;
- forwardedRef?: MutableRefObject;
- extensions?: any;
editorProps?: EditorProps;
+ extensions?: any;
+ forwardedRef?: MutableRefObject;
+ initialValue?: string;
fileHandler: Pick;
handleEditorReady?: (value: boolean) => void;
mentionHandler: {
@@ -29,6 +36,7 @@ interface CustomReadOnlyEditorProps {
export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
const {
+ disabledExtensions,
initialValue,
editorClassName,
forwardedRef,
@@ -54,6 +62,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
},
extensions: [
...CoreReadOnlyEditorExtensions({
+ disabledExtensions,
mentionConfig: {
mentionHighlights: mentionHandler.highlights,
},
@@ -117,6 +126,8 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
editorRef.current?.off("update");
};
},
+ emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
+ listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
getHeadings: () => editorRef?.current?.storage.headingList.headings,
}));
diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts
index 8609995ed..35fbdb996 100644
--- a/packages/editor/src/core/types/collaboration.ts
+++ b/packages/editor/src/core/types/collaboration.ts
@@ -20,7 +20,7 @@ export type TServerHandler = {
};
type TCollaborativeEditorHookProps = {
- disabledExtensions?: TExtensions[];
+ disabledExtensions: TExtensions[];
editorClassName: string;
editorProps?: EditorProps;
extensions?: Extensions;
diff --git a/packages/editor/src/core/types/document-collaborative-events.ts b/packages/editor/src/core/types/document-collaborative-events.ts
new file mode 100644
index 000000000..99936a5ad
--- /dev/null
+++ b/packages/editor/src/core/types/document-collaborative-events.ts
@@ -0,0 +1,10 @@
+import { DocumentCollaborativeEvents } from "@/constants/document-collaborative-events";
+
+export type TDocumentEventKey = keyof typeof DocumentCollaborativeEvents;
+export type TDocumentEventsClient = (typeof DocumentCollaborativeEvents)[TDocumentEventKey]["client"];
+export type TDocumentEventsServer = (typeof DocumentCollaborativeEvents)[TDocumentEventKey]["server"];
+
+export type TDocumentEventEmitter = {
+ on: (event: string, callback: (message: { payload: TDocumentEventsClient }) => void) => void;
+ off: (event: string, callback: (message: { payload: TDocumentEventsClient }) => void) => void;
+};
diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts
index 53aae1f26..e91af8e49 100644
--- a/packages/editor/src/core/types/editor.ts
+++ b/packages/editor/src/core/types/editor.ts
@@ -8,6 +8,8 @@ import {
IMentionSuggestion,
TAIHandler,
TDisplayConfig,
+ TDocumentEventEmitter,
+ TDocumentEventsServer,
TEmbedConfig,
TExtensions,
TFileHandler,
@@ -83,6 +85,8 @@ export type EditorReadOnlyRefApi = {
};
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
+ emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
+ listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
};
export interface EditorRefApi extends EditorReadOnlyRefApi {
@@ -104,7 +108,7 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
export interface IEditorProps {
containerClassName?: string;
displayConfig?: TDisplayConfig;
- disabledExtensions?: TExtensions[];
+ disabledExtensions: TExtensions[];
editorClassName?: string;
fileHandler: TFileHandler;
forwardedRef?: React.MutableRefObject;
@@ -121,7 +125,7 @@ export interface IEditorProps {
onEnterKeyPress?: (e?: any) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
- value?: string | null;
+ value?: string | null;
}
export interface ILiteTextEditor extends IEditorProps {
extensions?: any[];
@@ -146,6 +150,7 @@ export interface ICollaborativeDocumentEditor
// read only editor props
export interface IReadOnlyEditorProps {
containerClassName?: string;
+ disabledExtensions: TExtensions[];
displayConfig?: TDisplayConfig;
editorClassName?: string;
fileHandler: Pick;
diff --git a/packages/editor/src/core/types/extensions.ts b/packages/editor/src/core/types/extensions.ts
index 2be17a4ef..b3aacccc0 100644
--- a/packages/editor/src/core/types/extensions.ts
+++ b/packages/editor/src/core/types/extensions.ts
@@ -1 +1 @@
-export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands"| "enter-key";
+export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands" | "enter-key";
diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts
index 8da9ed276..527264d39 100644
--- a/packages/editor/src/core/types/index.ts
+++ b/packages/editor/src/core/types/index.ts
@@ -8,3 +8,4 @@ export * from "./image";
export * from "./mention-suggestion";
export * from "./slash-commands-suggestion";
export * from "@/plane-editor/types";
+export * from "./document-collaborative-events";
diff --git a/packages/editor/src/core/types/slash-commands-suggestion.ts b/packages/editor/src/core/types/slash-commands-suggestion.ts
index 91c93203a..d6dfae076 100644
--- a/packages/editor/src/core/types/slash-commands-suggestion.ts
+++ b/packages/editor/src/core/types/slash-commands-suggestion.ts
@@ -8,6 +8,8 @@ export type CommandProps = {
range: Range;
};
+export type TSlashCommandSectionKeys = "general" | "text-colors" | "background-colors";
+
export type ISlashCommandItem = {
commandKey: TEditorCommands;
key: string;
diff --git a/packages/editor/src/lib.ts b/packages/editor/src/lib.ts
index e14c40127..e32fa0785 100644
--- a/packages/editor/src/lib.ts
+++ b/packages/editor/src/lib.ts
@@ -1 +1,4 @@
export * from "@/extensions/core-without-props";
+export * from "@/constants/document-collaborative-events";
+export * from "@/helpers/get-document-server-event";
+export * from "@/types/document-collaborative-events";
diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css
index fff3b533e..db60c7cf5 100644
--- a/packages/editor/src/styles/editor.css
+++ b/packages/editor/src/styles/editor.css
@@ -133,7 +133,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"][checked]:hover {
}
/* the p tag just after the ul tag */
-ul[data-type="taskList"] + p {
+ul[data-type="taskList"] + p.editor-paragraph-block {
margin-top: 0.4rem !important;
}
@@ -179,13 +179,26 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
}
}
-ul[data-type="taskList"] li > div > p {
- margin-top: 10px;
+ul[data-type="taskList"] li > div {
+ & > p.editor-paragraph-block {
+ margin-top: 10px;
+ transition: color 0.2s ease;
+ }
+
+ [data-text-color] {
+ transition: opacity 0.2s ease;
+ }
}
-ul[data-type="taskList"] li[data-checked="true"] > div > p {
- color: rgb(var(--color-text-400));
- transition: color 0.2s ease;
+ul[data-type="taskList"] li[data-checked="true"] {
+ & > div > p.editor-paragraph-block {
+ color: rgb(var(--color-text-400));
+ }
+
+ [data-text-color] {
+ opacity: 0.6;
+ transition: opacity 0.2s ease;
+ }
}
/* end to-do list */
@@ -309,18 +322,18 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
}
/* end numbered, bulleted and to-do lists spacing */
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-p {
+h1.editor-heading-block,
+h2.editor-heading-block,
+h3.editor-heading-block,
+h4.editor-heading-block,
+h5.editor-heading-block,
+h6.editor-heading-block,
+p.editor-paragraph-block {
margin: 0 !important;
}
/* tailwind typography */
-.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
+.prose :where(h1.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
padding-top: 28px;
}
@@ -331,7 +344,7 @@ p {
font-weight: 600;
}
-.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
+.prose :where(h2.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
padding-top: 28px;
}
@@ -342,7 +355,7 @@ p {
font-weight: 600;
}
-.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
+.prose :where(h3.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
padding-top: 28px;
}
@@ -353,7 +366,7 @@ p {
font-weight: 600;
}
-.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
+.prose :where(h4.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
padding-top: 28px;
}
@@ -364,7 +377,7 @@ p {
font-weight: 600;
}
-.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
+.prose :where(h5.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
padding-top: 20px;
}
@@ -375,7 +388,7 @@ p {
font-weight: 600;
}
-.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
+.prose :where(h6.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
padding-top: 20px;
}
@@ -386,7 +399,7 @@ p {
font-weight: 600;
}
-.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
+.prose :where(p.editor-paragraph-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:first-child {
padding-top: 0;
}
@@ -407,12 +420,12 @@ p {
line-height: var(--line-height-regular);
}
-p + p {
+p.editor-paragraph-block + p.editor-paragraph-block {
padding-top: 8px !important;
}
-.prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p,
-.prose :where(ul):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p {
+.prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p.editor-paragraph-block,
+.prose :where(ul):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p.editor-paragraph-block {
font-size: var(--font-size-list);
line-height: var(--line-height-list);
}
diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json
index e8de3524c..66557c74e 100644
--- a/packages/eslint-config/package.json
+++ b/packages/eslint-config/package.json
@@ -1,7 +1,7 @@
{
"name": "@plane/eslint-config",
"private": true,
- "version": "0.24.0",
+ "version": "0.24.1",
"files": [
"library.js",
"next.js",
@@ -10,7 +10,7 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.6.0",
"@typescript-eslint/parser": "^8.6.0",
- "eslint": "8",
+ "eslint": "8.57.1",
"eslint-config-next": "^14.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "^1.12.4",
diff --git a/packages/helpers/helpers/emoji.helper.ts b/packages/helpers/helpers/emoji.helper.ts
deleted file mode 100644
index e0d5a1969..000000000
--- a/packages/helpers/helpers/emoji.helper.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export const convertHexEmojiToDecimal = (emojiUnified: string): string => {
- if (!emojiUnified) return "";
-
- return emojiUnified
- .toString()
- .split("-")
- .map((e) => parseInt(e, 16))
- .join("-");
-};
-
-export const emojiCodeToUnicode = (emoji: string) => {
- if (!emoji) return "";
-
- // convert emoji code to unicode
- const uniCodeEmoji = emoji
- .toString()
- .split("-")
- .map((emoji) => parseInt(emoji, 10).toString(16))
- .join("-");
-
- return uniCodeEmoji;
-};
diff --git a/packages/helpers/helpers/index.ts b/packages/helpers/helpers/index.ts
deleted file mode 100644
index e800e98fd..000000000
--- a/packages/helpers/helpers/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./emoji.helper"
-export * from "./string.helper"
\ No newline at end of file
diff --git a/packages/helpers/index.ts b/packages/helpers/index.ts
deleted file mode 100644
index f1216272d..000000000
--- a/packages/helpers/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./helpers";
-export * from "./hooks";
diff --git a/packages/hooks/.eslintignore b/packages/hooks/.eslintignore
new file mode 100644
index 000000000..6019047c3
--- /dev/null
+++ b/packages/hooks/.eslintignore
@@ -0,0 +1,3 @@
+build/*
+dist/*
+out/*
\ No newline at end of file
diff --git a/packages/hooks/.eslintrc.js b/packages/hooks/.eslintrc.js
new file mode 100644
index 000000000..558b8f76e
--- /dev/null
+++ b/packages/hooks/.eslintrc.js
@@ -0,0 +1,9 @@
+/** @type {import("eslint").Linter.Config} */
+module.exports = {
+ root: true,
+ extends: ["@plane/eslint-config/library.js"],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ project: true,
+ },
+};
diff --git a/packages/hooks/.prettierignore b/packages/hooks/.prettierignore
new file mode 100644
index 000000000..d5be669c5
--- /dev/null
+++ b/packages/hooks/.prettierignore
@@ -0,0 +1,4 @@
+.turbo
+out/
+dist/
+build/
\ No newline at end of file
diff --git a/packages/hooks/.prettierrc b/packages/hooks/.prettierrc
new file mode 100644
index 000000000..87d988f1b
--- /dev/null
+++ b/packages/hooks/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "printWidth": 120,
+ "tabWidth": 2,
+ "trailingComma": "es5"
+}
diff --git a/packages/hooks/package.json b/packages/hooks/package.json
new file mode 100644
index 000000000..b45723305
--- /dev/null
+++ b/packages/hooks/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@plane/hooks",
+ "version": "0.24.1",
+ "description": "React hooks that are shared across multiple apps internally",
+ "private": true,
+ "main": "./dist/index.js",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist/**"
+ ],
+ "scripts": {
+ "build": "tsup ./src/index.ts --format esm,cjs --dts --external react --minify",
+ "lint": "eslint src --ext .ts,.tsx",
+ "lint:errors": "eslint src --ext .ts,.tsx --quiet"
+ },
+ "dependencies": {
+ "react": "^18.3.1"
+ },
+ "devDependencies": {
+ "@plane/eslint-config": "*",
+ "@types/node": "^22.5.4",
+ "@types/react": "^18.3.11",
+ "tsup": "^7.2.0",
+ "typescript": "^5.3.3"
+ }
+}
diff --git a/packages/helpers/hooks/index.ts b/packages/hooks/src/index.ts
similarity index 55%
rename from packages/helpers/hooks/index.ts
rename to packages/hooks/src/index.ts
index c7a8f4c06..c07642907 100644
--- a/packages/helpers/hooks/index.ts
+++ b/packages/hooks/src/index.ts
@@ -1 +1,2 @@
+export * from "./use-local-storage";
export * from "./use-outside-click-detector";
diff --git a/packages/hooks/src/use-local-storage.tsx b/packages/hooks/src/use-local-storage.tsx
new file mode 100644
index 000000000..f04e0e71b
--- /dev/null
+++ b/packages/hooks/src/use-local-storage.tsx
@@ -0,0 +1,59 @@
+import { useState, useEffect, useCallback } from "react";
+
+export const getValueFromLocalStorage = (key: string, defaultValue: any) => {
+ if (typeof window === undefined || typeof window === "undefined")
+ return defaultValue;
+ try {
+ const item = window.localStorage.getItem(key);
+ return item ? JSON.parse(item) : defaultValue;
+ } catch (error) {
+ window.localStorage.removeItem(key);
+ return defaultValue;
+ }
+};
+
+export const setValueIntoLocalStorage = (key: string, value: any) => {
+ if (typeof window === undefined || typeof window === "undefined")
+ return false;
+ try {
+ window.localStorage.setItem(key, JSON.stringify(value));
+ return true;
+ } catch (error) {
+ return false;
+ }
+};
+
+export const useLocalStorage = (key: string, initialValue: T) => {
+ const [storedValue, setStoredValue] = useState(() =>
+ getValueFromLocalStorage(key, initialValue)
+ );
+
+ const setValue = useCallback(
+ (value: T) => {
+ window.localStorage.setItem(key, JSON.stringify(value));
+ setStoredValue(value);
+ window.dispatchEvent(new Event(`local-storage:${key}`));
+ },
+ [key]
+ );
+
+ const clearValue = useCallback(() => {
+ window.localStorage.removeItem(key);
+ setStoredValue(null);
+ window.dispatchEvent(new Event(`local-storage:${key}`));
+ }, [key]);
+
+ const reHydrate = useCallback(() => {
+ const data = getValueFromLocalStorage(key, initialValue);
+ setStoredValue(data);
+ }, [key, initialValue]);
+
+ useEffect(() => {
+ window.addEventListener(`local-storage:${key}`, reHydrate);
+ return () => {
+ window.removeEventListener(`local-storage:${key}`, reHydrate);
+ };
+ }, [key, reHydrate]);
+
+ return { storedValue, setValue, clearValue } as const;
+};
diff --git a/packages/helpers/hooks/use-outside-click-detector.tsx b/packages/hooks/src/use-outside-click-detector.tsx
similarity index 100%
rename from packages/helpers/hooks/use-outside-click-detector.tsx
rename to packages/hooks/src/use-outside-click-detector.tsx
diff --git a/packages/helpers/tsconfig.json b/packages/hooks/tsconfig.json
similarity index 88%
rename from packages/helpers/tsconfig.json
rename to packages/hooks/tsconfig.json
index f9715d3d8..e8af9092a 100644
--- a/packages/helpers/tsconfig.json
+++ b/packages/hooks/tsconfig.json
@@ -4,6 +4,6 @@
"jsx": "react",
"lib": ["esnext", "dom"]
},
- "include": ["."],
+ "include": ["./src"],
"exclude": ["dist", "build", "node_modules"]
}
diff --git a/packages/tailwind-config-custom/package.json b/packages/tailwind-config-custom/package.json
index 5c31544b3..a7fac6401 100644
--- a/packages/tailwind-config-custom/package.json
+++ b/packages/tailwind-config-custom/package.json
@@ -1,6 +1,6 @@
{
"name": "tailwind-config-custom",
- "version": "0.24.0",
+ "version": "0.24.1",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"private": true,
diff --git a/packages/types/package.json b/packages/types/package.json
index 9ce0fd077..6fc823e90 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
- "version": "0.24.0",
+ "version": "0.24.1",
"private": true,
"types": "./src/index.d.ts",
"main": "./src/index.d.ts"
diff --git a/packages/types/src/instance/base.d.ts b/packages/types/src/instance/base.d.ts
index 41198b27c..33d0734ad 100644
--- a/packages/types/src/instance/base.d.ts
+++ b/packages/types/src/instance/base.d.ts
@@ -57,6 +57,7 @@ export interface IInstanceConfig {
// intercom
is_intercom_enabled: boolean;
intercom_app_id: string | undefined;
+ instance_changelog_url?: string;
}
export interface IInstanceAdmin {
diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json
index 243877366..3237a184b 100644
--- a/packages/typescript-config/package.json
+++ b/packages/typescript-config/package.json
@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
- "version": "0.24.0",
+ "version": "0.24.1",
"private": true,
"files": [
"base.json",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 27189f606..2cfcc3643 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
- "version": "0.24.0",
+ "version": "0.24.1",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
@@ -18,7 +18,8 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"postcss": "postcss styles/globals.css -o styles/output.css --watch",
- "lint": "eslint src --ext .ts,.tsx"
+ "lint": "eslint src --ext .ts,.tsx",
+ "lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"peerDependencies": {
"react": "^18.3.1",
@@ -30,8 +31,9 @@
"@blueprintjs/core": "^4.16.3",
"@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^1.7.3",
+ "@plane/hooks": "*",
+ "@plane/utils": "*",
"@popperjs/core": "^2.11.8",
- "@plane/helpers": "*",
"clsx": "^2.0.0",
"emoji-picker-react": "^4.5.16",
"lodash": "^4.17.21",
@@ -44,6 +46,8 @@
},
"devDependencies": {
"@chromatic-com/storybook": "^1.4.0",
+ "@plane/eslint-config": "*",
+ "@plane/typescript-config": "*",
"@storybook/addon-essentials": "^8.1.1",
"@storybook/addon-interactions": "^8.1.1",
"@storybook/addon-links": "^8.1.1",
@@ -61,14 +65,15 @@
"@types/react-dom": "^18.2.18",
"autoprefixer": "^10.4.19",
"classnames": "^2.3.2",
- "@plane/eslint-config": "*",
"postcss-cli": "^11.0.0",
"postcss-nested": "^6.0.1",
"storybook": "^8.1.1",
"tailwind-config-custom": "*",
"tailwindcss": "^3.4.3",
- "@plane/typescript-config": "*",
"tsup": "^7.2.0",
"typescript": "5.3.3"
+ },
+ "resolutions": {
+ "@types/react": "^18.0.0"
}
}
diff --git a/packages/ui/src/collapsible/collapsible-button.tsx b/packages/ui/src/collapsible/collapsible-button.tsx
index a56a724b4..2a141aa41 100644
--- a/packages/ui/src/collapsible/collapsible-button.tsx
+++ b/packages/ui/src/collapsible/collapsible-button.tsx
@@ -8,12 +8,27 @@ type Props = {
hideChevron?: boolean;
indicatorElement?: React.ReactNode;
actionItemElement?: React.ReactNode;
+ className?: string;
+ titleClassName?: string;
};
export const CollapsibleButton: FC = (props) => {
- const { isOpen, title, hideChevron = false, indicatorElement, actionItemElement } = props;
+ const {
+ isOpen,
+ title,
+ hideChevron = false,
+ indicatorElement,
+ actionItemElement,
+ className = "",
+ titleClassName = "",
+ } = props;
return (
-
+
{!hideChevron && (
@@ -23,7 +38,7 @@ export const CollapsibleButton: FC
= (props) => {
})}
/>
)}
- {title}
+ {title}
{indicatorElement && indicatorElement}
diff --git a/packages/ui/src/dropdown/common/loader.tsx b/packages/ui/src/dropdown/common/loader.tsx
index 0ec1f053b..814ed4805 100644
--- a/packages/ui/src/dropdown/common/loader.tsx
+++ b/packages/ui/src/dropdown/common/loader.tsx
@@ -1,9 +1,10 @@
+import range from "lodash/range";
import React from "react";
export const DropdownOptionsLoader = () => (
- {Array.from({ length: 6 }, (_, i) => (
-
+ {range(6).map((index) => (
+
))}
);
diff --git a/packages/ui/src/dropdown/multi-select.tsx b/packages/ui/src/dropdown/multi-select.tsx
index 6b5018370..25f22c6be 100644
--- a/packages/ui/src/dropdown/multi-select.tsx
+++ b/packages/ui/src/dropdown/multi-select.tsx
@@ -5,7 +5,7 @@ import { Combobox } from "@headlessui/react";
// popper-js
import { usePopper } from "react-popper";
// plane helpers
-import { useOutsideClickDetector } from "@plane/helpers";
+import { useOutsideClickDetector } from "@plane/hooks";
// components
import { DropdownButton } from "./common";
import { DropdownOptions } from "./common/options";
diff --git a/packages/ui/src/dropdown/single-select.tsx b/packages/ui/src/dropdown/single-select.tsx
index 1c3b05f5b..123c9e698 100644
--- a/packages/ui/src/dropdown/single-select.tsx
+++ b/packages/ui/src/dropdown/single-select.tsx
@@ -5,7 +5,7 @@ import { Combobox } from "@headlessui/react";
// popper-js
import { usePopper } from "react-popper";
// plane helpers
-import { useOutsideClickDetector } from "@plane/helpers";
+import { useOutsideClickDetector } from "@plane/hooks";
// components
import { DropdownButton } from "./common";
import { DropdownOptions } from "./common/options";
diff --git a/packages/ui/src/dropdowns/context-menu/root.tsx b/packages/ui/src/dropdowns/context-menu/root.tsx
index 03fe0cf7b..f251696d2 100644
--- a/packages/ui/src/dropdowns/context-menu/root.tsx
+++ b/packages/ui/src/dropdowns/context-menu/root.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
// plane helpers
-import { useOutsideClickDetector } from "@plane/helpers";
+import { useOutsideClickDetector } from "@plane/hooks";
// components
import { ContextMenuItem } from "./item";
// helpers
diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx
index 274601822..39f01d1ed 100644
--- a/packages/ui/src/dropdowns/custom-menu.tsx
+++ b/packages/ui/src/dropdowns/custom-menu.tsx
@@ -4,7 +4,7 @@ import { Menu } from "@headlessui/react";
import { usePopper } from "react-popper";
import { ChevronDown, MoreHorizontal } from "lucide-react";
// plane helpers
-import { useOutsideClickDetector } from "@plane/helpers";
+import { useOutsideClickDetector } from "@plane/hooks";
// hooks
import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
// helpers
@@ -93,7 +93,14 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
useOutsideClickDetector(dropdownRef, closeDropdown, useCaptureForOutsideClick);
let menuItems = (
-
+
{
);
};
diff --git a/packages/ui/src/tabs/index.ts b/packages/ui/src/tabs/index.ts
new file mode 100644
index 000000000..811d3d4a7
--- /dev/null
+++ b/packages/ui/src/tabs/index.ts
@@ -0,0 +1 @@
+export * from "./tabs";
diff --git a/packages/ui/src/tabs/tabs.tsx b/packages/ui/src/tabs/tabs.tsx
new file mode 100644
index 000000000..a323d9721
--- /dev/null
+++ b/packages/ui/src/tabs/tabs.tsx
@@ -0,0 +1,94 @@
+import React, { FC, Fragment } from "react";
+import { Tab } from "@headlessui/react";
+import { LucideProps } from "lucide-react";
+// helpers
+import { useLocalStorage } from "@plane/hooks";
+import { cn } from "../../helpers";
+
+type TabItem = {
+ key: string;
+ icon?: FC;
+ label?: React.ReactNode;
+ content: React.ReactNode;
+ disabled?: boolean;
+};
+
+type TTabsProps = {
+ tabs: TabItem[];
+ storageKey: string;
+ actions?: React.ReactNode;
+ defaultTab?: string;
+ containerClassName?: string;
+ tabListContainerClassName?: string;
+ tabListClassName?: string;
+ tabClassName?: string;
+ tabPanelClassName?: string;
+};
+
+export const Tabs: FC = (props: TTabsProps) => {
+ const {
+ tabs,
+ storageKey,
+ actions,
+ defaultTab = tabs[0]?.key,
+ containerClassName = "",
+ tabListContainerClassName = "",
+ tabListClassName = "",
+ tabClassName = "",
+ tabPanelClassName = "",
+ } = props;
+ // local storage
+ const { storedValue, setValue } = useLocalStorage(`tab-${storageKey}`, defaultTab);
+
+ const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey);
+
+ return (
+
+
+
+
+
+ {tabs.map((tab) => (
+
+ cn(
+ `flex items-center justify-center p-1 min-w-fit w-full font-medium text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all rounded`,
+ selected
+ ? "bg-custom-background-100 text-custom-text-100 shadow-sm"
+ : tab.disabled
+ ? "text-custom-text-400 cursor-not-allowed"
+ : "text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60",
+ tabClassName
+ )
+ }
+ key={tab.key}
+ onClick={() => {
+ if (!tab.disabled) setValue(tab.key);
+ }}
+ disabled={tab.disabled}
+ >
+ {tab.icon && }
+ {tab.label}
+
+ ))}
+
+ {actions &&
{actions}
}
+
+
+ {tabs.map((tab) => (
+
+ {tab.content}
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/packages/ui/src/tooltip/tooltip.tsx b/packages/ui/src/tooltip/tooltip.tsx
index ca4f5c88a..e485166eb 100644
--- a/packages/ui/src/tooltip/tooltip.tsx
+++ b/packages/ui/src/tooltip/tooltip.tsx
@@ -23,6 +23,7 @@ export type TPosition =
interface ITooltipProps {
tooltipHeading?: string;
tooltipContent: string | React.ReactNode;
+ jsxContent?: string | React.ReactNode;
position?: TPosition;
children: JSX.Element;
disabled?: boolean;
@@ -38,13 +39,14 @@ export const Tooltip: React.FC = ({
tooltipContent,
position = "top",
children,
+ jsxContent,
disabled = false,
className = "",
openDelay = 200,
closeDelay,
isMobile = false,
renderByDefault = true, //FIXME: tooltip should always render on hover and not by default, this is a temporary fix
-}) => {
+}: ITooltipProps) => {
const toolTipRef = useRef(null);
const [shouldRender, setShouldRender] = useState(renderByDefault);
@@ -79,18 +81,22 @@ export const Tooltip: React.FC = ({
hoverOpenDelay={openDelay}
hoverCloseDelay={closeDelay}
content={
-
- {tooltipHeading &&
{tooltipHeading}
}
- {tooltipContent}
-
+ jsxContent ? (
+ <>{jsxContent}>
+ ) : (
+
+ {tooltipHeading &&
{tooltipHeading}
}
+ {tooltipContent}
+
+ )
}
position={position}
renderTarget={({
diff --git a/packages/utils/.eslintignore b/packages/utils/.eslintignore
new file mode 100644
index 000000000..6019047c3
--- /dev/null
+++ b/packages/utils/.eslintignore
@@ -0,0 +1,3 @@
+build/*
+dist/*
+out/*
\ No newline at end of file
diff --git a/packages/utils/.eslintrc.js b/packages/utils/.eslintrc.js
new file mode 100644
index 000000000..558b8f76e
--- /dev/null
+++ b/packages/utils/.eslintrc.js
@@ -0,0 +1,9 @@
+/** @type {import("eslint").Linter.Config} */
+module.exports = {
+ root: true,
+ extends: ["@plane/eslint-config/library.js"],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ project: true,
+ },
+};
diff --git a/packages/utils/.prettierrc b/packages/utils/.prettierrc
new file mode 100644
index 000000000..87d988f1b
--- /dev/null
+++ b/packages/utils/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "printWidth": 120,
+ "tabWidth": 2,
+ "trailingComma": "es5"
+}
diff --git a/packages/helpers/package.json b/packages/utils/package.json
similarity index 60%
rename from packages/helpers/package.json
rename to packages/utils/package.json
index 6e39a19d5..a0e0ecca1 100644
--- a/packages/helpers/package.json
+++ b/packages/utils/package.json
@@ -1,6 +1,6 @@
{
- "name": "@plane/helpers",
- "version": "0.24.0",
+ "name": "@plane/utils",
+ "version": "0.24.1",
"description": "Helper functions shared across multiple apps internally",
"private": true,
"main": "./dist/index.js",
@@ -10,16 +10,19 @@
"dist/**"
],
"scripts": {
- "build": "tsup ./index.ts --format esm,cjs --dts --external react --minify"
- },
- "devDependencies": {
- "@types/node": "^22.5.4",
- "@types/react": "^18.3.11",
- "tsup": "^7.2.0",
- "typescript": "^5.6.2"
+ "build": "tsup ./src/index.ts --format esm,cjs --dts --external react --minify",
+ "lint": "eslint src --ext .ts,.tsx",
+ "lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"isomorphic-dompurify": "^2.16.0",
"react": "^18.3.1"
+ },
+ "devDependencies": {
+ "@plane/eslint-config": "*",
+ "@types/node": "^22.5.4",
+ "@types/react": "^18.3.11",
+ "tsup": "^7.2.0",
+ "typescript": "^5.3.3"
}
}
diff --git a/packages/utils/src/color.ts b/packages/utils/src/color.ts
new file mode 100644
index 000000000..702719c79
--- /dev/null
+++ b/packages/utils/src/color.ts
@@ -0,0 +1,60 @@
+/**
+ * Represents an RGB color with numeric values for red, green, and blue components
+ * @typedef {Object} RGB
+ * @property {number} r - Red component (0-255)
+ * @property {number} g - Green component (0-255)
+ * @property {number} b - Blue component (0-255)
+ */
+export type RGB = { r: number; g: number; b: number };
+
+/**
+ * Validates and clamps color values to RGB range (0-255)
+ * @param {number} value - The color value to validate
+ * @returns {number} Clamped and floored value between 0-255
+ */
+export const validateColor = (value: number) => {
+ if (value < 0) return 0;
+ if (value > 255) return 255;
+ return Math.floor(value);
+};
+
+/**
+ * Converts a decimal color value to two-character hex
+ * @param {number} value - Decimal color value (0-255)
+ * @returns {string} Two-character hex value with leading zero if needed
+ */
+export const toHex = (value: number) => validateColor(value).toString(16).padStart(2, "0");
+
+/**
+ * Converts a hexadecimal color code to RGB values
+ * @param {string} hex - The hexadecimal color code (e.g., "#ff0000" for red)
+ * @returns {RGB} An object containing the RGB values
+ * @example
+ * hexToRgb("#ff0000") // returns { r: 255, g: 0, b: 0 }
+ * hexToRgb("#00ff00") // returns { r: 0, g: 255, b: 0 }
+ * hexToRgb("#0000ff") // returns { r: 0, g: 0, b: 255 }
+ */
+export const hexToRgb = (hex: string): RGB => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
+ return result
+ ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16),
+ }
+ : { r: 0, g: 0, b: 0 };
+};
+
+/**
+ * Converts RGB values to a hexadecimal color code
+ * @param {RGB} rgb - An object containing RGB values
+ * @param {number} rgb.r - Red component (0-255)
+ * @param {number} rgb.g - Green component (0-255)
+ * @param {number} rgb.b - Blue component (0-255)
+ * @returns {string} The hexadecimal color code (e.g., "#ff0000" for red)
+ * @example
+ * rgbToHex({ r: 255, g: 0, b: 0 }) // returns "#ff0000"
+ * rgbToHex({ r: 0, g: 255, b: 0 }) // returns "#00ff00"
+ * rgbToHex({ r: 0, g: 0, b: 255 }) // returns "#0000ff"
+ */
+export const rgbToHex = ({ r, g, b }: RGB): string => `#${toHex(r)}${toHex(g)}${toHex(b)}`;
diff --git a/packages/utils/src/emoji.ts b/packages/utils/src/emoji.ts
new file mode 100644
index 000000000..306d4afef
--- /dev/null
+++ b/packages/utils/src/emoji.ts
@@ -0,0 +1,40 @@
+/**
+ * Converts a hyphen-separated hexadecimal emoji code to its decimal representation
+ * @param {string} emojiUnified - The unified emoji code in hexadecimal format (e.g., "1f600" or "1f1e6-1f1e8")
+ * @returns {string} The decimal representation of the emoji code (e.g., "128512" or "127462-127464")
+ * @example
+ * convertHexEmojiToDecimal("1f600") // returns "128512"
+ * convertHexEmojiToDecimal("1f1e6-1f1e8") // returns "127462-127464"
+ * convertHexEmojiToDecimal("") // returns ""
+ */
+export const convertHexEmojiToDecimal = (emojiUnified: string): string => {
+ if (!emojiUnified) return "";
+
+ return emojiUnified
+ .toString()
+ .split("-")
+ .map((e) => parseInt(e, 16))
+ .join("-");
+};
+
+/**
+ * Converts a hyphen-separated decimal emoji code back to its hexadecimal representation
+ * @param {string} emoji - The emoji code in decimal format (e.g., "128512" or "127462-127464")
+ * @returns {string} The hexadecimal representation of the emoji code (e.g., "1f600" or "1f1e6-1f1e8")
+ * @example
+ * emojiCodeToUnicode("128512") // returns "1f600"
+ * emojiCodeToUnicode("127462-127464") // returns "1f1e6-1f1e8"
+ * emojiCodeToUnicode("") // returns ""
+ */
+export const emojiCodeToUnicode = (emoji: string): string => {
+ if (!emoji) return "";
+
+ // convert emoji code to unicode
+ const uniCodeEmoji = emoji
+ .toString()
+ .split("-")
+ .map((emoji) => parseInt(emoji, 10).toString(16))
+ .join("-");
+
+ return uniCodeEmoji;
+};
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
new file mode 100644
index 000000000..7e63eed79
--- /dev/null
+++ b/packages/utils/src/index.ts
@@ -0,0 +1,3 @@
+export * from "./color";
+export * from "./emoji";
+export * from "./string";
diff --git a/packages/helpers/helpers/string.helper.ts b/packages/utils/src/string.ts
similarity index 99%
rename from packages/helpers/helpers/string.helper.ts
rename to packages/utils/src/string.ts
index aad727262..c3c8b1541 100644
--- a/packages/helpers/helpers/string.helper.ts
+++ b/packages/utils/src/string.ts
@@ -12,4 +12,4 @@ import DOMPurify from "isomorphic-dompurify";
export const sanitizeHTML = (htmlString: string) => {
const sanitizedText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: [] }); // sanitize the string to remove all HTML tags
return sanitizedText.trim(); // trim the string to remove leading and trailing whitespaces
-};
\ No newline at end of file
+};
diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json
new file mode 100644
index 000000000..e8af9092a
--- /dev/null
+++ b/packages/utils/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "@plane/typescript-config/react-library.json",
+ "compilerOptions": {
+ "jsx": "react",
+ "lib": ["esnext", "dom"]
+ },
+ "include": ["./src"],
+ "exclude": ["dist", "build", "node_modules"]
+}
diff --git a/space/app/issues/[anchor]/client-layout.tsx b/space/app/issues/[anchor]/client-layout.tsx
new file mode 100644
index 000000000..0e24ab551
--- /dev/null
+++ b/space/app/issues/[anchor]/client-layout.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import { observer } from "mobx-react";
+import useSWR from "swr";
+// components
+import { LogoSpinner, PoweredBy } from "@/components/common";
+import { IssuesNavbarRoot } from "@/components/issues";
+import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
+// hooks
+import { useIssueFilter, usePublish, usePublishList } from "@/hooks/store";
+
+type Props = {
+ children: React.ReactNode;
+ anchor: string;
+};
+
+export const IssuesClientLayout = observer((props: Props) => {
+ const { children, anchor } = props;
+ // store hooks
+ const { fetchPublishSettings } = usePublishList();
+ const publishSettings = usePublish(anchor);
+ const { updateLayoutOptions } = useIssueFilter();
+ // fetch publish settings
+ const { error } = useSWR(
+ anchor ? `PUBLISH_SETTINGS_${anchor}` : null,
+ anchor
+ ? async () => {
+ const response = await fetchPublishSettings(anchor);
+ if (response.view_props) {
+ updateLayoutOptions({
+ list: !!response.view_props.list,
+ kanban: !!response.view_props.kanban,
+ calendar: !!response.view_props.calendar,
+ gantt: !!response.view_props.gantt,
+ spreadsheet: !!response.view_props.spreadsheet,
+ });
+ }
+ }
+ : null
+ );
+
+ if (!publishSettings && !error) return ;
+
+ if (error) return ;
+
+ return (
+ <>
+
+
+ >
+ );
+});
diff --git a/space/app/issues/[anchor]/layout.tsx b/space/app/issues/[anchor]/layout.tsx
index ac5dba430..91631d6c0 100644
--- a/space/app/issues/[anchor]/layout.tsx
+++ b/space/app/issues/[anchor]/layout.tsx
@@ -1,16 +1,6 @@
-"use client";
+"use server";
-import { observer } from "mobx-react";
-import Image from "next/image";
-import useSWR from "swr";
-// components
-import { LogoSpinner } from "@/components/common";
-import { IssuesNavbarRoot } from "@/components/issues";
-import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
-// hooks
-import { useIssueFilter, usePublish, usePublishList } from "@/hooks/store";
-// assets
-import planeLogo from "@/public/plane-logo.svg";
+import { IssuesClientLayout } from "./client-layout";
type Props = {
children: React.ReactNode;
@@ -19,58 +9,44 @@ type Props = {
};
};
-const IssuesLayout = observer((props: Props) => {
- const { children, params } = props;
- // params
+export async function generateMetadata({ params }: Props) {
const { anchor } = params;
- // store hooks
- const { fetchPublishSettings } = usePublishList();
- const publishSettings = usePublish(anchor);
- const { updateLayoutOptions } = useIssueFilter();
- // fetch publish settings
- const { error } = useSWR(
- anchor ? `PUBLISH_SETTINGS_${anchor}` : null,
- anchor
- ? async () => {
- const response = await fetchPublishSettings(anchor);
- if (response.view_props) {
- updateLayoutOptions({
- list: !!response.view_props.list,
- kanban: !!response.view_props.kanban,
- calendar: !!response.view_props.calendar,
- gantt: !!response.view_props.gantt,
- spreadsheet: !!response.view_props.spreadsheet,
- });
- }
- }
- : null
- );
+ const DEFAULT_TITLE = "Plane";
+ const DEFAULT_DESCRIPTION = "Made with Plane, an AI-powered work management platform with publishing capabilities.";
+ try {
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/public/anchor/${anchor}/meta/`);
+ const data = await response.json();
+ return {
+ title: data?.name || DEFAULT_TITLE,
+ description: data?.description || DEFAULT_DESCRIPTION,
+ openGraph: {
+ title: data?.name || DEFAULT_TITLE,
+ description: data?.description || DEFAULT_DESCRIPTION,
+ type: "website",
+ images: [
+ {
+ url: data?.cover_image,
+ width: 800,
+ height: 600,
+ alt: data?.name || DEFAULT_TITLE,
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: data?.name || DEFAULT_TITLE,
+ description: data?.description || DEFAULT_DESCRIPTION,
+ images: [data?.cover_image],
+ },
+ };
+ } catch {
+ return { title: DEFAULT_TITLE, description: DEFAULT_DESCRIPTION };
+ }
+}
- if (!publishSettings && !error) return ;
+export default async function IssuesLayout(props: Props) {
+ const { children, params } = props;
+ const { anchor } = params;
- if (error) return ;
-
- return (
-
- );
-});
-
-export default IssuesLayout;
+ return {children};
+}
diff --git a/space/app/layout.tsx b/space/app/layout.tsx
index ca6d11ea1..e457ae5d1 100644
--- a/space/app/layout.tsx
+++ b/space/app/layout.tsx
@@ -1,12 +1,10 @@
import { Metadata } from "next";
// helpers
-import { ASSET_PREFIX } from "@/helpers/common.helper";
-// components
-import { InstanceProvider } from "@/lib/instance-provider";
-import { StoreProvider } from "@/lib/store-provider";
+import { SPACE_BASE_PATH } from "@plane/constants";
// styles
import "@/styles/globals.css";
-import { ToastProvider } from "@/lib/toast-provider";
+// components
+import { AppProvider } from "./provider";
export const metadata: Metadata = {
title: "Plane Publish | Make your Plane boards public with one-click",
@@ -27,18 +25,14 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
-
-
-
-
-
+
+
+
+
+
-
-
- {children}
-
-
+ {children}
);
diff --git a/space/app/provider.tsx b/space/app/provider.tsx
new file mode 100644
index 000000000..c3ab6673f
--- /dev/null
+++ b/space/app/provider.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import { FC, ReactNode } from "react";
+// components
+import { InstanceProvider } from "@/lib/instance-provider";
+import { StoreProvider } from "@/lib/store-provider";
+import { ToastProvider } from "@/lib/toast-provider";
+
+interface IAppProvider {
+ children: ReactNode;
+}
+
+export const AppProvider: FC = (props) => {
+ const { children } = props;
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/space/app/views/[anchor]/layout.tsx b/space/app/views/[anchor]/layout.tsx
index cf7643bb6..57b2971c4 100644
--- a/space/app/views/[anchor]/layout.tsx
+++ b/space/app/views/[anchor]/layout.tsx
@@ -1,18 +1,15 @@
"use client";
import { observer } from "mobx-react";
-import Image from "next/image";
import useSWR from "swr";
// components
-import { LogoSpinner } from "@/components/common";
+import { LogoSpinner, PoweredBy } from "@/components/common";
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
// hooks
import { usePublish, usePublishList } from "@/hooks/store";
// Plane web
import { ViewNavbarRoot } from "@/plane-web/components/navbar";
import { useView } from "@/plane-web/hooks/store";
-// assets
-import planeLogo from "@/public/plane-logo.svg";
type Props = {
children: React.ReactNode;
@@ -53,19 +50,7 @@ const IssuesLayout = observer((props: Props) => {
{children}
-
-
-
-
-
- Powered by Plane Publish
-
-
+
);
});
diff --git a/space/app/views/[anchor]/page.tsx b/space/app/views/[anchor]/page.tsx
index 21bb5a965..1efd95a53 100644
--- a/space/app/views/[anchor]/page.tsx
+++ b/space/app/views/[anchor]/page.tsx
@@ -2,6 +2,8 @@
import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
+// components
+import { PoweredBy } from "@/components/common";
// hooks
import { usePublish } from "@/hooks/store";
// plane-web
@@ -24,7 +26,12 @@ const IssuesPage = observer((props: Props) => {
if (!publishSettings) return null;
- return ;
+ return (
+ <>
+
+
+ >
+ );
});
export default IssuesPage;
diff --git a/space/core/components/account/auth-forms/password.tsx b/space/core/components/account/auth-forms/password.tsx
index 3d87cded5..5f0384f9b 100644
--- a/space/core/components/account/auth-forms/password.tsx
+++ b/space/core/components/account/auth-forms/password.tsx
@@ -3,11 +3,11 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { observer } from "mobx-react";
import { Eye, EyeOff, XCircle } from "lucide-react";
+import { API_BASE_URL } from "@plane/constants";
import { Button, Input, Spinner } from "@plane/ui";
// components
import { PasswordStrengthMeter } from "@/components/account";
// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper";
// services
import { AuthService } from "@/services/auth.service";
diff --git a/space/core/components/account/auth-forms/unique-code.tsx b/space/core/components/account/auth-forms/unique-code.tsx
index 10c7b4f00..f16796d05 100644
--- a/space/core/components/account/auth-forms/unique-code.tsx
+++ b/space/core/components/account/auth-forms/unique-code.tsx
@@ -2,9 +2,8 @@
import React, { useEffect, useState } from "react";
import { CircleCheck, XCircle } from "lucide-react";
+import { API_BASE_URL } from "@plane/constants";
import { Button, Input, Spinner } from "@plane/ui";
-// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
// hooks
import useTimer from "@/hooks/use-timer";
// services
diff --git a/space/core/components/account/oauth/github-button.tsx b/space/core/components/account/oauth/github-button.tsx
index ba65f38c4..eaa83ebbb 100644
--- a/space/core/components/account/oauth/github-button.tsx
+++ b/space/core/components/account/oauth/github-button.tsx
@@ -2,8 +2,7 @@ import { FC } from "react";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
import { useTheme } from "next-themes";
-// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
+import { API_BASE_URL } from "@plane/constants";
// images
import githubLightModeImage from "/public/logos/github-black.png";
import githubDarkModeImage from "/public/logos/github-dark.svg";
diff --git a/space/core/components/account/oauth/gitlab-button.tsx b/space/core/components/account/oauth/gitlab-button.tsx
index 072a2f628..ba1880ae9 100644
--- a/space/core/components/account/oauth/gitlab-button.tsx
+++ b/space/core/components/account/oauth/gitlab-button.tsx
@@ -2,8 +2,7 @@ import { FC } from "react";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
import { useTheme } from "next-themes";
-// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
+import { API_BASE_URL } from "@plane/constants";
// images
import GitlabLogo from "/public/logos/gitlab-logo.svg";
diff --git a/space/core/components/account/oauth/google-button.tsx b/space/core/components/account/oauth/google-button.tsx
index 179b1c35a..dc28bdae4 100644
--- a/space/core/components/account/oauth/google-button.tsx
+++ b/space/core/components/account/oauth/google-button.tsx
@@ -2,8 +2,7 @@ import { FC } from "react";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
import { useTheme } from "next-themes";
-// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
+import { API_BASE_URL } from "@plane/constants";
// images
import GoogleLogo from "/public/logos/google-logo.svg";
diff --git a/space/core/components/account/user-logged-in.tsx b/space/core/components/account/user-logged-in.tsx
index 4bedc4596..0993ade92 100644
--- a/space/core/components/account/user-logged-in.tsx
+++ b/space/core/components/account/user-logged-in.tsx
@@ -4,6 +4,7 @@ import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
// components
+import { PoweredBy } from "@/components/common";
import { UserAvatar } from "@/components/issues";
// hooks
import { useUser } from "@/hooks/store";
@@ -45,6 +46,7 @@ export const UserLoggedIn = observer(() => {
+
);
});
diff --git a/space/core/components/common/index.ts b/space/core/components/common/index.ts
index 1949c069b..0a63ca1ac 100644
--- a/space/core/components/common/index.ts
+++ b/space/core/components/common/index.ts
@@ -1,2 +1,3 @@
export * from "./project-logo";
export * from "./logo-spinner";
+export * from "./powered-by";
diff --git a/space/core/components/common/powered-by.tsx b/space/core/components/common/powered-by.tsx
new file mode 100644
index 000000000..654089c55
--- /dev/null
+++ b/space/core/components/common/powered-by.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { FC } from "react";
+import Image from "next/image";
+import { WEBSITE_URL } from "@plane/constants";
+// assets
+import planeLogo from "@/public/plane-logo.svg";
+
+type TPoweredBy = {
+ disabled?: boolean;
+};
+
+export const PoweredBy: FC = (props) => {
+ // props
+ const { disabled = false } = props;
+
+ if (disabled || !WEBSITE_URL) return null;
+
+ return (
+
+
+
+
+
+ Powered by Plane Publish
+
+
+ );
+};
diff --git a/space/core/components/editor/lite-text-editor.tsx b/space/core/components/editor/lite-text-editor.tsx
index 0e3f34293..5d5027135 100644
--- a/space/core/components/editor/lite-text-editor.tsx
+++ b/space/core/components/editor/lite-text-editor.tsx
@@ -10,7 +10,8 @@ import { isCommentEmpty } from "@/helpers/string.helper";
// hooks
import { useMention } from "@/hooks/use-mention";
-interface LiteTextEditorWrapperProps extends Omit {
+interface LiteTextEditorWrapperProps
+ extends Omit {
anchor: string;
workspaceId: string;
isSubmitting?: boolean;
@@ -41,6 +42,7 @@ export const LiteTextEditor = React.forwardRef
& {
+type LiteTextReadOnlyEditorWrapperProps = Omit<
+ ILiteTextReadOnlyEditor,
+ "disabledExtensions" | "fileHandler" | "mentionHandler"
+> & {
anchor: string;
};
@@ -18,6 +21,7 @@ export const LiteTextReadOnlyEditor = React.forwardRef {
+interface RichTextEditorWrapperProps
+ extends Omit {
uploadFile: (file: File) => Promise;
}
@@ -27,6 +26,7 @@ export const RichTextEditor = forwardRef & {
+type RichTextReadOnlyEditorWrapperProps = Omit<
+ IRichTextReadOnlyEditor,
+ "disabledExtensions" | "fileHandler" | "mentionHandler"
+> & {
anchor: string;
};
@@ -18,6 +21,7 @@ export const RichTextReadOnlyEditor = React.forwardRef {
// hooks
@@ -41,6 +41,7 @@ export const AuthView = observer(() => {
+
);
});
diff --git a/space/core/lib/instance-provider.tsx b/space/core/lib/instance-provider.tsx
index 4f28dbcf9..06056364f 100644
--- a/space/core/lib/instance-provider.tsx
+++ b/space/core/lib/instance-provider.tsx
@@ -6,18 +6,17 @@ import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
import useSWR from "swr";
+import { SPACE_BASE_PATH } from "@plane/constants";
// components
import { LogoSpinner } from "@/components/common";
import { InstanceFailureView } from "@/components/instance";
-// helpers
-import { SPACE_BASE_PATH } from "@/helpers/common.helper";
// hooks
import { useInstance, useUser } from "@/hooks/store";
// assets
-import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
-import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
-import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png";
-import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png";
+import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
+import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
+import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
+import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
export const InstanceProvider = observer(({ children }: { children: ReactNode }) => {
const { fetchInstanceInfo, instance, error } = useInstance();
@@ -69,5 +68,5 @@ export const InstanceProvider = observer(({ children }: { children: ReactNode })
);
}
- return <>{children}>;
+ return children;
});
diff --git a/space/core/lib/store-provider.tsx b/space/core/lib/store-provider.tsx
index c1256ddc2..b810c1056 100644
--- a/space/core/lib/store-provider.tsx
+++ b/space/core/lib/store-provider.tsx
@@ -9,13 +9,8 @@ let rootStore = new RootStore();
export const StoreContext = createContext(rootStore);
-function initializeStore(initialData = {}) {
+function initializeStore() {
const singletonRootStore = rootStore ?? new RootStore();
- // If your page has Next.js data fetching methods that use a Mobx store, it will
- // get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details
- if (initialData) {
- singletonRootStore.hydrate(initialData);
- }
// For SSG and SSR always create a new store
if (typeof window === "undefined") return singletonRootStore;
// Create the store once in the client
@@ -29,8 +24,14 @@ export type StoreProviderProps = {
initialState?: any;
};
-export const StoreProvider = ({ children, initialState = {} }: StoreProviderProps) => {
- const store = initializeStore(initialState);
+export const StoreProvider = ({ children, initialState = undefined }: StoreProviderProps) => {
+ const store = initializeStore();
+ // If your page has Next.js data fetching methods that use a Mobx store, it will
+ // get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details
+ if (initialState) {
+ store.hydrate(initialState);
+ }
+
return (
{children}
diff --git a/space/core/services/auth.service.ts b/space/core/services/auth.service.ts
index 060da53f4..3bbfd149e 100644
--- a/space/core/services/auth.service.ts
+++ b/space/core/services/auth.service.ts
@@ -1,5 +1,4 @@
-// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
+import { API_BASE_URL } from "@plane/constants";
// services
import { APIService } from "@/services/api.service";
// types
diff --git a/space/core/services/cycle.service.ts b/space/core/services/cycle.service.ts
index 6df75ebde..7d4ff9a10 100644
--- a/space/core/services/cycle.service.ts
+++ b/space/core/services/cycle.service.ts
@@ -1,5 +1,7 @@
-import { API_BASE_URL } from "@/helpers/common.helper";
+import { API_BASE_URL } from "@plane/constants";
+// services
import { APIService } from "@/services/api.service";
+// types
import { TPublicCycle } from "@/types/cycle";
export class CycleService extends APIService {
diff --git a/space/core/services/file.service.ts b/space/core/services/file.service.ts
index 168738804..0b4807aff 100644
--- a/space/core/services/file.service.ts
+++ b/space/core/services/file.service.ts
@@ -1,7 +1,6 @@
-// plane types
+import { API_BASE_URL } from "@plane/constants";
import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types";
// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
import { generateFileUploadPayload, getAssetIdFromUrl, getFileMetaDataForUpload } from "@/helpers/file.helper";
// services
import { APIService } from "@/services/api.service";
diff --git a/space/core/services/instance.service.ts b/space/core/services/instance.service.ts
index a11599b0c..100929955 100644
--- a/space/core/services/instance.service.ts
+++ b/space/core/services/instance.service.ts
@@ -1,7 +1,5 @@
-// types
+import { API_BASE_URL } from "@plane/constants";
import type { IInstanceInfo } from "@plane/types";
-// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
diff --git a/space/core/services/issue.service.ts b/space/core/services/issue.service.ts
index b5ecb8077..8ec67ee45 100644
--- a/space/core/services/issue.service.ts
+++ b/space/core/services/issue.service.ts
@@ -1,4 +1,4 @@
-import { API_BASE_URL } from "@/helpers/common.helper";
+import { API_BASE_URL } from "@plane/constants";
// services
import { APIService } from "@/services/api.service";
// types
diff --git a/space/core/services/label.service.ts b/space/core/services/label.service.ts
index 2a2ee5ad9..3b5585578 100644
--- a/space/core/services/label.service.ts
+++ b/space/core/services/label.service.ts
@@ -1,5 +1,6 @@
+import { API_BASE_URL } from "@plane/constants";
import { IIssueLabel } from "@plane/types";
-import { API_BASE_URL } from "@/helpers/common.helper";
+// services
import { APIService } from "./api.service";
export class LabelService extends APIService {
diff --git a/space/core/services/member.service.ts b/space/core/services/member.service.ts
index 02cd1f776..9de19455b 100644
--- a/space/core/services/member.service.ts
+++ b/space/core/services/member.service.ts
@@ -1,5 +1,7 @@
-import { API_BASE_URL } from "@/helpers/common.helper";
+import { API_BASE_URL } from "@plane/constants";
+// services
import { APIService } from "@/services/api.service";
+// types
import { TPublicMember } from "@/types/member";
export class MemberService extends APIService {
diff --git a/space/core/services/module.service.ts b/space/core/services/module.service.ts
index f89202b6b..30d6ebecf 100644
--- a/space/core/services/module.service.ts
+++ b/space/core/services/module.service.ts
@@ -1,5 +1,7 @@
-import { API_BASE_URL } from "@/helpers/common.helper";
+import { API_BASE_URL } from "@plane/constants";
+// services
import { APIService } from "@/services/api.service";
+// types
import { TPublicModule } from "@/types/modules";
export class ModuleService extends APIService {
diff --git a/space/core/services/project-member.service.ts b/space/core/services/project-member.service.ts
index 722380efa..bac52e751 100644
--- a/space/core/services/project-member.service.ts
+++ b/space/core/services/project-member.service.ts
@@ -1,6 +1,5 @@
-// types
+import { API_BASE_URL } from "@plane/constants";
import type { IProjectMember, IProjectMembership } from "@plane/types";
-import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
diff --git a/space/core/services/publish.service.ts b/space/core/services/publish.service.ts
index 896f36ee9..3da72f59a 100644
--- a/space/core/services/publish.service.ts
+++ b/space/core/services/publish.service.ts
@@ -1,7 +1,5 @@
-// types
+import { API_BASE_URL } from "@plane/constants";
import { TProjectPublishSettings } from "@plane/types";
-// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
diff --git a/space/core/services/state.service.ts b/space/core/services/state.service.ts
index 153f96528..b877ac530 100644
--- a/space/core/services/state.service.ts
+++ b/space/core/services/state.service.ts
@@ -1,5 +1,6 @@
+import { API_BASE_URL } from "@plane/constants";
import { IState } from "@plane/types";
-import { API_BASE_URL } from "@/helpers/common.helper";
+// services
import { APIService } from "./api.service";
export class StateService extends APIService {
diff --git a/space/core/services/user.service.ts b/space/core/services/user.service.ts
index 1aeb13466..a00b1a350 100644
--- a/space/core/services/user.service.ts
+++ b/space/core/services/user.service.ts
@@ -1,7 +1,5 @@
-// types
+import { API_BASE_URL } from "@plane/constants";
import { IUser, TUserProfile } from "@plane/types";
-// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
diff --git a/space/helpers/common.helper.ts b/space/helpers/common.helper.ts
index 6db73a4a1..3ffc59573 100644
--- a/space/helpers/common.helper.ts
+++ b/space/helpers/common.helper.ts
@@ -1,21 +1,8 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
-export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
-
-export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || "";
-export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
-
-export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
-
export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "";
-export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
-
-export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}`);
-
-export const ASSET_PREFIX = SPACE_BASE_PATH;
-
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
export const resolveGeneralTheme = (resolvedTheme: string | undefined) =>
diff --git a/space/helpers/file.helper.ts b/space/helpers/file.helper.ts
index b149ebc7c..a94ed7efe 100644
--- a/space/helpers/file.helper.ts
+++ b/space/helpers/file.helper.ts
@@ -1,7 +1,5 @@
-// plane types
+import { API_BASE_URL } from "@plane/constants";
import { TFileMetaDataLite, TFileSignedURLResponse } from "@plane/types";
-// helpers
-import { API_BASE_URL } from "@/helpers/common.helper";
/**
* @description from the provided signed URL response, generate a payload to be used to upload the file
diff --git a/space/next.config.js b/space/next.config.js
index d18ce805f..58b6cfa0b 100644
--- a/space/next.config.js
+++ b/space/next.config.js
@@ -1,3 +1,4 @@
+// eslint-disable-next-line @typescript-eslint/no-var-requires
/* eslint-disable @typescript-eslint/no-var-requires */
/** @type {import('next').NextConfig} */
require("dotenv").config({ path: ".env" });
@@ -28,7 +29,6 @@ const nextConfig = {
},
};
-
const sentryConfig = {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
@@ -62,12 +62,10 @@ const sentryConfig = {
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
-}
-
+};
if (parseInt(process.env.SENTRY_MONITORING_ENABLED || "0", 10)) {
module.exports = withSentryConfig(nextConfig, sentryConfig);
} else {
module.exports = nextConfig;
}
-
diff --git a/space/package.json b/space/package.json
index 7d5f375dd..941f8419f 100644
--- a/space/package.json
+++ b/space/package.json
@@ -1,6 +1,6 @@
{
"name": "space",
- "version": "0.24.0",
+ "version": "0.24.1",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -34,7 +34,7 @@
"mobx": "^6.10.0",
"mobx-react": "^9.1.1",
"mobx-utils": "^6.0.8",
- "next": "^14.2.12",
+ "next": "^14.2.20",
"next-themes": "^0.2.1",
"nprogress": "^0.2.0",
"react": "^18.3.1",
diff --git a/web/.prettierignore b/web/.prettierignore
index 43e8a7b8f..e841c6b32 100644
--- a/web/.prettierignore
+++ b/web/.prettierignore
@@ -1,6 +1,5 @@
.next
-.vercel
-.tubro
+.turbo
out/
-dis/
+dist/
build/
\ No newline at end of file
diff --git a/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx b/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx
index b8b80b4d2..7d71948d8 100644
--- a/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx
+++ b/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx
@@ -1,12 +1,12 @@
"use client";
// components
-import { NotificationsSidebar } from "@/components/workspace-notifications";
+import { NotificationsSidebarRoot } from "@/plane-web/components/workspace-notifications";
export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
return (
);
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx
index e9debb2bc..4d3f395ea 100644
--- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx
+++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx
@@ -31,9 +31,9 @@ const PageDetailsPage = observer(() => {
? () => getPageById(workspaceSlug?.toString(), projectId?.toString(), pageId.toString())
: null,
{
- revalidateIfStale: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
+ revalidateIfStale: true,
+ revalidateOnFocus: true,
+ revalidateOnReconnect: true,
}
);
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx
index e696a08f4..d3646b31b 100644
--- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx
+++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx
@@ -13,9 +13,7 @@ import { BreadcrumbLink, Logo } from "@/components/common";
// constants
import { EPageAccess } from "@/constants/page";
// hooks
-import { useEventTracker, useProject, useProjectPages, useUserPermissions } from "@/hooks/store";
-// plane web hooks
-import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
+import { useEventTracker, useProject, useProjectPages } from "@/hooks/store";
export const PagesListHeader = observer(() => {
// states
@@ -26,16 +24,9 @@ export const PagesListHeader = observer(() => {
const searchParams = useSearchParams();
const pageType = searchParams.get("type");
// store hooks
- const { allowPermissions } = useUserPermissions();
-
const { currentProjectDetails, loader } = useProject();
- const { createPage } = useProjectPages();
+ const { canCurrentUserCreatePage, createPage } = useProjectPages();
const { setTrackElement } = useEventTracker();
- // auth
- const canUserCreatePage = allowPermissions(
- [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
- EUserPermissionsLevel.PROJECT
- );
// handle page create
const handleCreatePage = async () => {
setIsCreatingPage(true);
@@ -87,7 +78,7 @@ export const PagesListHeader = observer(() => {
- {canUserCreatePage ? (
+ {canCurrentUserCreatePage ? (