Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release: v0.24.1 #6183

Merged
merged 52 commits into from
Dec 10, 2024
Merged
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
d0f9a4d
chore: add redirection to plane logo in invitations page (#6125)
prateekshourya29 Nov 29, 2024
75ada1b
fix: constants package updates
sriramveeraghanta Dec 1, 2024
1b90339
[WEB-2799] chore: global component and code refactor (#6131)
anmolsinghbhatia Dec 2, 2024
1953d6f
[WEB-2762] chore: loader code refactor (#5992)
anmolsinghbhatia Dec 2, 2024
63bc01f
[WEB-2774]fix:reordering favorites and favorite folders (#6119)
vamsikrishnamathala Dec 2, 2024
5150c66
reduced the components moved (#6110)
vamsikrishnamathala Dec 2, 2024
fc52936
fix: escape markdown content for images (#6096)
aaryan610 Dec 2, 2024
11bfbe5
fix: checked colored todo list item (#6113)
aaryan610 Dec 2, 2024
9f14167
refactor: editor code splitting (#6102)
aaryan610 Dec 2, 2024
8c04aa6
dev: revamp pages authorization (#6094)
aaryan610 Dec 2, 2024
3c6006d
[PE-31] feat: Add lock unlock archive restore realtime sync (#5629)
Palanikannan1437 Dec 2, 2024
da00712
fix half block dragging (#6135)
rahulramesha Dec 2, 2024
fe5999c
fix: intake issue permission (#6136)
anmolsinghbhatia Dec 2, 2024
2768f56
[WEB-2802]fix:filters drop down fix safari (#6133)
vamsikrishnamathala Dec 3, 2024
849d989
chore: community edition product updates link (#6132)
prateekshourya29 Dec 3, 2024
fe43300
fix: pages empty state authorization (#6141)
aaryan610 Dec 3, 2024
b6ab853
chore: filter out the removed cycle from issue detail (#6138)
NarayanBavisetti Dec 3, 2024
f02a2b0
fix: export btn overlap issue (#6149)
gakshita Dec 4, 2024
bb8a156
fix: removed changelog endpoint (#6146)
gurusainath Dec 4, 2024
64a44f4
style: add custom class to editor paragraph and heading blocks (#6143)
aaryan610 Dec 4, 2024
a153de3
fixed piority icons shape (#6144)
vamsikrishnamathala Dec 4, 2024
31b6d52
fix root issue store to have updated url params at all times (#6147)
rahulramesha Dec 4, 2024
1b92a18
chore: updated the ssr rendering on sites (#6145)
gurusainath Dec 4, 2024
d537e56
[WEB-2802]fix: dorpdown visibility issue in safari (#6151)
vamsikrishnamathala Dec 4, 2024
b73ea37
chore: improve the cascading logic (#6152)
NarayanBavisetti Dec 4, 2024
fa3aa36
fix: lint errors
sriramveeraghanta Dec 4, 2024
fb3295f
fix: sites opengraph title and description added
sriramveeraghanta Dec 4, 2024
3bccda0
chore: formatting and typo fixes
sriramveeraghanta Dec 4, 2024
66652a5
refactor project states to ake way for new features (#6156)
rahulramesha Dec 5, 2024
6cd8af1
chore: updated powered by (#6160)
gurusainath Dec 5, 2024
aa1e192
improvement: update fetch map during workspace-level module fetch to …
prateekshourya29 Dec 5, 2024
d55ee6d
fix: remove unwanted states fetching logic to avoid multiple API call…
prateekshourya29 Dec 5, 2024
a612a17
chore remove unnecessary CTA (#6161)
rahulramesha Dec 5, 2024
b1c340b
fix: build branch workflow upload artifacts
sriramveeraghanta Dec 5, 2024
4b5a2bc
chore: lint related changes and packaging fixes (#6163)
sriramveeraghanta Dec 6, 2024
727dd40
fix: updated lint command in packages
sriramveeraghanta Dec 6, 2024
4499a5f
Sync issues and workspace data when the issue properties like labels/…
SatishGandham Dec 6, 2024
666ddf7
[WEB-2382]chore:notification snooze modal (#6164)
vamsikrishnamathala Dec 6, 2024
9ee41ec
fix: email check validation to handle case in-sensitive email (#6168)
gurusainath Dec 7, 2024
02308ee
fix: django version upgrade
sriramveeraghanta Dec 8, 2024
cba41e0
fix: upgrading the express version
sriramveeraghanta Dec 8, 2024
b21d190
fix: added github pull request template
sriramveeraghanta Dec 8, 2024
a85e592
fix: creating a new sub-issue from workspace level (#6169)
aaryan610 Dec 9, 2024
5c907db
[WEB-2818] chore: project navigation items code refactor (#6170)
anmolsinghbhatia Dec 9, 2024
547c138
fix: ui package module resolution
sriramveeraghanta Dec 9, 2024
d046194
[WEB-2382]chore: notifications code improvement (#6172)
vamsikrishnamathala Dec 9, 2024
ff8bbed
chore: changed the soft deletion logic (#6171)
NarayanBavisetti Dec 9, 2024
205395e
fix: changed checkboxes to toggles on notifications settings page (#6…
vihar Dec 9, 2024
216a69f
chore: workspace draft and inbox issue local db mutation (#6180)
anmolsinghbhatia Dec 10, 2024
6e56ea4
fix: updated changelog url in apiserver
sriramveeraghanta Dec 10, 2024
f06b1b8
fix: updated package version
sriramveeraghanta Dec 10, 2024
d0f3987
fix: instance changelog url updated
sriramveeraghanta Dec 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
### Description
<!-- Provide a detailed description of the changes in this PR -->

### Type of Change
<!-- Put an 'x' in the boxes that apply -->
- [ ] 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)
<!-- Add screenshots to help explain your changes, ideally showcasing before and after -->

### Test Scenarios
<!-- Please describe the tests that you ran to verify your changes -->

### References
<!-- Link related issues if there are any -->
4 changes: 2 additions & 2 deletions .github/workflows/build-branch.yml
Original file line number Diff line number Diff line change
@@ -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:
2 changes: 1 addition & 1 deletion admin/core/components/admin-sidebar/root.tsx
Original file line number Diff line number Diff line change
@@ -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
7 changes: 4 additions & 3 deletions admin/package.json
Original file line number Diff line number Diff line change
@@ -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",
2 changes: 1 addition & 1 deletion apiserver/Dockerfile.api
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion apiserver/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -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" \
2 changes: 1 addition & 1 deletion apiserver/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.24.0"
"version": "0.24.1"
}
4 changes: 1 addition & 3 deletions apiserver/plane/app/urls/workspace.py
Original file line number Diff line number Diff line change
@@ -68,9 +68,7 @@
# 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(
12 changes: 4 additions & 8 deletions apiserver/plane/app/views/issue/base.py
Original file line number Diff line number Diff line change
@@ -15,8 +15,6 @@
UUIDField,
Value,
Subquery,
Case,
When,
)
from django.db.models.functions import Coalesce
from django.utils import timezone
@@ -445,12 +443,10 @@ def retrieve(self, request, slug, project_id, pk=None):
.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(
16 changes: 8 additions & 8 deletions apiserver/plane/app/views/page/base.py
Original file line number Diff line number Diff line change
@@ -114,7 +114,7 @@ def get_queryset(self):
.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 @@ def create(self, request, slug, project_id):
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 @@ def retrieve(self, request, slug, project_id, pk=None):
)
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 @@ def lock(self, request, slug, project_id, pk):
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 @@ def unlock(self, request, slug, project_id, pk):

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 @@ def list(self, request, slug, project_id):
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 @@ def archive(self, request, slug, project_id, pk):

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 @@ def unarchive(self, request, slug, project_id, pk):

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)

40 changes: 9 additions & 31 deletions apiserver/plane/app/views/project/member.py
Original file line number Diff line number Diff line change
@@ -16,12 +16,7 @@
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,21 +78,15 @@ def create(self, request, slug, project_id):
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"
},
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 @@ def create(self, request, slug, project_id):
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 @@ def create(self, request, slug, project_id):
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 @@ def partial_update(self, request, slug, project_id, pk):
> 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 @@ def destroy(self, request, slug, project_id, pk):
# 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 @@ def leave(self, request, slug, project_id):
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 @@ def get(self, request, slug):
).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)
5 changes: 3 additions & 2 deletions apiserver/plane/app/views/workspace/base.py
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@
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 @@ def get_queryset(self):

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"),
},
}
]
)

41 changes: 8 additions & 33 deletions apiserver/plane/app/views/workspace/member.py
Original file line number Diff line number Diff line change
@@ -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 @@
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 @@ def destroy(self, request, slug, pk):

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 @@ def destroy(self, request, slug, pk):

# 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 @@ def destroy(self, request, slug, pk):
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 @@ def leave(self, request, slug):

# # 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 @@ def get(self, request, slug):
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()

3 changes: 3 additions & 0 deletions apiserver/plane/authentication/views/app/check.py
Original file line number Diff line number Diff line change
@@ -60,6 +60,9 @@ def post(self, request):
)
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)
1 change: 1 addition & 0 deletions apiserver/plane/authentication/views/space/check.py
Original file line number Diff line number Diff line change
@@ -60,6 +60,7 @@ def post(self, request):
)
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)

email = str(email).lower().strip()
# Validate email
try:
validate_email(email)
116 changes: 92 additions & 24 deletions apiserver/plane/bgtasks/deletion_task.py
Original file line number Diff line number Diff line change
@@ -3,39 +3,107 @@
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


@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:

# 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:
# 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
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
18 changes: 5 additions & 13 deletions apiserver/plane/bgtasks/export_task.py
Original file line number Diff line number Diff line change
@@ -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"]),
4 changes: 3 additions & 1 deletion apiserver/plane/bgtasks/notification_task.py
Original file line number Diff line number Diff line change
@@ -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
45 changes: 10 additions & 35 deletions apiserver/plane/db/management/commands/create_project_member.py
Original file line number Diff line number Diff line change
@@ -13,28 +13,14 @@


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 @@ def handle(self, *args: Any, **options: Any):

# 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,32 +63,23 @@ def handle(self, *args: Any, **options: Any):
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
IssueUserProperty.objects.get_or_create(user=user, project=project)

# 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:
10 changes: 0 additions & 10 deletions apiserver/plane/db/models/__init__.py
Original file line number Diff line number Diff line change
@@ -53,7 +53,6 @@
ProjectMemberInvite,
ProjectPublicMember,
)
from .deploy_board import DeployBoard
from .session import Session
from .social_connection import SocialLoginConnection
from .state import State
@@ -69,23 +68,14 @@
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

32 changes: 6 additions & 26 deletions apiserver/plane/db/models/asset.py
Original file line number Diff line number Diff line change
@@ -44,45 +44,25 @@ class EntityTypeContext(models.TextChoices):
"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)
39 changes: 8 additions & 31 deletions apiserver/plane/db/models/issue.py
Original file line number Diff line number Diff line change
@@ -661,9 +661,7 @@ def __str__(self):

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 @@ def log_issue_version(cls, issue, user):
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 @@ def log_issue_version(cls, issue, user):
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
4 changes: 1 addition & 3 deletions apiserver/plane/db/models/webhook.py
Original file line number Diff line number Diff line change
@@ -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
32 changes: 7 additions & 25 deletions apiserver/plane/db/models/workspace.py
Original file line number Diff line number Diff line change
@@ -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 @@ def save(self, *args, **kwargs):

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 @@ def __str__(self):

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"]
2 changes: 1 addition & 1 deletion apiserver/plane/license/api/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -2,4 +2,4 @@

from .configuration import InstanceConfigurationSerializer
from .admin import InstanceAdminSerializer, InstanceAdminMeSerializer
from .workspace import WorkspaceSerializer
from .workspace import WorkspaceSerializer
4 changes: 3 additions & 1 deletion apiserver/plane/license/api/serializers/user.py
Original file line number Diff line number Diff line change
@@ -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"]
6 changes: 4 additions & 2 deletions apiserver/plane/license/api/views/__init__.py
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@
InstanceAdminUserSessionEndpoint,
)

from .changelog import ChangeLogEndpoint

from .workspace import InstanceWorkSpaceAvailabilityCheckEndpoint, InstanceWorkSpaceEndpoint
from .workspace import (
InstanceWorkSpaceAvailabilityCheckEndpoint,
InstanceWorkSpaceEndpoint,
)
33 changes: 0 additions & 33 deletions apiserver/plane/license/api/views/changelog.py

This file was deleted.

14 changes: 6 additions & 8 deletions apiserver/plane/license/api/views/workspace.py
Original file line number Diff line number Diff line change
@@ -43,19 +43,19 @@ def get(self, request):
.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 @@ def get(self, request):
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)
8 changes: 1 addition & 7 deletions apiserver/plane/license/urls.py
Original file line number Diff line number Diff line change
@@ -11,14 +11,12 @@
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 @@
InstanceWorkSpaceAvailabilityCheckEndpoint.as_view(),
name="instance-workspace-availability",
),
path(
"workspaces/",
InstanceWorkSpaceEndpoint.as_view(),
name="instance-workspace",
),
path("workspaces/", InstanceWorkSpaceEndpoint.as_view(), name="instance-workspace"),
]
6 changes: 6 additions & 0 deletions apiserver/plane/space/urls/project.py
Original file line number Diff line number Diff line change
@@ -10,9 +10,15 @@
ProjectStatesEndpoint,
ProjectLabelsEndpoint,
ProjectMembersEndpoint,
ProjectMetaDataEndpoint,
)

urlpatterns = [
path(
"anchor/<str:anchor>/meta/",
ProjectMetaDataEndpoint.as_view(),
name="project-meta",
),
path(
"anchor/<str:anchor>/settings/",
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
2 changes: 2 additions & 0 deletions apiserver/plane/space/views/__init__.py
Original file line number Diff line number Diff line change
@@ -25,3 +25,5 @@
from .label import ProjectLabelsEndpoint

from .asset import EntityAssetEndpoint, AssetRestoreEndpoint, EntityBulkAssetEndpoint

from .meta import ProjectMetaDataEndpoint
34 changes: 34 additions & 0 deletions apiserver/plane/space/views/meta.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion apiserver/requirements/base.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# base requirements

# django
Django==4.2.16
Django==4.2.17
# rest framework
djangorestframework==3.15.2
# postgres
10 changes: 5 additions & 5 deletions live/package.json
Original file line number Diff line number Diff line change
@@ -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",
12 changes: 12 additions & 0 deletions live/src/core/hocuspocus-server.ts
Original file line number Diff line number Diff line change
@@ -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,
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": [
4 changes: 2 additions & 2 deletions packages/constants/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.24.0",
"version": "0.24.1",
"private": true,
"main": "./index.ts"
"main": "./src/index.ts"
}
File renamed without changes.
18 changes: 18 additions & 0 deletions packages/constants/src/endpoints.ts
Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./auth";
export * from "./endpoints";
export * from "./issue";
export * from "./workspace";
File renamed without changes.
76 changes: 76 additions & 0 deletions packages/constants/src/workspace.ts
Original file line number Diff line number Diff line change
@@ -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",
];
23 changes: 0 additions & 23 deletions packages/constants/workspace.ts

This file was deleted.

5 changes: 3 additions & 2 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
@@ -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",
12 changes: 12 additions & 0 deletions packages/editor/src/ce/extensions/core/extensions.ts
Original file line number Diff line number Diff line change
@@ -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 [];
};
2 changes: 2 additions & 0 deletions packages/editor/src/ce/extensions/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./extensions";
export * from "./read-only-extensions";
12 changes: 12 additions & 0 deletions packages/editor/src/ce/extensions/core/read-only-extensions.ts
Original file line number Diff line number Diff line change
@@ -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 [];
};
3 changes: 3 additions & 0 deletions packages/editor/src/ce/extensions/core/without-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Extensions } from "@tiptap/core";

export const CoreEditorAdditionalExtensionsWithoutProps: Extensions = [];
8 changes: 7 additions & 1 deletion packages/editor/src/ce/extensions/document-extensions.tsx
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 2 additions & 0 deletions packages/editor/src/ce/extensions/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from "./core";
export * from "./document-extensions";
export * from "./slash-commands";
14 changes: 14 additions & 0 deletions packages/editor/src/ce/extensions/slash-commands.tsx
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
const {
children,
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
extensions,
@@ -37,6 +38,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
} = props;

const editor = useEditor({
disabledExtensions,
editorClassName,
enableHistory: true,
extensions,
Original file line number Diff line number Diff line change
@@ -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,
13 changes: 6 additions & 7 deletions packages/editor/src/core/components/editors/rich-text/editor.tsx
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion packages/editor/src/core/extensions/callout/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// plane helpers
import { sanitizeHTML } from "@plane/helpers";
import { sanitizeHTML } from "@plane/utils";
// plane ui
import { TEmojiLogoProps } from "@plane/ui";
// types
13 changes: 13 additions & 0 deletions packages/editor/src/core/extensions/core-without-props.ts
Original file line number Diff line number Diff line change
@@ -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()];
Original file line number Diff line number Diff line change
@@ -118,7 +118,6 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
height: `${Math.round(initialHeight)}px` satisfies Pixel,
aspectRatio: aspectRatioCalculated,
};

setSize(initialComputedSize);
updateAttributesSafely(
initialComputedSize,
Original file line number Diff line number Diff line change
@@ -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 (
<NodeViewWrapper>
13 changes: 3 additions & 10 deletions packages/editor/src/core/extensions/custom-image/custom-image.ts
Original file line number Diff line number Diff line change
@@ -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<string, boolean>(),
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(`<img src="${state.esc(imageSource)}" width="${imageWidth}" />`);
state.closeBlock(node);
},
serialize() {},
},
};
},
Original file line number Diff line number Diff line change
@@ -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<TFileHandler, "getAsset
addStorage() {
return {
fileMap: new Map(),
// 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(`<img src="${state.esc(imageSource)}" width="${imageWidth}" />`);
state.closeBlock(node);
},
serialize() {},
},
};
},
23 changes: 20 additions & 3 deletions packages/editor/src/core/extensions/extensions.tsx
Original file line number Diff line number Diff line change
@@ -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,
}),
];
};
23 changes: 20 additions & 3 deletions packages/editor/src/core/extensions/read-only-extensions.tsx
Original file line number Diff line number Diff line change
@@ -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<TFileHandler, "getAssetSrc">;
mentionConfig: {
mentionHighlights?: () => Promise<IMentionHighlight[]>;
};
};

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,
}),
];
};
Original file line number Diff line number Diff line change
@@ -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) => ({
Original file line number Diff line number Diff line change
@@ -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") {
16 changes: 13 additions & 3 deletions packages/editor/src/core/extensions/slash-commands/root.tsx
Original file line number Diff line number Diff line change
@@ -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<SuggestionOptions, "editor">;
};

export type TSlashCommandAdditionalOption = ISlashCommandItem & {
section: TSlashCommandSectionKeys;
pushAfter: TEditorCommands;
};

const Command = Extension.create<SlashCommandOptions>({
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,
},
});
11 changes: 11 additions & 0 deletions packages/editor/src/core/helpers/get-document-server-event.ts
Original file line number Diff line number Diff line change
@@ -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;
};
23 changes: 11 additions & 12 deletions packages/editor/src/core/hooks/use-collaborative-editor.ts
Original file line number Diff line number Diff line change
@@ -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?.destroy();
localProvider?.destroy();
},
[provider]
[provider, localProvider]
);
// indexed db integration for offline support
useLayoutEffect(() => {
const localProvider = new IndexeddbPersistence(id, provider.document);
return () => {
localProvider?.destroy();
};
}, [provider, id]);

