Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
750364833b | ||
|
|
41447e566a | ||
|
|
cebd0b3599 | ||
|
|
caa522118d | ||
|
|
aea5f39059 | ||
|
|
f29867968a | ||
|
|
c91972cc0a | ||
|
|
84c7375d2a | ||
|
|
5cb37a0b9c | ||
|
|
c347dd7dcd | ||
|
|
0ec206b75d | ||
|
|
e8718a84fe | ||
|
|
983e0fa081 | ||
|
|
ef108839c4 | ||
|
|
fe04e5a292 | ||
|
|
50e0cb7ffd | ||
|
|
d37d210921 | ||
|
|
ab3eadf767 | ||
|
|
dbdf2f001a | ||
|
|
0d069bf46e | ||
|
|
962923ff4f | ||
|
|
f720a9afb2 | ||
|
|
4032aa62c5 | ||
|
|
cbe248591e | ||
|
|
75a9b71edb | ||
|
|
ef42ce04a4 | ||
|
|
6bafdb6dd8 | ||
|
|
72307ec100 | ||
|
|
2eb1d03c20 | ||
|
|
94bf90dac5 | ||
|
|
b0e941e4e2 | ||
|
|
e22265dc93 | ||
|
|
075c234385 | ||
|
|
bc539e0d01 | ||
|
|
04fb13cbca | ||
|
|
ca5cf27957 | ||
|
|
433682e913 | ||
|
|
f181b671f3 | ||
|
|
f82d4a9ead | ||
|
|
3f22642732 | ||
|
|
e339b7ad8f | ||
|
|
d4991b9a48 | ||
|
|
1bf683e044 | ||
|
|
807148671f | ||
|
|
d2b81ad2da | ||
|
|
3d14c9d9fe | ||
|
|
d7f40cf578 | ||
|
|
b370ef72ee | ||
|
|
0341205666 |
@@ -47,12 +47,6 @@ 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 }}
|
||||
@@ -123,46 +117,7 @@ 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]
|
||||
@@ -185,7 +140,6 @@ 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]
|
||||
@@ -208,7 +162,6 @@ 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]
|
||||
@@ -231,7 +184,6 @@ 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]
|
||||
@@ -254,7 +206,6 @@ 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]
|
||||
@@ -277,7 +228,6 @@ 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]
|
||||
|
||||
@@ -6,49 +6,9 @@ on:
|
||||
types: ["opened", "synchronize", "ready_for_review"]
|
||||
|
||||
jobs:
|
||||
get-changed-files:
|
||||
lint-apiserver:
|
||||
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
|
||||
@@ -63,8 +23,7 @@ jobs:
|
||||
run: ruff check --fix apiserver
|
||||
|
||||
lint-admin:
|
||||
needs: get-changed-files
|
||||
if: needs.get-changed-files.outputs.admin_changed == 'true'
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -76,8 +35,7 @@ jobs:
|
||||
- run: yarn lint --filter=admin
|
||||
|
||||
lint-space:
|
||||
needs: get-changed-files
|
||||
if: needs.get-changed-files.outputs.space_changed == 'true'
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -89,8 +47,7 @@ jobs:
|
||||
- run: yarn lint --filter=space
|
||||
|
||||
lint-web:
|
||||
needs: get-changed-files
|
||||
if: needs.get-changed-files.outputs.web_changed == 'true'
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -43,9 +43,6 @@ 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"
|
||||
|
||||
@@ -43,6 +43,7 @@ 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"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -93,6 +94,19 @@ 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[] = [
|
||||
@@ -150,6 +164,7 @@ 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));
|
||||
|
||||
@@ -9,6 +9,19 @@ 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;
|
||||
|
||||
+2
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Admin UI for Plane",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -21,7 +21,6 @@
|
||||
"@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",
|
||||
@@ -30,7 +29,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "^14.2.20",
|
||||
"next": "^14.2.25",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -145,11 +145,8 @@ 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
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
DEBUG=0
|
||||
CORS_ALLOWED_ORIGINS="http://localhost"
|
||||
|
||||
# Error logs
|
||||
SENTRY_DSN=""
|
||||
SENTRY_ENVIRONMENT="development"
|
||||
|
||||
# Database Settings
|
||||
POSTGRES_USER="plane"
|
||||
POSTGRES_PASSWORD="plane"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Third party imports
|
||||
import pytz
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
@@ -18,6 +19,14 @@ 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
|
||||
|
||||
@@ -137,10 +137,12 @@ 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
|
||||
queryset, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||
@@ -152,7 +154,8 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
start_date__lte=timezone.now(), end_date__gte=timezone.now()
|
||||
)
|
||||
data = CycleSerializer(
|
||||
queryset, many=True, fields=self.fields, expand=self.expand
|
||||
queryset, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -163,7 +166,8 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields, expand=self.expand
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data,
|
||||
)
|
||||
|
||||
@@ -174,7 +178,8 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields, expand=self.expand
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data,
|
||||
)
|
||||
|
||||
@@ -185,7 +190,8 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields, expand=self.expand
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data,
|
||||
)
|
||||
|
||||
@@ -198,14 +204,16 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields, expand=self.expand
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data,
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields, expand=self.expand
|
||||
cycles, many=True, fields=self.fields,
|
||||
expand=self.expand, context={"project": project}
|
||||
).data,
|
||||
)
|
||||
|
||||
|
||||
@@ -268,6 +268,20 @@ 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
|
||||
|
||||
@@ -268,7 +268,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
datetime_fields = ["start_date", "end_date"]
|
||||
data = user_timezone_converter(
|
||||
data, datetime_fields, request.user.user_timezone
|
||||
data, datetime_fields, project_timezone
|
||||
)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -318,9 +318,13 @@ 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, request.user.user_timezone
|
||||
cycle, datetime_fields, project_timezone
|
||||
)
|
||||
|
||||
# Send the model activity
|
||||
@@ -407,9 +411,13 @@ 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, request.user.user_timezone
|
||||
cycle, datetime_fields, project_timezone
|
||||
)
|
||||
|
||||
# Send the model activity
|
||||
@@ -480,10 +488,11 @@ 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, request.user.user_timezone
|
||||
)
|
||||
data = user_timezone_converter(data, datetime_fields, project_timezone)
|
||||
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
|
||||
@@ -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
|
||||
from plane.db.models import IssueActivity, IssueComment, CommentReaction, IntakeIssue
|
||||
|
||||
|
||||
class IssueActivityEndpoint(BaseAPIView):
|
||||
@@ -57,13 +57,22 @@ 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(
|
||||
|
||||
@@ -45,6 +45,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
UserRecentVisit,
|
||||
ModuleIssue,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -738,6 +739,13 @@ 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,7 +177,9 @@ class ProjectViewSet(BaseViewSet):
|
||||
"module_view",
|
||||
"page_view",
|
||||
"inbox_view",
|
||||
"guest_view_all_features",
|
||||
"project_lead",
|
||||
"network",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
|
||||
@@ -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,6 +128,7 @@ 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", [])
|
||||
|
||||
@@ -136,11 +137,20 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
member=request.user, workspace__slug=slug, is_active=True
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
# 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,
|
||||
)
|
||||
|
||||
workspace_role = workspace_member.role
|
||||
workspace = workspace_member.workspace
|
||||
|
||||
@@ -10,11 +10,7 @@ from plane.app.serializers import (
|
||||
ProjectMemberRoleSerializer,
|
||||
)
|
||||
|
||||
from plane.app.permissions import (
|
||||
ProjectMemberPermission,
|
||||
ProjectLitePermission,
|
||||
WorkspaceUserPermission,
|
||||
)
|
||||
from plane.app.permissions import WorkspaceUserPermission
|
||||
|
||||
from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember
|
||||
from plane.bgtasks.project_add_user_email_task import project_add_user_email
|
||||
@@ -26,14 +22,6 @@ 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):
|
||||
@@ -187,12 +175,20 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
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
|
||||
)
|
||||
if request.user.id == project_member.member_id:
|
||||
|
||||
# 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:
|
||||
return Response(
|
||||
{"error": "You cannot update your own role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -205,9 +201,6 @@ 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]:
|
||||
@@ -222,6 +215,7 @@ 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"},
|
||||
|
||||
@@ -117,7 +117,7 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView
|
||||
allowed_roles=[ROLE.ADMIN], level="WORKSPACE", creator=True, model=IssueView
|
||||
)
|
||||
def destroy(self, request, slug, pk):
|
||||
workspace_view = IssueView.objects.get(pk=pk, workspace__slug=slug)
|
||||
|
||||
@@ -34,6 +34,22 @@ 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,10 +68,11 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if workspace_member.role > int(request.data.get("role")):
|
||||
_ = ProjectMember.objects.filter(
|
||||
# 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(
|
||||
workspace__slug=slug, member_id=workspace_member.member_id
|
||||
).update(role=int(request.data.get("role")))
|
||||
).update(role=5)
|
||||
|
||||
serializer = WorkSpaceMemberSerializer(
|
||||
workspace_member, data=request.data, partial=True
|
||||
|
||||
@@ -36,10 +36,12 @@ 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,11 +18,16 @@ 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 = get_configuration_value(
|
||||
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_ORGANIZATION_ID = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "GITHUB_CLIENT_ID",
|
||||
@@ -32,6 +37,10 @@ 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"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
@@ -43,6 +52,10 @@ 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 = {
|
||||
@@ -113,12 +126,26 @@ 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(
|
||||
{
|
||||
|
||||
+24
-23
@@ -15,34 +15,35 @@ app = Celery("plane")
|
||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
|
||||
app.conf.beat_schedule = {
|
||||
# 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=0, minute=0),
|
||||
},
|
||||
"check-every-day-to-delete_exporter_history": {
|
||||
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
|
||||
"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=0, minute=0),
|
||||
},
|
||||
# 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"),
|
||||
},
|
||||
"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=0, minute=0),
|
||||
"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),
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from sentry_sdk import capture_exception
|
||||
import uuid
|
||||
|
||||
|
||||
@@ -29,7 +28,6 @@ def create_issue_relation(apps, schema_editor):
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
|
||||
|
||||
def update_issue_priority_choice(apps, schema_editor):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Python imports
|
||||
import pytz
|
||||
from uuid import uuid4
|
||||
from enum import Enum
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
@@ -17,6 +18,15 @@ 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,6 +71,12 @@ 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"),
|
||||
|
||||
@@ -7,13 +7,9 @@ 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
|
||||
|
||||
|
||||
@@ -267,25 +263,6 @@ 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,9 +5,6 @@ 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
|
||||
@@ -18,6 +15,4 @@ def log_exception(e):
|
||||
# Print the traceback if in debug mode
|
||||
print(traceback.format_exc())
|
||||
|
||||
# Capture in sentry if configured
|
||||
capture_exception(e)
|
||||
return
|
||||
|
||||
@@ -26,8 +26,6 @@ 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,3 +1,3 @@
|
||||
-r base.txt
|
||||
# server
|
||||
gunicorn==22.0.0
|
||||
gunicorn==23.0.0
|
||||
|
||||
@@ -6,16 +6,8 @@
|
||||
"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"
|
||||
@@ -61,10 +53,6 @@
|
||||
"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": ""
|
||||
@@ -82,4 +70,4 @@
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,6 @@ 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}
|
||||
|
||||
@@ -12,8 +12,6 @@ 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
|
||||
|
||||
|
||||
+2
-4
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"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": "concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
|
||||
"dev": "PORT=3100 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,8 +23,6 @@
|
||||
"@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",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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,
|
||||
});
|
||||
+1
-6
@@ -1,11 +1,8 @@
|
||||
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
|
||||
@@ -15,7 +12,7 @@ import { errorHandler } from "@/core/helpers/error-handler.js";
|
||||
// types
|
||||
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
|
||||
|
||||
const app = express();
|
||||
const app: any = express();
|
||||
expressWs(app);
|
||||
|
||||
app.set("port", process.env.PORT || 3000);
|
||||
@@ -92,8 +89,6 @@ app.use((_req, res) => {
|
||||
res.status(404).send("Not Found");
|
||||
});
|
||||
|
||||
Sentry.setupExpressErrorHandler(app);
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
const liveServer = app.listen(app.get("port"), () => {
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
"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
@@ -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.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"license": "AGPL-3.0"
|
||||
|
||||
@@ -339,6 +339,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
"target_date",
|
||||
],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// icons
|
||||
import {
|
||||
TProjectAppliedDisplayFilterKeys,
|
||||
TProjectOrderByOptions,
|
||||
} from "@plane/types";
|
||||
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
|
||||
|
||||
export type TNetworkChoiceIconKey = "Lock" | "Globe2";
|
||||
|
||||
@@ -55,11 +52,11 @@ export const GROUP_CHOICES = {
|
||||
};
|
||||
|
||||
export const PROJECT_AUTOMATION_MONTHS = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
export const PROJECT_UNSPLASH_COVERS = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TStaticViewTypes } from "@plane/types";
|
||||
import { TStaticViewTypes, IWorkspaceSearchResults } 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],
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
|
||||
},
|
||||
members: {
|
||||
key: "members",
|
||||
i18n_label: "workspace_settings.settings.members.title",
|
||||
href: `/settings/members`,
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
|
||||
},
|
||||
"billing-and-plans": {
|
||||
@@ -123,6 +123,10 @@ 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;
|
||||
@@ -324,3 +328,16 @@ 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,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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,6 +29,7 @@ import {
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
MarkdownClipboard,
|
||||
} from "@/extensions";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
@@ -130,10 +131,11 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
CustomCodeInlineExtension,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
transformCopiedText: false,
|
||||
transformPastedText: true,
|
||||
breaks: true,
|
||||
}),
|
||||
MarkdownClipboard,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
|
||||
@@ -23,3 +23,4 @@ export * from "./quote";
|
||||
export * from "./read-only-extensions";
|
||||
export * from "./side-menu";
|
||||
export * from "./text-align";
|
||||
export * from "./clipboard";
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
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 } from "./types";
|
||||
import { EMentionComponentAttributeNames, TMentionComponentAttributes } from "./types";
|
||||
|
||||
export type TMentionExtensionOptions = MentionOptions & {
|
||||
renderComponent: TMentionHandler["renderComponent"];
|
||||
getMentionedEntityDetails: TMentionHandler["getMentionedEntityDetails"];
|
||||
};
|
||||
|
||||
export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOptions>({
|
||||
@@ -40,9 +43,26 @@ export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOpti
|
||||
class: "mention",
|
||||
},
|
||||
|
||||
addStorage(this) {
|
||||
renderText({ node }) {
|
||||
return getMentionDisplayText(this.options, node);
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
const options = this.options;
|
||||
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,12 +9,13 @@ import { MentionNodeView } from "./mention-node-view";
|
||||
import { renderMentionsDropdown } from "./utils";
|
||||
|
||||
export const CustomMentionExtension = (props: TMentionHandler) => {
|
||||
const { searchCallback, renderComponent } = props;
|
||||
const { searchCallback, renderComponent, getMentionedEntityDetails } = props;
|
||||
return CustomMentionExtensionConfig.extend({
|
||||
addOptions(this) {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
renderComponent,
|
||||
getMentionedEntityDetails,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutReadOnlyExtension,
|
||||
CustomColorExtension,
|
||||
MarkdownClipboard,
|
||||
} from "@/extensions";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
@@ -114,8 +115,9 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
CustomCodeInlineExtension,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
transformCopiedText: false,
|
||||
}),
|
||||
MarkdownClipboard,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
|
||||
@@ -16,6 +16,22 @@ 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,5 +1,5 @@
|
||||
// plane types
|
||||
import { TSearchEntities } from "@plane/types";
|
||||
import { IUserLite, TSearchEntities } from "@plane/types";
|
||||
|
||||
export type TMentionSuggestion = {
|
||||
entity_identifier: string;
|
||||
@@ -20,6 +20,7 @@ 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,7 +1,7 @@
|
||||
{
|
||||
"name": "@plane/eslint-config",
|
||||
"private": true,
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"files": [
|
||||
"library.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/hooks",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "React hooks that are shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/i18n",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "I18n shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -7,10 +7,16 @@ 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-CN" },
|
||||
{ label: "繁體中文", value: "zh-TW" },
|
||||
{ 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 STORAGE_KEY = "userLanguage";
|
||||
export const LANGUAGE_STORAGE_KEY = "userLanguage";
|
||||
|
||||
@@ -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": "workspace",
|
||||
"workspace": "Pracovní prostor",
|
||||
"views": "Pohledy",
|
||||
"analytics": "Analytika",
|
||||
"work_items": "Pracovní položky",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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, STORAGE_KEY } from "../constants";
|
||||
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, LANGUAGE_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(STORAGE_KEY) as TLanguage;
|
||||
const savedLocale = localStorage.getItem(LANGUAGE_STORAGE_KEY) as TLanguage;
|
||||
if (this.isValidLanguage(savedLocale)) {
|
||||
this.setLanguage(savedLocale);
|
||||
return;
|
||||
}
|
||||
|
||||
const browserLang = this.getBrowserLanguage();
|
||||
this.setLanguage(browserLang);
|
||||
// Fallback to default language
|
||||
this.setLanguage(FALLBACK_LANGUAGE);
|
||||
}
|
||||
|
||||
/** Loads the translations for the current language */
|
||||
@@ -147,12 +147,24 @@ 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}`);
|
||||
}
|
||||
@@ -163,40 +175,6 @@ 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
|
||||
@@ -281,7 +259,7 @@ export class TranslationStore {
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(STORAGE_KEY, lng);
|
||||
localStorage.setItem(LANGUAGE_STORAGE_KEY, lng);
|
||||
document.documentElement.lang = lng;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "ru" | "it" | "cs";
|
||||
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "zh-TW" | "ru" | "it" | "cs" | "sk" | "de" | "ua" | "pl" | "ko";
|
||||
|
||||
export interface ILanguageOption {
|
||||
label: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/logger",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Logger shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/propel",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/services",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/shared-state",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Shared state shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/tailwind-config",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "tailwind.config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"types": "./src/index.d.ts",
|
||||
|
||||
@@ -6,6 +6,12 @@ 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,
|
||||
|
||||
Vendored
+3
-1
@@ -66,8 +66,10 @@ export type TInboxIssueWithPagination = TInboxIssuePaginationInfo & {
|
||||
results: TInboxIssue[];
|
||||
};
|
||||
|
||||
export type TAnchors = { [key: string]: string };
|
||||
|
||||
export type TInboxForm = {
|
||||
anchor: string;
|
||||
anchors: TAnchors;
|
||||
id: string;
|
||||
is_in_app_enabled: boolean;
|
||||
is_form_enabled: boolean;
|
||||
|
||||
+2
-1
@@ -21,7 +21,8 @@ export type TInstanceGoogleAuthenticationConfigurationKeys =
|
||||
|
||||
export type TInstanceGithubAuthenticationConfigurationKeys =
|
||||
| "GITHUB_CLIENT_ID"
|
||||
| "GITHUB_CLIENT_SECRET";
|
||||
| "GITHUB_CLIENT_SECRET"
|
||||
| "GITHUB_ORGANIZATION_ID";
|
||||
|
||||
export type TInstanceGitlabAuthenticationConfigurationKeys =
|
||||
| "GITLAB_HOST"
|
||||
|
||||
@@ -30,6 +30,13 @@ 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 = {
|
||||
|
||||
+2
-4
@@ -25,7 +25,9 @@ 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;
|
||||
@@ -46,13 +48,9 @@ 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,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/typescript-config",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -8,71 +8,76 @@ import { cn } from "../helpers";
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/utils",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"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,6 +7,8 @@ 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">,
|
||||
@@ -17,22 +19,29 @@ type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
|
||||
};
|
||||
|
||||
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
|
||||
({ 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")}
|
||||
/>
|
||||
)
|
||||
({ 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")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
LiteTextReadOnlyEditor.displayName = "LiteTextReadOnlyEditor";
|
||||
|
||||
@@ -6,6 +6,8 @@ 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"> {
|
||||
@@ -16,11 +18,14 @@ 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 ?? []}
|
||||
@@ -31,7 +36,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
||||
})}
|
||||
{...rest}
|
||||
containerClassName={containerClassName}
|
||||
editorClassName="min-h-[100px] max-h-[50vh] border border-gray-100 rounded-md pl-3 pb-3 overflow-y-scroll"
|
||||
editorClassName="min-h-[100px] max-h-[50vh] border-[0.5px] border-custom-border-200 rounded-md pl-3 py-2 overflow-y-scroll"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ 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">,
|
||||
@@ -17,22 +19,29 @@ type RichTextReadOnlyEditorWrapperProps = MakeOptional<
|
||||
};
|
||||
|
||||
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
|
||||
({ 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)}
|
||||
/>
|
||||
)
|
||||
({ 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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RichTextReadOnlyEditor.displayName = "RichTextReadOnlyEditor";
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { SignalHigh } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { TIssuePriorities } from "@plane/types";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { PriorityIcon, Tooltip } from "@plane/ui";
|
||||
// constants
|
||||
import { getIssuePriorityFilters } from "@plane/utils";
|
||||
import { cn, getIssuePriorityFilters } from "@plane/utils";
|
||||
|
||||
export const IssueBlockPriority = ({
|
||||
priority,
|
||||
@@ -18,14 +19,47 @@ 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="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>
|
||||
<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" />
|
||||
)}
|
||||
{shouldShowName && <span className="pl-2 text-sm">{t(priority_detail?.titleTranslationKey || "")}</span>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
Vendored
-11
@@ -1,11 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
+14
-44
@@ -1,8 +1,4 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/** @type {import('next').NextConfig} */
|
||||
require("dotenv").config({ path: ".env" });
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
const nextConfig = {
|
||||
trailingSlash: true,
|
||||
@@ -27,45 +23,19 @@ 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",
|
||||
],
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
module.exports = nextConfig;
|
||||
|
||||
+2
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -25,7 +25,6 @@
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/services": "*",
|
||||
"@sentry/nextjs": "^8.54.0",
|
||||
"axios": "^1.8.3",
|
||||
"clsx": "^2.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -37,7 +36,7 @@
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"mobx-utils": "^6.0.8",
|
||||
"next": "^14.2.20",
|
||||
"next": "^14.2.25",
|
||||
"next-themes": "^0.2.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// 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,
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
// 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,
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
defaults.url=https://sentry.io/
|
||||
defaults.org=plane
|
||||
defaults.project=plane-space
|
||||
@@ -1,19 +0,0 @@
|
||||
// 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',
|
||||
});
|
||||
+1
-7
@@ -18,13 +18,7 @@
|
||||
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||
"NEXT_PUBLIC_POSTHOG_HOST",
|
||||
"NEXT_PUBLIC_POSTHOG_DEBUG",
|
||||
"NEXT_PUBLIC_SUPPORT_EMAIL",
|
||||
"SENTRY_AUTH_TOKEN",
|
||||
"SENTRY_ORG_ID",
|
||||
"SENTRY_PROJECT_ID",
|
||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"SENTRY_MONITORING_ENABLED"
|
||||
"NEXT_PUBLIC_SUPPORT_EMAIL"
|
||||
],
|
||||
"tasks": {
|
||||
"build": {
|
||||
|
||||
+1
-1
@@ -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("cycles.label", { count: 2 })}` : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - ${t("common.cycles", { count: 2 })}` : undefined;
|
||||
const hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
const hasMemberLevelPermission = allowPermissions(
|
||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||
|
||||
+3
-1
@@ -15,10 +15,12 @@ const MembersSettingsPage = observer(() => {
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
|
||||
const canPerformProjectMemberActions = allowPermissions(
|
||||
const isProjectMemberOrAdmin = 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,7 +3,8 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_ACCESS } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { AppHeader } from "@/components/core";
|
||||
// hooks
|
||||
@@ -21,17 +22,26 @@ export interface IWorkspaceSettingLayout {
|
||||
const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = observer((props) => {
|
||||
const { children } = props;
|
||||
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { workspaceUserInfo } = useUserPermissions();
|
||||
const pathname = usePathname();
|
||||
const [workspaceSlug, suffix, route] = pathname.replace(/^\/|\/$/g, "").split("/"); // Regex removes leading and trailing slashes
|
||||
|
||||
// derived values
|
||||
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const userWorkspaceRole = workspaceUserInfo?.[workspaceSlug.toString()]?.role;
|
||||
const isAuthorized =
|
||||
pathname &&
|
||||
workspaceSlug &&
|
||||
userWorkspaceRole &&
|
||||
WORKSPACE_SETTINGS_ACCESS[route ? `/${suffix}/${route}` : `/${suffix}`]?.includes(
|
||||
userWorkspaceRole as EUserWorkspaceRoles
|
||||
);
|
||||
|
||||
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 && !isWorkspaceAdmin ? (
|
||||
{workspaceUserInfo && !isAuthorized ? (
|
||||
<NotAuthorizedView section="settings" />
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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: Plus,
|
||||
label: "Create workspace",
|
||||
Icon: CirclePlus,
|
||||
i18n_label: "create_workspace",
|
||||
href: "/create-workspace",
|
||||
},
|
||||
{
|
||||
key: "invitations",
|
||||
Icon: UserPlus,
|
||||
label: "Invitations",
|
||||
Icon: Mails,
|
||||
i18n_label: "workspace_invites",
|
||||
href: "/invitations",
|
||||
},
|
||||
];
|
||||
@@ -243,8 +243,8 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
{<link.Icon className="h-4 w-4" />}
|
||||
{!sidebarCollapsed && t(link.key)}
|
||||
{<link.Icon className="flex-shrink-0 size-4" />}
|
||||
{!sidebarCollapsed && t(link.i18n_label)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user