Compare commits

..
Author SHA1 Message Date
sriram veeraghantaandGitHub e7468292c7 Merge pull request #3175 from makeplane/preview
release: moving changes from preview to master
2023-12-18 19:29:43 +05:30
215 changed files with 2341 additions and 8208 deletions
+12 -13
View File
@@ -9,7 +9,6 @@ on:
- preview
- qa
- develop
- release-*
release:
types: [released, prereleased]
@@ -63,14 +62,14 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
steps:
- name: Set Frontend Docker Tag
run: |
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable
else
TAG=${{ env.FRONTEND_TAG }}
fi
@@ -105,14 +104,14 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
steps:
- name: Set Space Docker Tag
run: |
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable
else
TAG=${{ env.SPACE_TAG }}
fi
@@ -147,14 +146,14 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
steps:
- name: Set Backend Docker Tag
run: |
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable
else
TAG=${{ env.BACKEND_TAG }}
fi
@@ -189,14 +188,14 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
steps:
- name: Set Proxy Docker Tag
run: |
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable
else
TAG=${{ env.PROXY_TAG }}
fi
-3
View File
@@ -80,6 +80,3 @@ tmp/
## packages
dist
.temp/
# logs
combined.log
+1 -1
View File
@@ -1,3 +1,3 @@
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: celery -A plane worker -l info -Q internal_tasks,external_tasks
worker: celery -A plane worker -l info
beat: celery -A plane beat -l INFO
+36 -40
View File
@@ -281,22 +281,20 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
)
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue_activity.apply_async(
args=[],
kwargs={
'type': "cycle.activity.deleted",
'requested_data': json.dumps({
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(pk),
"cycle_name": str(cycle.name),
"issues": [str(issue_id) for issue_id in cycle_issues],
}),
'actor_id': str(request.user.id),
'issue_id': None,
'project_id': str(project_id),
'current_instance': None,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
}
),
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
# Delete the cycle
cycle.delete()
@@ -456,21 +454,21 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
)
# Capture Issue Activity
issue_activity.apply_async(
args=[],
kwargs={
'type': "cycle.activity.created",
'requested_data': json.dumps({"cycles_list": str(issues)}),
'actor_id': str(self.request.user.id),
'issue_id': None,
'project_id': str(self.kwargs.get("project_id", None)),
'current_instance': json.dumps({
issue_activity.delay(
type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": str(issues)}),
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": serializers.serialize("json", record_to_create),
}),
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
"created_cycle_issues": serializers.serialize(
"json", record_to_create
),
}
),
epoch=int(timezone.now().timestamp()),
)
# Return all Cycle Issues
@@ -485,21 +483,19 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
)
issue_id = cycle_issue.issue_id
cycle_issue.delete()
issue_activity.apply_async(
args=[],
kwargs={
'type': "cycle.activity.deleted",
'requested_data': json.dumps({
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(self.kwargs.get("cycle_id")),
"issues": [str(issue_id)],
}),
'actor_id': str(self.request.user.id),
'issue_id': str(issue_id),
'project_id': str(self.kwargs.get("project_id", None)),
'current_instance': None,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
}
),
actor_id=str(self.request.user.id),
issue_id=str(issue_id),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
return Response(status=status.HTTP_204_NO_CONTENT)
+19 -27
View File
@@ -142,18 +142,14 @@ class InboxIssueAPIEndpoint(BaseAPIView):
)
# Create an Issue Activity
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
"type": "issue.activity.created",
"requested_data": json.dumps(request.data, cls=DjangoJSONEncoder),
"actor_id": str(request.user.id),
"issue_id": str(issue.id),
"project_id": str(project_id),
"current_instance": None,
"epoch": int(timezone.now().timestamp()),
},
routing_key="external",
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
# create an inbox issue
@@ -236,21 +232,17 @@ class InboxIssueAPIEndpoint(BaseAPIView):
# Log all the updates
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
if issue is not None:
issue_activity.apply_async(
args=[],
kwargs={
"type": "issue.activity.updated",
"requested_data": requested_data,
"actor_id": str(request.user.id),
"issue_id": str(issue_id),
"project_id": str(project_id),
"current_instance": json.dumps(
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
"epoch": int(timezone.now().timestamp()),
},
routing_key="external",
issue_activity.delay(
type="issue.activity.updated",
requested_data=requested_data,
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
)
issue_serializer.save()
else:
+75 -117
View File
@@ -207,18 +207,14 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
serializer.save()
# Track the issue
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
'type': "issue.activity.created",
'requested_data': json.dumps(self.request.data, cls=DjangoJSONEncoder),
'actor_id': str(request.user.id),
'issue_id': str(serializer.data.get("id", None)),
'project_id': str(project_id),
'current_instance': None,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -232,18 +228,14 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
serializer = IssueSerializer(issue, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
issue_activity.apply_async(
args=[],
kwargs={
'type': "issue.activity.updated",
'requested_data': requested_data,
'actor_id': str(request.user.id),
'issue_id': str(pk),
'project_id': str(project_id),
'current_instance': current_instance,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
issue_activity.delay(
type="issue.activity.updated",
requested_data=requested_data,
actor_id=str(request.user.id),
issue_id=str(pk),
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -254,19 +246,14 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
IssueSerializer(issue).data, cls=DjangoJSONEncoder
)
issue.delete()
issue_activity.apply_async(
args=[],
kwargs={
'type': "issue.activity.deleted",
'requested_data': json.dumps({"issue_id": str(pk)}),
'actor_id': str(request.user.id),
'issue_id': str(pk),
'project_id': str(project_id),
'current_instance': current_instance,
'epoch': int(timezone.now().timestamp()),
},
routing_key='your_routing_key',
queue='your_queue_name'
issue_activity.delay(
type="issue.activity.deleted",
requested_data=json.dumps({"issue_id": str(pk)}),
actor_id=str(request.user.id),
issue_id=str(pk),
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -322,11 +309,7 @@ class LabelAPIEndpoint(BaseAPIView):
).data,
)
label = self.get_queryset().get(pk=pk)
serializer = LabelSerializer(
label,
fields=self.fields,
expand=self.expand,
)
serializer = LabelSerializer(label, fields=self.fields, expand=self.expand,)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, pk=None):
@@ -336,6 +319,7 @@ class LabelAPIEndpoint(BaseAPIView):
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk=None):
label = self.get_queryset().get(pk=pk)
@@ -400,18 +384,14 @@ class IssueLinkAPIEndpoint(BaseAPIView):
project_id=project_id,
issue_id=issue_id,
)
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
'type': "link.activity.created",
'requested_data': json.dumps(serializer.data, cls=DjangoJSONEncoder),
'actor_id': str(self.request.user.id),
'issue_id': str(self.kwargs.get("issue_id")),
'project_id': str(self.kwargs.get("project_id")),
'current_instance': None,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
issue_activity.delay(
type="link.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -428,18 +408,14 @@ class IssueLinkAPIEndpoint(BaseAPIView):
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
'type': "link.activity.updated",
'requested_data': requested_data,
'actor_id': str(request.user.id),
'issue_id': str(issue_id),
'project_id': str(project_id),
'current_instance': current_instance,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
issue_activity.delay(
type="link.activity.updated",
requested_data=requested_data,
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -452,18 +428,14 @@ class IssueLinkAPIEndpoint(BaseAPIView):
IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder,
)
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
'type': "link.activity.deleted",
'requested_data': json.dumps({"link_id": str(pk)}),
'actor_id': str(request.user.id),
'issue_id': str(issue_id),
'project_id': str(project_id),
'current_instance': current_instance,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
issue_activity.delay(
type="link.activity.deleted",
requested_data=json.dumps({"link_id": str(pk)}),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
issue_link.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -535,20 +507,14 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
issue_id=issue_id,
actor=request.user,
)
issue_activity.apply_async(
args=[],
kwargs={
"type": "comment.activity.created",
"requested_data": json.dumps(
serializer.data, cls=DjangoJSONEncoder
),
"actor_id": str(self.request.user.id),
"issue_id": str(self.kwargs.get("issue_id")),
"project_id": str(self.kwargs.get("project_id")),
"current_instance": None,
"epoch": int(timezone.now().timestamp()),
},
routing_key="external",
issue_activity.delay(
type="comment.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -567,18 +533,14 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
)
if serializer.is_valid():
serializer.save()
issue_activity.apply_async(
args=[],
kwargs={
"type": "comment.activity.updated",
"requested_data": requested_data,
"actor_id": str(request.user.id),
"issue_id": str(issue_id),
"project_id": str(project_id),
"current_instance": current_instance,
"epoch": int(timezone.now().timestamp()),
},
routing_key="external",
issue_activity.delay(
type="comment.activity.updated",
requested_data=requested_data,
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -592,18 +554,14 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
cls=DjangoJSONEncoder,
)
issue_comment.delete()
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
"type": "comment.activity.deleted",
"requested_data": json.dumps({"comment_id": str(pk)}),
"actor_id": str(request.user.id),
"issue_id": str(issue_id),
"project_id": str(project_id),
"current_instance": current_instance,
"epoch": int(timezone.now().timestamp()),
},
routing_key="external",
issue_activity.delay(
type="comment.activity.deleted",
requested_data=json.dumps({"comment_id": str(pk)}),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -624,7 +582,7 @@ class IssueActivityAPIEndpoint(BaseAPIView):
)
.select_related("actor", "workspace", "issue", "project")
).order_by(request.GET.get("order_by", "created_at"))
if pk:
issue_activities = issue_activities.get(pk=pk)
serializer = IssueActivitySerializer(issue_activities)
+36 -40
View File
@@ -166,22 +166,20 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
module_issues = list(
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
)
issue_activity.apply_async(
args=[],
kwargs={
'type': "module.activity.deleted",
'requested_data': json.dumps({
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(pk),
"module_name": str(module.name),
"issues": [str(issue_id) for issue_id in module_issues],
}),
'actor_id': str(request.user.id),
'issue_id': None,
'project_id': str(project_id),
'current_instance': None,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
}
),
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
module.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -332,21 +330,21 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
)
# Capture Issue Activity
issue_activity.apply_async(
args=[],
kwargs={
'type': "module.activity.created",
'requested_data': json.dumps({"modules_list": str(issues)}),
'actor_id': str(self.request.user.id),
'issue_id': None,
'project_id': str(self.kwargs.get("project_id", None)),
'current_instance': json.dumps({
issue_activity.delay(
type="module.activity.created",
requested_data=json.dumps({"modules_list": str(issues)}),
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_module_issues": update_module_issue_activity,
"created_module_issues": serializers.serialize("json", record_to_create),
}),
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
"created_module_issues": serializers.serialize(
"json", record_to_create
),
}
),
epoch=int(timezone.now().timestamp()),
)
return Response(
@@ -359,20 +357,18 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id
)
module_issue.delete()
issue_activity.apply_async(
args=[], # If no positional arguments are required
kwargs={
'type': "module.activity.deleted",
'requested_data': json.dumps({
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(module_id),
"issues": [str(module_issue.issue_id)],
}),
'actor_id': str(request.user.id),
'issue_id': str(issue_id),
'project_id': str(project_id),
'current_instance': None,
'epoch': int(timezone.now().timestamp()),
},
routing_key='external',
}
),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
return Response(status=status.HTTP_204_NO_CONTENT)
+112 -147
View File
@@ -1,7 +1,5 @@
# Python imports
import uuid
import json
import requests
# Third party imports
from rest_framework import status
@@ -9,7 +7,7 @@ from rest_framework.response import Response
# Django imports
from django.db.models import Max, Q
from django.conf import settings
# Module imports
from plane.app.views import BaseAPIView
from plane.db.models import (
@@ -36,15 +34,20 @@ from plane.app.serializers import (
IssueFlatSerializer,
ModuleSerializer,
)
from plane.utils.integrations.github import get_github_repo_details
from plane.utils.importers.jira import jira_project_issue_summary
from plane.bgtasks.importer_task import service_importer
from plane.utils.html_processor import strip_tags
from plane.app.permissions import WorkSpaceAdminPermission
from plane.bgtasks.importer_task import service_importer
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
def get(self, request, slug, service):
if service == "github":
owner = request.GET.get("owner", False)
repo = request.GET.get("repo", False)
if not owner or not repo:
return Response(
{"error": "Owner and repo are required"},
@@ -55,10 +58,11 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
integration__provider="github", workspace__slug=slug
)
installtion_id = workspace_integration.config.get("installation_id", False)
access_tokens_url = workspace_integration.metadata.get(
"access_tokens_url", False
)
# Check for the installation id
if not installtion_id:
if not access_tokens_url:
return Response(
{
"error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app."
@@ -66,33 +70,18 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
# Request segway for the required information
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {
"owner": owner,
"repo": repo,
"installationId": installtion_id,
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
res.json(),
status=status.HTTP_200_OK,
)
return Response(
{"error": "Inetgration service is not available please try later"},
status=status.HTTP_400_BAD_REQUEST,
issue_count, labels, collaborators = get_github_repo_details(
access_tokens_url, owner, repo
)
return Response(
{
"issue_count": issue_count,
"labels": labels,
"collaborators": collaborators,
},
status=status.HTTP_200_OK,
)
if service == "jira":
# Check for all the keys
params = {
@@ -113,35 +102,16 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
email = request.GET.get("email", "")
cloud_hostname = request.GET.get("cloud_hostname", "")
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {
"project_key": project_key,
"api_token": api_token,
"email": email,
"cloud_hostname": cloud_hostname,
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/jira",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
res.json(),
status=status.HTTP_200_OK,
)
return Response(
{"error": "Inetgration service is not available please try later"},
status=status.HTTP_400_BAD_REQUEST,
response = jira_project_issue_summary(
email, api_token, project_key, cloud_hostname
)
if "error" in response:
return Response(response, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
response,
status=status.HTTP_200_OK,
)
return Response(
{"error": "Service not supported yet"},
status=status.HTTP_400_BAD_REQUEST,
@@ -152,21 +122,7 @@ class ImportServiceEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
def post(self, request, slug, service):
if service not in ["github", "jira"]:
return Response(
{"error": "Servivce not supported yet"},
status=status.HTTP_400_BAD_REQUEST,
)
if service == "github":
workspace_integration = WorkspaceIntegration.objects.get(
integration__provider="github", workspace__slug=slug
)
installation_id = workspace_integration.config.get("installation_id", False)
project_id = request.data.get("project_id", False)
if not project_id:
@@ -174,84 +130,87 @@ class ImportServiceEndpoint(BaseAPIView):
{"error": "Project ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
# Validate the data
data = request.data.get("data", False)
metadata = request.data.get("metadata", False)
config = request.data.get("config", False)
if not data or not metadata or not config:
return Response(
{"error": "Data, config and metadata are required"},
status=status.HTTP_400_BAD_REQUEST,
if service == "github":
data = request.data.get("data", False)
metadata = request.data.get("metadata", False)
config = request.data.get("config", False)
if not data or not metadata or not config:
return Response(
{"error": "Data, config and metadata are required"},
status=status.HTTP_400_BAD_REQUEST,
)
api_token = APIToken.objects.filter(
user=request.user, workspace=workspace
).first()
if api_token is None:
api_token = APIToken.objects.create(
user=request.user,
label="Importer",
workspace=workspace,
)
importer = Importer.objects.create(
service=service,
project_id=project_id,
status="queued",
initiated_by=request.user,
data=data,
metadata=metadata,
token=api_token,
config=config,
created_by=request.user,
updated_by=request.user,
)
# Update config
if config and service == "github":
config.update({"installation_id": installation_id})
service_importer.delay(service, importer.id)
serializer = ImporterSerializer(importer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
# Get the api token -- # derecated
api_token = APIToken.objects.filter(
user=request.user, workspace=workspace
).first()
if api_token is None:
api_token = APIToken.objects.create(
user=request.user,
label="Importer",
workspace=workspace,
if service == "jira":
data = request.data.get("data", False)
metadata = request.data.get("metadata", False)
config = request.data.get("config", False)
if not data or not metadata:
return Response(
{"error": "Data, config and metadata are required"},
status=status.HTTP_400_BAD_REQUEST,
)
api_token = APIToken.objects.filter(
user=request.user, workspace=workspace
).first()
if api_token is None:
api_token = APIToken.objects.create(
user=request.user,
label="Importer",
workspace=workspace,
)
importer = Importer.objects.create(
service=service,
project_id=project_id,
status="queued",
initiated_by=request.user,
data=data,
metadata=metadata,
token=api_token,
config=config,
created_by=request.user,
updated_by=request.user,
)
# Create an import
importer = Importer.objects.create(
service=service,
project_id=project_id,
status="queued",
initiated_by=request.user,
data=data,
metadata=metadata,
token=api_token,
config=config,
created_by=request.user,
updated_by=request.user,
service_importer.delay(service, importer.id)
serializer = ImporterSerializer(importer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(
{"error": "Servivce not supported yet"},
status=status.HTTP_400_BAD_REQUEST,
)
# Push it to segway
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {
"metadata": metadata,
"data": data,
"config": config,
"workspace_id": str(workspace.id),
"project_id": str(project_id),
"created_by": str(request.user.id),
"importer_id": str(importer.id),
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github/import",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
importer.status = "failed"
importer.reason = str(res.json())
importer.save()
else:
importer.status = "processing"
importer.save(update_fields=["status"])
else:
importer.status = "failed"
importer.reason = "Segway base url is not present"
importer.save(update_fields=["status", "reason"])
# return the response
serializer = ImporterSerializer(importer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug):
imports = (
Importer.objects.filter(workspace__slug=slug)
@@ -262,7 +221,9 @@ class ImportServiceEndpoint(BaseAPIView):
return Response(serializer.data)
def delete(self, request, slug, service, pk):
importer = Importer.objects.get(pk=pk, service=service, workspace__slug=slug)
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
if importer.imported_data is not None:
# Delete all imported Issues
@@ -280,7 +241,9 @@ class ImportServiceEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
def patch(self, request, slug, service, pk):
importer = Importer.objects.get(pk=pk, service=service, workspace__slug=slug)
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
serializer = ImporterSerializer(importer, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
@@ -516,7 +479,9 @@ class BulkImportModulesEndpoint(BaseAPIView):
[
ModuleLink(
module=module,
url=module_data.get("link", {}).get("url", "https://plane.so"),
url=module_data.get("link", {}).get(
"url", "https://plane.so"
),
title=module_data.get("link", {}).get(
"title", "Original Issue"
),
+11 -25
View File
@@ -1,11 +1,8 @@
# Python improts
import uuid
import requests
import json
# Django imports
from django.contrib.auth.hashers import make_password
from django.conf import settings
# Third party imports
from rest_framework.response import Response
@@ -30,7 +27,6 @@ from plane.utils.integrations.github import (
from plane.app.permissions import WorkSpaceAdminPermission
from plane.utils.integrations.slack import slack_oauth
class IntegrationViewSet(BaseViewSet):
serializer_class = IntegrationSerializer
model = Integration
@@ -50,7 +46,9 @@ class IntegrationViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = IntegrationSerializer(integration, data=request.data, partial=True)
serializer = IntegrationSerializer(
integration, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
@@ -96,30 +94,14 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
{"error": "Installation ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
# Push it to segway
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {"installationId": installation_id}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github/metadata",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
metadata = res.json()
metadata = get_github_metadata(installation_id)
config = {"installation_id": installation_id}
if provider == "slack":
code = request.data.get("code", False)
if not code:
return Response(
{"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
)
return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST)
slack_response = slack_oauth(code=code)
@@ -141,7 +123,9 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
is_password_autoset=True,
is_bot=True,
first_name=integration.title,
avatar=integration.avatar_url if integration.avatar_url is not None else "",
avatar=integration.avatar_url
if integration.avatar_url is not None
else "",
)
# Create an API Token for the bot user
@@ -177,7 +161,9 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
)
if workspace_integration.integration.provider == "github":
installation_id = workspace_integration.config.get("installation_id", False)
installation_id = workspace_integration.config.get(
"installation_id", False
)
if installation_id:
delete_github_installation(installation_id=installation_id)
@@ -1,15 +1,8 @@
# Python imports
import json
import requests
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from sentry_sdk import capture_exception
# Django imports
from django.conf import settings
# Module imports
from plane.app.views import BaseViewSet, BaseAPIView
from plane.db.models import (
@@ -42,32 +35,19 @@ class GithubRepositoriesEndpoint(BaseAPIView):
workspace__slug=slug, pk=workspace_integration_id
)
installation_id = workspace_integration.config.get("installation_id")
if not installation_id:
if workspace_integration.integration.provider != "github":
return Response(
{"error": "Not a github integration"},
status=status.HTTP_400_BAD_REQUEST,
)
# Push it to segway
if settings.SEGWAY_BASE_URL:
headers = {
"Content-Type": "application/json",
"x-api-key": settings.SEGWAY_KEY,
}
data = {
"installationId": installation_id,
"page": page,
}
res = requests.post(
f"{settings.SEGWAY_BASE_URL}/api/github/repos",
data=json.dumps(data),
headers=headers,
)
if "error" in res.json():
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
else:
return Response(res.json(), status=status.HTTP_200_OK)
access_tokens_url = workspace_integration.metadata["access_tokens_url"]
repositories_url = (
workspace_integration.metadata["repositories_url"]
+ f"?per_page=100&page={page}"
)
repositories = get_github_repos(access_tokens_url, repositories_url)
return Response(repositories, status=status.HTTP_200_OK)
class GithubRepositorySyncViewSet(BaseViewSet):
-1
View File
@@ -1 +0,0 @@
from .issue_sync_task import issue_sync
@@ -373,7 +373,7 @@ def generate_non_segmented_rows(
return [tuple(row_zero)] + rows
@shared_task(queue='internal_tasks')
@shared_task
def analytic_export_task(email, data, slug):
try:
filters = issue_filters(data, "POST")
@@ -29,7 +29,7 @@ def posthogConfiguration():
return None, None
@shared_task(queue='internal_tasks')
@shared_task
def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
try:
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
@@ -54,7 +54,7 @@ def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
capture_exception(e)
@shared_task(queue='internal_tasks')
@shared_task
def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_from):
try:
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
+1 -1
View File
@@ -259,7 +259,7 @@ def generate_xlsx(header, project_id, issues, files):
files.append((f"{project_id}.xlsx", xlsx_file))
@shared_task(queue='internal_tasks')
@shared_task
def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, slug):
try:
exporter_instance = ExporterHistory.objects.get(token=token_id)
@@ -15,7 +15,7 @@ from botocore.client import Config
from plane.db.models import ExporterHistory
@shared_task(queue='internal_tasks')
@shared_task
def delete_old_s3_link():
# Get a list of keys and IDs to process
expired_exporter_history = ExporterHistory.objects.filter(
+1 -1
View File
@@ -12,7 +12,7 @@ from celery import shared_task
from plane.db.models import FileAsset
@shared_task(queue='internal_tasks')
@shared_task
def delete_file_asset():
# file assets to delete
@@ -17,7 +17,7 @@ from sentry_sdk import capture_exception
from plane.license.utils.instance_value import get_email_configuration
@shared_task(queue='internal_tasks')
@shared_task
def forgot_password(first_name, email, uidb64, token, current_site):
try:
relative_link = (
+159 -426
View File
@@ -1,467 +1,200 @@
# Python imports
import json
import requests
import uuid
# Django imports
from django.db.models import Q, Max
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.auth.hashers import make_password
# Third Party imports
from celery import shared_task
from celery.exceptions import MaxRetriesExceededError
from sentry_sdk import capture_exception
# Module imports
from plane.app.serializers import ImporterSerializer
from plane.db.models import (
Importer,
WorkspaceMember,
GithubRepositorySync,
GithubRepository,
ProjectMember,
WorkspaceIntegration,
Label,
User,
IssueProperty,
IssueAssignee,
IssueLabel,
IssueSequence,
IssueActivity,
IssueComment,
IssueLink,
ModuleIssue,
State,
Module,
Issue,
Cycle,
)
from plane.bgtasks.user_welcome_task import send_welcome_slack
@shared_task(queue="internal_tasks")
@shared_task
def service_importer(service, importer_id):
pass
## Utility functions
def get_label_id(name, data):
try:
existing_label = (
Label.objects.filter(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
name__iexact=name,
)
.values("id")
.first()
)
return existing_label
except Label.DoesNotExist:
return None
importer = Importer.objects.get(pk=importer_id)
importer.status = "processing"
importer.save()
users = importer.data.get("users", [])
def get_state_id(name, data):
try:
existing_state = (
State.objects.filter(
name__iexact=name,
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
.values("id")
.first()
)
return existing_state
except State.DoesNotExist:
return None
def get_user_id(name):
try:
existing_user = User.objects.filter(email=name).values("id").first()
return existing_user
except User.DoesNotExist:
return None
def update_imported_items(importer_id, entity, entity_id):
importer = Importer.objects.get(pk=importer_id)
if importer.imported_data:
importer.imported_data.setdefault(str(entity), []).append(str(entity_id))
else:
importer.imported_data = {
str(entity): [str(entity_id)]
}
importer.save()
## Sync functions
def members_sync(data):
try:
user = User.objects.get(email=data.get("email"))
_ = WorkspaceMember.objects.get_or_create(
member_id=user.id, workspace_id=data.get("workspace_id")
)
_ = ProjectMember.objects.get_or_create(
member_id=user.id,
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
_ = IssueProperty.objects.get_or_create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
user_id=user.id,
created_by_id=data.get("created_by"),
)
except User.DoesNotExist:
# For all invited users create the users
new_user = User.objects.create(
email=data.get("email").strip().lower(),
username=uuid.uuid4().hex,
password=make_password(uuid.uuid4().hex),
is_password_autoset=True,
)
service = data.get("external_source")
WorkspaceMember.objects.create(
member_id=new_user.id,
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by"),
)
ProjectMember.objects.create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
member_id=new_user.id,
created_by_id=data.get("created_by"),
)
IssueProperty.objects.create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
user_id=new_user.id,
created_by_id=data.get("created_by"),
)
if data.get("source", False) == "slack":
send_welcome_slack.delay(
str(new_user.id),
True,
f"{new_user.email} was imported to Plane from {service}",
# Check if we need to import users as well
if len(users):
# For all invited users create the users
new_users = User.objects.bulk_create(
[
User(
email=user.get("email").strip().lower(),
username=uuid.uuid4().hex,
password=make_password(uuid.uuid4().hex),
is_password_autoset=True,
)
for user in users
if user.get("import", False) == "invite"
],
batch_size=10,
ignore_conflicts=True,
)
_ = [
send_welcome_slack.delay(
str(user.id),
True,
f"{user.email} was imported to Plane from {service}",
)
for user in new_users
]
def label_sync(data):
existing_label = Label.objects.filter(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
name__iexact=data.get("name"),
external_id=data.get("external_id", None),
external_source=data.get("external_source"),
)
if not existing_label.exists() and data.get("name"):
label = Label.objects.create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
name=data.get("name"),
color=data.get("color"),
created_by_id=data.get("created_by"),
external_id=data.get("external_id", None),
external_source=data.get("external_source"),
)
update_imported_items(data.get("importer_id"), "labels", label.id)
def state_sync(data):
try:
state = State.objects.get(
external_id=data.get("external_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
except State.DoesNotExist:
existing_states = State.objects.filter(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
group=data.get("state_group"),
name__iexact=data.get("state_name"),
)
if existing_states.exists():
existing_state = existing_states.first()
existing_state.external_id = data.get("external_id")
existing_state.external_source = data.get("external_source")
existing_state.save()
else:
state = State.objects.create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
name=data.get("state_name"),
group=data.get("state_group"),
created_by_id=data.get("created_by"),
external_id=data.get("external_id"),
external_source=data.get("external_source"),
workspace_users = User.objects.filter(
email__in=[
user.get("email").strip().lower()
for user in users
if user.get("import", False) == "invite"
or user.get("import", False) == "map"
]
)
update_imported_items(data.get("importer_id"), "states", state.id)
# Check if any of the users are already member of workspace
_ = WorkspaceMember.objects.filter(
member__in=[user for user in workspace_users],
workspace_id=importer.workspace_id,
).update(is_active=True)
def issue_sync(data):
try:
issue = Issue.objects.get(
external_id=data.get("external_id"),
external_source=data.get("external_source"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
except Issue.DoesNotExist:
# Get the default state
default_state = State.objects.filter(
~Q(name="Triage"), project_id=data.get("project_id"), default=True
).first()
# Add new users to Workspace and project automatically
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
member=user,
workspace_id=importer.workspace_id,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
# if there is no default state assign any random state
if default_state is None:
default_state = State.objects.filter(
~Q(name="Triage"), project_id=data.get("project_id")
ProjectMember.objects.bulk_create(
[
ProjectMember(
project_id=importer.project_id,
workspace_id=importer.workspace_id,
member=user,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
IssueProperty.objects.bulk_create(
[
IssueProperty(
project_id=importer.project_id,
workspace_id=importer.workspace_id,
user=user,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
# Check if sync config is on for github importers
if service == "github" and importer.config.get("sync", False):
name = importer.metadata.get("name", False)
url = importer.metadata.get("url", False)
config = importer.metadata.get("config", {})
owner = importer.metadata.get("owner", False)
repository_id = importer.metadata.get("repository_id", False)
workspace_integration = WorkspaceIntegration.objects.get(
workspace_id=importer.workspace_id, integration__provider="github"
)
# Delete the old repository object
GithubRepositorySync.objects.filter(project_id=importer.project_id).delete()
GithubRepository.objects.filter(project_id=importer.project_id).delete()
# Create a Label for github
label = Label.objects.filter(
name="GitHub", project_id=importer.project_id
).first()
# Get the maximum sequence_id
last_id = IssueSequence.objects.filter(
project_id=data.get("project_id")
).aggregate(largest=Max("sequence"))["largest"]
if label is None:
label = Label.objects.create(
name="GitHub",
project_id=importer.project_id,
description="Label to sync Plane issues with GitHub issues",
color="#003773",
)
# Create repository
repo = GithubRepository.objects.create(
name=name,
url=url,
config=config,
repository_id=repository_id,
owner=owner,
project_id=importer.project_id,
)
last_id = 1 if last_id is None else last_id + 1
# Create repo sync
_ = GithubRepositorySync.objects.create(
repository=repo,
workspace_integration=workspace_integration,
actor=workspace_integration.actor,
credentials=importer.data.get("credentials", {}),
project_id=importer.project_id,
label=label,
)
# Get the maximum sort order
largest_sort_order = Issue.objects.filter(
project_id=data.get("project_id"), state=default_state
).aggregate(largest=Max("sort_order"))["largest"]
# Add bot as a member in the project
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor,
role=20,
project_id=importer.project_id,
)
largest_sort_order = (
65535 if largest_sort_order is None else largest_sort_order + 10000
)
parent_id = None
if data.get("parent_id", False):
parent_id = Issue.objects.filter(
external_id=data.get("parent_id"),
external_source=data.get("external_source"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
).values("id")
if settings.PROXY_BASE_URL:
headers = {"Content-Type": "application/json"}
import_data_json = json.dumps(
ImporterSerializer(importer).data,
cls=DjangoJSONEncoder,
)
_ = requests.post(
f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/",
json=import_data_json,
headers=headers,
)
# Issues
issue = Issue.objects.create(
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
state_id=get_state_id(data.get("state"), data).get("id")
if get_state_id(data.get("state"), data)
else default_state.id,
name=data.get("name", "Issue Created through Importer")[:255],
description_html=data.get("description_html", "<p></p>"),
sequence_id=last_id,
sort_order=largest_sort_order,
start_date=data.get("start_date", None),
target_date=data.get("target_date", None),
priority=data.get("priority", "none"),
created_by_id=data.get("created_by_id"),
external_id=data.get("external_id"),
external_source=data.get("external_source"),
parent_id=parent_id,
)
# Sequences
_ = IssueSequence.objects.create(
issue=issue,
sequence=issue.sequence_id,
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
# Attach Links
_ = IssueLink.objects.create(
issue=issue,
url=data.get("link", {}).get("url", "https://github.com"),
title=data.get("link", {}).get("title", "Original Issue"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by_id"),
)
# Track the issue activities
_ = IssueActivity.objects.create(
issue=issue,
actor_id=data.get("created_by_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
comment=f"imported the issue from {data.get('external_source')}",
verb="created",
created_by_id=data.get("created_by_id"),
)
update_imported_items(data.get("importer_id"), "issues", issue.id)
def issue_label_sync(data):
issue = Issue.objects.get(
external_source=data.get("external_issue_source"),
external_id=data.get("external_issue_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
if get_label_id(data.get("name"), data):
IssueLabel.objects.create(
issue=issue,
label_id=get_label_id(data.get("name"), data).get("id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by_id"),
)
def issue_assignee_sync(data):
issue = Issue.objects.get(
external_source=data.get("external_issue_source"),
external_id=data.get("external_issue_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
user = User.objects.filter(email=data.get("email")).values("id")
IssueAssignee.objects.create(
issue=issue,
assignee_id=user,
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by_id"),
)
def issue_comment_sync(data):
# Create Comments
issue = Issue.objects.get(
external_source=data.get("external_issue_source"),
external_id=data.get("external_issue_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
IssueComment.objects.create(
issue=issue,
comment_html=data.get("comment_html", "<p></p>"),
actor_id=data.get("created_by_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=get_user_id(data.get("created_by_id")).get("id")
if get_user_id(data.get("created_by_id"))
else data.get("created_by_id"),
external_id=data.get("external_id"),
external_source=data.get("external_source"),
)
def cycles_sync(data):
try:
_ = Cycle.objects.get(
external_id=data.get("external_id"),
external_source=data.get("external_source"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
except Cycle.DoesNotExist:
cycle = Cycle.objects.create(
name=data.get("name"),
description_html=data.get("description_html", "<p></p>"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by"),
external_id=data.get("external_id"),
external_source=data.get("external_source"),
)
update_imported_items(data.get("importer_id"), "cycles", cycle.id)
def module_sync(data):
try:
_ = Module.objects.get(
external_id=data.get("external_id"),
external_source=data.get("external_source"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
except Module.DoesNotExist:
module = Module.objects.create(
name=data.get("name"),
description_html=data.get("description_html", "<p></p>"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by"),
external_id=data.get("external_id"),
external_source=data.get("external_source"),
)
update_imported_items(data.get("importer_id"), "modules", module.id)
def modules_issue_sync(data):
module = Module.objects.get(
external_id=data.get("module_id"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
external_source=data.get("external_source"),
)
issue = Issue.objects.get(
external_id=data.get("issue_id"),
external_source=data.get("external_source"),
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
)
_ = ModuleIssue.objects.create(
module=module,
issue=issue,
project_id=data.get("project_id"),
workspace_id=data.get("workspace_id"),
created_by_id=data.get("created_by"),
)
def import_sync(data):
importer = Importer.objects.get(pk=data.get("importer_id"))
importer.status = data.get("status")
importer.save(update_fields=["status"])
@shared_task(bind=True, queue="segway_task", max_retries=5)
def import_task(self, data):
type = data.get("type")
if type is None:
return
TYPE_MAPPER = {
"member.sync": members_sync,
"label.sync": label_sync,
"state.sync": state_sync,
"issue.sync": issue_sync,
"issue.label.sync": issue_label_sync,
"issue.assignee.sync": issue_assignee_sync,
"issue.comment.sync": issue_comment_sync,
"cycle.sync": cycles_sync,
"module.sync": module_sync,
"module.issue.sync": modules_issue_sync,
"import.sync": import_sync,
}
try:
func = TYPE_MAPPER.get(type)
if func is None:
return
# Call the function
func(data)
return
except Exception as e:
try:
# Retry with exponential backoff
self.retry(exc=e, countdown=50, backoff=2)
except MaxRetriesExceededError:
# For max retries reached items fail the import
importer = Importer.objects.get(pk=data.get("importer_id"))
importer.status = "failed"
importer.reason = e
importer.save()
importer = Importer.objects.get(pk=importer_id)
importer.status = "failed"
importer.save()
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
capture_exception(e)
return
@@ -1460,7 +1460,7 @@ def delete_draft_issue_activity(
# Receive message from room group
@shared_task(queue='internal_tasks')
@shared_task
def issue_activity(
type,
requested_data,
@@ -16,7 +16,7 @@ from plane.db.models import Issue, Project, State
from plane.bgtasks.issue_activites_task import issue_activity
@shared_task(queue='internal_tasks')
@shared_task
def archive_and_close_old_issues():
archive_old_issues()
close_old_issues()
@@ -1,5 +0,0 @@
from celery import shared_task
@shared_task(queue="segway_tasks")
def issue_sync(data):
print(f"Received data from Segway: {data}")
@@ -17,7 +17,7 @@ from sentry_sdk import capture_exception
from plane.license.utils.instance_value import get_email_configuration
@shared_task(queue='internal_tasks')
@shared_task
def magic_link(email, key, token, current_site):
try:
(
+1 -1
View File
@@ -183,7 +183,7 @@ def createMentionNotification(project, notification_comment, issue, actor_id, me
)
@shared_task(queue='internal_tasks')
@shared_task
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance):
issue_activities_created = (
json.loads(
@@ -15,7 +15,7 @@ from sentry_sdk import capture_exception
from plane.db.models import Project, User, ProjectMemberInvite
from plane.license.utils.instance_value import get_email_configuration
@shared_task(queue='internal_tasks')
@shared_task
def project_invitation(email, project_id, token, current_site, invitor):
try:
user = User.objects.get(email=invitor)
+1 -1
View File
@@ -11,7 +11,7 @@ from slack_sdk.errors import SlackApiError
from plane.db.models import User
@shared_task(queue='internal_tasks')
@shared_task
def send_welcome_slack(user_id, created, message):
try:
instance = User.objects.get(pk=user_id)
+1 -2
View File
@@ -71,7 +71,6 @@ def get_model_data(event, event_id, many=False):
retry_backoff=600,
max_retries=5,
retry_jitter=True,
queue='internal_tasks'
)
def webhook_task(self, webhook, slug, event, event_data, action):
try:
@@ -162,7 +161,7 @@ def webhook_task(self, webhook, slug, event, event_data, action):
return
@shared_task(queue='internal_tasks')
@shared_task()
def send_webhook(event, payload, kw, action, slug, bulk):
try:
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
@@ -20,7 +20,7 @@ from plane.db.models import Workspace, WorkspaceMemberInvite, User
from plane.license.utils.instance_value import get_email_configuration
@shared_task(queue='internal_tasks')
@shared_task
def workspace_invitation(email, workspace_id, token, current_site, invitor):
try:
user = User.objects.get(email=invitor)
+1 -1
View File
@@ -8,7 +8,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
ri = redis_instance()
app = Celery('tasks', broker='pyamqp://guest:guest@localhost:5672//')
app = Celery("plane")
# Using a string here means the worker will not have to
# pickle the object when using Windows.
@@ -1,78 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-20 14:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0050_user_use_case_alter_workspace_organization_size'),
]
operations = [
migrations.AddField(
model_name='cycle',
name='external_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='cycle',
name='external_source',
field=models.CharField(blank=True, null=True),
),
migrations.AddField(
model_name='importer',
name='reason',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='issue',
name='external_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='issue',
name='external_source',
field=models.CharField(blank=True, null=True),
),
migrations.AddField(
model_name='issuecomment',
name='external_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='issuecomment',
name='external_source',
field=models.CharField(blank=True, null=True),
),
migrations.AddField(
model_name='label',
name='external_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='label',
name='external_source',
field=models.CharField(blank=True, null=True),
),
migrations.AddField(
model_name='module',
name='external_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='module',
name='external_source',
field=models.CharField(blank=True, null=True),
),
migrations.AddField(
model_name='state',
name='external_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='state',
name='external_source',
field=models.CharField(blank=True, null=True),
),
]
+2
View File
@@ -22,6 +22,8 @@ class BaseModel(AuditModel):
user = get_current_user()
if user is None or user.is_anonymous:
self.created_by = None
self.updated_by = None
super(BaseModel, self).save(*args, **kwargs)
else:
# Check if the model is being created or updated
+3 -5
View File
@@ -18,8 +18,6 @@ class Cycle(ProjectBaseModel):
)
view_props = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
external_source = models.CharField(null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
class Meta:
verbose_name = "Cycle"
@@ -29,9 +27,9 @@ class Cycle(ProjectBaseModel):
def save(self, *args, **kwargs):
if self._state.adding:
smallest_sort_order = Cycle.objects.filter(project=self.project).aggregate(
smallest=models.Min("sort_order")
)["smallest"]
smallest_sort_order = Cycle.objects.filter(
project=self.project
).aggregate(smallest=models.Min("sort_order"))["smallest"]
if smallest_sort_order is not None:
self.sort_order = smallest_sort_order - 10000
-1
View File
@@ -34,7 +34,6 @@ class Importer(ProjectBaseModel):
"db.APIToken", on_delete=models.CASCADE, related_name="importer"
)
imported_data = models.JSONField(null=True)
reason = models.TextField(blank=True)
class Meta:
verbose_name = "Importer"
+4 -11
View File
@@ -102,8 +102,6 @@ class Issue(ProjectBaseModel):
completed_at = models.DateTimeField(null=True)
archived_at = models.DateField(null=True)
is_draft = models.BooleanField(default=False)
external_source = models.CharField(null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
objects = models.Manager()
issue_objects = IssueManager()
@@ -134,6 +132,7 @@ class Issue(ProjectBaseModel):
except ImportError:
pass
if self._state.adding:
# Get the maximum display_id value from the database
last_id = IssueSequence.objects.filter(project=self.project).aggregate(
@@ -211,9 +210,8 @@ class IssueRelation(ProjectBaseModel):
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.related_issue.name}"
return f"{self.issue.name} {self.related_issue.name}"
class IssueMention(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_mention"
@@ -223,7 +221,6 @@ class IssueMention(ProjectBaseModel):
on_delete=models.CASCADE,
related_name="issue_mention",
)
class Meta:
unique_together = ["issue", "mention"]
verbose_name = "Issue Mention"
@@ -232,7 +229,7 @@ class IssueMention(ProjectBaseModel):
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.mention.email}"
return f"{self.issue.name} {self.mention.email}"
class IssueAssignee(ProjectBaseModel):
@@ -369,8 +366,6 @@ class IssueComment(ProjectBaseModel):
default="INTERNAL",
max_length=100,
)
external_source = models.CharField(null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
def save(self, *args, **kwargs):
self.comment_stripped = (
@@ -421,8 +416,6 @@ class Label(ProjectBaseModel):
description = models.TextField(blank=True)
color = models.CharField(max_length=255, blank=True)
sort_order = models.FloatField(default=65535)
external_source = models.CharField(null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
class Meta:
unique_together = ["name", "project"]
-2
View File
@@ -41,8 +41,6 @@ class Module(ProjectBaseModel):
)
view_props = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
external_source = models.CharField(null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
class Meta:
unique_together = ["name", "project"]
-2
View File
@@ -24,8 +24,6 @@ class State(ProjectBaseModel):
max_length=20,
)
default = models.BooleanField(default=False)
external_source = models.CharField(null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
def __str__(self):
"""Return name of the state"""
+17 -34
View File
@@ -5,7 +5,6 @@ import ssl
import certifi
from datetime import timedelta
from urllib.parse import urlparse
from kombu import Exchange, Queue
# Django imports
from django.core.management.utils import get_random_secret_key
@@ -149,9 +148,6 @@ else:
REDIS_URL = os.environ.get("REDIS_URL")
REDIS_SSL = REDIS_URL and "rediss" in REDIS_URL
# RabbitMq Config
RABBITMQ_URL = os.environ.get("RABBITMQ_URL")
if REDIS_SSL:
CACHES = {
"default": {
@@ -274,28 +270,18 @@ SIMPLE_JWT = {
# Celery Configuration
CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_SERIALIZER = "json"
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_ACCEPT_CONTENT = ["application/json"]
CELERY_BROKER_URL = RABBITMQ_URL
CELERY_RESULT_BACKEND = REDIS_URL
CELERY_QUEUES = (
Queue(
"internal_tasks",
Exchange("internal_exchange", type="direct"),
routing_key="internal",
),
Queue(
"external_tasks",
Exchange("external_exchange", type="direct"),
routing_key="external",
),
Queue(
"segway_tasks",
Exchange("segway_exchange", type="direct"),
routing_key="segway",
),
)
if REDIS_SSL:
redis_url = os.environ.get("REDIS_URL")
broker_url = (
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
)
CELERY_BROKER_URL = broker_url
CELERY_RESULT_BACKEND = broker_url
else:
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
CELERY_IMPORTS = (
"plane.bgtasks.issue_automation_task",
@@ -305,9 +291,7 @@ CELERY_IMPORTS = (
# Sentry Settings
# Enable Sentry Settings
if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get(
"SENTRY_DSN"
).startswith("https://"):
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=[
@@ -343,11 +327,10 @@ USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
# instance key
INSTANCE_KEY = os.environ.get(
"INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3"
)
# Skip environment variable configuration
SKIP_ENV_VAR = os.environ.get("SKIP_ENV_VAR", "1") == "1"
DATA_UPLOAD_MAX_MEMORY_SIZE = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
# Segway
SEGWAY_BASE_URL = os.environ.get("SEGWAY_BASE_URL", "http://localhost:9000")
SEGWAY_KEY = os.environ.get("SEGWAY_KEY", False)
+21 -15
View File
@@ -28,22 +28,28 @@
"react-dom": "18.2.0"
},
"dependencies": {
"@tiptap/core": "^2.1.13",
"@plane/editor-types": "*",
"@tiptap/core": "^2.1.7",
"@tiptap/extension-blockquote": "^2.1.13",
"@tiptap/extension-code-block-lowlight": "^2.1.13",
"@tiptap/extension-color": "^2.1.13",
"@tiptap/extension-image": "^2.1.13",
"@tiptap/extension-link": "^2.1.13",
"@tiptap/extension-list-item": "^2.1.13",
"@tiptap/extension-mention": "^2.1.13",
"@tiptap/extension-task-item": "^2.1.13",
"@tiptap/extension-task-list": "^2.1.13",
"@tiptap/extension-text-style": "^2.1.13",
"@tiptap/extension-underline": "^2.1.13",
"@tiptap/pm": "^2.1.13",
"@tiptap/react": "^2.1.13",
"@tiptap/starter-kit": "^2.1.13",
"@tiptap/suggestion": "^2.0.13",
"@tiptap/extension-code-block-lowlight": "^2.1.12",
"@tiptap/extension-color": "^2.1.11",
"@tiptap/extension-image": "^2.1.7",
"@tiptap/extension-link": "^2.1.7",
"@tiptap/extension-list-item": "^2.1.12",
"@tiptap/extension-mention": "^2.1.12",
"@tiptap/extension-table": "^2.1.6",
"@tiptap/extension-table-cell": "^2.1.6",
"@tiptap/extension-table-header": "^2.1.6",
"@tiptap/extension-table-row": "^2.1.6",
"@tiptap/extension-task-item": "^2.1.7",
"@tiptap/extension-task-list": "^2.1.7",
"@tiptap/extension-text-style": "^2.1.11",
"@tiptap/extension-underline": "^2.1.7",
"@tiptap/pm": "^2.1.7",
"@tiptap/prosemirror-tables": "^1.1.4",
"@tiptap/react": "^2.1.7",
"@tiptap/starter-kit": "^2.1.10",
"@tiptap/suggestion": "^2.0.4",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"highlight.js": "^11.8.0",
+12 -21
View File
@@ -1,32 +1,23 @@
// styles
// import "./styles/tailwind.css";
import "src/styles/editor.css";
import "src/styles/table.css";
import "src/styles/github-dark.css";
// import "./styles/editor.css";
import "./styles/github-dark.css";
export { isCellSelection } from "src/ui/extensions/table/table/utilities/is-cell-selection";
export { isCellSelection } from "./ui/extensions/table/table/utilities/is-cell-selection";
// utils
export * from "src/lib/utils";
export * from "src/ui/extensions/table/table";
export { startImageUpload } from "src/ui/plugins/upload-image";
export * from "./lib/utils";
export * from "./ui/extensions/table/table";
export { startImageUpload } from "./ui/plugins/upload-image";
// components
export { EditorContainer } from "src/ui/components/editor-container";
export { EditorContentWrapper } from "src/ui/components/editor-content";
export { EditorContainer } from "./ui/components/editor-container";
export { EditorContentWrapper } from "./ui/components/editor-content";
// hooks
export { useEditor } from "src/hooks/use-editor";
export { useReadOnlyEditor } from "src/hooks/use-read-only-editor";
export { useEditor } from "./ui/hooks/use-editor";
export { useReadOnlyEditor } from "./ui/hooks/use-read-only-editor";
// helper items
export * from "src/ui/menus/menu-items";
export * from "src/lib/editor-commands";
// types
export type { DeleteImage } from "src/types/delete-image";
export type { UploadImage } from "src/types/upload-image";
export type { RestoreImage } from "src/types/restore-image";
export type { IMentionHighlight, IMentionSuggestion } from "src/types/mention-suggestion";
export type { ISlashCommandItem, CommandProps } from "src/types/slash-commands-suggestion";
export type { LucideIconType } from "src/types/lucide-icon";
export * from "./ui/menus/menu-items";
export * from "./lib/editor-commands";
@@ -1,7 +1,7 @@
import { UploadImage } from "@plane/editor-types";
import { Editor, Range } from "@tiptap/core";
import { startImageUpload } from "src/ui/plugins/upload-image";
import { findTableAncestor } from "src/lib/utils";
import { UploadImage } from "src/types/upload-image";
import { startImageUpload } from "../ui/plugins/upload-image";
import { findTableAncestor } from "./utils";
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
+8 -50
View File
@@ -6,12 +6,6 @@
height: 0;
}
/* block quotes */
.ProseMirror blockquote p::before,
.ProseMirror blockquote p::after {
display: none;
}
.ProseMirror .is-empty::before {
content: attr(data-placeholder);
float: left;
@@ -21,10 +15,9 @@
}
/* Custom image styles */
.ProseMirror img {
transition: filter 0.1s ease-in-out;
margin-top: 0 !important;
margin-bottom: 0 !important;
&:hover {
cursor: pointer;
@@ -60,12 +53,11 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
background-color: rgb(var(--color-background-100));
margin: 0;
cursor: pointer;
width: 0.8rem;
height: 0.8rem;
width: 1.2rem;
height: 1.2rem;
position: relative;
border: 1.5px solid rgb(var(--color-text-100));
margin-right: 0.2rem;
margin-top: 0.15rem;
border: 2px solid rgb(var(--color-text-100));
margin-right: 0.3rem;
display: grid;
place-content: center;
@@ -79,8 +71,8 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
&::before {
content: "";
width: 0.5em;
height: 0.5em;
width: 0.65em;
height: 0.65em;
transform: scale(0);
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em;
@@ -141,8 +133,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
.img-placeholder {
position: relative;
width: 35%;
margin-top: 0 !important;
margin-bottom: 0 !important;
&:before {
content: "";
@@ -169,8 +159,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
table {
border-collapse: collapse;
table-layout: fixed;
margin: 0.5em 0 0.5em 0;
margin: 0;
border: 1px solid rgb(var(--color-border-200));
width: 100%;
@@ -240,34 +229,3 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
.ProseMirror table * .is-empty::before {
opacity: 0;
}
.ProseMirror pre {
background: rgba(var(--color-background-80));
border-radius: 0.5rem;
color: rgba(var(--color-text-100));
font-family: "JetBrainsMono", monospace;
padding: 0.75rem 1rem;
}
.ProseMirror pre code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
div[data-type="horizontalRule"] {
line-height: 0;
padding: 0.25rem 0;
margin-top: 0;
margin-bottom: 0;
& > div {
border-bottom: 1px solid rgb(var(--color-text-100));
}
}
/* image resizer */
.moveable-control-box {
z-index: 10 !important;
}
@@ -1,3 +0,0 @@
import { Smile } from "lucide-react";
export type LucideIconType = typeof Smile;
@@ -1,6 +1,6 @@
import { Editor, EditorContent } from "@tiptap/react";
import { ReactNode } from "react";
import { ImageResizer } from "src/ui/extensions/image/image-resize";
import { ImageResizer } from "../extensions/image/image-resize";
interface EditorContentProps {
editor: Editor | null;
@@ -1,7 +1,7 @@
import { getNodeAtPosition } from "@tiptap/core";
import { EditorState } from "@tiptap/pm/state";
import { findListItemPos } from "src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos";
import { findListItemPos } from "./find-list-item-pos";
export const getNextListDepth = (typeOrName: string, state: EditorState) => {
const listItemPos = findListItemPos(typeOrName, state);
@@ -1,8 +1,8 @@
import { Editor, isAtStartOfNode, isNodeActive } from "@tiptap/core";
import { Node } from "@tiptap/pm/model";
import { findListItemPos } from "src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos";
import { hasListBefore } from "src/ui/extensions/custom-list-keymap/list-helpers/has-list-before";
import { findListItemPos } from "./find-list-item-pos";
import { hasListBefore } from "./has-list-before";
export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => {
// this is required to still handle the undo handling
@@ -1,7 +1,7 @@
import { Editor, isAtEndOfNode, isNodeActive } from "@tiptap/core";
import { nextListIsDeeper } from "src/ui/extensions/custom-list-keymap/list-helpers/next-list-is-deeper";
import { nextListIsHigher } from "src/ui/extensions/custom-list-keymap/list-helpers/next-list-is-higher";
import { nextListIsDeeper } from "./next-list-is-deeper";
import { nextListIsHigher } from "./next-list-is-higher";
export const handleDelete = (editor: Editor, name: string) => {
// if the cursor is not inside the current node type
@@ -1,7 +1,7 @@
import { EditorState } from "@tiptap/pm/state";
import { findListItemPos } from "src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos";
import { getNextListDepth } from "src/ui/extensions/custom-list-keymap/list-helpers/get-next-list-depth";
import { findListItemPos } from "./find-list-item-pos";
import { getNextListDepth } from "./get-next-list-depth";
export const nextListIsDeeper = (typeOrName: string, state: EditorState) => {
const listDepth = getNextListDepth(typeOrName, state);
@@ -1,7 +1,7 @@
import { EditorState } from "@tiptap/pm/state";
import { findListItemPos } from "src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos";
import { getNextListDepth } from "src/ui/extensions/custom-list-keymap/list-helpers/get-next-list-depth";
import { findListItemPos } from "./find-list-item-pos";
import { getNextListDepth } from "./get-next-list-depth";
export const nextListIsHigher = (typeOrName: string, state: EditorState) => {
const listDepth = getNextListDepth(typeOrName, state);
@@ -1,6 +1,6 @@
import { Extension } from "@tiptap/core";
import { handleBackspace, handleDelete } from "src/ui/extensions/custom-list-keymap/list-helpers";
import { handleBackspace, handleDelete } from "./list-helpers";
export type ListKeymapOptions = {
listTypes: Array<{
@@ -22,7 +22,7 @@ declare module "@tiptap/core" {
}
}
export const HorizontalRule = Node.create<HorizontalRuleOptions>({
export default Node.create<HorizontalRuleOptions>({
name: "horizontalRule",
addOptions() {
@@ -1,10 +1,9 @@
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { UploadImagesPlugin } from "src/ui/plugins/upload-image";
import UploadImagesPlugin from "../../plugins/upload-image";
import ImageExt from "@tiptap/extension-image";
import { onNodeDeleted, onNodeRestored } from "src/ui/plugins/delete-image";
import { DeleteImage } from "src/types/delete-image";
import { RestoreImage } from "src/types/restore-image";
import { onNodeDeleted, onNodeRestored } from "../../plugins/delete-image";
import { DeleteImage, RestoreImage } from "@plane/editor-types";
interface ImageNode extends ProseMirrorNode {
attrs: {
@@ -16,7 +15,7 @@ interface ImageNode extends ProseMirrorNode {
const deleteKey = new PluginKey("delete-image");
const IMAGE_NODE_TYPE = "image";
export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
ImageExt.extend({
addProseMirrorPlugins() {
return [
@@ -131,3 +130,5 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma
};
},
});
export default ImageExtension;
@@ -1,6 +1,6 @@
import Image from "@tiptap/extension-image";
export const ReadOnlyImageExtension = Image.extend({
const ReadOnlyImageExtension = Image.extend({
addAttributes() {
return {
...this.parent?.(),
@@ -13,3 +13,5 @@ export const ReadOnlyImageExtension = Image.extend({
};
},
});
export default ReadOnlyImageExtension;
@@ -7,25 +7,22 @@ import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
import { Markdown } from "tiptap-markdown";
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { Table } from "src/ui/extensions/table/table";
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
import TableHeader from "./table/table-header/table-header";
import Table from "./table/table";
import TableCell from "./table/table-cell/table-cell";
import TableRow from "./table/table-row/table-row";
import HorizontalRule from "./horizontal-rule";
import { ImageExtension } from "src/ui/extensions/image";
import ImageExtension from "./image";
import { isValidHttpUrl } from "src/lib/utils";
import { Mentions } from "src/ui/mentions";
import { isValidHttpUrl } from "../../lib/utils";
import { Mentions } from "../mentions";
import { CustomKeymap } from "src/ui/extensions/keymap";
import { CustomCodeBlock } from "src/ui/extensions/code";
import { CustomQuoteExtension } from "src/ui/extensions/quote";
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
import { DeleteImage } from "src/types/delete-image";
import { IMentionSuggestion } from "src/types/mention-suggestion";
import { RestoreImage } from "src/types/restore-image";
import { CustomKeymap } from "./keymap";
import { CustomCodeBlock } from "./code";
import { CustomQuoteExtension } from "./quote";
import { ListKeymap } from "./custom-list-keymap";
import { IMentionSuggestion, DeleteImage, RestoreImage } from "@plane/editor-types";
export const CoreEditorExtensions = (
mentionConfig: {
@@ -1 +1 @@
export { TableCell } from "./table-cell";
export { default as default } from "./table-cell";
@@ -4,7 +4,7 @@ export interface TableCellOptions {
HTMLAttributes: Record<string, any>;
}
export const TableCell = Node.create<TableCellOptions>({
export default Node.create<TableCellOptions>({
name: "tableCell",
addOptions() {
@@ -1 +1 @@
export { TableHeader } from "./table-header";
export { default as default } from "./table-header";
@@ -3,8 +3,7 @@ import { mergeAttributes, Node } from "@tiptap/core";
export interface TableHeaderOptions {
HTMLAttributes: Record<string, any>;
}
export const TableHeader = Node.create<TableHeaderOptions>({
export default Node.create<TableHeaderOptions>({
name: "tableHeader",
addOptions() {
@@ -1 +1 @@
export { TableRow } from "./table-row";
export { default as default } from "./table-row";
@@ -4,7 +4,7 @@ export interface TableRowOptions {
HTMLAttributes: Record<string, any>;
}
export const TableRow = Node.create<TableRowOptions>({
export default Node.create<TableRowOptions>({
name: "tableRow",
addOptions() {
@@ -1,4 +1,4 @@
export const icons = {
const icons = {
colorPicker: `<svg xmlns="http://www.w3.org/2000/svg" length="24" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path fill="rgb(var(--color-text-300))" d="M20 14c-.092.064-2 2.083-2 3.5 0 1.494.949 2.448 2 2.5.906.044 2-.891 2-2.5 0-1.5-1.908-3.436-2-3.5zM9.586 20c.378.378.88.586 1.414.586s1.036-.208 1.414-.586l7-7-.707-.707L11 4.586 8.707 2.293 7.293 3.707 9.586 6 4 11.586c-.378.378-.586.88-.586 1.414s.208 1.036.586 1.414L9.586 20zM11 7.414 16.586 13H5.414L11 7.414z"></path></svg>`,
deleteColumn: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" length="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M12 3c.552 0 1 .448 1 1v8c.835-.628 1.874-1 3-1 2.761 0 5 2.239 5 5s-2.239 5-5 5c-1.032 0-1.99-.313-2.787-.848L13 20c0 .552-.448 1-1 1H6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2H7v14h4V5zm8 10h-6v2h6v-2z"/></svg>`,
deleteRow: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" length="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M20 5c.552 0 1 .448 1 1v6c0 .552-.448 1-1 1 .628.835 1 1.874 1 3 0 2.761-2.239 5-5 5s-5-2.239-5-5c0-1.126.372-2.165 1-3H4c-.552 0-1-.448-1-1V6c0-.552.448-1 1-1h16zm-7 10v2h6v-2h-6zm6-8H5v4h14V7z"/></svg>`,
@@ -47,3 +47,5 @@ export const icons = {
</svg>
`,
};
export default icons;
@@ -1 +1 @@
export { Table } from "./table";
export { default as default } from "./table";
@@ -4,9 +4,9 @@ import { Decoration, NodeView } from "@tiptap/pm/view";
import tippy, { Instance, Props } from "tippy.js";
import { Editor } from "@tiptap/core";
import { CellSelection, TableMap, updateColumnsOnResize } from "@tiptap/pm/tables";
import { CellSelection, TableMap, updateColumnsOnResize } from "@tiptap/prosemirror-tables";
import { icons } from "src/ui/extensions/table/table/icons";
import icons from "./icons";
export function updateColumns(
node: ProseMirrorNode,
@@ -19,12 +19,12 @@ import {
tableEditing,
toggleHeader,
toggleHeaderCell,
} from "@tiptap/pm/tables";
} from "@tiptap/prosemirror-tables";
import { tableControls } from "src/ui/extensions/table/table/table-controls";
import { TableView } from "src/ui/extensions/table/table/table-view";
import { createTable } from "src/ui/extensions/table/table/utilities/create-table";
import { deleteTableWhenAllCellsSelected } from "src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected";
import { tableControls } from "./table-controls";
import { TableView } from "./table-view";
import { createTable } from "./utilities/create-table";
import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected";
export interface TableOptions {
HTMLAttributes: Record<string, any>;
@@ -72,7 +72,7 @@ declare module "@tiptap/core" {
}
}
export const Table = Node.create({
export default Node.create({
name: "table",
addOptions() {
@@ -1,7 +1,7 @@
import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model";
import { createCell } from "src/ui/extensions/table/table/utilities/create-cell";
import { getTableNodeTypes } from "src/ui/extensions/table/table/utilities/get-table-node-types";
import { createCell } from "./create-cell";
import { getTableNodeTypes } from "./get-table-node-types";
export function createTable(
schema: Schema,
@@ -1,6 +1,6 @@
import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core";
import { isCellSelection } from "src/ui/extensions/table/table/utilities/is-cell-selection";
import { isCellSelection } from "./is-cell-selection";
export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ editor }) => {
const { selection } = editor.state;
@@ -1,4 +1,4 @@
import { CellSelection } from "@tiptap/pm/tables";
import { CellSelection } from "@tiptap/prosemirror-tables";
export function isCellSelection(value: unknown): value is CellSelection {
return value instanceof CellSelection;
@@ -1,13 +1,10 @@
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
import { useImperativeHandle, useRef, MutableRefObject } from "react";
import { CoreEditorProps } from "src/ui/props";
import { CoreEditorExtensions } from "src/ui/extensions";
import { CoreEditorProps } from "../props";
import { CoreEditorExtensions } from "../extensions";
import { EditorProps } from "@tiptap/pm/view";
import { getTrimmedHTML } from "src/lib/utils";
import { DeleteImage } from "src/types/delete-image";
import { IMentionSuggestion } from "src/types/mention-suggestion";
import { RestoreImage } from "src/types/restore-image";
import { UploadImage } from "src/types/upload-image";
import { getTrimmedHTML } from "../../lib/utils";
import { DeleteImage, IMentionSuggestion, RestoreImage, UploadImage } from "@plane/editor-types";
interface CustomEditorProps {
uploadFile: UploadImage;
@@ -1,9 +1,9 @@
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
import { useImperativeHandle, useRef, MutableRefObject } from "react";
import { CoreReadOnlyEditorExtensions } from "src/ui/read-only/extensions";
import { CoreReadOnlyEditorProps } from "src/ui/read-only/props";
import { CoreReadOnlyEditorExtensions } from "../read-only/extensions";
import { CoreReadOnlyEditorProps } from "../read-only/props";
import { EditorProps } from "@tiptap/pm/view";
import { IMentionSuggestion } from "src/types/mention-suggestion";
import { IMentionSuggestion } from "@plane/editor-types";
interface CustomReadOnlyEditorProps {
value: string;
@@ -1,6 +1,6 @@
import { IMentionSuggestion } from "@plane/editor-types";
import { Editor } from "@tiptap/react";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { IMentionSuggestion } from "src/types/mention-suggestion";
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
interface MentionListProps {
items: IMentionSuggestion[];
@@ -9,7 +9,7 @@ interface MentionListProps {
}
// eslint-disable-next-line react/display-name
export const MentionList = forwardRef((props: MentionListProps, ref) => {
const MentionList = forwardRef((props: MentionListProps, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index: number) => {
@@ -98,3 +98,5 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
});
MentionList.displayName = "MentionList";
export default MentionList;
@@ -1,8 +1,8 @@
import { Mention, MentionOptions } from "@tiptap/extension-mention";
import { mergeAttributes } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { MentionNodeView } from "src/ui/mentions/mention-node-view";
import { IMentionHighlight } from "src/types/mention-suggestion";
import mentionNodeView from "./mentionNodeView";
import { IMentionHighlight } from "@plane/editor-types";
export interface CustomMentionOptions extends MentionOptions {
mentionHighlights: IMentionHighlight[];
@@ -31,7 +31,7 @@ export const CustomMention = Mention.extend<CustomMentionOptions>({
},
addNodeView() {
return ReactNodeViewRenderer(MentionNodeView);
return ReactNodeViewRenderer(mentionNodeView);
},
parseHTML() {
@@ -1,8 +1,8 @@
// @ts-nocheck
import { Suggestion } from "src/ui/mentions/suggestion";
import { CustomMention } from "src/ui/mentions/custom";
import { IMentionHighlight } from "src/types/mention-suggestion";
import suggestion from "./suggestion";
import { CustomMention } from "./custom";
import { IMentionHighlight, IMentionSuggestion } from "@plane/editor-types";
export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighlights: IMentionHighlight[], readonly) =>
CustomMention.configure({
@@ -11,5 +11,5 @@ export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighli
},
readonly: readonly,
mentionHighlights: mentionHighlights,
suggestion: Suggestion(mentionSuggestions),
suggestion: suggestion(mentionSuggestions),
});
@@ -1,12 +1,12 @@
/* eslint-disable react/display-name */
// @ts-nocheck
import { NodeViewWrapper } from "@tiptap/react";
import { cn } from "src/lib/utils";
import { cn } from "../../lib/utils";
import { useRouter } from "next/router";
import { IMentionHighlight } from "src/types/mention-suggestion";
import { IMentionHighlight } from "@plane/editor-types";
// eslint-disable-next-line import/no-anonymous-default-export
export const MentionNodeView = (props) => {
export default (props) => {
const router = useRouter();
const highlights = props.extension.options.mentionHighlights as IMentionHighlight[];
@@ -2,10 +2,10 @@ import { ReactRenderer } from "@tiptap/react";
import { Editor } from "@tiptap/core";
import tippy from "tippy.js";
import { MentionList } from "src/ui/mentions/mention-list";
import { IMentionSuggestion } from "src/types/mention-suggestion";
import MentionList from "./MentionList";
import { IMentionSuggestion } from "@plane/editor-types";
export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
const Suggestion = (suggestions: IMentionSuggestion[]) => ({
items: ({ query }: { query: string }) =>
suggestions.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5),
render: () => {
@@ -55,3 +55,5 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
};
},
});
export default Suggestion;
@@ -30,15 +30,14 @@ import {
toggleStrike,
toggleTaskList,
toggleUnderline,
} from "src/lib/editor-commands";
import { LucideIconType } from "src/types/lucide-icon";
import { UploadImage } from "src/types/upload-image";
} from "../../../lib/editor-commands";
import { UploadImage } from "@plane/editor-types";
export interface EditorMenuItem {
name: string;
isActive: () => boolean;
command: () => void;
icon: LucideIconType;
icon: typeof BoldIcon;
}
export const HeadingOneItem = (editor: Editor): EditorMenuItem => ({
@@ -1,7 +1,6 @@
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { DeleteImage } from "src/types/delete-image";
import { RestoreImage } from "src/types/restore-image";
import { DeleteImage, RestoreImage } from "@plane/editor-types";
const deleteKey = new PluginKey("delete-image");
const IMAGE_NODE_TYPE = "image";
@@ -13,7 +12,7 @@ interface ImageNode extends ProseMirrorNode {
};
}
export const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
new Plugin({
key: deleteKey,
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
@@ -54,6 +53,8 @@ export const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
},
});
export default TrackImageDeletionPlugin;
export async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
try {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
@@ -1,10 +1,10 @@
import { UploadImage } from "@plane/editor-types";
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
import { UploadImage } from "src/types/upload-image";
const uploadKey = new PluginKey("upload-image");
export const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
new Plugin({
key: uploadKey,
state: {
@@ -43,7 +43,7 @@ export const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
cancelButton.appendChild(svgElement);
placeholder.appendChild(cancelButton);
const deco = Decoration.widget(pos, placeholder, {
const deco = Decoration.widget(pos + 1, placeholder, {
id,
});
set = set.add(tr.doc, [deco]);
@@ -60,6 +60,8 @@ export const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
},
});
export default UploadImagesPlugin;
function findPlaceholder(state: EditorState, id: {}) {
const decos = uploadKey.getState(state);
const found = decos.find(undefined, undefined, (spec: { id: number | undefined }) => spec.id == id);
@@ -131,8 +133,7 @@ export async function startImageUpload(
const imageSrc = typeof src === "object" ? reader.result : src;
const node = schema.nodes.image.create({ src: imageSrc });
const transaction = view.state.tr.insert(pos - 1, node).setMeta(uploadKey, { remove: { id } });
const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } });
view.dispatch(transaction);
} catch (error) {
console.error("Upload error: ", error);
+3 -3
View File
@@ -1,7 +1,7 @@
import { UploadImage } from "@plane/editor-types";
import { EditorProps } from "@tiptap/pm/view";
import { findTableAncestor } from "src/lib/utils";
import { UploadImage } from "src/types/upload-image";
import { startImageUpload } from "src/ui/plugins/upload-image";
import { findTableAncestor } from "../lib/utils";
import { startImageUpload } from "./plugins/upload-image";
export function CoreEditorProps(
uploadFile: UploadImage,
@@ -8,16 +8,15 @@ import TaskList from "@tiptap/extension-task-list";
import { Markdown } from "tiptap-markdown";
import Gapcursor from "@tiptap/extension-gapcursor";
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
import { Table } from "src/ui/extensions/table/table";
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
import TableHeader from "../extensions/table/table-header/table-header";
import Table from "../extensions/table/table";
import TableCell from "../extensions/table/table-cell/table-cell";
import TableRow from "../extensions/table/table-row/table-row";
import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image";
import { isValidHttpUrl } from "src/lib/utils";
import { Mentions } from "src/ui/mentions";
import { IMentionSuggestion } from "src/types/mention-suggestion";
import ReadOnlyImageExtension from "../extensions/image/read-only-image";
import { isValidHttpUrl } from "../../lib/utils";
import { Mentions } from "../mentions";
import { IMentionSuggestion } from "@plane/editor-types";
export const CoreReadOnlyEditorExtensions = (mentionConfig: {
mentionSuggestions: IMentionSuggestion[];
@@ -72,7 +71,6 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
class: "rounded-lg border border-custom-border-300",
},
}),
HorizontalRule,
TiptapUnderline,
TextStyle,
Color,
+2 -12
View File
@@ -1,15 +1,5 @@
{
"extends": "tsconfig/react-library.json",
"include": [
"src/**/*",
"index.d.ts"
],
"exclude": [
"dist",
"build",
"node_modules"
],
"compilerOptions": {
"baseUrl": "."
}
"include": ["src/**/*", "index.d.ts"],
"exclude": ["dist", "build", "node_modules"]
}
+5 -4
View File
@@ -30,11 +30,12 @@
"dependencies": {
"@plane/editor-core": "*",
"@plane/editor-extensions": "*",
"@plane/editor-types": "*",
"@plane/ui": "*",
"@tiptap/core": "^2.1.13",
"@tiptap/extension-placeholder": "^2.1.13",
"@tiptap/pm": "^2.1.13",
"@tiptap/suggestion": "^2.1.13",
"@tiptap/core": "^2.1.7",
"@tiptap/extension-placeholder": "^2.1.11",
"@tiptap/pm": "^2.1.12",
"@tiptap/suggestion": "^2.1.12",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"react-popper": "^2.3.0",
+3 -3
View File
@@ -1,3 +1,3 @@
export { DocumentEditor, DocumentEditorWithRef } from "src/ui";
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "src/ui/readonly";
export { FixedMenu } from "src/ui/menu/fixed-menu";
export { DocumentEditor, DocumentEditorWithRef } from "./ui";
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "./ui/readonly";
export { FixedMenu } from "./ui/menu/fixed-menu";
@@ -1,11 +1,12 @@
import { LucideIconType } from "@plane/editor-core";
import { Icon } from "lucide-react";
interface IAlertLabelProps {
Icon?: LucideIconType;
Icon?: Icon;
backgroundColor: string;
textColor?: string;
label: string;
}
export const AlertLabel = (props: IAlertLabelProps) => {
const { Icon, backgroundColor, textColor, label } = props;
@@ -1,7 +1,7 @@
import { HeadingComp, HeadingThreeComp, SubheadingComp } from "src/ui/components/heading-component";
import { IMarking } from "src/types/editor-types";
import { HeadingComp, HeadingThreeComp, SubheadingComp } from "./heading-component";
import { IMarking } from "..";
import { Editor } from "@tiptap/react";
import { scrollSummary } from "src/utils/editor-summary-utils";
import { scrollSummary } from "../utils/editor-summary-utils";
interface ContentBrowserProps {
editor: Editor;
@@ -1,12 +1,13 @@
import { Editor } from "@tiptap/react";
import { Archive, RefreshCw, Lock } from "lucide-react";
import { IMarking, DocumentDetails } from "src/types/editor-types";
import { FixedMenu } from "src/ui/menu";
import { UploadImage } from "@plane/editor-core";
import { AlertLabel } from "src/ui/components/alert-label";
import { IVerticalDropdownItemProps, VerticalDropdownMenu } from "src/ui/components/vertical-dropdown-menu";
import { SummaryPopover } from "src/ui/components/summary-popover";
import { InfoPopover } from "src/ui/components/info-popover";
import { IMarking } from "..";
import { FixedMenu } from "../menu";
import { UploadImage } from "@plane/editor-types";
import { DocumentDetails } from "../types/editor-types";
import { AlertLabel } from "./alert-label";
import { IVerticalDropdownItemProps, VerticalDropdownMenu } from "./vertical-dropdown-menu";
import { SummaryPopover } from "./summary-popover";
import { InfoPopover } from "./info-popover";
interface IEditorHeader {
editor: Editor;
@@ -2,7 +2,7 @@ import { useState } from "react";
import { usePopper } from "react-popper";
import { Calendar, History, Info } from "lucide-react";
// types
import { DocumentDetails } from "src/types/editor-types";
import { DocumentDetails } from "../types/editor-types";
type Props = {
documentDetails: DocumentDetails;
@@ -1,7 +1,7 @@
import { EditorContainer, EditorContentWrapper } from "@plane/editor-core";
import { Editor } from "@tiptap/react";
import { useState } from "react";
import { DocumentDetails } from "src/types/editor-types";
import { DocumentDetails } from "../types/editor-types";
type IPageRenderer = {
documentDetails: DocumentDetails;
@@ -3,9 +3,9 @@ import { Editor } from "@tiptap/react";
import { usePopper } from "react-popper";
import { List } from "lucide-react";
// components
import { ContentBrowser } from "src/ui/components/content-browser";
import { ContentBrowser } from "./content-browser";
// types
import { IMarking } from "src/types/editor-types";
import { IMarking } from "..";
type Props = {
editor: Editor;
@@ -1,6 +1,6 @@
import { Editor } from "@tiptap/react";
import { IMarking } from "src/types/editor-types";
import { ContentBrowser } from "src/ui/components/content-browser";
import { IMarking } from "..";
import { ContentBrowser } from "./content-browser";
interface ISummarySideBarProps {
editor: Editor;
@@ -8,12 +8,14 @@ interface ISummarySideBarProps {
sidePeekVisible: boolean;
}
export const SummarySideBar = ({ editor, markings, sidePeekVisible }: ISummarySideBarProps) => (
<div
className={`h-full transform overflow-hidden p-5 transition-all duration-200 ${
sidePeekVisible ? "translate-x-0" : "-translate-x-full"
}`}
>
<ContentBrowser editor={editor} markings={markings} />
</div>
);
export const SummarySideBar = ({ editor, markings, sidePeekVisible }: ISummarySideBarProps) => {
return (
<div
className={`h-full transform overflow-hidden p-5 transition-all duration-200 ${
sidePeekVisible ? "translate-x-0" : "-translate-x-full"
}`}
>
<ContentBrowser editor={editor} markings={markings} />
</div>
);
};
@@ -1,6 +1,5 @@
import { LucideIconType } from "@plane/editor-core";
import { CustomMenu } from "@plane/ui";
import { MoreVertical } from "lucide-react";
import { Button, CustomMenu } from "@plane/ui";
import { ChevronUp, Icon, MoreVertical } from "lucide-react";
type TMenuItems =
| "archive_page"
@@ -15,7 +14,7 @@ type TMenuItems =
export interface IVerticalDropdownItemProps {
key: number;
type: TMenuItems;
Icon: LucideIconType;
Icon: Icon;
label: string;
action: () => Promise<void> | void;
}
@@ -24,23 +23,27 @@ export interface IVerticalDropdownMenuProps {
items: IVerticalDropdownItemProps[];
}
const VerticalDropdownItem = ({ Icon, label, action }: IVerticalDropdownItemProps) => (
<CustomMenu.MenuItem onClick={action} className="flex items-center gap-2">
<Icon className="h-3 w-3" />
<div className="text-custom-text-300">{label}</div>
</CustomMenu.MenuItem>
);
const VerticalDropdownItem = ({ Icon, label, action }: IVerticalDropdownItemProps) => {
return (
<CustomMenu.MenuItem onClick={action} className="flex items-center gap-2">
<Icon className="h-3 w-3" />
<div className="text-custom-text-300">{label}</div>
</CustomMenu.MenuItem>
);
};
export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => (
<CustomMenu
maxHeight={"md"}
className={"h-4.5 mt-1"}
placement={"bottom-start"}
optionsClassName={"border-custom-border border-r border-solid transition-all duration-200 ease-in-out "}
customButton={<MoreVertical size={14} />}
>
{items.map((item) => (
<VerticalDropdownItem key={item.key} type={item.type} Icon={item.Icon} label={item.label} action={item.action} />
))}
</CustomMenu>
);
export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => {
return (
<CustomMenu
maxHeight={"md"}
className={"h-4.5 mt-1"}
placement={"bottom-start"}
optionsClassName={"border-custom-border border-r border-solid transition-all duration-200 ease-in-out "}
customButton={<MoreVertical size={14} />}
>
{items.map((item, index) => (
<VerticalDropdownItem key={index} type={item.type} Icon={item.Icon} label={item.label} action={item.action} />
))}
</CustomMenu>
);
};
@@ -1,11 +1,11 @@
import Placeholder from "@tiptap/extension-placeholder";
import { IssueWidgetExtension } from "src/ui/extensions/widgets/issue-embed-widget";
import { IssueWidgetExtension } from "./widgets/IssueEmbedWidget";
import { IIssueEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types";
import { IIssueEmbedConfig } from "./widgets/IssueEmbedWidget/types";
import { SlashCommand, DragAndDrop } from "@plane/editor-extensions";
import { ISlashCommandItem, UploadImage } from "@plane/editor-core";
import { IssueSuggestions } from "src/ui/extensions/widgets/issue-embed-suggestion-list";
import { ISlashCommandItem, UploadImage } from "@plane/editor-types";
import { IssueSuggestions } from "./widgets/IssueEmbedSuggestionList";
import { LayersIcon } from "@plane/ui";
export const DocumentEditorExtensions = (
@@ -1,7 +1,7 @@
import { Editor, Range } from "@tiptap/react";
import { IssueEmbedSuggestions } from "src/ui/extensions/widgets/issue-embed-suggestion-list/issue-suggestion-extension";
import { getIssueSuggestionItems } from "src/ui/extensions/widgets/issue-embed-suggestion-list/issue-suggestion-items";
import { IssueListRenderer } from "src/ui/extensions/widgets/issue-embed-suggestion-list/issue-suggestion-renderer";
import { IssueEmbedSuggestions } from "./issue-suggestion-extension";
import { getIssueSuggestionItems } from "./issue-suggestion-items";
import { IssueListRenderer } from "./issue-suggestion-renderer";
import { v4 as uuidv4 } from "uuid";
export type CommandProps = {
@@ -19,7 +19,7 @@ export interface IIssueListSuggestion {
export const IssueSuggestions = (suggestions: any[]) => {
const mappedSuggestions: IIssueListSuggestion[] = suggestions.map((suggestion): IIssueListSuggestion => {
const transactionId = uuidv4();
let transactionId = uuidv4();
return {
title: suggestion.name,
priority: suggestion.priority.toString(),
@@ -0,0 +1,16 @@
import { IIssueListSuggestion } from ".";
export const getIssueSuggestionItems = (issueSuggestions: Array<IIssueListSuggestion>) => {
return ({ query }: { query: string }) => {
const search = query.toLowerCase();
const filteredSuggestions = issueSuggestions.filter((item) => {
return (
item.title.toLowerCase().includes(search) ||
item.identifier.toLowerCase().includes(search) ||
item.priority.toLowerCase().includes(search)
);
});
return filteredSuggestions;
};
};
@@ -171,7 +171,7 @@ const IssueSuggestionList = ({
section === currentSection && index === selectedIndex,
}
)}
key={item.identifier}
key={index}
onClick={() => selectItem(index)}
>
<h5 className="whitespace-nowrap text-xs text-custom-text-300">{item.identifier}</h5>
@@ -1,5 +1,5 @@
import { IssueWidget } from "src/ui/extensions/widgets/issue-embed-widget/issue-widget-node";
import { IIssueEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types";
import { IssueWidget } from "./issue-widget-node";
import { IIssueEmbedConfig } from "./types";
interface IssueWidgetExtensionProps {
issueEmbedConfig?: IIssueEmbedConfig;
@@ -4,7 +4,7 @@ import { NodeViewWrapper } from "@tiptap/react";
import { Avatar, AvatarGroup, Loader, PriorityIcon } from "@plane/ui";
import { Calendar, AlertTriangle } from "lucide-react";
export const IssueWidgetCard = (props) => {
const IssueWidgetCard = (props) => {
const [loading, setLoading] = useState<number>(1);
const [issueDetails, setIssueDetails] = useState();
@@ -42,9 +42,11 @@ export const IssueWidgetCard = (props) => {
</div>
<div>
<AvatarGroup size="sm">
{issueDetails.assignee_details.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} className={"m-0"} />
))}
{issueDetails.assignee_details.map((assignee) => {
return (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} className={"m-0"} />
);
})}
</AvatarGroup>
</div>
{issueDetails.target_date && (
@@ -74,3 +76,5 @@ export const IssueWidgetCard = (props) => {
</NodeViewWrapper>
);
};
export default IssueWidgetCard;
@@ -1,5 +1,5 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { IssueWidgetCard } from "src/ui/extensions/widgets/issue-embed-widget/issue-widget-card";
import IssueWidgetCard from "./issue-widget-card";
import { ReactNodeViewRenderer } from "@tiptap/react";
export const IssueWidget = Node.create({
@@ -1,15 +0,0 @@
import { IIssueListSuggestion } from "src/ui/extensions/widgets/issue-embed-suggestion-list";
export const getIssueSuggestionItems =
(issueSuggestions: Array<IIssueListSuggestion>) =>
({ query }: { query: string }) => {
const search = query.toLowerCase();
const filteredSuggestions = issueSuggestions.filter(
(item) =>
item.title.toLowerCase().includes(search) ||
item.identifier.toLowerCase().includes(search) ||
item.priority.toLowerCase().includes(search)
);
return filteredSuggestions;
};
@@ -1,5 +1,6 @@
import { Editor } from "@tiptap/react";
import { useState } from "react";
import { IMarking } from "src/types/editor-types";
import { IMarking } from "..";
export const useEditorMarkings = () => {
const [markings, setMarkings] = useState<IMarking[]>([]);

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