const editor = useEditor({
disabledExtensions,
id,
onTransaction,
editorProps,
19 changes: 16 additions & 3 deletions packages/editor/src/core/hooks/use-editor.ts
Original file line number Diff line number Diff line change
@@ -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<EditorRefApi | null>;
@@ -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]
);
Original file line number Diff line number Diff line change
@@ -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();
localProvider?.destroy();
},
[provider]
[provider, localProvider]
);
// indexed db integration for offline support
useLayoutEffect(() => {
const localProvider = new IndexeddbPersistence(id, provider.document);
return () => {
localProvider?.destroy();
};
}, [provider, id]);

const editor = useReadOnlyEditor({
disabledExtensions,
editorProps,
editorClassName,
extensions: [
19 changes: 15 additions & 4 deletions packages/editor/src/core/hooks/use-read-only-editor.ts
Original file line number Diff line number Diff line change
@@ -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<EditorReadOnlyRefApi | null>;
extensions?: any;
editorProps?: EditorProps;
extensions?: any;
forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>;
initialValue?: string;
fileHandler: Pick<TFileHandler, "getAssetSrc">;
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,
}));

2 changes: 1 addition & 1 deletion packages/editor/src/core/types/collaboration.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ export type TServerHandler = {
};

type TCollaborativeEditorHookProps = {
disabledExtensions?: TExtensions[];
disabledExtensions: TExtensions[];
editorClassName: string;
editorProps?: EditorProps;
extensions?: Extensions;
10 changes: 10 additions & 0 deletions packages/editor/src/core/types/document-collaborative-events.ts
Original file line number Diff line number Diff line change
@@ -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;
};
9 changes: 7 additions & 2 deletions packages/editor/src/core/types/editor.ts
Original file line number Diff line number Diff line change
@@ -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<EditorRefApi | null>;
@@ -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<TFileHandler, "getAssetSrc">;
2 changes: 1 addition & 1 deletion packages/editor/src/core/types/extensions.ts
Original file line number Diff line number Diff line change
@@ -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";
1 change: 1 addition & 0 deletions packages/editor/src/core/types/index.ts
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 2 additions & 0 deletions packages/editor/src/core/types/slash-commands-suggestion.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@ export type CommandProps = {
range: Range;
};

