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/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 ( + +
+ Plane logo +
+
+ 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 (
- +
{children}
); 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 ? (