Compare commits

..
Author SHA1 Message Date
VipinDevelops 9af759b333 feat: add scroll to issue 2025-03-13 16:15:43 +05:30
191 changed files with 2021 additions and 15626 deletions
+50
View File
@@ -47,6 +47,12 @@ jobs:
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
build_apiserver: ${{ steps.changed_files.outputs.apiserver_any_changed }}
build_admin: ${{ steps.changed_files.outputs.admin_any_changed }}
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
build_web: ${{ steps.changed_files.outputs.web_any_changed }}
build_live: ${{ steps.changed_files.outputs.live_any_changed }}
dh_img_web: ${{ steps.set_env_variables.outputs.DH_IMG_WEB }}
dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }}
@@ -117,7 +123,46 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Get changed files
id: changed_files
uses: tj-actions/changed-files@v42
with:
files_yaml: |
apiserver:
- apiserver/**
proxy:
- nginx/**
admin:
- admin/**
- packages/**
- "package.json"
- "yarn.lock"
- "tsconfig.json"
- "turbo.json"
space:
- space/**
- packages/**
- "package.json"
- "yarn.lock"
- "tsconfig.json"
- "turbo.json"
web:
- web/**
- packages/**
- "package.json"
- "yarn.lock"
- "tsconfig.json"
- "turbo.json"
live:
- live/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
branch_build_push_admin:
if: ${{ needs.branch_build_setup.outputs.build_admin == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Admin Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -140,6 +185,7 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_web:
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Web Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -162,6 +208,7 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_space:
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Space Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -184,6 +231,7 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_live:
if: ${{ needs.branch_build_setup.outputs.build_live == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Live Collaboration Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -206,6 +254,7 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_apiserver:
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push API Server Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@@ -228,6 +277,7 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_proxy:
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Proxy Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
+47 -4
View File
@@ -6,9 +6,49 @@ on:
types: ["opened", "synchronize", "ready_for_review"]
jobs:
lint-apiserver:
get-changed-files:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
outputs:
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
admin_changed: ${{ steps.changed-files.outputs.admin_any_changed }}
space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
files_yaml: |
apiserver:
- apiserver/**
admin:
- admin/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
space:
- space/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
web:
- web/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
lint-apiserver:
needs: get-changed-files
runs-on: ubuntu-latest
if: needs.get-changed-files.outputs.apiserver_changed == 'true'
steps:
- uses: actions/checkout@v4
- name: Set up Python
@@ -23,7 +63,8 @@ jobs:
run: ruff check --fix apiserver
lint-admin:
if: github.event.pull_request.draft == false
needs: get-changed-files
if: needs.get-changed-files.outputs.admin_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -35,7 +76,8 @@ jobs:
- run: yarn lint --filter=admin
lint-space:
if: github.event.pull_request.draft == false
needs: get-changed-files
if: needs.get-changed-files.outputs.space_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -47,7 +89,8 @@ jobs:
- run: yarn lint --filter=space
lint-web:
if: github.event.pull_request.draft == false
needs: get-changed-files
if: needs.get-changed-files.outputs.web_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+3
View File
@@ -43,6 +43,9 @@ NGINX_PORT=80
# Debug value for api server use it as 0 for production use
DEBUG=0
CORS_ALLOWED_ORIGINS="http://localhost"
# Error logs
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"
# Database Settings
POSTGRES_USER="plane"
POSTGRES_PASSWORD="plane"
-15
View File
@@ -43,7 +43,6 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
defaultValues: {
GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"],
GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"],
GITHUB_ORGANIZATION_ID: config["GITHUB_ORGANIZATION_ID"],
},
});
@@ -94,19 +93,6 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
error: Boolean(errors.GITHUB_CLIENT_SECRET),
required: true,
},
{
key: "GITHUB_ORGANIZATION_ID",
type: "text",
label: "Organization ID",
description: (
<>
The organization github ID.
</>
),
placeholder: "123456789",
error: Boolean(errors.GITHUB_ORGANIZATION_ID),
required: false,
},
];
const GITHUB_SERVICE_FIELD: TCopyField[] = [
@@ -164,7 +150,6 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
reset({
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value,
GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value,
});
})
.catch((err) => console.error(err));
-13
View File
@@ -9,19 +9,6 @@ const nextConfig = {
unoptimized: true,
},
basePath: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "",
transpilePackages: [
"@plane/constants",
"@plane/editor",
"@plane/hooks",
"@plane/i18n",
"@plane/logger",
"@plane/propel",
"@plane/services",
"@plane/shared-state",
"@plane/types",
"@plane/ui",
"@plane/utils",
],
};
module.exports = nextConfig;
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "admin",
"description": "Admin UI for Plane",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@@ -21,6 +21,7 @@
"@plane/ui": "*",
"@plane/utils": "*",
"@plane/services": "*",
"@sentry/nextjs": "^8.54.0",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14",
@@ -29,7 +30,7 @@
"lucide-react": "^0.469.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"next": "^14.2.25",
"next": "^14.2.20",
"next-themes": "^0.2.1",
"postcss": "^8.4.38",
"react": "^18.3.1",
+3
View File
@@ -145,8 +145,11 @@ RUN chmod +x /app/pg-setup.sh
# APPLICATION ENVIRONMENT SETTINGS
# *****************************************************************************
ENV APP_DOMAIN=localhost
ENV WEB_URL=http://${APP_DOMAIN}
ENV DEBUG=0
ENV SENTRY_DSN=
ENV SENTRY_ENVIRONMENT=production
ENV CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN},https://${APP_DOMAIN}
# Secret Key
ENV SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
+4
View File
@@ -3,6 +3,10 @@
DEBUG=0
CORS_ALLOWED_ORIGINS="http://localhost"
# Error logs
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"
# Database Settings
POSTGRES_USER="plane"
POSTGRES_PASSWORD="plane"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "plane-api",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"private": true,
"description": "API server powering Plane's backend"
-9
View File
@@ -1,5 +1,4 @@
# Third party imports
import pytz
from rest_framework import serializers
# Module imports
@@ -19,14 +18,6 @@ class CycleSerializer(BaseSerializer):
completed_estimates = serializers.FloatField(read_only=True)
started_estimates = serializers.FloatField(read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
project = self.context.get("project")
if project and project.timezone:
project_timezone = pytz.timezone(project.timezone)
self.fields["start_date"].timezone = project_timezone
self.fields["end_date"].timezone = project_timezone
def validate(self, data):
if (
data.get("start_date", None) is not None
+7 -15
View File
@@ -137,12 +137,10 @@ class CycleAPIEndpoint(BaseAPIView):
)
def get(self, request, slug, project_id, pk=None):
project = Project.objects.get(workspace__slug=slug, pk=project_id)
if pk:
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
data = CycleSerializer(
queryset, fields=self.fields,
expand=self.expand, context={"project": project}
queryset, fields=self.fields, expand=self.expand
).data
return Response(data, status=status.HTTP_200_OK)
queryset = self.get_queryset().filter(archived_at__isnull=True)
@@ -154,8 +152,7 @@ class CycleAPIEndpoint(BaseAPIView):
start_date__lte=timezone.now(), end_date__gte=timezone.now()
)
data = CycleSerializer(
queryset, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
queryset, many=True, fields=self.fields, expand=self.expand
).data
return Response(data, status=status.HTTP_200_OK)
@@ -166,8 +163,7 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -178,8 +174,7 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -190,8 +185,7 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -204,16 +198,14 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
-14
View File
@@ -268,20 +268,6 @@ class IssueActivitySerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
source_data = serializers.SerializerMethodField()
def get_source_data(self, obj):
if (
hasattr(obj, "issue")
and hasattr(obj.issue, "source_data")
and obj.issue.source_data
):
return {
"source": obj.issue.source_data[0].source,
"source_email": obj.issue.source_data[0].source_email,
"extra": obj.issue.source_data[0].extra,
}
return None
class Meta:
model = IssueActivity
+6 -15
View File
@@ -268,7 +268,7 @@ class CycleViewSet(BaseViewSet):
)
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(
data, datetime_fields, project_timezone
data, datetime_fields, request.user.user_timezone
)
return Response(data, status=status.HTTP_200_OK)
@@ -318,13 +318,9 @@ class CycleViewSet(BaseViewSet):
.first()
)
# Fetch the project timezone
project = Project.objects.get(id=self.kwargs.get("project_id"))
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter(
cycle, datetime_fields, project_timezone
cycle, datetime_fields, request.user.user_timezone
)
# Send the model activity
@@ -411,13 +407,9 @@ class CycleViewSet(BaseViewSet):
"created_by",
).first()
# Fetch the project timezone
project = Project.objects.get(id=self.kwargs.get("project_id"))
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter(
cycle, datetime_fields, project_timezone
cycle, datetime_fields, request.user.user_timezone
)
# Send the model activity
@@ -488,11 +480,10 @@ class CycleViewSet(BaseViewSet):
)
queryset = queryset.first()
# Fetch the project timezone
project = Project.objects.get(id=self.kwargs.get("project_id"))
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(data, datetime_fields, project_timezone)
data = user_timezone_converter(
data, datetime_fields, request.user.user_timezone
)
recent_visited_task.delay(
slug=slug,
+3 -12
View File
@@ -14,7 +14,7 @@ from rest_framework import status
from .. import BaseAPIView
from plane.app.serializers import IssueActivitySerializer, IssueCommentSerializer
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
from plane.db.models import IssueActivity, IssueComment, CommentReaction, IntakeIssue
from plane.db.models import IssueActivity, IssueComment, CommentReaction
class IssueActivityEndpoint(BaseAPIView):
@@ -57,22 +57,13 @@ class IssueActivityEndpoint(BaseAPIView):
)
)
)
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
if request.GET.get("activity_type", None) == "issue-property":
issue_activities = issue_activities.prefetch_related(
Prefetch(
"issue__issue_intake",
queryset=IntakeIssue.objects.only(
"source_email", "source", "extra"
),
to_attr="source_data",
)
)
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
return Response(issue_activities, status=status.HTTP_200_OK)
if request.GET.get("activity_type", None) == "issue-comment":
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
return Response(issue_comments, status=status.HTTP_200_OK)
result_list = sorted(
-8
View File
@@ -45,7 +45,6 @@ from plane.db.models import (
ProjectMember,
CycleIssue,
UserRecentVisit,
ModuleIssue,
)
from plane.utils.grouper import (
issue_group_values,
@@ -739,13 +738,6 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
total_issues = len(issues)
# First, delete all related cycle issues
CycleIssue.objects.filter(issue_id__in=issue_ids).delete()
# Then, delete all related module issues
ModuleIssue.objects.filter(issue_id__in=issue_ids).delete()
# Finally, delete the issues themselves
issues.delete()
return Response(
@@ -177,9 +177,7 @@ class ProjectViewSet(BaseViewSet):
"module_view",
"page_view",
"inbox_view",
"guest_view_all_features",
"project_lead",
"network",
"created_at",
"updated_at",
"created_by",
+7 -17
View File
@@ -16,17 +16,17 @@ from rest_framework.permissions import AllowAny
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.app.serializers import ProjectMemberInviteSerializer
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import (
ProjectMember,
Workspace,
ProjectMemberInvite,
User,
WorkspaceMember,
Project,
IssueUserProperty,
)
from plane.db.models.project import ProjectNetwork
class ProjectInvitationsViewset(BaseViewSet):
@@ -128,7 +128,6 @@ class UserProjectInvitationsViewset(BaseViewSet):
.select_related("workspace", "workspace__owner", "project")
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def create(self, request, slug):
project_ids = request.data.get("project_ids", [])
@@ -137,20 +136,11 @@ class UserProjectInvitationsViewset(BaseViewSet):
member=request.user, workspace__slug=slug, is_active=True
)
# Get all the projects
projects = Project.objects.filter(
id__in=project_ids, workspace__slug=slug
).only("id", "network")
# Check if user has permission to join each project
for project in projects:
if (
project.network == ProjectNetwork.SECRET.value
and workspace_member.role != ROLE.ADMIN.value
):
return Response(
{"error": "Only workspace admins can join private project"},
status=status.HTTP_403_FORBIDDEN,
)
if workspace_member.role not in [ROLE.ADMIN.value, ROLE.MEMBER.value]:
return Response(
{"error": "You do not have permission to join the project"},
status=status.HTTP_403_FORBIDDEN,
)
workspace_role = workspace_member.role
workspace = workspace_member.workspace
+18 -12
View File
@@ -10,7 +10,11 @@ from plane.app.serializers import (
ProjectMemberRoleSerializer,
)
from plane.app.permissions import WorkspaceUserPermission
from plane.app.permissions import (
ProjectMemberPermission,
ProjectLitePermission,
WorkspaceUserPermission,
)
from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember
from plane.bgtasks.project_add_user_email_task import project_add_user_email
@@ -22,6 +26,14 @@ class ProjectMemberViewSet(BaseViewSet):
serializer_class = ProjectMemberAdminSerializer
model = ProjectMember
def get_permissions(self):
if self.action == "leave":
self.permission_classes = [ProjectLitePermission]
else:
self.permission_classes = [ProjectMemberPermission]
return super(ProjectMemberViewSet, self).get_permissions()
search_fields = ["member__display_name", "member__first_name"]
def get_queryset(self):
@@ -175,20 +187,12 @@ class ProjectMemberViewSet(BaseViewSet):
)
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission([ROLE.ADMIN])
def partial_update(self, request, slug, project_id, pk):
project_member = ProjectMember.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, is_active=True
)
# Fetch the workspace role of the project member
workspace_role = WorkspaceMember.objects.get(
workspace__slug=slug, member=project_member.member, is_active=True
).role
is_workspace_admin = workspace_role == ROLE.ADMIN.value
# Check if the user is not editing their own role if they are not an admin
if request.user.id == project_member.member_id and not is_workspace_admin:
if request.user.id == project_member.member_id:
return Response(
{"error": "You cannot update your own role"},
status=status.HTTP_400_BAD_REQUEST,
@@ -201,6 +205,9 @@ class ProjectMemberViewSet(BaseViewSet):
is_active=True,
)
workspace_role = WorkspaceMember.objects.get(
workspace__slug=slug, member=project_member.member, is_active=True
).role
if workspace_role in [5] and int(
request.data.get("role", project_member.role)
) in [15, 20]:
@@ -215,7 +222,6 @@ class ProjectMemberViewSet(BaseViewSet):
"role" in request.data
and int(request.data.get("role", project_member.role))
> requested_project_member.role
and not is_workspace_admin
):
return Response(
{"error": "You cannot update a role that is higher than your own role"},
+1 -1
View File
@@ -117,7 +117,7 @@ class WorkspaceViewViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[ROLE.ADMIN], level="WORKSPACE", creator=True, model=IssueView
allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView
)
def destroy(self, request, slug, pk):
workspace_view = IssueView.objects.get(pk=pk, workspace__slug=slug)
@@ -34,22 +34,6 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
def post(self, request, slug):
try:
workspace = Workspace.objects.get(slug=slug)
# If the favorite exists return
if request.data.get("entity_identifier"):
user_favorites = UserFavorite.objects.filter(
workspace=workspace,
user_id=request.user.id,
entity_type=request.data.get("entity_type"),
entity_identifier=request.data.get("entity_identifier"),
).first()
# If the favorite exists return
if user_favorites:
serializer = UserFavoriteSerializer(user_favorites)
return Response(serializer.data, status=status.HTTP_200_OK)
# else create a new favorite
serializer = UserFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
@@ -68,11 +68,10 @@ class WorkSpaceMemberViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
# If a user is moved to a guest role he can't have any other role in projects
if "role" in request.data and int(request.data.get("role")) == 5:
ProjectMember.objects.filter(
if workspace_member.role > int(request.data.get("role")):
_ = ProjectMember.objects.filter(
workspace__slug=slug, member_id=workspace_member.member_id
).update(role=5)
).update(role=int(request.data.get("role")))
serializer = WorkSpaceMemberSerializer(
workspace_member, data=request.data, partial=True
@@ -36,12 +36,10 @@ AUTHENTICATION_ERROR_CODES = {
"OAUTH_NOT_CONFIGURED": 5104,
"GOOGLE_NOT_CONFIGURED": 5105,
"GITHUB_NOT_CONFIGURED": 5110,
"GITHUB_USER_NOT_IN_ORG": 5122,
"GITLAB_NOT_CONFIGURED": 5111,
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
"GITLAB_OAUTH_PROVIDER_ERROR": 5121,
# Reset Password
"INVALID_PASSWORD_TOKEN": 5125,
"EXPIRED_PASSWORD_TOKEN": 5130,
@@ -18,16 +18,11 @@ from plane.authentication.adapter.error import (
class GitHubOAuthProvider(OauthAdapter):
token_url = "https://github.com/login/oauth/access_token"
userinfo_url = "https://api.github.com/user"
org_membership_url = f"https://api.github.com/orgs"
provider = "github"
scope = "read:user user:email"
organization_scope = "read:org"
def __init__(self, request, code=None, state=None, callback=None):
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_ORGANIZATION_ID = get_configuration_value(
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET = get_configuration_value(
[
{
"key": "GITHUB_CLIENT_ID",
@@ -37,10 +32,6 @@ class GitHubOAuthProvider(OauthAdapter):
"key": "GITHUB_CLIENT_SECRET",
"default": os.environ.get("GITHUB_CLIENT_SECRET"),
},
{
"key": "GITHUB_ORGANIZATION_ID",
"default": os.environ.get("GITHUB_ORGANIZATION_ID"),
},
]
)
@@ -52,10 +43,6 @@ class GitHubOAuthProvider(OauthAdapter):
client_id = GITHUB_CLIENT_ID
client_secret = GITHUB_CLIENT_SECRET
self.organization_id = GITHUB_ORGANIZATION_ID
if self.organization_id:
self.scope += f" {self.organization_scope}"
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/github/callback/"""
url_params = {
@@ -126,26 +113,12 @@ class GitHubOAuthProvider(OauthAdapter):
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
)
def is_user_in_organization(self, github_username):
headers = {"Authorization": f"Bearer {self.token_data.get('access_token')}"}
response = requests.get(f"{self.org_membership_url}/{self.organization_id}/memberships/{github_username}", headers=headers)
return response.status_code == 200 # 200 means the user is a member
def set_user_data(self):
user_info_response = self.get_user_response()
headers = {
"Authorization": f"Bearer {self.token_data.get('access_token')}",
"Accept": "application/json",
}
if self.organization_id:
if not self.is_user_in_organization(user_info_response.get("login")):
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_USER_NOT_IN_ORG"],
error_message="GITHUB_USER_NOT_IN_ORG",
)
email = self.__get_email(headers=headers)
super().set_user_data(
{
+17 -18
View File
@@ -15,35 +15,34 @@ app = Celery("plane")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.conf.beat_schedule = {
# Intra day recurring jobs
"check-every-five-minutes-to-send-email-notifications": {
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
"schedule": crontab(minute="*/5"), # Every 5 minutes
},
"run-every-6-hours-for-instance-trace": {
"task": "plane.license.bgtasks.tracer.instance_traces",
"schedule": crontab(hour="*/6", minute=0), # Every 6 hours
},
# Occurs once every day
"check-every-day-to-delete-hard-delete": {
"task": "plane.bgtasks.deletion_task.hard_delete",
"schedule": crontab(hour=0, minute=0), # UTC 00:00
},
# Executes every day at 12 AM
"check-every-day-to-archive-and-close": {
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
"schedule": crontab(hour=1, minute=0), # UTC 01:00
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete_exporter_history": {
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
"schedule": crontab(hour=1, minute=30), # UTC 01:30
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete-file-asset": {
"task": "plane.bgtasks.file_asset_task.delete_unuploaded_file_asset",
"schedule": crontab(hour=2, minute=0), # UTC 02:00
"schedule": crontab(hour=0, minute=0),
},
"check-every-five-minutes-to-send-email-notifications": {
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
"schedule": crontab(minute="*/5"),
},
"check-every-day-to-delete-hard-delete": {
"task": "plane.bgtasks.deletion_task.hard_delete",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete-api-logs": {
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
"schedule": crontab(hour=2, minute=30), # UTC 02:30
"schedule": crontab(hour=0, minute=0),
},
"run-every-6-hours-for-instance-trace": {
"task": "plane.license.bgtasks.tracer.instance_traces",
"schedule": crontab(hour="*/6", minute=0),
},
}
@@ -3,6 +3,7 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from sentry_sdk import capture_exception
import uuid
@@ -28,6 +29,7 @@ def create_issue_relation(apps, schema_editor):
)
except Exception as e:
print(e)
capture_exception(e)
def update_issue_priority_choice(apps, schema_editor):
-10
View File
@@ -1,7 +1,6 @@
# Python imports
import pytz
from uuid import uuid4
from enum import Enum
# Django imports
from django.conf import settings
@@ -18,15 +17,6 @@ from .base import BaseModel
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
class ProjectNetwork(Enum):
SECRET = 0
PUBLIC = 2
@classmethod
def choices(cls):
return [(0, "Secret"), (2, "Public")]
def get_default_props():
return {
"filters": {
@@ -71,12 +71,6 @@ class Command(BaseCommand):
"category": "GITHUB",
"is_encrypted": True,
},
{
"key": "GITHUB_ORGANIZATION_ID",
"value": os.environ.get("GITHUB_ORGANIZATION_ID"),
"category": "GITHUB",
"is_encrypted": False,
},
{
"key": "GITLAB_HOST",
"value": os.environ.get("GITLAB_HOST"),
+23
View File
@@ -7,9 +7,13 @@ from urllib.parse import urlparse
# Third party imports
import dj_database_url
import sentry_sdk
# Django imports
from django.core.management.utils import get_random_secret_key
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from corsheaders.defaults import default_headers
@@ -263,6 +267,25 @@ CELERY_IMPORTS = (
"plane.bgtasks.issue_description_version_sync",
)
# Sentry Settings
# Enable Sentry Settings
if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get(
"SENTRY_DSN"
).startswith("https://"):
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN", ""),
integrations=[
DjangoIntegration(),
RedisIntegration(),
CeleryIntegration(monitor_beat_tasks=True),
],
traces_sample_rate=1,
send_default_pii=True,
environment=os.environ.get("SENTRY_ENVIRONMENT", "development"),
profiles_sample_rate=float(os.environ.get("SENTRY_PROFILE_SAMPLE_RATE", 0)),
)
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
# Unsplash Access key
@@ -5,6 +5,9 @@ import traceback
# Django imports
from django.conf import settings
# Third party imports
from sentry_sdk import capture_exception
def log_exception(e):
# Log the error
@@ -15,4 +18,6 @@ def log_exception(e):
# Print the traceback if in debug mode
print(traceback.format_exc())
# Capture in sentry if configured
capture_exception(e)
return
+2
View File
@@ -26,6 +26,8 @@ faker==25.0.0
django-filter==24.2
# json model
jsonmodels==2.7.0
# sentry
sentry-sdk==2.8.0
# storage
django-storages==1.14.2
# user management
+1 -1
View File
@@ -1,3 +1,3 @@
-r base.txt
# server
gunicorn==23.0.0
gunicorn==22.0.0
+15 -3
View File
@@ -6,8 +6,16 @@
"website": "https://plane.so/",
"success_url": "/",
"stack": "heroku-22",
"keywords": ["plane", "project management", "django", "next"],
"addons": ["heroku-postgresql:mini", "heroku-redis:mini"],
"keywords": [
"plane",
"project management",
"django",
"next"
],
"addons": [
"heroku-postgresql:mini",
"heroku-redis:mini"
],
"buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-python.git"
@@ -53,6 +61,10 @@
"description": "AWS Bucket Name to use for S3",
"value": ""
},
"SENTRY_DSN": {
"description": "",
"value": ""
},
"WEB_URL": {
"description": "Web URL for Plane this will be used for redirections in the emails",
"value": ""
@@ -70,4 +82,4 @@
"value": ""
}
}
}
}
+2
View File
@@ -42,6 +42,8 @@ x-live-env: &live-env
x-app-env: &app-env
WEB_URL: ${WEB_URL:-http://localhost}
DEBUG: ${DEBUG:-0}
SENTRY_DSN: ${SENTRY_DSN}
SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT:-production}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
GUNICORN_WORKERS: 1
USE_MINIO: ${USE_MINIO:-1}
+2
View File
@@ -12,6 +12,8 @@ LIVE_REPLICAS=1
NGINX_PORT=80
WEB_URL=http://${APP_DOMAIN}
DEBUG=0
SENTRY_DSN=
SENTRY_ENVIRONMENT=production
CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
API_BASE_URL=http://api:8000
+4 -2
View File
@@ -1,13 +1,13 @@
{
"name": "live",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"description": "A realtime collaborative server powers Plane's rich text editor",
"main": "./src/server.ts",
"private": true,
"type": "module",
"scripts": {
"dev": "PORT=3100 concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
"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 src --ext .ts,.tsx",
@@ -23,6 +23,8 @@
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/types": "*",
"@sentry/node": "^9.0.1",
"@sentry/profiling-node": "^8.28.0",
"@tiptap/core": "2.10.4",
"@tiptap/html": "2.11.0",
"axios": "^1.8.3",
+19
View File
@@ -0,0 +1,19 @@
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
// Ensure to call this before importing any other modules!
Sentry.init({
dsn: process.env.LIVE_SENTRY_DSN,
environment: process.env.LIVE_SENTRY_ENVIRONMENT || "development",
integrations: [
// Add our Profiling integration
nodeProfilingIntegration(),
],
// Add Tracing by setting tracesSampleRate
// We recommend adjusting this value in production
tracesSampleRate: Number(process.env.LIVE_SENTRY_TRACES_SAMPLE_RATE) || 0.5,
// Set sampling rate for profiling
// This is relative to tracesSampleRate
profilesSampleRate: 1.0,
});
+6 -1
View File
@@ -1,8 +1,11 @@
import * as Sentry from "@sentry/node";
import compression from "compression";
import cors from "cors";
import expressWs from "express-ws";
import express from "express";
import helmet from "helmet";
// config
import "@/core/config/sentry-config.js";
// hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
// helpers
@@ -12,7 +15,7 @@ import { errorHandler } from "@/core/helpers/error-handler.js";
// types
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
const app: any = express();
const app = express();
expressWs(app);
app.set("port", process.env.PORT || 3000);
@@ -89,6 +92,8 @@ app.use((_req, res) => {
res.status(404).send("Not Found");
});
Sentry.setupExpressErrorHandler(app);
app.use(errorHandler);
const liveServer = app.listen(app.get("port"), () => {
+3
View File
@@ -16,6 +16,9 @@
"skipLibCheck": true,
"sourceMap": true,
"inlineSources": true,
// Set `sourceRoot` to "/" to strip the build path prefix
// from generated source code references.
// This improves issue grouping in Sentry.
"sourceRoot": "/"
},
"include": ["src/**/*.ts", "tsup.config.ts"],
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "plane",
"description": "Open-source project management that unlocks customer value",
"repository": "https://github.com/makeplane/plane.git",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.25.3",
"version": "0.25.2",
"private": true,
"main": "./src/index.ts",
"license": "AGPL-3.0"
-1
View File
@@ -339,7 +339,6 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
"-updated_at",
"start_date",
"-priority",
"target_date",
],
type: [null, "active", "backlog"],
},
+9 -6
View File
@@ -1,5 +1,8 @@
// icons
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
import {
TProjectAppliedDisplayFilterKeys,
TProjectOrderByOptions,
} from "@plane/types";
export type TNetworkChoiceIconKey = "Lock" | "Globe2";
@@ -52,11 +55,11 @@ export const GROUP_CHOICES = {
};
export const PROJECT_AUTOMATION_MONTHS = [
{ i18n_label: "workspace_projects.common.months_count", value: 1 },
{ i18n_label: "workspace_projects.common.months_count", value: 3 },
{ i18n_label: "workspace_projects.common.months_count", value: 6 },
{ i18n_label: "workspace_projects.common.months_count", value: 9 },
{ i18n_label: "workspace_projects.common.months_count", value: 12 },
{ i18n_label: "common.months_count", value: 1 },
{ i18n_label: "common.months_count", value: 3 },
{ i18n_label: "common.months_count", value: 6 },
{ i18n_label: "common.months_count", value: 9 },
{ i18n_label: "common.months_count", value: 12 },
];
export const PROJECT_UNSPLASH_COVERS = [
+3 -20
View File
@@ -1,4 +1,4 @@
import { TStaticViewTypes, IWorkspaceSearchResults } from "@plane/types";
import { TStaticViewTypes } from "@plane/types";
import { EUserWorkspaceRoles } from "./user";
export const ORGANIZATION_SIZE = [
@@ -83,14 +83,14 @@ export const WORKSPACE_SETTINGS = {
key: "general",
i18n_label: "workspace_settings.settings.general.title",
href: `/settings`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
},
members: {
key: "members",
i18n_label: "workspace_settings.settings.members.title",
href: `/settings/members`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
},
"billing-and-plans": {
@@ -123,10 +123,6 @@ export const WORKSPACE_SETTINGS = {
},
};
export const WORKSPACE_SETTINGS_ACCESS = Object.fromEntries(
Object.entries(WORKSPACE_SETTINGS).map(([_, { href, access }]) => [href, access])
);
export const WORKSPACE_SETTINGS_LINKS: {
key: string;
i18n_label: string;
@@ -328,16 +324,3 @@ export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS: IWorkspaceSidebarN
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["inbox"],
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["projects"],
];
export const IS_FAVORITE_MENU_OPEN = "is_favorite_menu_open";
export const WORKSPACE_DEFAULT_SEARCH_RESULT: IWorkspaceSearchResults = {
results: {
workspace: [],
project: [],
issue: [],
cycle: [],
module: [],
issue_view: [],
page: [],
},
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.25.3",
"version": "0.25.2",
"description": "Core Editor that powers Plane",
"license": "AGPL-3.0",
"private": true,
@@ -1,89 +0,0 @@
import { Extension } from "@tiptap/core";
import { Fragment, Node } from "@tiptap/pm/model";
import { Plugin, PluginKey } from "@tiptap/pm/state";
export const MarkdownClipboard = Extension.create({
name: "markdownClipboard",
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey("markdownClipboard"),
props: {
clipboardTextSerializer: (slice) => {
const markdownSerializer = this.editor.storage.markdown.serializer;
const isTableRow = slice.content.firstChild?.type?.name === "tableRow";
const nodeSelect = slice.openStart === 0 && slice.openEnd === 0;
if (nodeSelect) {
return markdownSerializer.serialize(slice.content);
}
const processTableContent = (tableNode: Node | Fragment) => {
let result = "";
tableNode.content?.forEach?.((tableRowNode: Node | Fragment) => {
tableRowNode.content?.forEach?.((cell: Node) => {
const cellContent = cell.content ? markdownSerializer.serialize(cell.content) : "";
result += cellContent + "\n";
});
});
return result;
};
if (isTableRow) {
const rowsCount = slice.content?.childCount || 0;
const cellsCount = slice.content?.firstChild?.content?.childCount || 0;
if (rowsCount === 1 || cellsCount === 1) {
return processTableContent(slice.content);
} else {
return markdownSerializer.serialize(slice.content);
}
}
const traverseToParentOfLeaf = (
node: Node | null,
parent: Fragment | Node,
depth: number
): Node | Fragment => {
let currentNode = node;
let currentParent = parent;
let currentDepth = depth;
while (currentNode && currentDepth > 1 && currentNode.content?.firstChild) {
if (currentNode.content?.childCount > 1) {
if (currentNode.content.firstChild?.type?.name === "listItem") {
return currentParent;
} else {
return currentNode.content;
}
}
currentParent = currentNode;
currentNode = currentNode.content?.firstChild || null;
currentDepth--;
}
return currentParent;
};
if (slice.content.childCount > 1) {
return markdownSerializer.serialize(slice.content);
} else {
const targetNode = traverseToParentOfLeaf(slice.content.firstChild, slice.content, slice.openStart);
let currentNode = targetNode;
while (currentNode && currentNode.content && currentNode.childCount === 1 && currentNode.firstChild) {
currentNode = currentNode.firstChild;
}
if (currentNode instanceof Node && currentNode.isText) {
return currentNode.text;
}
return markdownSerializer.serialize(targetNode);
}
},
},
}),
];
},
});
@@ -29,7 +29,6 @@ import {
TableCell,
TableHeader,
TableRow,
MarkdownClipboard,
} from "@/extensions";
// helpers
import { isValidHttpUrl } from "@/helpers/common";
@@ -131,11 +130,10 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
CustomCodeInlineExtension,
Markdown.configure({
html: true,
transformCopiedText: false,
transformCopiedText: true,
transformPastedText: true,
breaks: true,
}),
MarkdownClipboard,
Table,
TableHeader,
TableCell,
@@ -23,4 +23,3 @@ export * from "./quote";
export * from "./read-only-extensions";
export * from "./side-menu";
export * from "./text-align";
export * from "./clipboard";
@@ -1,15 +1,12 @@
import { mergeAttributes } from "@tiptap/core";
import Mention, { MentionOptions } from "@tiptap/extension-mention";
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
import { Node as NodeType } from "@tiptap/pm/model";
// types
import { TMentionHandler } from "@/types";
// local types
import { EMentionComponentAttributeNames, TMentionComponentAttributes } from "./types";
import { EMentionComponentAttributeNames } from "./types";
export type TMentionExtensionOptions = MentionOptions & {
renderComponent: TMentionHandler["renderComponent"];
getMentionedEntityDetails: TMentionHandler["getMentionedEntityDetails"];
};
export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOptions>({
@@ -43,26 +40,9 @@ export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOpti
class: "mention",
},
renderText({ node }) {
return getMentionDisplayText(this.options, node);
},
addStorage() {
const options = this.options;
addStorage(this) {
return {
mentionsOpen: false,
markdown: {
serialize(state: MarkdownSerializerState, node: NodeType) {
state.write(getMentionDisplayText(options, node));
},
},
};
},
});
function getMentionDisplayText(options: TMentionExtensionOptions, node: NodeType): string {
const attrs = node.attrs as TMentionComponentAttributes;
const mentionEntityId = attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER];
const mentionEntityDetails = options.getMentionedEntityDetails?.(mentionEntityId ?? "");
return `@${mentionEntityDetails?.display_name ?? attrs[EMentionComponentAttributeNames.ID] ?? mentionEntityId}`;
}
@@ -9,13 +9,12 @@ import { MentionNodeView } from "./mention-node-view";
import { renderMentionsDropdown } from "./utils";
export const CustomMentionExtension = (props: TMentionHandler) => {
const { searchCallback, renderComponent, getMentionedEntityDetails } = props;
const { searchCallback, renderComponent } = props;
return CustomMentionExtensionConfig.extend({
addOptions(this) {
return {
...this.parent?.(),
renderComponent,
getMentionedEntityDetails,
};
},
@@ -24,7 +24,6 @@ import {
CustomTextAlignExtension,
CustomCalloutReadOnlyExtension,
CustomColorExtension,
MarkdownClipboard,
} from "@/extensions";
// helpers
import { isValidHttpUrl } from "@/helpers/common";
@@ -115,9 +114,8 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
CustomCodeInlineExtension,
Markdown.configure({
html: true,
transformCopiedText: false,
transformCopiedText: true,
}),
MarkdownClipboard,
Table,
TableHeader,
TableCell,
@@ -16,22 +16,6 @@ export function tableControls() {
},
},
props: {
handleTripleClickOn(view, pos, node, nodePos, event, direct) {
if (node.type.name === 'tableCell') {
event.preventDefault();
const $pos = view.state.doc.resolve(pos);
const line = $pos.parent;
const linePos = $pos.start();
const start = linePos;
const end = linePos + line.nodeSize - 1;
const tr = view.state.tr.setSelection(
TextSelection.create(view.state.doc, start, end)
);
view.dispatch(tr);
return true;
}
return false;
},
handleDOMEvents: {
mousemove: (view, event) => {
const pluginState = key.getState(view.state);
+1 -2
View File
@@ -1,5 +1,5 @@
// plane types
import { IUserLite, TSearchEntities } from "@plane/types";
import { TSearchEntities } from "@plane/types";
export type TMentionSuggestion = {
entity_identifier: string;
@@ -20,7 +20,6 @@ export type TMentionComponentProps = Pick<TMentionSuggestion, "entity_identifier
export type TReadOnlyMentionHandler = {
renderComponent: (props: TMentionComponentProps) => React.ReactNode;
getMentionedEntityDetails?: (entity_identifier: string) => { display_name: string } | undefined;
};
export type TMentionHandler = TReadOnlyMentionHandler & {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@plane/eslint-config",
"private": true,
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"files": [
"library.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/hooks",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"description": "React hooks that are shared across multiple apps internally",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/i18n",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"description": "I18n shared across multiple apps internally",
"private": true,
+2 -8
View File
@@ -7,16 +7,10 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "Français", value: "fr" },
{ label: "Español", value: "es" },
{ label: "日本語", value: "ja" },
{ label: "简体中文", value: "zh-CN" },
{ label: "繁體中文", value: "zh-TW" },
{ label: "中文", value: "zh-CN" },
{ label: "Русский", value: "ru" },
{ label: "Italian", value: "it" },
{ label: "Čeština", value: "cs" },
{ label: "Slovenčina", value: "sk" },
{ label: "Deutsch", value: "de" },
{ label: "Українська", value: "ua" },
{ label: "Polski", value: "pl" },
{ label: "한국어", value: "ko" },
];
export const LANGUAGE_STORAGE_KEY = "userLanguage";
export const STORAGE_KEY = "userLanguage";
+4 -4
View File
@@ -1,8 +1,8 @@
import { useContext } from "react";
import { useContext } from 'react';
// context
import { TranslationContext } from "../context";
import { TranslationContext } from '../context';
// types
import { ILanguageOption, TLanguage } from "../types";
import { ILanguageOption, TLanguage } from '../types';
export type TTranslationStore = {
t: (key: string, params?: Record<string, any>) => string;
@@ -23,7 +23,7 @@ export type TTranslationStore = {
export function useTranslation(): TTranslationStore {
const store = useContext(TranslationContext);
if (!store) {
throw new Error("useTranslation must be used within a TranslationProvider");
throw new Error('useTranslation must be used within a TranslationProvider');
}
return {
@@ -6,7 +6,7 @@
"home": "Domov",
"your_work": "Vaše práce",
"inbox": "Doručená pošta",
"workspace": "Pracovní prostor",
"workspace": "workspace",
"views": "Pohledy",
"analytics": "Analytika",
"work_items": "Pracovní položky",
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@
"workspace": "Workspace",
"views": "Views",
"analytics": "Analytics",
"work_items": "Work items",
"work_items": "Work Items",
"cycles": "Cycles",
"modules": "Modules",
"intake": "Intake",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+39 -17
View File
@@ -3,7 +3,7 @@ import get from "lodash/get";
import merge from "lodash/merge";
import { makeAutoObservable, runInAction } from "mobx";
// constants
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, LANGUAGE_STORAGE_KEY } from "../constants";
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants";
// core translations imports
import coreEn from "../locales/en/core.json";
// types
@@ -48,14 +48,14 @@ export class TranslationStore {
private initializeLanguage() {
if (typeof window === "undefined") return;
const savedLocale = localStorage.getItem(LANGUAGE_STORAGE_KEY) as TLanguage;
const savedLocale = localStorage.getItem(STORAGE_KEY) as TLanguage;
if (this.isValidLanguage(savedLocale)) {
this.setLanguage(savedLocale);
return;
}
// Fallback to default language
this.setLanguage(FALLBACK_LANGUAGE);
const browserLang = this.getBrowserLanguage();
this.setLanguage(browserLang);
}
/** Loads the translations for the current language */
@@ -147,24 +147,12 @@ export class TranslationStore {
return import("../locales/ja/translations.json");
case "zh-CN":
return import("../locales/zh-CN/translations.json");
case "zh-TW":
return import("../locales/zh-TW/translations.json");
case "ru":
return import("../locales/ru/translations.json");
case "it":
return import("../locales/it/translations.json");
case "cs":
return import("../locales/cs/translations.json");
case "sk":
return import("../locales/sk/translations.json");
case "de":
return import("../locales/de/translations.json");
case "ua":
return import("../locales/ua/translations.json");
case "pl":
return import("../locales/pl/translations.json");
case "ko":
return import("../locales/ko/translations.json");
default:
throw new Error(`Unsupported language: ${language}`);
}
@@ -175,6 +163,40 @@ export class TranslationStore {
return lang !== null && this.availableLanguages.some((l) => l.value === lang);
}
/** Checks if a language code is similar to any supported language */
private findSimilarLanguage(lang: string): TLanguage | null {
// Convert to lowercase for case-insensitive comparison
const normalizedLang = lang.toLowerCase();
// Find a supported language that includes or is included in the browser language
const similarLang = this.availableLanguages.find(
(l) => normalizedLang.includes(l.value.toLowerCase()) || l.value.toLowerCase().includes(normalizedLang)
);
return similarLang ? similarLang.value : null;
}
/** Gets the browser language based on the navigator.language */
private getBrowserLanguage(): TLanguage {
const browserLang = navigator.language;
// Check exact match first
if (this.isValidLanguage(browserLang)) {
return browserLang;
}
// Check base language without region code
const baseLang = browserLang.split("-")[0];
if (this.isValidLanguage(baseLang)) {
return baseLang as TLanguage;
}
// Try to find a similar language
const similarLang = this.findSimilarLanguage(browserLang) || this.findSimilarLanguage(baseLang);
return similarLang || FALLBACK_LANGUAGE;
}
/**
* Gets the cache key for the given key and locale
* @param key - the key to get the cache key for
@@ -259,7 +281,7 @@ export class TranslationStore {
}
if (typeof window !== "undefined") {
localStorage.setItem(LANGUAGE_STORAGE_KEY, lng);
localStorage.setItem(STORAGE_KEY, lng);
document.documentElement.lang = lng;
}
+1 -1
View File
@@ -1,4 +1,4 @@
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "zh-TW" | "ru" | "it" | "cs" | "sk" | "de" | "ua" | "pl" | "ko";
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "ru" | "it" | "cs";
export interface ILanguageOption {
label: string;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/logger",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"description": "Logger shared across multiple apps internally",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/propel",
"version": "0.25.3",
"version": "0.25.2",
"private": true,
"license": "AGPL-3.0",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/services",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"private": true,
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/shared-state",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"description": "Shared state shared across multiple apps internally",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/tailwind-config",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"description": "common tailwind configuration across monorepo",
"main": "tailwind.config.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"private": true,
"types": "./src/index.d.ts",
-6
View File
@@ -6,12 +6,6 @@ export enum EUserPermissions {
export type TUserPermissions = EUserPermissions.ADMIN | EUserPermissions.MEMBER | EUserPermissions.GUEST;
// project network
export enum EProjectNetwork {
PRIVATE = 0,
PUBLIC = 2,
}
// project pages
export enum EPageAccess {
PUBLIC = 0,
+1 -3
View File
@@ -66,10 +66,8 @@ export type TInboxIssueWithPagination = TInboxIssuePaginationInfo & {
results: TInboxIssue[];
};
export type TAnchors = { [key: string]: string };
export type TInboxForm = {
anchors: TAnchors;
anchor: string;
id: string;
is_in_app_enabled: boolean;
is_form_enabled: boolean;
+1 -2
View File
@@ -21,8 +21,7 @@ export type TInstanceGoogleAuthenticationConfigurationKeys =
export type TInstanceGithubAuthenticationConfigurationKeys =
| "GITHUB_CLIENT_ID"
| "GITHUB_CLIENT_SECRET"
| "GITHUB_ORGANIZATION_ID";
| "GITHUB_CLIENT_SECRET";
export type TInstanceGitlabAuthenticationConfigurationKeys =
| "GITLAB_HOST"
@@ -30,13 +30,6 @@ export type TIssueActivity = {
new_identifier: string | undefined;
epoch: number;
issue_comment: string | null;
source_data: {
source: "IN_APP" | "FORM" | "EMAIL";
source_email?: string;
extra: {
username?: string;
};
};
};
export type TIssueActivityMap = {
+4 -2
View File
@@ -25,9 +25,7 @@ export interface IPartialProject {
module_view: boolean;
page_view: boolean;
inbox_view: boolean;
guest_view_all_features?: boolean;
project_lead?: IUserLite | string | null;
network?: number;
// Timestamps
created_at?: Date;
updated_at?: Date;
@@ -48,9 +46,13 @@ export interface IProject extends IPartialProject {
default_state?: string | null;
description?: string;
estimate?: string | null;
guest_view_all_features?: boolean;
anchor?: string | null;
is_favorite?: boolean;
is_issue_type_enabled?: boolean;
is_time_tracking_enabled?: boolean;
members?: string[];
network?: number;
timezone?: string;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "0.25.3",
"version": "0.25.2",
"license": "AGPL-3.0",
"private": true,
"files": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.25.3",
"version": "0.25.2",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
+68 -73
View File
@@ -8,76 +8,71 @@ import { cn } from "../helpers";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
export const Calendar = ({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) => {
const currentYear = new Date().getFullYear();
const thirtyYearsAgoFirstDay = new Date(currentYear - 30, 0, 1);
const thirtyYearsFromNowFirstDay = new Date(currentYear + 30, 11, 31);
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
// classNames={{
// months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
// month: "space-y-4",
// // caption: "flex justify-center pt-1 relative items-center",
// // caption_label: "hidden",
// nav: "box-border absolute top-[1.2rem] right-[1rem] flex items-center",
// button_next:
// "size-[1.25rem] border-none bg-none p-[0.25rem] m-0 cursor-pointer inline-flex items-center justify-center relative appearance-none rounded-sm hover:bg-custom-background-80 focus-visible:bg-custom-background-80",
// button_previous:
// "size-[1.25rem] border-none bg-none p-[0.25rem] m-0 cursor-pointer inline-flex items-center justify-center relative appearance-none rounded-sm hover:bg-custom-background-80 focus-visible:bg-custom-background-80",
// chevron: "m-0 ml-1 size-[0.75rem]",
// // nav_button: cn("h-10 bg-transparent p-0 opacity-50 hover:opacity-100"),
// // nav_button_previous: "absolute left-1",
// // nav_button_next: "absolute right-1",
// table: "w-full border-collapse space-y-1",
// head_row: "flex w-full items-center",
// head_cell: "rounded-md w-10 text-[10px] text-center m-auto font-semibold uppercase",
// row: "flex w-full mt-2",
// cell: cn(
// "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-custom-primary-100/50 [&:has([aria-selected].day-range-end)]:rounded-r-full",
// props.mode === "range"
// ? "[&:has(>.day-range-end)]:rounded-r-full [&:has(>.day-range-start)]:rounded-l-full first:[&:has([aria-selected])]:rounded-l-full last:[&:has([aria-selected])]:rounded-r-full"
// : "[&:has([aria-selected])]:rounded-full [&:has([aria-selected])]:bg-custom-primary-100 [&:has([aria-selected])]:text-white"
// ),
// // day_button:
// // "size-10 flex items-center justify-center overflow-hidden box-border m-0 border-2 border-transparent rounded-full",
// day: "size-10 p-0 font-normal aria-selected:opacity-100 rounded-full hover:bg-custom-primary-100/60",
// day_range_start: "day-range-start bg-custom-primary-100 text-white",
// day_range_end: "day-range-end bg-custom-primary-100 text-white",
// day_selected: "",
// day_today:
// "relative after:content-[''] after:absolute after:m-auto after:left-1/3 after:bottom-[6px] after:w-[6px] after:h-[6px] after:bg-custom-primary-100/50 after:rounded-full after:translate-x-1/2 after:translate-y-1/2",
// day_outside: "day-outside",
// day_disabled: "opacity-50 hover:!bg-transparent",
// day_range_middle: "text-black",
// day_hidden: "invisible",
// caption_dropdowns: "inline-flex bg-transparent",
// dropdown_root: "m-0 relative inline-flex items-center",
// dropdowns: "relative inline-flex items-center",
// dropdown:
// "appearance-none absolute z-[2] top-0 bottom-0 left-0 w-full m-0 p-0 opacity-0 border-none text-[1rem] cursor-pointer bg-transparent hover:bg-custom-background-80",
// months_dropdown: "capitalize",
// caption_label:
// "z-[1] inline-flex items-center gap-[0.25rem] m-0 py-0 px-[0.25rem] whitespace-nowrap border-2 border-transparent font-semibold bg-transparent rounded",
// ...classNames,
// }}
components={{
Chevron: ({ className, ...props }) => (
<ChevronLeft
className={cn(
"size-4",
{ "rotate-180": props.orientation === "right", "-rotate-90": props.orientation === "down" },
className
)}
{...props}
/>
),
}}
startMonth={thirtyYearsAgoFirstDay}
endMonth={thirtyYearsFromNowFirstDay}
{...props}
/>
);
};
export const Calendar = ({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) => (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
// classNames={{
// months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
// month: "space-y-4",
// // caption: "flex justify-center pt-1 relative items-center",
// // caption_label: "hidden",
// nav: "box-border absolute top-[1.2rem] right-[1rem] flex items-center",
// button_next:
// "size-[1.25rem] border-none bg-none p-[0.25rem] m-0 cursor-pointer inline-flex items-center justify-center relative appearance-none rounded-sm hover:bg-custom-background-80 focus-visible:bg-custom-background-80",
// button_previous:
// "size-[1.25rem] border-none bg-none p-[0.25rem] m-0 cursor-pointer inline-flex items-center justify-center relative appearance-none rounded-sm hover:bg-custom-background-80 focus-visible:bg-custom-background-80",
// chevron: "m-0 ml-1 size-[0.75rem]",
// // nav_button: cn("h-10 bg-transparent p-0 opacity-50 hover:opacity-100"),
// // nav_button_previous: "absolute left-1",
// // nav_button_next: "absolute right-1",
// table: "w-full border-collapse space-y-1",
// head_row: "flex w-full items-center",
// head_cell: "rounded-md w-10 text-[10px] text-center m-auto font-semibold uppercase",
// row: "flex w-full mt-2",
// cell: cn(
// "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-custom-primary-100/50 [&:has([aria-selected].day-range-end)]:rounded-r-full",
// props.mode === "range"
// ? "[&:has(>.day-range-end)]:rounded-r-full [&:has(>.day-range-start)]:rounded-l-full first:[&:has([aria-selected])]:rounded-l-full last:[&:has([aria-selected])]:rounded-r-full"
// : "[&:has([aria-selected])]:rounded-full [&:has([aria-selected])]:bg-custom-primary-100 [&:has([aria-selected])]:text-white"
// ),
// // day_button:
// // "size-10 flex items-center justify-center overflow-hidden box-border m-0 border-2 border-transparent rounded-full",
// day: "size-10 p-0 font-normal aria-selected:opacity-100 rounded-full hover:bg-custom-primary-100/60",
// day_range_start: "day-range-start bg-custom-primary-100 text-white",
// day_range_end: "day-range-end bg-custom-primary-100 text-white",
// day_selected: "",
// day_today:
// "relative after:content-[''] after:absolute after:m-auto after:left-1/3 after:bottom-[6px] after:w-[6px] after:h-[6px] after:bg-custom-primary-100/50 after:rounded-full after:translate-x-1/2 after:translate-y-1/2",
// day_outside: "day-outside",
// day_disabled: "opacity-50 hover:!bg-transparent",
// day_range_middle: "text-black",
// day_hidden: "invisible",
// caption_dropdowns: "inline-flex bg-transparent",
// dropdown_root: "m-0 relative inline-flex items-center",
// dropdowns: "relative inline-flex items-center",
// dropdown:
// "appearance-none absolute z-[2] top-0 bottom-0 left-0 w-full m-0 p-0 opacity-0 border-none text-[1rem] cursor-pointer bg-transparent hover:bg-custom-background-80",
// months_dropdown: "capitalize",
// caption_label:
// "z-[1] inline-flex items-center gap-[0.25rem] m-0 py-0 px-[0.25rem] whitespace-nowrap border-2 border-transparent font-semibold bg-transparent rounded",
// ...classNames,
// }}
components={{
Chevron: ({ className, ...props }) => (
<ChevronLeft
className={cn(
"size-4",
{
"rotate-180": props.orientation === "right",
"-rotate-90": props.orientation === "down",
},
className
)}
{...props}
/>
),
}}
{...props}
/>
);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/utils",
"version": "0.25.3",
"version": "0.25.2",
"description": "Helper functions shared across multiple apps internally",
"license": "AGPL-3.0",
"private": true,
@@ -21,7 +21,7 @@ export const OAuthOptions: React.FC = observer(() => {
<GoogleOAuthButton text="Sign in with Google" />
</div>
)}
{config?.is_github_enabled && <GithubOAuthButton text="Sign in with GitHub" />}
{config?.is_github_enabled && <GithubOAuthButton text="Sign in with Github" />}
{config?.is_gitlab_enabled && <GitlabOAuthButton text="Sign in with GitLab" />}
</div>
</>
@@ -7,8 +7,6 @@ import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// store hooks
import { useMember } from "@/hooks/store";
type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
Omit<ILiteTextReadOnlyEditor, "fileHandler" | "mentionHandler">,
@@ -19,29 +17,22 @@ type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
};
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => {
const { getMemberById } = useMember();
return (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({
display_name: getMemberById(id)?.member__display_name ?? "",
}),
}}
{...props}
// overriding the customClassName to add relative class passed
containerClassName={cn(props.containerClassName, "relative p-2")}
/>
);
}
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
{...props}
// overriding the customClassName to add relative class passed
containerClassName={cn(props.containerClassName, "relative p-2")}
/>
)
);
LiteTextReadOnlyEditor.displayName = "LiteTextReadOnlyEditor";
@@ -6,8 +6,6 @@ import { MakeOptional } from "@plane/types";
import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
// store hooks
import { useMember } from "@/hooks/store";
interface RichTextEditorWrapperProps
extends MakeOptional<Omit<IRichTextEditor, "fileHandler" | "mentionHandler">, "disabledExtensions"> {
@@ -18,14 +16,11 @@ interface RichTextEditorWrapperProps
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, ...rest } = props;
const { getMemberById } = useMember();
return (
<RichTextEditorWithRef
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({
display_name: getMemberById(id)?.member__display_name ?? "",
}),
}}
ref={ref}
disabledExtensions={disabledExtensions ?? []}
@@ -36,7 +31,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
})}
{...rest}
containerClassName={containerClassName}
editorClassName="min-h-[100px] max-h-[50vh] border-[0.5px] border-custom-border-200 rounded-md pl-3 py-2 overflow-y-scroll"
editorClassName="min-h-[100px] max-h-[50vh] border border-gray-100 rounded-md pl-3 pb-3 overflow-y-scroll"
/>
);
});
@@ -7,8 +7,6 @@ import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// store hooks
import { useMember } from "@/hooks/store";
type RichTextReadOnlyEditorWrapperProps = MakeOptional<
Omit<IRichTextReadOnlyEditor, "fileHandler" | "mentionHandler">,
@@ -19,29 +17,22 @@ type RichTextReadOnlyEditorWrapperProps = MakeOptional<
};
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => {
const { getMemberById } = useMember();
return (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({
display_name: getMemberById(id)?.member__display_name ?? "",
}),
}}
{...props}
// overriding the customClassName to add relative class passed
containerClassName={cn("relative p-0 border-none", props.containerClassName)}
/>
);
}
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
{...props}
// overriding the customClassName to add relative class passed
containerClassName={cn("relative p-0 border-none", props.containerClassName)}
/>
)
);
RichTextReadOnlyEditor.displayName = "RichTextReadOnlyEditor";
@@ -1,12 +1,11 @@
"use client";
import { SignalHigh } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// types
import { TIssuePriorities } from "@plane/types";
import { PriorityIcon, Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/ui";
// constants
import { cn, getIssuePriorityFilters } from "@plane/utils";
import { getIssuePriorityFilters } from "@plane/utils";
export const IssueBlockPriority = ({
priority,
@@ -19,47 +18,14 @@ export const IssueBlockPriority = ({
const { t } = useTranslation();
const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null;
const priorityClasses = {
urgent: "bg-red-600/10 text-red-600 border-red-600 px-1",
high: "bg-orange-500/20 text-orange-950 border-orange-500",
medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500",
low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100",
none: "hover:bg-custom-background-80 border-custom-border-300",
};
if (priority_detail === null) return <></>;
return (
<Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
priorityClasses[priority ?? "none"],
{
// compact the icons if text is hidden
"px-0.5": !shouldShowName,
// highlight the whole button if text is hidden and priority is urgent
"bg-red-600/10 border-red-600": priority === "urgent" && shouldShowName,
}
)}
>
{priority ? (
<PriorityIcon
priority={priority}
size={12}
className={cn("flex-shrink-0", {
// increase the icon size if text is hidden
"h-3.5 w-3.5": !shouldShowName,
// centre align the icons if text is hidden
"translate-x-[0.0625rem]": !shouldShowName && priority === "high",
"translate-x-0.5": !shouldShowName && priority === "medium",
"translate-x-1": !shouldShowName && priority === "low",
// highlight the icon if priority is urgent
})}
/>
) : (
<SignalHigh className="size-3" />
)}
<div className="flex items-center relative w-full h-full">
<div className={`grid h-5 w-5 place-items-center rounded border-[0.5px] gap-2 ${priority_detail?.className}`}>
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
</div>
{shouldShowName && <span className="pl-2 text-sm">{t(priority_detail?.titleTranslationKey || "")}</span>}
</div>
</Tooltip>
+11
View File
@@ -0,0 +1,11 @@
// google.d.ts
interface GsiButtonConfiguration {
type: "standard" | "icon";
theme?: "outline" | "filled_blue" | "filled_black";
size?: "large" | "medium" | "small";
text?: "signin_with" | "signup_with" | "continue_with" | "signup_with";
shape?: "rectangular" | "pill" | "circle" | "square";
logo_alignment?: "left" | "center";
width?: number;
local?: string;
}
+9
View File
@@ -0,0 +1,9 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}
+44 -14
View File
@@ -1,4 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
/* eslint-disable @typescript-eslint/no-var-requires */
/** @type {import('next').NextConfig} */
require("dotenv").config({ path: ".env" });
const { withSentryConfig } = require("@sentry/nextjs");
const nextConfig = {
trailingSlash: true,
@@ -23,19 +27,45 @@ const nextConfig = {
],
unoptimized: true,
},
transpilePackages: [
"@plane/constants",
"@plane/editor",
"@plane/hooks",
"@plane/i18n",
"@plane/logger",
"@plane/propel",
"@plane/services",
"@plane/shared-state",
"@plane/types",
"@plane/ui",
"@plane/utils",
],
};
module.exports = nextConfig;
const sentryConfig = {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: process.env.SENTRY_ORG_ID || "plane-hq",
project: process.env.SENTRY_PROJECT_ID || "plane-space",
authToken: process.env.SENTRY_AUTH_TOKEN,
// Only print logs for uploading source maps in CI
silent: true,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
};
if (parseInt(process.env.SENTRY_MONITORING_ENABLED || "0", 10)) {
module.exports = withSentryConfig(nextConfig, sentryConfig);
} else {
module.exports = nextConfig;
}
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.25.3",
"version": "0.25.2",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@@ -25,6 +25,7 @@
"@plane/types": "*",
"@plane/ui": "*",
"@plane/services": "*",
"@sentry/nextjs": "^8.54.0",
"axios": "^1.8.3",
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
@@ -36,7 +37,7 @@
"mobx": "^6.10.0",
"mobx-react": "^9.1.1",
"mobx-utils": "^6.0.8",
"next": "^14.2.25",
"next": "^14.2.20",
"next-themes": "^0.2.1",
"nprogress": "^0.2.0",
"react": "^18.3.1",
+31
View File
@@ -0,0 +1,31 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
replaysOnErrorSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
integrations: [
Sentry.replayIntegration({
// Additional Replay configuration goes in here, for example:
maskAllText: true,
blockAllMedia: true,
}),
],
});
+17
View File
@@ -0,0 +1,17 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
+3
View File
@@ -0,0 +1,3 @@
defaults.url=https://sentry.io/
defaults.org=plane
defaults.project=plane-space
+19
View File
@@ -0,0 +1,19 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: process.env.NODE_ENV === 'development',
});
+7 -1
View File
@@ -18,7 +18,13 @@
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG",
"NEXT_PUBLIC_SUPPORT_EMAIL"
"NEXT_PUBLIC_SUPPORT_EMAIL",
"SENTRY_AUTH_TOKEN",
"SENTRY_ORG_ID",
"SENTRY_PROJECT_ID",
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
"NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_MONITORING_ENABLED"
],
"tasks": {
"build": {
@@ -38,7 +38,7 @@ const ProjectCyclesPage = observer(() => {
// derived values
const totalCycles = currentProjectCycleIds?.length ?? 0;
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - ${t("common.cycles", { count: 2 })}` : undefined;
const pageTitle = project?.name ? `${project?.name} - ${t("cycles.label", { count: 2 })}` : undefined;
const hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const hasMemberLevelPermission = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
@@ -15,12 +15,10 @@ const MembersSettingsPage = observer(() => {
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
// derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
const isProjectMemberOrAdmin = allowPermissions(
const canPerformProjectMemberActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const canPerformProjectMemberActions = isProjectMemberOrAdmin || isWorkspaceAdmin;
if (workspaceUserInfo && !canPerformProjectMemberActions) {
return <NotAuthorizedView section="settings" isProjectView />;
@@ -3,8 +3,7 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react";
// components
import { useParams, usePathname } from "next/navigation";
import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_ACCESS } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens";
import { AppHeader } from "@/components/core";
// hooks
@@ -22,26 +21,17 @@ export interface IWorkspaceSettingLayout {
const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = observer((props) => {
const { children } = props;
const { workspaceUserInfo } = useUserPermissions();
const pathname = usePathname();
const [workspaceSlug, suffix, route] = pathname.replace(/^\/|\/$/g, "").split("/"); // Regex removes leading and trailing slashes
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
// derived values
const userWorkspaceRole = workspaceUserInfo?.[workspaceSlug.toString()]?.role;
const isAuthorized =
pathname &&
workspaceSlug &&
userWorkspaceRole &&
WORKSPACE_SETTINGS_ACCESS[route ? `/${suffix}/${route}` : `/${suffix}`]?.includes(
userWorkspaceRole as EUserWorkspaceRoles
);
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
return (
<>
<AppHeader header={<WorkspaceSettingHeader />} />
<MobileWorkspaceSettingsTabs />
<div className="inset-y-0 flex flex-row vertical-scrollbar scrollbar-lg h-full w-full overflow-y-auto">
{workspaceUserInfo && !isAuthorized ? (
{workspaceUserInfo && !isWorkspaceAdmin ? (
<NotAuthorizedView section="settings" />
) : (
<>
+8 -8
View File
@@ -9,13 +9,13 @@ import {
ChevronLeft,
LogOut,
MoveLeft,
Plus,
UserPlus,
Activity,
Bell,
CircleUser,
KeyRound,
Settings2,
CirclePlus,
Mails,
} from "lucide-react";
// plane imports
import { PROFILE_ACTION_LINKS } from "@plane/constants";
@@ -35,14 +35,14 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
const WORKSPACE_ACTION_LINKS = [
{
key: "create_workspace",
Icon: CirclePlus,
i18n_label: "create_workspace",
Icon: Plus,
label: "Create workspace",
href: "/create-workspace",
},
{
key: "invitations",
Icon: Mails,
i18n_label: "workspace_invites",
Icon: UserPlus,
label: "Invitations",
href: "/invitations",
},
];
@@ -243,8 +243,8 @@ export const ProfileLayoutSidebar = observer(() => {
sidebarCollapsed ? "justify-center" : ""
}`}
>
{<link.Icon className="flex-shrink-0 size-4" />}
{!sidebarCollapsed && t(link.i18n_label)}
{<link.Icon className="h-4 w-4" />}
{!sidebarCollapsed && t(link.key)}
</div>
</Tooltip>
</Link>

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