export type TSlashCommandSectionKeys = "general" | "text-colors" | "background-colors";

export type ISlashCommandItem = {
commandKey: TEditorCommands;
key: string;
3 changes: 3 additions & 0 deletions packages/editor/src/lib.ts
Original file line number Diff line number Diff line change
@@ -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";
59 changes: 36 additions & 23 deletions packages/editor/src/styles/editor.css
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 2 additions & 2 deletions packages/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -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",
22 changes: 0 additions & 22 deletions packages/helpers/helpers/emoji.helper.ts

This file was deleted.

2 changes: 0 additions & 2 deletions packages/helpers/helpers/index.ts

This file was deleted.

2 changes: 0 additions & 2 deletions packages/helpers/index.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/hooks/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/*
dist/*
out/*
9 changes: 9 additions & 0 deletions packages/hooks/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -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,
},
};
4 changes: 4 additions & 0 deletions packages/hooks/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.turbo
out/
dist/
build/
5 changes: 5 additions & 0 deletions packages/hooks/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
27 changes: 27 additions & 0 deletions packages/hooks/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./use-local-storage";
export * from "./use-outside-click-detector";
59 changes: 59 additions & 0 deletions packages/hooks/src/use-local-storage.tsx
Original file line number Diff line number Diff line change
@@ -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 = <T,>(key: string, initialValue: T) => {
const [storedValue, setStoredValue] = useState<T | null>(() =>
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;
};
Original file line number Diff line number Diff line change
@@ -4,6 +4,6 @@
"jsx": "react",
"lib": ["esnext", "dom"]
},
"include": ["."],
"include": ["./src"],
"exclude": ["dist", "build", "node_modules"]
}
2 changes: 1 addition & 1 deletion packages/tailwind-config-custom/package.json
Original file line number Diff line number Diff line change
@@ -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,
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions packages/types/src/instance/base.d.ts
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@ export interface IInstanceConfig {
// intercom
is_intercom_enabled: boolean;
intercom_app_id: string | undefined;
instance_changelog_url?: string;
}

export interface IInstanceAdmin {
2 changes: 1 addition & 1 deletion packages/typescript-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "0.24.0",
"version": "0.24.1",
"private": true,
"files": [
"base.json",
15 changes: 10 additions & 5 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
21 changes: 18 additions & 3 deletions packages/ui/src/collapsible/collapsible-button.tsx
Original file line number Diff line number Diff line change
@@ -8,12 +8,27 @@ type Props = {
hideChevron?: boolean;
indicatorElement?: React.ReactNode;
actionItemElement?: React.ReactNode;
className?: string;
titleClassName?: string;
};

export const CollapsibleButton: FC<Props> = (props) => {
const { isOpen, title, hideChevron = false, indicatorElement, actionItemElement } = props;
const {
isOpen,
title,
hideChevron = false,
indicatorElement,
actionItemElement,
className = "",
titleClassName = "",
} = props;
return (
<div className="flex items-center justify-between gap-3 h-12 px-2.5 py-3 border-b border-custom-border-200">
<div
className={cn(
"flex items-center justify-between gap-3 h-12 px-2.5 py-3 border-b border-custom-border-200",
className
)}
>
<div className="flex items-center gap-3.5">
<div className="flex items-center gap-3">
{!hideChevron && (
@@ -23,7 +38,7 @@ export const CollapsibleButton: FC<Props> = (props) => {
})}
/>
)}
<span className="text-base text-custom-text-100 font-medium">{title}</span>
<span className={cn("text-base text-custom-text-100 font-medium", titleClassName)}>{title}</span>
</div>
{indicatorElement && indicatorElement}
</div>
5 changes: 3 additions & 2 deletions packages/ui/src/dropdown/common/loader.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import range from "lodash/range";
import React from "react";

export const DropdownOptionsLoader = () => (
<div className="flex flex-col gap-1 animate-pulse">
{Array.from({ length: 6 }, (_, i) => (
<div key={i} className="flex h-[1.925rem] w-full rounded px-1 py-1.5 bg-custom-background-90" />
{range(6).map((index) => (
<div key={index} className="flex h-[1.925rem] w-full rounded px-1 py-1.5 bg-custom-background-90" />
))}
</div>
);
2 changes: 1 addition & 1 deletion packages/ui/src/dropdown/multi-select.tsx
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 1 addition & 1 deletion packages/ui/src/dropdown/single-select.tsx
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 1 addition & 1 deletion packages/ui/src/dropdowns/context-menu/root.tsx
Original file line number Diff line number Diff line change
@@ -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
11 changes: 9 additions & 2 deletions packages/ui/src/dropdowns/custom-menu.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<Menu.Items data-prevent-outside-click={!!portalElement} className={cn("fixed z-10", menuItemsClassName)} static>
<Menu.Items
data-prevent-outside-click={!!portalElement}
className={cn(
"fixed z-10 translate-y-0",
menuItemsClassName
)} /** translate-y-0 is a hack to create new stacking context. Required for safari */
static
>
<div
className={cn(
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap",
27 changes: 13 additions & 14 deletions packages/ui/src/dropdowns/custom-search-select.tsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { Combobox } from "@headlessui/react";
import { Check, ChevronDown, Info, Search } from "lucide-react";
import { createPortal } from "react-dom";
// plane helpers
import { useOutsideClickDetector } from "@plane/helpers";
import { useOutsideClickDetector } from "@plane/hooks";
// hooks
import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
// helpers
@@ -97,10 +97,11 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
<button
ref={setReferenceElement}
type="button"
className={`flex w-full items-center justify-between gap-1 text-xs ${disabled
? "cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80"
} ${customButtonClassName}`}
className={`flex w-full items-center justify-between gap-1 text-xs ${
disabled
? "cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80"
} ${customButtonClassName}`}
onClick={toggleDropdown}
>
{customButton}
@@ -186,15 +187,13 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
{option.tooltip && (
<>
{
typeof option.tooltip === "string" ? (
<Tooltip tooltipContent={option.tooltip}>
<Info className="h-3.5 w-3.5 flex-shrink-0 cursor-pointer text-custom-text-200" />
</Tooltip>
) : (
option.tooltip
)
}
{typeof option.tooltip === "string" ? (
<Tooltip tooltipContent={option.tooltip}>
<Info className="h-3.5 w-3.5 flex-shrink-0 cursor-pointer text-custom-text-200" />
</Tooltip>
) : (
option.tooltip
)}
</>
)}
</>
2 changes: 1 addition & 1 deletion packages/ui/src/dropdowns/custom-select.tsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { usePopper } from "react-popper";
import { Listbox } from "@headlessui/react";
import { Check, ChevronDown } 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
2 changes: 1 addition & 1 deletion packages/ui/src/emoji/emoji-icon-picker-new.tsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { usePopper } from "react-popper";
import { Popover, Tab } from "@headlessui/react";
import EmojiPicker from "emoji-picker-react";
// plane helpers
import { useOutsideClickDetector } from "@plane/helpers";
import { useOutsideClickDetector } from "@plane/hooks";
// helpers
import { cn } from "../../helpers";
// hooks
2 changes: 1 addition & 1 deletion packages/ui/src/emoji/emoji-icon-picker.tsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { usePopper } from "react-popper";
import EmojiPicker from "emoji-picker-react";
import { Popover, Tab } from "@headlessui/react";
// plane helpers
import { useOutsideClickDetector } from "@plane/helpers";
import { useOutsideClickDetector } from "@plane/hooks";
// components
import { IconsList } from "./icons-list";
// helpers
14 changes: 14 additions & 0 deletions packages/ui/src/icons/comment-fill-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from "react";

import { ISvgIcons } from "./type";

export const CommentFillIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg viewBox="0 0 24 24" className={`${className}`} xmlns="http://www.w3.org/2000/svg" {...rest}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.848 2.771C7.21613 2.4234 9.60649 2.24927 12 2.25C14.43 2.25 16.817 2.428 19.152 2.77C21.13 3.062 22.5 4.794 22.5 6.74V12.76C22.5 14.706 21.13 16.438 19.152 16.73C17.212 17.014 15.236 17.185 13.23 17.235C13.1303 17.2369 13.0351 17.277 12.964 17.347L8.78 21.53C8.67511 21.6348 8.54153 21.7061 8.39614 21.735C8.25074 21.7638 8.10004 21.749 7.96308 21.6923C7.82611 21.6356 7.70903 21.5395 7.62661 21.4163C7.54419 21.2931 7.50013 21.1482 7.5 21V17.045C6.61329 16.9639 5.72895 16.8585 4.848 16.729C2.87 16.439 1.5 14.705 1.5 12.759V6.741C1.5 4.795 2.87 3.061 4.848 2.771Z"
fill="currentColor"
/>
</svg>
);
28 changes: 28 additions & 0 deletions packages/ui/src/icons/epic-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react";

export type Props = {
className?: string;
width?: string | number;
height?: string | number;
color?: string;
};

export const EpicIcon: React.FC<Props> = ({ width = "16", height = "16", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.900146 9.33203V12.0142C0.900146 12.3736 1.17392 12.6654 1.51126 12.6654H14.9557C15.1178 12.6654 15.2732 12.5968 15.3878 12.4746C15.5024 12.3525 15.5668 12.1869 15.5668 12.0142V10.3299L13.375 7.99523C13.1458 7.75134 12.8351 7.61436 12.5113 7.61436C12.1874 7.61436 11.8767 7.75134 11.6476 7.99523L10.1257 9.35919L10.2534 9.56204L11.7209 9.60056C11.7809 9.66017 11.8291 9.73206 11.8625 9.81194C11.8959 9.89181 11.9138 9.97804 11.9153 10.0655C11.9167 10.1529 11.9017 10.2397 11.8709 10.3208C11.8402 10.4019 11.7944 10.4756 11.7364 10.5374C11.6784 10.5992 11.6092 10.648 11.5332 10.6807C11.4571 10.7135 11.3756 10.7296 11.2935 10.728C11.2114 10.7265 11.1305 10.7073 11.0556 10.6717C10.9806 10.6362 10.9131 10.5848 10.8572 10.5209L10.2534 9.56204L6.60385 3.76614C6.37468 3.52226 6.11293 3.33203 5.78904 3.33203C5.46515 3.33203 5.20339 3.52226 4.97422 3.76614L0.900146 9.33203Z"
fill="currentColor"
/>
<path
d="M11.7209 9.60056L10.2534 9.56204L10.8572 10.5209C10.9131 10.5848 10.9806 10.6362 11.0556 10.6717C11.1305 10.7073 11.2114 10.7265 11.2935 10.728C11.3756 10.7296 11.4571 10.7135 11.5332 10.6807C11.6092 10.648 11.6784 10.5992 11.7364 10.5374C11.7944 10.4756 11.8402 10.4019 11.8709 10.3208C11.9017 10.2397 11.9167 10.1529 11.9153 10.0655C11.9138 9.97804 11.8959 9.89181 11.8625 9.81194C11.8291 9.73206 11.7809 9.66017 11.7209 9.60056Z"
fill="currentColor"
/>
</svg>
);
4 changes: 4 additions & 0 deletions packages/ui/src/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type { ISvgIcons } from "./type";
export * from "./cycle";
export * from "./module";
export * from "./state";
@@ -7,12 +8,15 @@ export * from "./blocker-icon";
export * from "./calendar-after-icon";
export * from "./calendar-before-icon";
export * from "./center-panel-icon";
export * from "./comment-fill-icon";
export * from "./create-icon";
export * from "./dice-icon";
export * from "./discord-icon";
export * from "./epic-icon";
export * from "./full-screen-panel-icon";
export * from "./github-icon";
export * from "./gitlab-icon";
export * from "./info-icon";
export * from "./layer-stack";
export * from "./layers-icon";
export * from "./monospace-icon";
14 changes: 14 additions & 0 deletions packages/ui/src/icons/info-fill-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from "react";

import { ISvgIcons } from "./type";

export const InfoFillIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg viewBox="0 0 24 24" className={`${className}`} xmlns="http://www.w3.org/2000/svg" {...rest}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.25 12C2.25 6.615 6.615 2.25 12 2.25C17.385 2.25 21.75 6.615 21.75 12C21.75 17.385 17.385 21.75 12 21.75C6.615 21.75 2.25 17.385 2.25 12ZM10.956 10.558C12.102 9.985 13.393 11.021 13.082 12.264L12.373 15.1L12.415 15.08C12.5912 15.0025 12.7905 14.9958 12.9715 15.0612C13.1526 15.1265 13.3016 15.259 13.3877 15.4312C13.4737 15.6033 13.4903 15.802 13.434 15.9861C13.3777 16.1702 13.2527 16.3255 13.085 16.42L13.045 16.442C11.898 17.015 10.607 15.979 10.918 14.736L11.628 11.9L11.586 11.92C11.4975 11.9692 11.4 11.9999 11.2994 12.0104C11.1987 12.0209 11.097 12.0109 11.0003 11.981C10.9036 11.9511 10.8139 11.902 10.7367 11.8366C10.6595 11.7711 10.5964 11.6907 10.551 11.6002C10.5057 11.5098 10.4792 11.411 10.4731 11.31C10.4669 11.209 10.4813 11.1078 10.5153 11.0124C10.5493 10.9171 10.6022 10.8297 10.6709 10.7553C10.7396 10.681 10.8226 10.6214 10.915 10.58L10.956 10.558ZM12 9C12.1989 9 12.3897 8.92098 12.5303 8.78033C12.671 8.63968 12.75 8.44891 12.75 8.25C12.75 8.05109 12.671 7.86032 12.5303 7.71967C12.3897 7.57902 12.1989 7.5 12 7.5C11.8011 7.5 11.6103 7.57902 11.4697 7.71967C11.329 7.86032 11.25 8.05109 11.25 8.25C11.25 8.44891 11.329 8.63968 11.4697 8.78033C11.6103 8.92098 11.8011 9 12 9Z"
fill="currentColor"
/>
</svg>
);
File renamed without changes.
1 change: 1 addition & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -28,3 +28,4 @@ export * from "./row";
export * from "./content-wrapper";
export * from "./card";
export * from "./tag";
export * from "./tabs";
17 changes: 10 additions & 7 deletions packages/ui/src/progress/linear-progress-indicator.tsx
Original file line number Diff line number Diff line change
@@ -6,14 +6,18 @@ type Props = {
data: any;
noTooltip?: boolean;
inPercentage?: boolean;
size?: "sm" | "md" | "lg";
size?: "sm" | "md" | "lg" | "xl";
className?: string;
barClassName?: string;
};

export const LinearProgressIndicator: React.FC<Props> = ({
data,
noTooltip = false,
inPercentage = false,
size = "sm",
className = "",
barClassName = "",
}) => {
const total = data.reduce((acc: any, cur: any) => acc + cur.value, 0);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -31,7 +35,7 @@ export const LinearProgressIndicator: React.FC<Props> = ({
else
return (
<Tooltip key={item.id} tooltipContent={`${item.name} ${Math.round(item.value)}${inPercentage ? "%" : ""}`}>
<div style={style} className="first:rounded-l-sm last:rounded-r-sm" />
<div style={style} className={cn("first:rounded-l-sm last:rounded-r-sm", barClassName)} />
</Tooltip>
);
});
@@ -42,13 +46,12 @@ export const LinearProgressIndicator: React.FC<Props> = ({
"h-2": size === "sm",
"h-3": size === "md",
"h-3.5": size === "lg",
"h-[14px]": size === "xl",
})}
>
{total === 0 ? (
<div className="flex h-full w-full gap-[1.5px] p-[2px] bg-custom-background-90 rounded-sm">{bars}</div>
) : (
<div className="flex h-full w-full gap-[1.5px] p-[2px] bg-custom-background-90 rounded-sm">{bars}</div>
)}
<div className={cn("flex h-full w-full gap-[1.5px] p-[2px] bg-custom-background-90 rounded-sm", className)}>
{bars}
</div>
</div>
);
};
1 change: 1 addition & 0 deletions packages/ui/src/tabs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./tabs";
94 changes: 94 additions & 0 deletions packages/ui/src/tabs/tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, { FC, Fragment } from "react";
import { Tab } from "@headlessui/react";
import { LucideProps } from "lucide-react";
// helpers
import { useLocalStorage } from "@plane/hooks";
import { cn } from "../../helpers";

type TabItem = {
key: string;
icon?: FC<LucideProps>;
label?: React.ReactNode;
content: React.ReactNode;
disabled?: boolean;
};

type TTabsProps = {
tabs: TabItem[];
storageKey: string;
actions?: React.ReactNode;
defaultTab?: string;
containerClassName?: string;
tabListContainerClassName?: string;
tabListClassName?: string;
tabClassName?: string;
tabPanelClassName?: string;
};

export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
const {
tabs,
storageKey,
actions,
defaultTab = tabs[0]?.key,
containerClassName = "",
tabListContainerClassName = "",
tabListClassName = "",
tabClassName = "",
tabPanelClassName = "",
} = props;
// local storage
const { storedValue, setValue } = useLocalStorage(`tab-${storageKey}`, defaultTab);

const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey);

return (
<div className="flex flex-col w-full h-full">
<Tab.Group defaultIndex={currentTabIndex(storedValue ?? defaultTab)}>
<div className={cn("flex flex-col w-full h-full gap-2", containerClassName)}>
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
<Tab.List
as="div"
className={cn(
"flex w-full min-w-fit items-center justify-between gap-1.5 rounded-md text-sm p-0.5 bg-custom-background-80/60",
tabListClassName
)}
>
{tabs.map((tab) => (
<Tab
className={({ selected }) =>
cn(
`flex items-center justify-center p-1 min-w-fit w-full font-medium text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all rounded`,
selected
? "bg-custom-background-100 text-custom-text-100 shadow-sm"
: tab.disabled
? "text-custom-text-400 cursor-not-allowed"
: "text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60",
tabClassName
)
}
key={tab.key}
onClick={() => {
if (!tab.disabled) setValue(tab.key);
}}
disabled={tab.disabled}
>
{tab.icon && <tab.icon className="size-4" />}
{tab.label}
</Tab>
))}
</Tab.List>
{actions && <div className="flex-grow">{actions}</div>}
</div>
<Tab.Panels as={Fragment}>
{tabs.map((tab) => (
<Tab.Panel key={tab.key} as="div" className={cn("relative outline-none", tabPanelClassName)}>
{tab.content}
</Tab.Panel>
))}
</Tab.Panels>
</div>
</Tab.Group>
</div>
);
};
32 changes: 19 additions & 13 deletions packages/ui/src/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ export type TPosition =
interface ITooltipProps {
tooltipHeading?: string;
tooltipContent: string | React.ReactNode;
jsxContent?: string | React.ReactNode;
position?: TPosition;
children: JSX.Element;
disabled?: boolean;
@@ -38,13 +39,14 @@ export const Tooltip: React.FC<ITooltipProps> = ({
tooltipContent,
position = "top",
children,
jsxContent,
disabled = false,
className = "",
openDelay = 200,
closeDelay,
isMobile = false,
renderByDefault = true, //FIXME: tooltip should always render on hover and not by default, this is a temporary fix
}) => {
}: ITooltipProps) => {
const toolTipRef = useRef<HTMLDivElement | null>(null);

const [shouldRender, setShouldRender] = useState(renderByDefault);
@@ -79,18 +81,22 @@ export const Tooltip: React.FC<ITooltipProps> = ({
hoverOpenDelay={openDelay}
hoverCloseDelay={closeDelay}
content={
<div
className={cn(
"relative block z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md",
{
hidden: isMobile,
},
className
)}
>
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
{tooltipContent}
</div>
jsxContent ? (
<>{jsxContent}</>
) : (
<div
className={cn(
"relative block z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md",
{
hidden: isMobile,
},
className
)}
>
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
{tooltipContent}
</div>
)
}
position={position}
renderTarget={({
3 changes: 3 additions & 0 deletions packages/utils/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/*
dist/*
out/*
9 changes: 9 additions & 0 deletions packages/utils/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -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,
},
};
5 changes: 5 additions & 0 deletions packages/utils/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
19 changes: 11 additions & 8 deletions packages/helpers/package.json → packages/utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plane/helpers",
"version": "0.24.0",
"name": "@plane/utils",
"version": "0.24.1",
"description": "Helper functions shared across multiple apps internally",
"private": true,
"main": "./dist/index.js",
@@ -10,16 +10,19 @@
"dist/**"
],
"scripts": {
"build": "tsup ./index.ts --format esm,cjs --dts --external react --minify"
"build": "tsup ./src/index.ts --format esm,cjs --dts --external react --minify",
"lint": "eslint src --ext .ts,.tsx",
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"isomorphic-dompurify": "^2.16.0",
"react": "^18.3.1"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"@types/react": "^18.3.11",
"tsup": "^7.2.0",
"typescript": "^5.6.2"
},
"dependencies": {
"isomorphic-dompurify": "^2.16.0",
"react": "^18.3.1"
"typescript": "^5.3.3"
}
}
60 changes: 60 additions & 0 deletions packages/utils/src/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Represents an RGB color with numeric values for red, green, and blue components
* @typedef {Object} RGB
* @property {number} r - Red component (0-255)
* @property {number} g - Green component (0-255)
* @property {number} b - Blue component (0-255)
*/
export type RGB = { r: number; g: number; b: number };

/**
* Validates and clamps color values to RGB range (0-255)
* @param {number} value - The color value to validate
* @returns {number} Clamped and floored value between 0-255
*/
export const validateColor = (value: number) => {
if (value < 0) return 0;
if (value > 255) return 255;
return Math.floor(value);
};

/**
* Converts a decimal color value to two-character hex
* @param {number} value - Decimal color value (0-255)
* @returns {string} Two-character hex value with leading zero if needed
*/
export const toHex = (value: number) => validateColor(value).toString(16).padStart(2, "0");

/**
* Converts a hexadecimal color code to RGB values
* @param {string} hex - The hexadecimal color code (e.g., "#ff0000" for red)
* @returns {RGB} An object containing the RGB values
* @example
* hexToRgb("#ff0000") // returns { r: 255, g: 0, b: 0 }
* hexToRgb("#00ff00") // returns { r: 0, g: 255, b: 0 }
* hexToRgb("#0000ff") // returns { r: 0, g: 0, b: 255 }
*/
export const hexToRgb = (hex: string): RGB => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: { r: 0, g: 0, b: 0 };
};

/**
* Converts RGB values to a hexadecimal color code
* @param {RGB} rgb - An object containing RGB values
* @param {number} rgb.r - Red component (0-255)
* @param {number} rgb.g - Green component (0-255)
* @param {number} rgb.b - Blue component (0-255)
* @returns {string} The hexadecimal color code (e.g., "#ff0000" for red)
* @example
* rgbToHex({ r: 255, g: 0, b: 0 }) // returns "#ff0000"
* rgbToHex({ r: 0, g: 255, b: 0 }) // returns "#00ff00"
* rgbToHex({ r: 0, g: 0, b: 255 }) // returns "#0000ff"
*/
export const rgbToHex = ({ r, g, b }: RGB): string => `#${toHex(r)}${toHex(g)}${toHex(b)}`;
40 changes: 40 additions & 0 deletions packages/utils/src/emoji.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Converts a hyphen-separated hexadecimal emoji code to its decimal representation
* @param {string} emojiUnified - The unified emoji code in hexadecimal format (e.g., "1f600" or "1f1e6-1f1e8")
* @returns {string} The decimal representation of the emoji code (e.g., "128512" or "127462-127464")
* @example
* convertHexEmojiToDecimal("1f600") // returns "128512"
* convertHexEmojiToDecimal("1f1e6-1f1e8") // returns "127462-127464"
* convertHexEmojiToDecimal("") // returns ""
*/
export const convertHexEmojiToDecimal = (emojiUnified: string): string => {
if (!emojiUnified) return "";

return emojiUnified
.toString()
.split("-")
.map((e) => parseInt(e, 16))
.join("-");
};

/**
* Converts a hyphen-separated decimal emoji code back to its hexadecimal representation
* @param {string} emoji - The emoji code in decimal format (e.g., "128512" or "127462-127464")
* @returns {string} The hexadecimal representation of the emoji code (e.g., "1f600" or "1f1e6-1f1e8")
* @example
* emojiCodeToUnicode("128512") // returns "1f600"
* emojiCodeToUnicode("127462-127464") // returns "1f1e6-1f1e8"
* emojiCodeToUnicode("") // returns ""
*/
export const emojiCodeToUnicode = (emoji: string): string => {
if (!emoji) return "";

// convert emoji code to unicode
const uniCodeEmoji = emoji
.toString()
.split("-")
.map((emoji) => parseInt(emoji, 10).toString(16))
.join("-");

return uniCodeEmoji;
};
3 changes: 3 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./color";
export * from "./emoji";
export * from "./string";
Loading

Unchanged files with check annotations Beta

if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
Untrusted URL redirection depends on a
user-provided value
.
try:
state = uuid.uuid4().hex
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
Untrusted URL redirection depends on a
user-provided value
.
class GitHubCallbackSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
Untrusted URL redirection depends on a
user-provided value
.
class GitLabCallbackSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
Untrusted URL redirection depends on a
user-provided value
.
try:
state = uuid.uuid4().hex
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
Untrusted URL redirection depends on a
user-provided value
.
class GoogleCallbackSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
existing_user = User.objects.filter(email=email).first()
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Active User
try:
# redirect to referer path
path = str(next_path) if next_path else ""
url = f"{base_host(request=request, is_space=True)}{path}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
except AuthenticationException as e:
params = e.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
class MagicSignUpSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Existing User
existing_user = User.objects.filter(email=email).first()
# Already existing
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
try:
provider = MagicCodeProvider(
user_login(request=request, user=user, is_space=True)
# redirect to referer path
url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
except AuthenticationException as e:
params = e.get_error_dict()
if next_path:
params["next_path"] = str(next_path)
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
)
params = exc.get_error_dict()
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(params)}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
password = request.POST.get("password", False)
error_message="INVALID_PASSWORD",
)
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Check the password complexity
results = zxcvbn(password)
error_message="INVALID_PASSWORD",
)
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# set_password also hashes the password that the user will get
user.set_password(password)
error_message="EXPIRED_PASSWORD_TOKEN",
)
url = f"{base_host(request=request, is_space=True)}/accounts/reset-password/?{urlencode(exc.get_error_dict())}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Log the user out
logout(request)
url = f"{base_host(request=request, is_space=True)}{next_path}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
except Exception:
url = f"{base_host(request=request, is_space=True)}{next_path}"
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# check if the instance has already an admin registered
if InstanceAdmin.objects.first():
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Get the email and password from all the user
email = request.POST.get("email", False)
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Validate the email
email = email.strip().lower()
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Check if already a user exists or not
# Existing user
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
else:
results = zxcvbn(password)
if results["score"] < 3:
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
user = User.objects.create(
first_name=first_name,
# get tokens for user
user_login(request=request, user=user, is_admin=True)
url = urljoin(base_host(request=request, is_admin=True), "general")
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
class InstanceAdminSignInEndpoint(View):
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Get email and password
email = request.POST.get("email", False)
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Validate the email
email = email.strip().lower()
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Fetch the user
user = User.objects.filter(email=email).first()
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Error out if the user is not present
if not user:
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Check password of the user
if not user.check_password(password):
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# Check if the user is an instance admin
if not InstanceAdmin.objects.filter(instance=instance, user=user):
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
# settings last active for the user
user.is_active = True
user.last_active = timezone.now()
# get tokens for user
user_login(request=request, user=user, is_admin=True)
url = urljoin(base_host(request=request, is_admin=True), "general")
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
class InstanceAdminUserMeEndpoint(BaseAPIView):
# Log the user out
logout(request)
url = urljoin(base_host(request=request, is_admin=True))
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
except Exception:
return HttpResponseRedirect(base_host(request=request, is_admin=True))
<ToggleSwitch
value={Boolean(parseInt(enableGithubConfig))}
onChange={() => {
Boolean(parseInt(enableGithubConfig)) === true

Check warning on line 84 in admin/app/authentication/github/page.tsx

GitHub Actions / lint-admin

Expected an assignment or function call and instead saw an expression
? updateConfig("IS_GITHUB_ENABLED", "0")
: updateConfig("IS_GITHUB_ENABLED", "1");
}}
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
Boolean(parseInt(enableGitlabConfig)) === true

Check warning on line 71 in admin/app/authentication/gitlab/page.tsx

GitHub Actions / lint-admin

Expected an assignment or function call and instead saw an expression
? updateConfig("IS_GITLAB_ENABLED", "0")
: updateConfig("IS_GITLAB_ENABLED", "1");
}}
<ToggleSwitch
value={Boolean(parseInt(enableGoogleConfig))}
onChange={() => {
Boolean(parseInt(enableGoogleConfig)) === true

Check warning on line 72 in admin/app/authentication/google/page.tsx

GitHub Actions / lint-admin

Expected an assignment or function call and instead saw an expression
? updateConfig("IS_GOOGLE_ENABLED", "0")
: updateConfig("IS_GOOGLE_ENABLED", "1");
}}
super();
}
hydrate(initialData: any) {

Check warning on line 12 in admin/ce/store/root.store.ts

GitHub Actions / lint-admin

Unexpected any. Specify a different type
super.hydrate(initialData);
}
<ToggleSwitch
value={Boolean(parseInt(enableMagicLogin))}
onChange={() => {
Boolean(parseInt(enableMagicLogin)) === true

Check warning on line 28 in admin/core/components/authentication/email-config-switch.tsx

GitHub Actions / lint-admin

Expected an assignment or function call and instead saw an expression
? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
: updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
}}
<ToggleSwitch
value={Boolean(parseInt(enableGithubConfig))}
onChange={() => {
Boolean(parseInt(enableGithubConfig)) === true

Check warning on line 40 in admin/core/components/authentication/github-config.tsx

GitHub Actions / lint-admin

Expected an assignment or function call and instead saw an expression
? updateConfig("IS_GITHUB_ENABLED", "0")
: updateConfig("IS_GITHUB_ENABLED", "1");
}}
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
Boolean(parseInt(enableGitlabConfig)) === true

Check warning on line 40 in admin/core/components/authentication/gitlab-config.tsx

GitHub Actions / lint-admin

Expected an assignment or function call and instead saw an expression
? updateConfig("IS_GITLAB_ENABLED", "0")
: updateConfig("IS_GITLAB_ENABLED", "1");
}}
<ToggleSwitch
value={Boolean(parseInt(enableGoogleConfig))}
onChange={() => {
Boolean(parseInt(enableGoogleConfig)) === true

Check warning on line 40 in admin/core/components/authentication/google-config.tsx

GitHub Actions / lint-admin

Expected an assignment or function call and instead saw an expression
? updateConfig("IS_GOOGLE_ENABLED", "0")
: updateConfig("IS_GOOGLE_ENABLED", "1");
}}
<ToggleSwitch
value={Boolean(parseInt(enableEmailPassword))}
onChange={() => {
Boolean(parseInt(enableEmailPassword)) === true

Check warning on line 28 in admin/core/components/authentication/password-config-switch.tsx

GitHub Actions / lint-admin

Expected an assignment or function call and instead saw an expression
? updateConfig("ENABLE_EMAIL_PASSWORD", "0")
: updateConfig("ENABLE_EMAIL_PASSWORD", "1");
}}
import { cn } from "@/helpers/common.helper";
type Props = {
control: Control<any>;

Check warning on line 13 in admin/core/components/common/controller-input.tsx

GitHub Actions / lint-admin

Unexpected any. Specify a different type
type: "text" | "password";
name: string;
label: string;
workspaceSlug: string;
projectId: string;
};
searchParams: any;

Check warning on line 14 in space/app/[workspaceSlug]/[projectId]/page.ts

GitHub Actions / lint-space

Unexpected any. Specify a different type
};
export default async function IssuesPage(props: Props) {
let response: TProjectPublishSettings | undefined = undefined;
try {
response = await publishService.fetchAnchorFromProjectDetails(workspaceSlug, projectId);
} catch (error) {

Check warning on line 26 in space/app/[workspaceSlug]/[projectId]/page.ts

GitHub Actions / lint-space

'error' is defined but never used
// redirect to 404 page on error
notFound();
}
values.length > 0 &&
values.map((priority) => (
<div key={priority} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<PriorityIcon priority={priority as any} className={`h-3 w-3`} />

Check warning on line 20 in space/core/components/issues/filters/applied-filters/priority.tsx

GitHub Actions / lint-space

Unexpected any. Specify a different type
{priority}
<button
type="button"
const activeLayout = issueFilters?.display_filters?.layout || undefined;
const userFilters = issueFilters?.filters || {};
const appliedFilters: any = {};

Check warning on line 29 in space/core/components/issues/filters/applied-filters/root.tsx

GitHub Actions / lint-space

Unexpected any. Specify a different type
Object.entries(userFilters).forEach(([key, value]) => {
if (!value) return;
if (Array.isArray(value) && value.length === 0) return;
const priority = key === "priority" ? value : issueFilters?.filters?.priority ?? [];
const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? [];
let params: any = { board: activeLayout || "list" };

Check warning on line 42 in space/core/components/issues/filters/applied-filters/root.tsx

GitHub Actions / lint-space

Unexpected any. Specify a different type
if (priority.length > 0) params = { ...params, priority: priority.join(",") };
if (state.length > 0) params = { ...params, states: state.join(",") };
if (labels.length > 0) params = { ...params, labels: labels.join(",") };
<div className="border-b border-custom-border-200 bg-custom-background-100 p-4">
<AppliedFiltersList
appliedFilters={appliedFilters || {}}
handleRemoveFilter={handleFilters as any}

Check warning on line 89 in space/core/components/issues/filters/applied-filters/root.tsx

GitHub Actions / lint-space

Unexpected any. Specify a different type
handleRemoveAllFilters={handleRemoveAllFilters}
/>
</div>
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFilters={handleFilters as any}

Check warning on line 65 in space/core/components/issues/filters/root.tsx

GitHub Actions / lint-space

Unexpected any. Specify a different type
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[activeLayout]?.filters : []}
/>
</FiltersDropdown>
fetchNextPublicIssues(anchor, groupId, subgroupId);
}
},
[fetchNextPublicIssues]

Check warning on line 40 in space/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx

GitHub Actions / lint-space

React Hook useCallback has missing dependencies: 'anchor' and 'getIssueLoader'. Either include them or remove the dependency array
);
const debouncedFetchMoreIssues = debounce(
(groupId?: string) => {
fetchNextPublicIssues(anchor, groupId);
},
[fetchNextPublicIssues]

Check warning on line 44 in space/core/components/issues/issue-layouts/list/base-list-root.tsx

GitHub Actions / lint-space

React Hook useCallback has a missing dependency: 'anchor'. Either include it or remove the dependency array
);
return (
export const CyclesListHeader: FC = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug } = useParams();

Check warning on line 20 in web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx

GitHub Actions / lint-web

'workspaceSlug' is assigned a value but never used
// store hooks
const { toggleCreateCycleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
export const ModulesListHeader: React.FC = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug } = useParams();

Check warning on line 19 in web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx

GitHub Actions / lint-web

'workspaceSlug' is assigned a value but never used
// store hooks
const { toggleCreateModuleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
element,
})
);
}, [scrollableContainerRef?.current]);

Check warning on line 41 in web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx

GitHub Actions / lint-web

React Hook useEffect has an unnecessary dependency: 'scrollableContainerRef.current'. Either exclude it or remove the dependency array. Mutable values like 'scrollableContainerRef.current' aren't valid dependencies because mutating them doesn't re-render the component
if (workspaceUserInfo && !canPerformProjectMemberActions) {
return <NotAuthorizedView section="settings" isProjectView />;
export const ProjectViewsHeader = observer(() => {
// router
const { workspaceSlug } = useParams();

Check warning on line 16 in web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx

GitHub Actions / lint-web

'workspaceSlug' is assigned a value but never used
// store hooks
const { toggleCreateViewModal } = useCommandPalette();
const { currentProjectDetails, loader } = useProject();
if (!csrfToken) throw new Error("csrf token not found");
await handleSetPassword(csrfToken, { password: passwordFormData.password });
router.push("/");
} catch (err: any) {

Check warning on line 95 in web/app/accounts/set-password/page.tsx

GitHub Actions / lint-web

Unexpected any. Specify a different type
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
<CreateWorkspaceForm
onSubmit={onSubmit}
defaultValues={defaultValues}
setDefaultValues={setDefaultValues as any}

Check warning on line 92 in web/app/create-workspace/page.tsx

GitHub Actions / lint-web

Unexpected any. Specify a different type
/>
</div>
</div>
const workspacesList = Object.values(workspaces ?? {});
// fetching workspaces list
const { isLoading: workspaceListLoader } = useSWR(USER_WORKSPACES_LIST, () => {
user?.id && fetchWorkspaces();

Check warning on line 47 in web/app/onboarding/page.tsx

GitHub Actions / lint-web

Expected an assignment or function call and instead saw an expression
});
// fetching user workspace invitations
const { isLoading: invitationsLoader, data: invitations } = useSWR(
title: "Success!",
message: "Password changed successfully.",
});
} catch (err: any) {

Check warning on line 82 in web/app/profile/security/page.tsx

GitHub Actions / lint-web

Unexpected any. Specify a different type
const errorInfo = authErrorHandler(err.error_code?.toString());
setToast({
type: TOAST_TYPE.ERROR,
};
export const DeDupeButtonRoot: FC<TDeDupeButtonRoot> = (props) => {
const { workspaceSlug, isDuplicateModalOpen, label, handleOnClick } = props;

Check warning on line 13 in web/ce/components/de-dupe/de-dupe-button.tsx

GitHub Actions / lint-web

'workspaceSlug' is assigned a value but never used
return <></>;
};