Compare commits

...
Author SHA1 Message Date
Anmol Singh Bhatia ff3b56d916 Merge branch 'develop' of github.com:makeplane/plane into chore/breadcrumb_component_revamp 2023-11-03 18:02:35 +05:30
Anmol Singh BhatiaandGitHub 8919b724c5 chore: breadcrumbs ui revamp and refactor (#2634) 2023-11-03 18:01:49 +05:30
Anmol Singh BhatiaandGitHub 7eeac188d7 chore: peek overview improvement and bug fixes (#2627)
* chore: peekoverview issue properties text size fix

* chore: peekoverview icon updated and active view indicator added

* chore: peekoverview and issue sidebar improvement
2023-11-03 18:01:34 +05:30
Anmol Singh Bhatia c8f5014c21 Merge branch 'develop' of github.com:makeplane/plane into chore/breadcrumb_component_revamp 2023-11-03 17:26:15 +05:30
Anmol Singh BhatiaandGitHub f639e467f8 refactor: replace ui components with plane ui components (#2626)
* refactor: replace button component with plane ui component and remove old button component

* refactor: replace dropdown component with plane ui component

* refactor: replace tooltip, input, textarea, spinner and loader component with plane ui component

* refactor: plane ui code refactor
2023-11-03 17:21:38 +05:30
Anmol Singh BhatiaandGitHub 737fea28c6 chore: refactor and improve project member settings (#2625)
* fix: project member setting improvement and refactor

* fix: typo fix in automations setting
2023-11-03 17:20:49 +05:30
4c1aee0cfc fix: resolve z-index and peek overview component bug (#2624)
* fix: resolved z-index issue on peek overview component

* fix: fix issue with peekover view in spreadsheet view

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-11-03 17:20:14 +05:30
Anmol Singh Bhatia b0fd935141 Merge branch 'develop' of github.com:makeplane/plane into chore/breadcrumb_component_revamp 2023-11-03 15:40:57 +05:30
Anmol Singh Bhatia 20b2f6e8a8 chore: breadcrumbs ui revamp and refactor 2023-11-03 15:39:36 +05:30
guru_sainathandGitHub 1352c200dd fix: Project Rendering Error in Kanban Layout and Layout Rendering Fixes in Subscribed Profile Issues (#2629)
* fix: rendering projects error in kanabn layout in profile issues and resolved multiplr layout rendering in subscribed profile issues

* fix: implemented spinner loader in profile issues and remove logs in kanban layout
2023-11-03 13:17:52 +05:30
Aaryan KhandelwalandGitHub dd2ba2ec6f fix: slug field not working (#2622) 2023-11-03 13:17:01 +05:30
Aaryan KhandelwalandGitHub c66d76df26 chore: set sub group by to null if group by and sub group by are same (#2621) 2023-11-03 13:15:50 +05:30
Bavisetti NarayanandGitHub 7a11161cd0 dev: migrations for 0.14 (#2630)
* chore: migration files

* dev: deleted the old migration
2023-11-03 13:00:37 +05:30
260974b0de fix: user authentication on the index page (#2619)
* fix: user authentication on the index page

* fix: login redirection cleanup

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-03 12:42:43 +05:30
sriram veeraghantaandGitHub 5efc6993cd fix: Update analytics page layout fixes (#2623) 2023-11-03 00:09:13 +05:30
sriram veeraghantaandGitHub 3c884fd46e fix: implementing layouts using _app.tsx get layout method. (#2620)
* fix: implementing layouts in all pages

* fix: layout fixes, implemting using standard nextjs parctice
2023-11-02 23:57:44 +05:30
Anmol Singh BhatiaandGitHub a582021f2c fix: active cycle fix (#2618) 2023-11-02 22:17:10 +05:30
Bavisetti NarayanandGitHub caca2bb548 chore: bug fixes (#2609)
* chore: sub issue activity task

* fix: mentions and issue comment

* chore: added string for issue

* chore: changed sub issue id
2023-11-02 19:32:44 +05:30
da391064aa [FIX] Minor bug fixes in MentionList and MentionNode UI (#2600)
* fix: removed text color in peek view

* fix: fixed list view UI bugs and node view colors

* feat: update imports in suggestions for mentionSuggestion type

* fix: updated mention list css

* fix: updated mention node UI according to the design provided

* style: update the mentions dropdown UI

* style: mentioned users UI in the editor

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-11-02 19:31:25 +05:30
Aaryan KhandelwalandGitHub a9b72fa1d2 chore: implemented MobX in the onboarding screens (#2617)
* fix: wrap the onboarding route with the UserWrapper

* chore: implement mobx in the onboarding screens
2023-11-02 19:27:25 +05:30
Anmol Singh BhatiaandGitHub b0397dfd74 fix: module sidebar status select (#2614) 2023-11-02 18:48:57 +05:30
02d4d32f7a chore: Improved Handling of Empty Properties for Labels and Assignees (#2616)
* fix: show empty group

* chore: handled None values for lables and assignees in list and kanban layouts

---------

Co-authored-by: dakshesh14 <dakshesh.jain14@gmail.com>
2023-11-02 18:44:02 +05:30
Anmol Singh BhatiaandGitHub 43e42f1896 fix: project view item edit action fix (#2612) 2023-11-02 17:12:04 +05:30
Aaryan KhandelwalandGitHub d5fd69354e chore: removed unused hooks and components (#2611)
* chore: remove unused hooks

* chore: removed useProjectMembers hook

* chore: removed issue hooks

* fix: build errors
2023-11-02 17:11:33 +05:30
Aaryan KhandelwalandGitHub c987c6f308 fix: calendar layout not being rendered (#2610) 2023-11-02 17:08:48 +05:30
Anmol Singh BhatiaandGitHub 56e4152756 fix: select label dropdown fix for list view (#2608) 2023-11-02 16:28:07 +05:30
Aaryan KhandelwalandGitHub 7b5ed252ef chore: updated the contact email (#2605)
Updated the contact email to squawk@plane.so
2023-11-02 16:27:23 +05:30
Aaryan KhandelwalandGitHub c394a4f64e style: lite text editor editor toolbar (#2601)
* style: comment editor toolbar

* style: updated icon styling
2023-11-02 16:26:57 +05:30
Dakshesh JainandGitHub 5b808571e5 fix: exception error (#2606)
* fix: exception error

* fix: invitation type
2023-11-02 16:26:16 +05:30
Dakshesh JainandGitHub 2cda47dc8a refactor: user profile store setup and bug fixes (#2586)
* fix: autorun not working when filters are changed

* fix: filter/display on overview page

* refactor: store implementation & loader in 'created' & 'subscribed' page
2023-11-02 16:25:44 +05:30
Anmol Singh BhatiaandGitHub 7f3dbe298c fix: bug fixes (#2607)
* fix: module card issue count fix

* fix: project kanban view add issue bug fix

* fix: draft issue modal button alignment fix
2023-11-02 16:03:03 +05:30
Anmol Singh BhatiaandGitHub 0072160891 fix: peekoverview (#2603)
* fix: peekoverview mutation fix

* fix: peekoverview mutation fix

* fix: sub-issue peekoverview
2023-11-02 16:02:34 +05:30
Anmol Singh BhatiaandGitHub 4512651f8b fix: spreadsheet view properties fix (#2599) 2023-11-02 16:01:49 +05:30
guru_sainathandGitHub f6b95b8d31 fix: Issue properties dropdown overflow issue for date and labels (#2604) 2023-11-02 15:59:43 +05:30
Anmol Singh BhatiaandGitHub 325fb4a377 chore: fixes and improvement (#2595)
* fix: project card fix

* chore: bug fixes and ui improvement
2023-11-02 14:01:56 +05:30
guru_sainathandGitHub ba7b7d6f8b chore: implemented module and cycle select dropdown in issue create modal (#2602) 2023-11-02 13:55:45 +05:30
NikhilandGitHub 7249f84e18 dev: code improvements and minor performance upgrades (#2201)
* dev: remove len for empty comparison

* dev: using in instead of multiple ors

* dev: assign expression to empty variables

* dev: use f-string

* dev: remove list comprehension and use generators

* dev: remove assert from paginator

* dev: use is for identity comparison with singleton

* dev: remove unnecessary else statements

* dev: fix does not exists error for both project and workspace

* dev: remove reimports

* dev: iterate a dictionary

* dev: remove unused commented code

* dev: remove redefinition

* dev: remove unused imports

* dev: remove unused imports

* dev: remove unnecessary f strings

* dev: remove unused variables

* dev: use literal structure to create the data structure

* dev: add empty lines at the end of the file

* dev: remove user middleware

* dev: remove unnecessary default None
2023-11-01 20:35:06 +05:30
d63e7cf254 chore: filters view more and less buttons (#2583)
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-01 20:34:02 +05:30
Aaryan KhandelwalandGitHub 36152ea2fa chore: loading state for all layouts (#2588)
* chore: add loading states to layouts

* chore: don't show count for 0 inbox issues
2023-11-01 20:24:57 +05:30
Lakhan BahetiandGitHub 1a46c6c399 feat: search project & workspace members (#2590)
* feat: search project & workspace members

* chore: formatting
2023-11-01 20:23:21 +05:30
Bavisetti NarayanandGitHub 4f09a89f5e chore: unarchived issue and date format changes (#2598)
* chore: unarchived issue message corrected

* chore: passing the date in archived at
2023-11-01 20:11:40 +05:30
sriram veeraghantaandGitHub 8c620c4f96 fix: store level fixes (#2597) 2023-11-01 19:22:10 +05:30
Anmol Singh BhatiaandGitHub d46eb9c59a chore: add issue option added in header group (#2592)
* chore: add issue option added in list view group header

* chore: add issue option added in kanban view group header
2023-11-01 19:10:29 +05:30
Bavisetti NarayanandGitHub e9321a66e7 chore: added validation for archived issue (#2593)
* chore: added validation for archived issue

* fix: optimised code
2023-11-01 17:20:55 +05:30
sriram veeraghantaandGitHub 0121a4ab51 [FED-594] fix: user change theme interface bug fixes (#2587)
* fix: user change theme interface bugfixes

* fix: handling error case
2023-11-01 17:11:29 +05:30
Anmol Singh BhatiaandGitHub 548e95c7e0 fix: bug fixes (#2581)
* fix: module sidebar fix for kanban layout

* chore: cycle & module sidebar improvement

* chore: join project content updated

* chore: project empty state header fix

* chore: create project modal dropdown consistency

* chore: list view group header overlapping issue fix

* chore: popover code refactor

* chore: module sidebar fix for cycle kanban view

* chore: add existing issue option added in module empty state

* chore: add existing issue option added in cycle empty state
2023-11-01 17:11:07 +05:30
Aaryan KhandelwalandGitHub 13ead7c314 fix: project wrapper (#2589)
* fix: project wrapper

* fix: project wrapper for unjoined project

* chore: update store structure
2023-11-01 17:10:10 +05:30
sriram veeraghantaandGitHub 4fcc4b4a01 fix: build fixes (#2591) 2023-11-01 16:56:44 +05:30
d511799f31 [FEATURE] Enabled User @mentions and @mention-filters in core editor package (#2544)
* feat: created custom mention component

* feat: added mention suggestions and suggestion highlights

* feat: created mention suggestion list for displaying mention suggestions

* feat: created custom mention text component, for handling click event

* feat: exposed mention component

* feat: integrated and exposed `mentions` componenet with `editor-core`

* feat: integrated mentions extension with the core editor package

* feat: exposed suggestion types from mentions

* feat: added `mention-suggestion` parameters in `r-t-e` and `l-t-e`

* feat: added `IssueMention` model in apiserver models

* chore: updated activities background job and added bs4 in requirements

* feat: added mention removal logic in issue_activity

* chore: exposed mention types from `r-t-e` and `l-t-e`

* feat: integrated mentions in side peek view description form

* feat: added mentions in issue modal form

* feat: created custom react-hook for editor suggestions

* feat: integrated mention suggestions block in RichTextEditor

* feat: added `mentions` integration in `lite-text-editor` instances

* fix: tailwind loading nodemodules from packages

* feat: added styles for the mention suggestion list

* fix: update module import to resolve build failure

* feat: added mentions as an issue filter

* feat: added UI Changes to Implement `mention` filters

* feat: added `mentions` as a filter option in the header

* feat: added mentions in the filter list options

* feat: added mentions in default display filter options

* feat: added filters in applied and issue params in store

* feat: modified types for adding mentions as a filter option

* feat: modified `notification-card` to display message when it exists in object

* feat: rewrote user mention management upon the changes made in develop

* chore: merged debounce PR with the current PR for tracing changes

* fix: mentions_filters updated with the new setup

* feat: updated requirements for bs4

* feat: modified `mentions-filter` to remove many to many dependency

* feat: implemented list manipulation instead of for loop

* feat: added readonly functionality in `read-only` editor core

* feat: added UI Changes for read-only mode

* feat: added mentions store in web Root Store

* chore: renamed `use-editor-suggestions` hook

* feat: UI Improvements for conditional highlights w.r.t readonly in mentionNode

* fix: removed mentions from `filter_set` parameters

* fix: minor merge fixes

* fix: package lock updates

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-01 16:36:37 +05:30
402 changed files with 7048 additions and 6830 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
hello@plane.so.
squawk@plane.so.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
+1 -1
View File
@@ -1,4 +1,4 @@
import os, sys, random, string
import os, sys
import uuid
sys.path.append("/code")
+1 -1
View File
@@ -3,4 +3,4 @@ from psycogreen.gevent import patch_psycopg
def post_fork(server, worker):
patch_psycopg()
worker.log.info("Made Psycopg2 Green")
worker.log.info("Made Psycopg2 Green")
+1 -1
View File
@@ -101,4 +101,4 @@ class ProjectLitePermission(BasePermission):
workspace__slug=view.workspace_slug,
member=request.user,
project_id=view.project_id,
).exists()
).exists()
+2 -2
View File
@@ -17,7 +17,7 @@ class AnalyticViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
return AnalyticView.objects.create(**validated_data)
def update(self, instance, validated_data):
@@ -25,6 +25,6 @@ class AnalyticViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
-3
View File
@@ -1,6 +1,3 @@
# Django imports
from django.db.models.functions import TruncDate
# Third party imports
from rest_framework import serializers
-1
View File
@@ -6,7 +6,6 @@ from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .project import ProjectLiteSerializer
from .user import UserLiteSerializer
from plane.db.models import Inbox, InboxIssue, Issue
@@ -5,4 +5,4 @@ from .github import (
GithubIssueSyncSerializer,
GithubCommentSyncSerializer,
)
from .slack import SlackProjectSyncSerializer
from .slack import SlackProjectSyncSerializer
+1 -22
View File
@@ -8,8 +8,7 @@ from rest_framework import serializers
from .base import BaseSerializer
from .user import UserLiteSerializer
from .state import StateSerializer, StateLiteSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from plane.db.models import (
User,
@@ -232,25 +231,6 @@ class IssueActivitySerializer(BaseSerializer):
fields = "__all__"
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueComment
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssuePropertySerializer(BaseSerializer):
class Meta:
@@ -287,7 +267,6 @@ class LabelLiteSerializer(BaseSerializer):
class IssueLabelSerializer(BaseSerializer):
# label_details = LabelSerializer(read_only=True, source="label")
class Meta:
model = IssueLabel
+1 -2
View File
@@ -4,9 +4,8 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .issue import IssueStateSerializer
from plane.db.models import (
User,
+2 -2
View File
@@ -57,7 +57,7 @@ class IssueViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
return IssueView.objects.create(**validated_data)
def update(self, instance, validated_data):
@@ -65,7 +65,7 @@ class IssueViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
+3 -5
View File
@@ -110,9 +110,8 @@ class TeamSerializer(BaseSerializer):
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return team
else:
team = Team.objects.create(**validated_data)
return team
team = Team.objects.create(**validated_data)
return team
def update(self, instance, validated_data):
if "members" in validated_data:
@@ -124,8 +123,7 @@ class TeamSerializer(BaseSerializer):
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return super().update(instance, validated_data)
else:
return super().update(instance, validated_data)
return super().update(instance, validated_data)
class WorkspaceThemeSerializer(BaseSerializer):
-1
View File
@@ -28,7 +28,6 @@ from plane.api.views import (
## End User
# Workspaces
WorkSpaceViewSet,
UserWorkspaceInvitationsEndpoint,
UserWorkSpacesEndpoint,
InviteWorkspaceEndpoint,
JoinWorkspaceEndpoint,
+1 -1
View File
@@ -166,4 +166,4 @@ from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkA
from .exporter import ExportIssuesEndpoint
from .config import ConfigurationEndpoint
from .config import ConfigurationEndpoint
+2 -2
View File
@@ -55,11 +55,11 @@ class VerifyEmailEndpoint(BaseAPIView):
return Response(
{"email": "Successfully activated"}, status=status.HTTP_200_OK
)
except jwt.ExpiredSignatureError as indentifier:
except jwt.ExpiredSignatureError as _indentifier:
return Response(
{"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
)
except jwt.exceptions.DecodeError as indentifier:
except jwt.exceptions.DecodeError as _indentifier:
return Response(
{"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
)
-1
View File
@@ -3,7 +3,6 @@ import json
# Django imports
from django.db.models import (
OuterRef,
Func,
F,
Q,
+1 -2
View File
@@ -360,8 +360,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
)
.select_related("issue", "workspace", "project")
)
else:
return InboxIssue.objects.none()
return InboxIssue.objects.none()
def list(self, request, slug, project_id, inbox_id):
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
+31 -28
View File
@@ -39,7 +39,6 @@ from plane.api.serializers import (
IssueActivitySerializer,
IssueCommentSerializer,
IssuePropertySerializer,
LabelSerializer,
IssueSerializer,
LabelSerializer,
IssueFlatSerializer,
@@ -235,10 +234,7 @@ class IssueViewSet(BaseViewSet):
status=status.HTTP_200_OK,
)
return Response(
issues, status=status.HTTP_200_OK
)
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
project = Project.objects.get(pk=project_id)
@@ -443,9 +439,7 @@ class UserWorkSpaceIssues(BaseAPIView):
status=status.HTTP_200_OK,
)
return Response(
issues, status=status.HTTP_200_OK
)
return Response(issues, status=status.HTTP_200_OK)
class WorkSpaceIssuesEndpoint(BaseAPIView):
@@ -623,13 +617,12 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug, project_id):
issue_property, _ = IssueProperty.objects.get_or_create(
user=request.user, project_id=project_id
)
serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK)
issue_property, _ = IssueProperty.objects.get_or_create(
user=request.user, project_id=project_id
)
serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK)
class LabelViewSet(BaseViewSet):
@@ -780,6 +773,20 @@ class SubIssuesEndpoint(BaseAPIView):
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids)
# Track the issue
_ = [
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"parent": str(issue_id)}),
actor_id=str(request.user.id),
issue_id=str(sub_issue_id),
project_id=str(project_id),
current_instance=json.dumps({"parent": str(sub_issue_id)}),
epoch=int(timezone.now().timestamp()),
)
for sub_issue_id in sub_issue_ids
]
return Response(
IssueFlatSerializer(updated_sub_issues, many=True).data,
status=status.HTTP_200_OK,
@@ -1092,17 +1099,19 @@ class IssueArchiveViewSet(BaseViewSet):
archived_at__isnull=False,
pk=pk,
)
issue.archived_at = None
issue.save()
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": None}),
actor_id=str(request.user.id),
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
current_instance=json.dumps(
IssueSerializer(issue).data, cls=DjangoJSONEncoder
),
epoch=int(timezone.now().timestamp()),
)
issue.archived_at = None
issue.save()
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
@@ -1396,8 +1405,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
)
.distinct()
).order_by("created_at")
else:
return IssueComment.objects.none()
return IssueComment.objects.none()
except ProjectDeployBoard.DoesNotExist:
return IssueComment.objects.none()
@@ -1522,8 +1530,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
.order_by("-created_at")
.distinct()
)
else:
return IssueReaction.objects.none()
return IssueReaction.objects.none()
except ProjectDeployBoard.DoesNotExist:
return IssueReaction.objects.none()
@@ -1618,8 +1625,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
.order_by("-created_at")
.distinct()
)
else:
return CommentReaction.objects.none()
return CommentReaction.objects.none()
except ProjectDeployBoard.DoesNotExist:
return CommentReaction.objects.none()
@@ -1713,8 +1719,7 @@ class IssueVotePublicViewSet(BaseViewSet):
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
)
else:
return IssueVote.objects.none()
return IssueVote.objects.none()
except ProjectDeployBoard.DoesNotExist:
return IssueVote.objects.none()
@@ -2160,9 +2165,7 @@ class IssueDraftViewSet(BaseViewSet):
status=status.HTTP_200_OK,
)
return Response(
issues, status=status.HTTP_200_OK
)
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
project = Project.objects.get(pk=project_id)
+3 -6
View File
@@ -11,7 +11,6 @@ from django.conf import settings
from rest_framework.response import Response
from rest_framework import exceptions
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework import status
from sentry_sdk import capture_exception
@@ -113,7 +112,7 @@ def get_user_data(access_token: str) -> dict:
url="https://api.github.com/user/emails", headers=headers
).json()
[
_ = [
user_data.update({"email": item.get("email")})
for item in response
if item.get("primary") is True
@@ -147,7 +146,7 @@ class OauthEndpoint(BaseAPIView):
data = get_user_data(access_token)
email = data.get("email", None)
if email == None:
if email is None:
return Response(
{
"error": "Something went wrong. Please try again later or contact the support team."
@@ -158,7 +157,6 @@ class OauthEndpoint(BaseAPIView):
if "@" in email:
user = User.objects.get(email=email)
email = data["email"]
channel = "email"
mobile_number = uuid.uuid4().hex
email_verified = True
else:
@@ -182,7 +180,7 @@ class OauthEndpoint(BaseAPIView):
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR")
user.last_login_medium = f"oauth"
user.last_login_medium = "oauth"
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.is_email_verified = email_verified
user.save()
@@ -233,7 +231,6 @@ class OauthEndpoint(BaseAPIView):
if "@" in email:
email = data["email"]
mobile_number = uuid.uuid4().hex
channel = "email"
email_verified = True
else:
return Response(
+1 -1
View File
@@ -1,5 +1,5 @@
# Python imports
from datetime import timedelta, datetime, date
from datetime import timedelta, date
# Django imports
from django.db.models import Exists, OuterRef, Q, Prefetch
+2 -4
View File
@@ -11,7 +11,6 @@ from django.db.models import (
Q,
Exists,
OuterRef,
Func,
F,
Func,
Subquery,
@@ -35,7 +34,6 @@ from plane.api.serializers import (
ProjectDetailSerializer,
ProjectMemberInviteSerializer,
ProjectFavoriteSerializer,
IssueLiteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
)
@@ -84,7 +82,7 @@ class ProjectViewSet(BaseViewSet):
]
def get_serializer_class(self, *args, **kwargs):
if self.action == "update" or self.action == "partial_update":
if self.action in ["update", "partial_update"]:
return ProjectSerializer
return ProjectDetailSerializer
@@ -336,7 +334,7 @@ class ProjectViewSet(BaseViewSet):
{"name": "The project name is already taken"},
status=status.HTTP_410_GONE,
)
except Project.DoesNotExist or Workspace.DoesNotExist as e:
except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
)
-1
View File
@@ -19,7 +19,6 @@ from plane.db.models import (
WorkspaceMemberInvite,
Issue,
IssueActivity,
WorkspaceMember,
)
from plane.utils.paginator import BasePaginator
-3
View File
@@ -6,12 +6,10 @@ from uuid import uuid4
# Django imports
from django.db import IntegrityError
from django.db.models import Prefetch
from django.conf import settings
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.contrib.sites.shortcuts import get_current_site
from django.db.models import (
Prefetch,
OuterRef,
@@ -55,7 +53,6 @@ from . import BaseViewSet
from plane.db.models import (
User,
Workspace,
WorkspaceMember,
WorkspaceMemberInvite,
Team,
ProjectMember,
@@ -23,7 +23,7 @@ def email_verification(first_name, email, token, current_site):
from_email_string = settings.EMAIL_FROM
subject = f"Verify your Email!"
subject = "Verify your Email!"
context = {
"first_name": first_name,
-1
View File
@@ -4,7 +4,6 @@ import io
import json
import boto3
import zipfile
from urllib.parse import urlparse, urlunparse
# Django imports
from django.conf import settings
@@ -8,8 +8,6 @@ from django.conf import settings
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import User
@shared_task
@@ -21,7 +19,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
from_email_string = settings.EMAIL_FROM
subject = f"Reset Your Password - Plane"
subject = "Reset Your Password - Plane"
context = {
"first_name": first_name,
+3 -6
View File
@@ -2,8 +2,6 @@
import json
import requests
import uuid
import jwt
from datetime import datetime
# Django imports
from django.conf import settings
@@ -27,7 +25,6 @@ from plane.db.models import (
User,
IssueProperty,
)
from .workspace_invitation_task import workspace_invitation
from plane.bgtasks.user_welcome_task import send_welcome_slack
@@ -58,7 +55,7 @@ def service_importer(service, importer_id):
ignore_conflicts=True,
)
[
_ = [
send_welcome_slack.delay(
str(user.id),
True,
@@ -157,7 +154,7 @@ def service_importer(service, importer_id):
)
# Create repo sync
repo_sync = GithubRepositorySync.objects.create(
_ = GithubRepositorySync.objects.create(
repository=repo,
workspace_integration=workspace_integration,
actor=workspace_integration.actor,
@@ -179,7 +176,7 @@ def service_importer(service, importer_id):
ImporterSerializer(importer).data,
cls=DjangoJSONEncoder,
)
res = requests.post(
_ = 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,
+47 -44
View File
@@ -131,7 +131,7 @@ def track_parent(
else "",
field="parent",
project_id=project_id,
workspace=workspace_id,
workspace_id=workspace_id,
comment=f"updated the parent issue to",
old_identifier=old_parent.id if old_parent is not None else None,
new_identifier=new_parent.id if new_parent is not None else None,
@@ -334,9 +334,7 @@ def track_assignees(
issue_activities,
epoch,
):
requested_assignees = set(
[str(asg) for asg in requested_data.get("assignees", [])]
)
requested_assignees = set([str(asg) for asg in requested_data.get("assignees", [])])
current_assignees = set([str(asg) for asg in current_instance.get("assignees", [])])
added_assignees = requested_assignees - current_assignees
@@ -363,17 +361,19 @@ def track_assignees(
for dropped_assignee in dropped_assginees:
assignee = User.objects.get(pk=dropped_assignee)
issue_activities.append(
issue_id=issue_id,
actor_id=actor_id,
verb="updated",
old_value=assignee.display_name,
new_value="",
field="assignees",
project_id=project_id,
workspace_id=workspace_id,
comment=f"removed assignee ",
old_identifier=assignee.id,
epoch=epoch,
IssueActivity(
issue_id=issue_id,
actor_id=actor_id,
verb="updated",
old_value=assignee.display_name,
new_value="",
field="assignees",
project_id=project_id,
workspace_id=workspace_id,
comment=f"removed assignee ",
old_identifier=assignee.id,
epoch=epoch,
)
)
@@ -418,36 +418,37 @@ def track_archive_at(
issue_activities,
epoch,
):
if requested_data.get("archived_at") is None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project_id=project_id,
workspace_id=workspace_id,
comment=f"has restored the issue",
verb="updated",
actor_id=actor_id,
field="archived_at",
old_value="archive",
new_value="restore",
epoch=epoch,
if current_instance.get("archived_at") != requested_data.get("archived_at"):
if requested_data.get("archived_at") is None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project_id=project_id,
workspace_id=workspace_id,
comment="has restored the issue",
verb="updated",
actor_id=actor_id,
field="archived_at",
old_value="archive",
new_value="restore",
epoch=epoch,
)
)
)
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project_id=project_id,
workspace_id=workspace_id,
comment=f"Plane has archived the issue",
verb="updated",
actor_id=actor_id,
field="archived_at",
old_value=None,
new_value="archive",
epoch=epoch,
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project_id=project_id,
workspace_id=workspace_id,
comment="Plane has archived the issue",
verb="updated",
actor_id=actor_id,
field="archived_at",
old_value=None,
new_value="archive",
epoch=epoch,
)
)
)
def track_closed_to(
@@ -536,7 +537,7 @@ def update_issue_activity(
)
for key in requested_data:
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
func = ISSUE_ACTIVITY_MAPPER.get(key)
if func is not None:
func(
requested_data=requested_data,
@@ -1534,6 +1535,8 @@ def issue_activity(
IssueActivitySerializer(issue_activities_created, many=True).data,
cls=DjangoJSONEncoder,
),
requested_data=requested_data,
current_instance=current_instance,
)
return
@@ -59,7 +59,7 @@ def archive_old_issues():
# Check if Issues
if issues:
# Set the archive time to current time
archive_at = timezone.now()
archive_at = timezone.now().date()
issues_to_update = []
for issue in issues:
@@ -71,14 +71,14 @@ def archive_old_issues():
Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100
)
[
_ = [
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": str(archive_at)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
current_instance=json.dumps({"archived_at": None}),
subscriber=False,
epoch=int(timezone.now().timestamp())
)
@@ -17,7 +17,7 @@ def magic_link(email, key, token, current_site):
from_email_string = settings.EMAIL_FROM
subject = f"Login for Plane"
subject = "Login for Plane"
context = {"magic_url": abs_url, "code": token}
+179 -7
View File
@@ -5,16 +5,107 @@ import json
from django.utils import timezone
# Module imports
from plane.db.models import IssueSubscriber, Project, IssueAssignee, Issue, Notification
from plane.db.models import (
IssueMention,
IssueSubscriber,
Project,
User,
IssueAssignee,
Issue,
Notification,
IssueComment,
)
# Third Party imports
from celery import shared_task
from bs4 import BeautifulSoup
def get_new_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database
# extract mentions from both the instance of data
mentions_older = extract_mentions(current_instance)
mentions_newer = extract_mentions(requested_instance)
# Getting Set Difference from mentions_newer
new_mentions = [
mention for mention in mentions_newer if mention not in mentions_older]
return new_mentions
# Get Removed Mention
def get_removed_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database
# extract mentions from both the instance of data
mentions_older = extract_mentions(current_instance)
mentions_newer = extract_mentions(requested_instance)
# Getting Set Difference from mentions_newer
removed_mentions = [
mention for mention in mentions_older if mention not in mentions_newer]
return removed_mentions
# Adds mentions as subscribers
def extract_mentions_as_subscribers(project_id, issue_id, mentions):
# mentions is an array of User IDs representing the FILTERED set of mentioned users
bulk_mention_subscribers = []
for mention_id in mentions:
# If the particular mention has not already been subscribed to the issue, he must be sent the mentioned notification
if not IssueSubscriber.objects.filter(
issue_id=issue_id,
subscriber=mention_id,
project=project_id,
).exists():
mentioned_user = User.objects.get(pk=mention_id)
project = Project.objects.get(pk=project_id)
issue = Issue.objects.get(pk=issue_id)
bulk_mention_subscribers.append(IssueSubscriber(
workspace=project.workspace,
project=project,
issue=issue,
subscriber=mentioned_user,
))
return bulk_mention_subscribers
# Parse Issue Description & extracts mentions
def extract_mentions(issue_instance):
try:
# issue_instance has to be a dictionary passed, containing the description_html and other set of activity data.
mentions = []
# Convert string to dictionary
data = json.loads(issue_instance)
html = data.get("description_html")
soup = BeautifulSoup(html, 'html.parser')
mention_tags = soup.find_all(
'mention-component', attrs={'target': 'users'})
mentions = [mention_tag['id'] for mention_tag in mention_tags]
return list(set(mentions))
except Exception as e:
return []
@shared_task
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created):
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance):
issue_activities_created = (
json.loads(issue_activities_created) if issue_activities_created is not None else None
json.loads(
issue_activities_created) if issue_activities_created is not None else None
)
if type not in [
"cycle.activity.created",
@@ -33,14 +124,35 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
]:
# Create Notifications
bulk_notifications = []
"""
Mention Tasks
1. Perform Diffing and Extract the mentions, that mention notification needs to be sent
2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers
"""
# Get new mentions from the newer instance
new_mentions = get_new_mentions(
requested_instance=requested_data, current_instance=current_instance)
removed_mention = get_removed_mentions(
requested_instance=requested_data, current_instance=current_instance)
# Get New Subscribers from the mentions of the newer instance
requested_mentions = extract_mentions(
issue_instance=requested_data)
mention_subscribers = extract_mentions_as_subscribers(
project_id=project_id, issue_id=issue_id, mentions=requested_mentions)
issue_subscribers = list(
IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id)
.exclude(subscriber_id=actor_id)
IssueSubscriber.objects.filter(
project_id=project_id, issue_id=issue_id)
.exclude(subscriber_id__in=list(new_mentions + [actor_id]))
.values_list("subscriber", flat=True)
)
issue_assignees = list(
IssueAssignee.objects.filter(project_id=project_id, issue_id=issue_id)
IssueAssignee.objects.filter(
project_id=project_id, issue_id=issue_id)
.exclude(assignee_id=actor_id)
.values_list("assignee", flat=True)
)
@@ -62,6 +174,9 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
for subscriber in list(set(issue_subscribers)):
for issue_activity in issue_activities_created:
issue_comment = issue_activity.get("issue_comment")
if issue_comment is not None:
issue_comment = IssueComment.objects.get(id=issue_comment, issue_id=issue_id, project_id=project_id, workspace_id=project.workspace_id)
bulk_notifications.append(
Notification(
workspace=project.workspace,
@@ -89,7 +204,7 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
"new_value": str(issue_activity.get("new_value")),
"old_value": str(issue_activity.get("old_value")),
"issue_comment": str(
issue_activity.get("issue_comment").comment_stripped
issue_comment.comment_stripped
if issue_activity.get("issue_comment") is not None
else ""
),
@@ -98,5 +213,62 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
)
)
# Add Mentioned as Issue Subscribers
IssueSubscriber.objects.bulk_create(
mention_subscribers, batch_size=100)
for mention_id in new_mentions:
if (mention_id != actor_id):
for issue_activity in issue_activities_created:
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities:mention",
triggered_by_id=actor_id,
receiver_id=mention_id,
entity_identifier=issue_id,
entity_name="issue",
project=project,
message=f"You have been mentioned in the issue {issue.name}",
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(issue.project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.get("id")),
"verb": str(issue_activity.get("verb")),
"field": str(issue_activity.get("field")),
"actor": str(issue_activity.get("actor_id")),
"new_value": str(issue_activity.get("new_value")),
"old_value": str(issue_activity.get("old_value")),
},
},
)
)
# Create New Mentions Here
aggregated_issue_mentions = []
for mention_id in new_mentions:
mentioned_user = User.objects.get(pk=mention_id)
aggregated_issue_mentions.append(
IssueMention(
mention=mentioned_user,
issue=issue,
project=project,
workspace=project.workspace
)
)
IssueMention.objects.bulk_create(
aggregated_issue_mentions, batch_size=100)
IssueMention.objects.filter(
issue=issue, mention__in=removed_mention).delete()
# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
@@ -11,7 +11,7 @@ from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
# Module imports
from plane.db.models import Workspace, User, WorkspaceMemberInvite
from plane.db.models import Workspace, WorkspaceMemberInvite
@shared_task
+1 -1
View File
@@ -29,4 +29,4 @@ app.conf.beat_schedule = {
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler'
app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler'
@@ -4,7 +4,7 @@ from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.issue
import uuid
class Migration(migrations.Migration):
@@ -13,9 +13,29 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name="issue_mentions",
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4,editable=False, primary_key=True, serialize=False, unique=True)),
('mention', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,related_name='issuemention_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuemention', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuemention_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issuemention', to='db.workspace')),
],
options={
'verbose_name': 'IssueMention',
'verbose_name_plural': 'IssueMentions',
'db_table': 'issue_mentions',
'ordering': ('-created_at',),
},
),
migrations.AlterField(
model_name='issueproperty',
name='properties',
field=models.JSONField(default=plane.db.models.issue.get_default_properties),
),
]
]
+2 -2
View File
@@ -27,12 +27,12 @@ from .issue import (
IssueActivity,
IssueProperty,
IssueComment,
IssueBlocker,
IssueLabel,
IssueAssignee,
Label,
IssueBlocker,
IssueRelation,
IssueMention,
IssueLink,
IssueSequence,
IssueAttachment,
@@ -78,4 +78,4 @@ from .analytic import AnalyticView
from .notification import Notification
from .exporter import ExporterHistory
from .exporter import ExporterHistory
+1 -1
View File
@@ -53,4 +53,4 @@ class ExporterHistory(BaseModel):
def __str__(self):
"""Return name of the service"""
return f"{self.provider} <{self.workspace.name}>"
return f"{self.provider} <{self.workspace.name}>"
@@ -1,3 +1,3 @@
from .base import Integration, WorkspaceIntegration
from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync
from .slack import SlackProjectSync
from .slack import SlackProjectSync
@@ -6,7 +6,6 @@ from django.db import models
# Module imports
from plane.db.models import ProjectBaseModel
from plane.db.mixins import AuditModel
class GithubRepository(ProjectBaseModel):
+20 -1
View File
@@ -226,7 +226,26 @@ 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"
)
mention = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="issue_mention",
)
class Meta:
unique_together = ["issue", "mention"]
verbose_name = "Issue Mention"
verbose_name_plural = "Issue Mentions"
db_table = "issue_mentions"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.mention.email}"
class IssueAssignee(ProjectBaseModel):
-3
View File
@@ -4,9 +4,6 @@ from uuid import uuid4
# Django imports
from django.db import models
from django.conf import settings
from django.template.defaultfilters import slugify
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.validators import MinValueValidator, MaxValueValidator
# Modeule imports
@@ -1,33 +0,0 @@
import jwt
import pytz
from django.conf import settings
from django.utils import timezone
from plane.db.models import User
class UserMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
if request.headers.get("Authorization"):
authorization_header = request.headers.get("Authorization")
access_token = authorization_header.split(" ")[1]
decoded = jwt.decode(
access_token, settings.SECRET_KEY, algorithms=["HS256"]
)
id = decoded['user_id']
user = User.objects.get(id=id)
user.last_active = timezone.now()
user.token_updated_at = None
user.save()
timezone.activate(pytz.timezone(user.user_timezone))
except Exception as e:
print(e)
response = self.get_response(request)
return response
-1
View File
@@ -4,7 +4,6 @@ import ssl
import certifi
import dj_database_url
from urllib.parse import urlparse
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
+1 -1
View File
@@ -1 +1 @@
from .api import *
from .api import *
-3
View File
@@ -2,16 +2,13 @@
"""
# from django.contrib import admin
from django.urls import path, include, re_path
from django.views.generic import TemplateView
from django.conf import settings
# from django.conf.urls.static import static
urlpatterns = [
# path("admin/", admin.site.urls),
path("", TemplateView.as_view(template_name="index.html")),
path("api/", include("plane.api.urls")),
path("", include("plane.web.urls")),
+5 -9
View File
@@ -81,7 +81,6 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
# Total Issues in Cycle or Module
total_issues = queryset.total_issues
if cycle_id:
# Get all dates between the two dates
date_range = [
@@ -103,7 +102,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
.values("date", "total_completed")
.order_by("date")
)
if module_id:
# Get all dates between the two dates
date_range = [
@@ -126,18 +125,15 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
.order_by("date")
)
for date in date_range:
cumulative_pending_issues = total_issues
total_completed = 0
total_completed = sum(
[
item["total_completed"]
for item in completed_issues_distribution
if item["date"] is not None and item["date"] <= date
]
item["total_completed"]
for item in completed_issues_distribution
if item["date"] is not None and item["date"] <= date
)
cumulative_pending_issues -= total_completed
chart_data[str(date)] = cumulative_pending_issues
return chart_data
return chart_data
+1 -1
View File
@@ -127,7 +127,7 @@ def group_results(results_data, group_by, sub_group_by=False):
return main_responsive_dict
else:
response_dict = dict()
response_dict = {}
if group_by == "priority":
response_dict = {
+1 -1
View File
@@ -17,4 +17,4 @@ def import_submodules(context, root_module, path):
for k, v in six.iteritems(vars(module)):
if not k.startswith('_'):
context[k] = v
context[module_name] = module
context[module_name] = module
+1 -1
View File
@@ -4,4 +4,4 @@ def get_client_ip(request):
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
return ip
+13 -1
View File
@@ -150,6 +150,17 @@ def filter_assignees(params, filter, method):
filter["assignees__in"] = params.get("assignees")
return filter
def filter_mentions(params, filter, method):
if method == "GET":
mentions = [item for item in params.get("mentions").split(",") if item != 'null']
mentions = filter_valid_uuids(mentions)
if len(mentions) and "" not in mentions:
filter["issue_mention__mention__id__in"] = mentions
else:
if params.get("mentions", None) and len(params.get("mentions")) and params.get("mentions") != 'null':
filter["issue_mention__mention__id__in"] = params.get("mentions")
return filter
def filter_created_by(params, filter, method):
if method == "GET":
@@ -316,7 +327,7 @@ def filter_start_target_date_issues(params, filter, method):
def issue_filters(query_params, method):
filter = dict()
filter = {}
ISSUE_FILTER = {
"state": filter_state,
@@ -326,6 +337,7 @@ def issue_filters(query_params, method):
"parent": filter_parent,
"labels": filter_labels,
"assignees": filter_assignees,
"mentions": filter_mentions,
"created_by": filter_created_by,
"name": filter_name,
"created_at": filter_created_at,
+1 -1
View File
@@ -1,3 +1,3 @@
import mistune
markdown = mistune.Markdown()
markdown = mistune.Markdown()
+1 -10
View File
@@ -21,12 +21,7 @@ class Cursor:
)
def __repr__(self):
return "<{}: value={} offset={} is_prev={}>".format(
type(self).__name__,
self.value,
self.offset,
int(self.is_prev),
)
return f"{type(self).__name__,}: value={self.value} offset={self.offset}, is_prev={int(self.is_prev)}"
def __bool__(self):
return bool(self.has_results)
@@ -176,10 +171,6 @@ class BasePaginator:
**paginator_kwargs,
):
"""Paginate the request"""
assert (paginator and not paginator_kwargs) or (
paginator_cls and paginator_kwargs
)
per_page = self.get_per_page(request, default_per_page, max_per_page)
# Convert the cursor value to integer and float from string
+2 -1
View File
@@ -33,4 +33,5 @@ django_celery_beat==2.5.0
psycopg-binary==3.1.10
psycopg-c==3.1.10
scout-apm==2.26.1
openpyxl==3.1.2
openpyxl==3.1.2
beautifulsoup4==4.12.2
+7 -5
View File
@@ -21,18 +21,18 @@
"check-types": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "18.2.0",
"next": "12.3.2",
"next-themes": "^0.2.1"
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "18.2.0"
},
"dependencies": {
"react-moveable" : "^0.54.2",
"@blueprintjs/popover2": "^2.0.10",
"@tiptap/core": "^2.1.7",
"@tiptap/extension-color": "^2.1.11",
"@tiptap/extension-image": "^2.1.7",
"@tiptap/extension-link": "^2.1.7",
"@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",
@@ -44,9 +44,10 @@
"@tiptap/pm": "^2.1.7",
"@tiptap/react": "^2.1.7",
"@tiptap/starter-kit": "^2.1.10",
"@tiptap/suggestion": "^2.0.4",
"@types/node": "18.15.3",
"@types/react": "^18.2.5",
"@types/react-dom": "18.0.11",
"@types/node": "18.15.3",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"eslint": "8.36.0",
@@ -54,6 +55,7 @@
"eventsource-parser": "^0.1.0",
"lucide-react": "^0.244.0",
"react-markdown": "^8.0.7",
"react-moveable": "^0.54.2",
"tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.2",
@@ -0,0 +1,10 @@
export type IMentionSuggestion = {
id: string;
type: string;
avatar: string;
title: string;
subtitle: string;
redirect_uri: string;
}
export type IMentionHighlight = string
@@ -7,7 +7,11 @@ interface EditorContainerProps {
children: ReactNode;
}
export const EditorContainer = ({ editor, editorClassNames, children }: EditorContainerProps) => (
export const EditorContainer = ({
editor,
editorClassNames,
children,
}: EditorContainerProps) => (
<div
id="editor-container"
onClick={() => {
@@ -17,9 +17,12 @@ import ImageExtension from "./image";
import { DeleteImage } from "../../types/delete-image";
import { isValidHttpUrl } from "../../lib/utils";
import { IMentionSuggestion } from "../../types/mention-suggestion";
import { Mentions } from "../mentions";
export const CoreEditorExtensions = (
mentionConfig: { mentionSuggestions: IMentionSuggestion[], mentionHighlights: string[] },
deleteFile: DeleteImage,
) => [
StarterKit.configure({
@@ -94,4 +97,5 @@ export const CoreEditorExtensions = (
TableHeader,
CustomTableCell,
TableRow,
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
];
@@ -12,6 +12,7 @@ import { EditorProps } from "@tiptap/pm/view";
import { getTrimmedHTML } from "../../lib/utils";
import { UploadImage } from "../../types/upload-image";
import { useInitializedContent } from "./useInitializedContent";
import { IMentionSuggestion } from "../../types/mention-suggestion";
interface CustomEditorProps {
uploadFile: UploadImage;
@@ -26,6 +27,8 @@ interface CustomEditorProps {
extensions?: any;
editorProps?: EditorProps;
forwardedRef?: any;
mentionHighlights?: string[];
mentionSuggestions?: IMentionSuggestion[];
}
export const useEditor = ({
@@ -38,6 +41,8 @@ export const useEditor = ({
setIsSubmitting,
forwardedRef,
setShouldShowAlert,
mentionHighlights,
mentionSuggestions
}: CustomEditorProps) => {
const editor = useCustomEditor(
{
@@ -45,7 +50,7 @@ export const useEditor = ({
...CoreEditorProps(uploadFile, setIsSubmitting),
...editorProps,
},
extensions: [...CoreEditorExtensions(deleteFile), ...extensions],
extensions: [...CoreEditorExtensions({ mentionSuggestions: mentionSuggestions ?? [], mentionHighlights: mentionHighlights ?? []}, deleteFile), ...extensions],
content:
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
onUpdate: async ({ editor }) => {
@@ -77,4 +82,4 @@ export const useEditor = ({
}
return editor;
};
};
@@ -7,21 +7,19 @@ import {
} from "react";
import { CoreReadOnlyEditorExtensions } from "../../ui/read-only/extensions";
import { CoreReadOnlyEditorProps } from "../../ui/read-only/props";
import { EditorProps } from "@tiptap/pm/view";
import { EditorProps } from '@tiptap/pm/view';
import { IMentionSuggestion } from "../../types/mention-suggestion";
interface CustomReadOnlyEditorProps {
value: string;
forwardedRef?: any;
extensions?: any;
editorProps?: EditorProps;
mentionHighlights?: string[];
mentionSuggestions?: IMentionSuggestion[];
}
export const useReadOnlyEditor = ({
value,
forwardedRef,
extensions = [],
editorProps = {},
}: CustomReadOnlyEditorProps) => {
export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editorProps = {}, mentionHighlights, mentionSuggestions}: CustomReadOnlyEditorProps) => {
const editor = useCustomEditor({
editable: false,
content:
@@ -30,7 +28,7 @@ export const useReadOnlyEditor = ({
...CoreReadOnlyEditorProps,
...editorProps,
},
extensions: [...CoreReadOnlyEditorExtensions, ...extensions],
extensions: [...CoreReadOnlyEditorExtensions({ mentionSuggestions: mentionSuggestions ?? [], mentionHighlights: mentionHighlights ?? []}), ...extensions],
});
const hasIntiliazedContent = useRef(false);
+28 -17
View File
@@ -1,13 +1,14 @@
"use client"
import * as React from 'react';
"use client";
import * as React from "react";
import { Extension } from "@tiptap/react";
import { UploadImage } from '../types/upload-image';
import { DeleteImage } from '../types/delete-image';
import { getEditorClassNames } from '../lib/utils';
import { EditorProps } from '@tiptap/pm/view';
import { useEditor } from './hooks/useEditor';
import { EditorContainer } from '../ui/components/editor-container';
import { EditorContentWrapper } from '../ui/components/editor-content';
import { UploadImage } from "../types/upload-image";
import { DeleteImage } from "../types/delete-image";
import { getEditorClassNames } from "../lib/utils";
import { EditorProps } from "@tiptap/pm/view";
import { useEditor } from "./hooks/useEditor";
import { EditorContainer } from "../ui/components/editor-container";
import { EditorContentWrapper } from "../ui/components/editor-content";
import { IMentionSuggestion } from "../types/mention-suggestion";
interface ICoreEditor {
value: string;
@@ -18,7 +19,9 @@ interface ICoreEditor {
customClassName?: string;
editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void;
editable?: boolean;
forwardedRef?: any;
@@ -30,6 +33,8 @@ interface ICoreEditor {
key: string;
label: "Private" | "Public";
}[];
mentionHighlights?: string[];
mentionSuggestions?: IMentionSuggestion[];
extensions?: Extension[];
editorProps?: EditorProps;
}
@@ -61,7 +66,6 @@ const CoreEditor = ({
const editor = useEditor({
onChange,
debouncedUpdatesEnabled,
editable,
setIsSubmitting,
setShouldShowAlert,
value,
@@ -70,22 +74,29 @@ const CoreEditor = ({
forwardedRef,
});
const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName });
const editorClassNames = getEditorClassNames({
noBorder,
borderOnFocus,
customClassName,
});
if (!editor) return null;
return (
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
<div className="flex flex-col">
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
<EditorContentWrapper
editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames}
/>
</div>
</EditorContainer >
</EditorContainer>
);
};
const CoreEditorWithRef = React.forwardRef<EditorHandle, ICoreEditor>((props, ref) => (
<CoreEditor {...props} forwardedRef={ref} />
));
const CoreEditorWithRef = React.forwardRef<EditorHandle, ICoreEditor>(
(props, ref) => <CoreEditor {...props} forwardedRef={ref} />,
);
CoreEditorWithRef.displayName = "CoreEditorWithRef";
@@ -0,0 +1,122 @@
import { Editor } from "@tiptap/react";
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useState,
} from "react";
import { IMentionSuggestion } from "../../types/mention-suggestion";
interface MentionListProps {
items: IMentionSuggestion[];
command: (item: {
id: string;
label: string;
target: string;
redirect_uri: string;
}) => void;
editor: Editor;
}
// eslint-disable-next-line react/display-name
const MentionList = forwardRef((props: MentionListProps, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index: number) => {
const item = props.items[index];
console.log(props.command);
if (item) {
props.command({
id: item.id,
label: item.title,
target: "users",
redirect_uri: item.redirect_uri,
});
}
};
const upHandler = () => {
setSelectedIndex(
(selectedIndex + props.items.length - 1) % props.items.length,
);
};
const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};
const enterHandler = () => {
selectItem(selectedIndex);
};
useEffect(() => {
setSelectedIndex(0);
}, [props.items]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }: { event: KeyboardEvent }) => {
if (event.key === "ArrowUp") {
upHandler();
return true;
}
if (event.key === "ArrowDown") {
downHandler();
return true;
}
if (event.key === "Enter") {
enterHandler();
return false;
}
return false;
},
}));
return props.items && props.items.length !== 0 ? (
<div className="absolute max-h-40 bg-custom-background-100 rounded-md shadow-custom-shadow-sm text-custom-text-300 text-sm overflow-y-auto w-48 p-1 space-y-0.5">
{props.items.length ? (
props.items.map((item, index) => (
<div
key={item.id}
className={`flex items-center gap-2 rounded p-1 hover:bg-custom-background-80 cursor-pointer ${
index === selectedIndex ? "bg-custom-background-80" : ""
}`}
onClick={() => selectItem(index)}
>
<div className="flex-shrink-0 h-4 w-4 grid place-items-center overflow-hidden">
{item.avatar && item.avatar.trim() !== "" ? (
<img
src={item.avatar}
className="h-full w-full object-cover rounded-sm"
alt={item.title}
/>
) : (
<div className="h-full w-full grid place-items-center text-xs capitalize text-white rounded-sm bg-gray-700">
{item.title[0]}
</div>
)}
</div>
<div className="flex-grow space-y-1 truncate">
<p className="text-sm font-medium truncate">{item.title}</p>
{/* <p className="text-xs text-gray-400">{item.subtitle}</p> */}
</div>
</div>
))
) : (
<div className="item">No result</div>
)}
</div>
) : (
<></>
);
});
MentionList.displayName = "MentionList";
export default MentionList;
@@ -0,0 +1,59 @@
import { Mention, MentionOptions } from '@tiptap/extension-mention'
import { mergeAttributes } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import mentionNodeView from './mentionNodeView'
import { IMentionHighlight } from '../../types/mention-suggestion'
export interface CustomMentionOptions extends MentionOptions {
mentionHighlights: IMentionHighlight[]
readonly?: boolean
}
export const CustomMention = Mention.extend<CustomMentionOptions>({
addAttributes() {
return {
id: {
default: null,
},
label: {
default: null,
},
target: {
default: null,
},
self: {
default: false
},
redirect_uri: {
default: "/"
}
}
},
addNodeView() {
return ReactNodeViewRenderer(mentionNodeView)
},
parseHTML() {
return [{
tag: 'mention-component',
getAttrs: (node: string | HTMLElement) => {
if (typeof node === 'string') {
return null;
}
return {
id: node.getAttribute('data-mention-id') || '',
target: node.getAttribute('data-mention-target') || '',
label: node.innerText.slice(1) || '',
redirect_uri: node.getAttribute('redirect_uri')
}
},
}]
},
renderHTML({ HTMLAttributes }) {
return ['mention-component', mergeAttributes(HTMLAttributes)]
},
})
@@ -0,0 +1,15 @@
// @ts-nocheck
import suggestion from "./suggestion";
import { CustomMention } from "./custom";
import { IMentionHighlight, IMentionSuggestion } from "../../types/mention-suggestion";
export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighlights: IMentionHighlight[], readonly) => CustomMention.configure({
HTMLAttributes: {
'class' : "mention",
},
readonly: readonly,
mentionHighlights: mentionHighlights,
suggestion: suggestion(mentionSuggestions),
})
@@ -0,0 +1,41 @@
/* eslint-disable react/display-name */
// @ts-nocheck
import { NodeViewWrapper } from "@tiptap/react";
import { cn } from "../../lib/utils";
import { useRouter } from "next/router";
import { IMentionHighlight } from "../../types/mention-suggestion";
// eslint-disable-next-line import/no-anonymous-default-export
export default (props) => {
const router = useRouter();
const highlights = props.extension.options
.mentionHighlights as IMentionHighlight[];
const handleClick = () => {
if (!props.extension.options.readonly) {
router.push(props.node.attrs.redirect_uri);
}
};
return (
<NodeViewWrapper className="w-fit inline mention-component">
<span
className={cn(
"px-1 py-0.5 bg-custom-primary-100/20 text-custom-primary-100 rounded font-medium mention",
{
"text-yellow-500 bg-yellow-500/20": highlights
? highlights.includes(props.node.attrs.id)
: false,
"cursor-pointer": !props.extension.options.readonly,
// "hover:bg-custom-primary-300" : !props.extension.options.readonly && !highlights.includes(props.node.attrs.id)
},
)}
onClick={handleClick}
data-mention-target={props.node.attrs.target}
data-mention-id={props.node.attrs.id}
>
@{props.node.attrs.label}
</span>
</NodeViewWrapper>
);
};
@@ -0,0 +1,59 @@
import { ReactRenderer } from '@tiptap/react'
import { Editor } from "@tiptap/core";
import tippy from 'tippy.js'
import MentionList from './MentionList'
import { IMentionSuggestion } from '../../types/mention-suggestion';
const Suggestion = (suggestions: IMentionSuggestion[]) => ({
items: ({ query }: { query: string }) => suggestions.filter(suggestion => suggestion.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5),
render: () => {
let reactRenderer: ReactRenderer | null = null;
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
reactRenderer = new ReactRenderer(MentionList, {
props,
editor: props.editor,
});
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.querySelector("#editor-container"),
content: reactRenderer.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
},
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
reactRenderer?.updateProps(props)
popup &&
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === "Escape") {
popup?.[0].hide();
return true;
}
// @ts-ignore
return reactRenderer?.ref?.onKeyDown(props);
},
onExit: () => {
popup?.[0].destroy();
reactRenderer?.destroy()
},
}
},
})
export default Suggestion;
@@ -15,8 +15,12 @@ import { TableRow } from "@tiptap/extension-table-row";
import ReadOnlyImageExtension from "../extensions/image/read-only-image";
import { isValidHttpUrl } from "../../lib/utils";
import { Mentions } from "../mentions";
import { IMentionSuggestion } from "../../types/mention-suggestion";
export const CoreReadOnlyEditorExtensions = [
export const CoreReadOnlyEditorExtensions = (
mentionConfig: { mentionSuggestions: IMentionSuggestion[], mentionHighlights: string[] },
) => [
StarterKit.configure({
bulletList: {
HTMLAttributes: {
@@ -89,4 +93,5 @@ export const CoreReadOnlyEditorExtensions = [
TableHeader,
CustomTableCell,
TableRow,
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, true),
];
@@ -1,2 +1,3 @@
export { LiteTextEditor, LiteTextEditorWithRef } from "./ui";
export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "./ui/read-only";
export type { IMentionSuggestion, IMentionHighlight } from "./ui"
@@ -11,6 +11,16 @@ import { LiteTextEditorExtensions } from "./extensions";
export type UploadImage = (file: File) => Promise<string>;
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
export type IMentionSuggestion = {
id: string;
type: string;
avatar: string;
title: string;
subtitle: string;
redirect_uri: string;
};
export type IMentionHighlight = string;
interface ILiteTextEditor {
value: string;
@@ -22,7 +32,7 @@ interface ILiteTextEditor {
editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void;
setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved"
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void;
forwardedRef?: any;
@@ -38,6 +48,9 @@ interface ILiteTextEditor {
}[];
};
onEnterKeyPress?: (e?: any) => void;
mentionHighlights?: string[];
mentionSuggestions?: IMentionSuggestion[];
submitButton?: React.ReactNode;
}
interface LiteTextEditorProps extends ILiteTextEditor {
@@ -49,22 +62,27 @@ interface EditorHandle {
setEditorValue: (content: string) => void;
}
const LiteTextEditor = ({
onChange,
debouncedUpdatesEnabled,
setIsSubmitting,
setShouldShowAlert,
editorContentCustomClassNames,
value,
uploadFile,
deleteFile,
noBorder,
borderOnFocus,
customClassName,
forwardedRef,
commentAccessSpecifier,
onEnterKeyPress,
}: LiteTextEditorProps) => {
const LiteTextEditor = (props: LiteTextEditorProps) => {
const {
onChange,
debouncedUpdatesEnabled,
setIsSubmitting,
setShouldShowAlert,
editorContentCustomClassNames,
value,
uploadFile,
deleteFile,
noBorder,
borderOnFocus,
customClassName,
forwardedRef,
commentAccessSpecifier,
onEnterKeyPress,
mentionHighlights,
mentionSuggestions,
submitButton,
} = props;
const editor = useEditor({
onChange,
debouncedUpdatesEnabled,
@@ -75,6 +93,8 @@ const LiteTextEditor = ({
deleteFile,
forwardedRef,
extensions: LiteTextEditorExtensions(onEnterKeyPress),
mentionHighlights,
mentionSuggestions,
});
const editorClassNames = getEditorClassNames({
@@ -98,6 +118,7 @@ const LiteTextEditor = ({
uploadFile={uploadFile}
setIsSubmitting={setIsSubmitting}
commentAccessSpecifier={commentAccessSpecifier}
submitButton={submitButton}
/>
</div>
</div>
@@ -106,7 +127,7 @@ const LiteTextEditor = ({
};
const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>(
(props, ref) => <LiteTextEditor {...props} forwardedRef={ref} />
(props, ref) => <LiteTextEditor {...props} forwardedRef={ref} />,
);
LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef";
@@ -1,5 +1,5 @@
import { Editor } from "@tiptap/react";
import { BoldIcon, LucideIcon } from "lucide-react";
import { BoldIcon } from "lucide-react";
import {
BoldItem,
@@ -14,7 +14,6 @@ import {
TableItem,
UnderLineItem,
} from "@plane/editor-core";
import { Icon } from "./icon";
import { Tooltip } from "../../tooltip";
import { UploadImage } from "../..";
@@ -41,8 +40,9 @@ type EditorBubbleMenuProps = {
};
uploadFile: UploadImage;
setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved"
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
submitButton: React.ReactNode;
};
export const FixedMenu = (props: EditorBubbleMenuProps) => {
@@ -72,116 +72,132 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
props.commentAccessSpecifier?.onAccessChange(accessKey);
};
console.log(complexItems);
return (
<div className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl">
<div className="flex items-stretch gap-1.5 w-full h-9">
{props.commentAccessSpecifier && (
<div className="flex border border-custom-border-300 mt-0 divide-x divide-custom-border-300 rounded overflow-hidden">
<div className="flex-shrink-0 flex items-stretch gap-0.5 border border-custom-border-200 rounded p-1">
{props?.commentAccessSpecifier.commentAccess?.map((access) => (
<Tooltip key={access.key} tooltipContent={access.label}>
<button
type="button"
onClick={() => handleAccessChange(access.key)}
className={`grid place-basicMarkItems-center p-1 hover:bg-custom-background-80 ${
className={`aspect-square grid place-items-center p-1 rounded-sm hover:bg-custom-background-90 ${
props.commentAccessSpecifier?.accessValue === access.key
? "bg-custom-background-80"
? "bg-custom-background-90"
: ""
}`}
>
<access.icon
className={`w-4 h-4 ${
className={`w-3.5 h-3.5 ${
props.commentAccessSpecifier?.accessValue === access.key
? "!text-custom-text-100"
: "!text-custom-text-400"
? "text-custom-text-100"
: "text-custom-text-400"
}`}
strokeWidth={2}
/>
</button>
</Tooltip>
))}
</div>
)}
<div className="flex">
{basicMarkItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
{
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
}
)}
>
<item.icon
className={cn("h-4 w-4", {
"text-custom-text-100": item.isActive(),
})}
/>
</button>
))}
</div>
<div className="flex">
{listItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
{
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
}
)}
>
<item.icon
className={cn("h-4 w-4", {
"text-custom-text-100": item.isActive(),
})}
/>
</button>
))}
</div>
<div className="flex">
{userActionItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
{
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
}
)}
>
<item.icon
className={cn("h-4 w-4", {
"text-custom-text-100": item.isActive(),
})}
/>
</button>
))}
</div>
<div className="flex">
{complexItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
{
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
}
)}
>
<item.icon
className={cn("h-4 w-4", {
"text-custom-text-100": item.isActive(),
})}
/>
</button>
))}
<div className="flex items-stretch justify-between gap-2 w-full border border-custom-border-200 bg-custom-background-90 rounded p-1">
<div className="flex items-stretch">
<div className="flex items-stretch gap-0.5 pr-2.5 border-r border-custom-border-200">
{basicMarkItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
{
"text-custom-text-100 bg-custom-background-80":
item.isActive(),
},
)}
>
<item.icon
className={cn("h-3.5 w-3.5", {
"text-custom-text-100": item.isActive(),
})}
strokeWidth={2.5}
/>
</button>
))}
</div>
<div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200">
{listItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
{
"text-custom-text-100 bg-custom-background-80":
item.isActive(),
},
)}
>
<item.icon
className={cn("h-3.5 w-3.5", {
"text-custom-text-100": item.isActive(),
})}
strokeWidth={2.5}
/>
</button>
))}
</div>
<div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200">
{userActionItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
{
"text-custom-text-100 bg-custom-background-80":
item.isActive(),
},
)}
>
<item.icon
className={cn("h-3.5 w-3.5", {
"text-custom-text-100": item.isActive(),
})}
strokeWidth={2.5}
/>
</button>
))}
</div>
<div className="flex items-stretch gap-0.5 pl-2.5">
{complexItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
{
"text-custom-text-100 bg-custom-background-80":
item.isActive(),
},
)}
>
<item.icon
className={cn("h-3.5 w-3.5", {
"text-custom-text-100": item.isActive(),
})}
strokeWidth={2.5}
/>
</button>
))}
</div>
</div>
{props.submitButton}
</div>
</div>
);
@@ -2,3 +2,4 @@ import "./styles/github-dark.css";
export { RichTextEditor, RichTextEditorWithRef } from "./ui";
export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only";
export type { IMentionSuggestion, IMentionHighlight } from "./ui"
@@ -7,6 +7,17 @@ import { RichTextEditorExtensions } from './extensions';
export type UploadImage = (file: File) => Promise<string>;
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
export type IMentionSuggestion = {
id: string;
type: string;
avatar: string;
title: string;
subtitle: string;
redirect_uri: string;
}
export type IMentionHighlight = string
interface IRichTextEditor {
value: string;
uploadFile: UploadImage;
@@ -20,6 +31,8 @@ interface IRichTextEditor {
setShouldShowAlert?: (showAlert: boolean) => void;
forwardedRef?: any;
debouncedUpdatesEnabled?: boolean;
mentionHighlights?: string[];
mentionSuggestions?: IMentionSuggestion[];
}
interface RichTextEditorProps extends IRichTextEditor {
@@ -44,6 +57,8 @@ const RichTextEditor = ({
borderOnFocus,
customClassName,
forwardedRef,
mentionHighlights,
mentionSuggestions
}: RichTextEditorProps) => {
const editor = useEditor({
onChange,
@@ -54,7 +69,9 @@ const RichTextEditor = ({
uploadFile,
deleteFile,
forwardedRef,
extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting)
extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting),
mentionHighlights,
mentionSuggestions
});
const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName });
@@ -12,7 +12,7 @@ module.exports = {
"./pages/**/*.tsx",
"./ui/**/*.tsx",
"../packages/ui/**/*.{js,ts,jsx,tsx}",
"../packages/editor/**/*.{js,ts,jsx,tsx}",
"../packages/editor/**/src/**/*.{js,ts,jsx,tsx}",
],
},
theme: {
+67 -45
View File
@@ -1,59 +1,81 @@
import * as React from "react";
// icons
import { MoveLeft } from "lucide-react";
import { ChevronRight } from "lucide-react";
// components
import { Tooltip } from "../tooltip";
type BreadcrumbsProps = {
onBack: () => void;
children: any;
};
const Breadcrumbs = ({ onBack, children }: BreadcrumbsProps) => (
<>
<div className="flex w-full flex-grow items-center overflow-hidden overflow-ellipsis whitespace-nowrap">
<button
type="button"
className="group grid h-7 w-7 flex-shrink-0 cursor-pointer place-items-center rounded border border-custom-sidebar-border-200 text-center text-sm hover:bg-custom-sidebar-background-90"
onClick={onBack}
>
<MoveLeft className="h-4 w-4 text-custom-sidebar-text-200 group-hover:text-custom-sidebar-text-100" />
</button>
{children}
</div>
</>
);
type BreadcrumbItemProps = {
title?: string;
link?: JSX.Element;
icon?: any;
unshrinkTitle?: boolean;
};
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({
title,
link,
icon,
unshrinkTitle = false,
}) => (
<>
{link ? (
link
) : (
<div
className={`truncate px-3 text-sm ${
unshrinkTitle ? "flex-shrink-0" : ""
}`}
>
<p className={`truncate ${icon ? "flex items-center gap-2" : ""}`}>
{icon}
<span className="break-words">{title}</span>
</p>
const Breadcrumbs = ({ children }: BreadcrumbsProps) => (
<div className="flex items-center space-x-2">
{React.Children.map(children, (child, index) => (
<div key={index} className="flex items-center flex-wrap gap-2.5">
{child}
{index !== React.Children.count(children) - 1 && (
<ChevronRight
className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400"
aria-hidden="true"
/>
)}
</div>
)}
</>
))}
</div>
);
type Props = {
type?: "text" | "component";
component?: React.ReactNode;
label?: string;
icon?: React.ReactNode;
link?: string;
};
const BreadcrumbItem: React.FC<Props> = (props) => {
const { type = "text", component, label, icon, link } = props;
return (
<>
{type != "text" ? (
<div className="flex items-center space-x-2">{component}</div>
) : (
<Tooltip tooltipContent={label} position="bottom">
<li className="flex items-center space-x-2">
<div className="flex items-center flex-wrap gap-2.5">
{link ? (
<a
className="flex items-center gap-1 text-sm font-medium text-custom-text-300 hover:text-custom-text-100"
href={link}
>
{icon && (
<div className="overflow-hidden w-5 h-5 flex justify-center items-center !text-[1rem]">
{icon}
</div>
)}
<div className="relative block overflow-hidden truncate line-clamp-1 max-w-[150px]">
{label}
</div>
</a>
) : (
<div className="flex items-center gap-1 text-sm font-medium text-custom-text-100 cursor-default">
{icon && (
<div className="overflow-hidden w-5 h-5 flex justify-center items-center">
{icon}
</div>
)}
<div className="relative block overflow-hidden truncate line-clamp-1 max-w-[150px]">
{label}
</div>
</div>
)}
</div>
</li>
</Tooltip>
)}
</>
);
};
Breadcrumbs.BreadcrumbItem = BreadcrumbItem;
export { Breadcrumbs, BreadcrumbItem };
@@ -2,7 +2,7 @@ import * as React from "react";
import { ISvgIcons } from "./type";
export const FullScreenPeekIcon: React.FC<ISvgIcons> = ({
export const CenterPanelIcon: React.FC<ISvgIcons> = ({
className = "text-current",
...rest
}) => (
@@ -16,14 +16,18 @@ export const FullScreenPeekIcon: React.FC<ISvgIcons> = ({
>
<path
d="M19 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3Z"
strokeLinecap="round"
strokeLinejoin="round"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M20 3H12V5V19V21H20C20.5523 21 21 20.1046 21 19V5C21 3.89543 20.5523 3 20 3Z"
d="M15.1111 8.00009H8.8001C8.33334 8.00007 8.00003 8.0001 8.00012 8.88897V15.1111C8.00012 16 8.00012 16 8.8001 16H15.1111C16 16 16 16 16 15.1111V8.88897C16 8.00009 16 8.00009 15.1111 8.00009H15.1111Z"
fill="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
@@ -7,14 +7,19 @@ export const CircleDotFullIcon: React.FC<ISvgIcons> = ({
...rest
}) => (
<svg
viewBox="0 0 24 24"
className={`${className} stroke-2`}
stroke="currentColor"
viewBox="0 0 16 16"
className={`${className} stroke-1`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<circle cx="8.33333" cy="8.33333" r="5.33333" stroke-linecap="round" />
<circle
cx="8.33333"
cy="8.33333"
r="5.33333"
stroke="currentColor"
stroke-linecap="round"
/>
<circle cx="8.33333" cy="8.33333" r="4.33333" fill="currentColor" />
</svg>
);
@@ -2,7 +2,7 @@ import * as React from "react";
import { ISvgIcons } from "./type";
export const ModalPeekIcon: React.FC<ISvgIcons> = ({
export const FullScreenPanelIcon: React.FC<ISvgIcons> = ({
className = "text-current",
...rest
}) => (
@@ -9,9 +9,9 @@ export * from "./subscribe-icon";
export * from "./external-link-icon";
export * from "./copy-icon";
export * from "./layer-stack";
export * from "./side-peek-icon";
export * from "./modal-peek-icon";
export * from "./panel-center-icon";
export * from "./side-panel-icon";
export * from "./center-panel-icon";
export * from "./full-screen-panel-icon";
export * from "./priority-icon";
export * from "./state";
export * from "./blocked-icon";
@@ -2,7 +2,7 @@ import * as React from "react";
import { ISvgIcons } from "./type";
export const SidePeekIcon: React.FC<ISvgIcons> = ({
export const SidePanelIcon: React.FC<ISvgIcons> = ({
className = "text-current",
...rest
}) => (
@@ -1,23 +1,20 @@
import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { mutate } from "swr";
// services
import { ProjectService } from "services/project";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { Button } from "@plane/ui";
// icons
import { ClipboardList } from "lucide-react";
// images
import JoinProjectImg from "public/auth/project-not-authorized.svg";
// fetch-keys
import { USER_PROJECT_VIEW } from "constants/fetch-keys";
const projectService = new ProjectService();
export const JoinProject: React.FC = () => {
const [isJoiningProject, setIsJoiningProject] = useState(false);
const { project: projectStore } = useMobxStore();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -25,16 +22,10 @@ export const JoinProject: React.FC = () => {
if (!workspaceSlug || !projectId) return;
setIsJoiningProject(true);
projectService
.joinProject(workspaceSlug as string, [projectId as string])
.then(async () => {
await mutate(USER_PROJECT_VIEW(projectId.toString()));
setIsJoiningProject(false);
})
.catch((err) => {
console.error(err);
setIsJoiningProject(false);
});
projectStore.joinProject(workspaceSlug.toString(), [projectId.toString()]).finally(() => {
setIsJoiningProject(false);
});
};
return (
@@ -38,7 +38,7 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
<div className="">
<h4 className="text-sm font-medium">Auto-archive closed issues</h4>
<p className="text-sm text-custom-text-200 tracking-tight">
Plane will auto archive issues that have been completed or canceled.
Plane will auto archive issues that have been completed or cancelled.
</p>
</div>
</div>
@@ -82,7 +82,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
<div className="">
<h4 className="text-sm font-medium">Auto-close issues</h4>
<p className="text-sm text-custom-text-200 tracking-tight">
Plane will automatically close issue that havent been completed or canceled.
Plane will automatically close issue that havent been completed or cancelled.
</p>
</div>
</div>
@@ -1,33 +1,36 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import React, { FC, Dispatch, SetStateAction, useEffect, useState } from "react";
import { Command } from "cmdk";
import { THEME_OPTIONS } from "constants/themes";
import { useTheme } from "next-themes";
import useUser from "hooks/use-user";
import { Settings } from "lucide-react";
import { observer } from "mobx-react-lite";
// mobx store
// hooks
import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
// constants
import { THEME_OPTIONS } from "constants/themes";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
};
export const ChangeInterfaceTheme: React.FC<Props> = observer(({ setIsPaletteOpen }) => {
const store: any = useMobxStore();
export const ChangeInterfaceTheme: FC<Props> = observer((props) => {
const { setIsPaletteOpen } = props;
// store
const { user: userStore } = useMobxStore();
// states
const [mounted, setMounted] = useState(false);
// hooks
const { setTheme } = useTheme();
const { user } = useUser();
const { setToastAlert } = useToast();
const updateUserTheme = (newTheme: string) => {
if (!user) return;
setTheme(newTheme);
return store.user
.updateCurrentUserSettings({ theme: { ...user.theme, theme: newTheme } })
.then((response: any) => response)
.catch((error: any) => error);
return userStore.updateCurrentUserTheme(newTheme).catch(() => {
setToastAlert({
title: "Failed to save user theme settings!",
type: "error",
});
});
};
// useEffect only runs on the client, so now we can safely show the UI
@@ -1,19 +1,19 @@
import { Dispatch, SetStateAction, useCallback, FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { mutate } from "swr";
import { Command } from "cmdk";
import { Check } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { IssueService } from "services/issue";
// hooks
import useProjectMembers from "hooks/use-project-members";
// constants
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
// ui
import { Avatar } from "@plane/ui";
// icons
import { Check } from "lucide-react";
// types
import { IUser, IIssue } from "types";
// constants
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@@ -24,11 +24,14 @@ type Props = {
// services
const issueService = new IssueService();
export const ChangeIssueAssignee: FC<Props> = ({ setIsPaletteOpen, issue, user }) => {
export const ChangeIssueAssignee: FC<Props> = observer((props) => {
const { setIsPaletteOpen, issue, user } = props;
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const { members } = useProjectMembers(workspaceSlug as string, projectId as string);
const { project: projectStore } = useMobxStore();
const members = projectId ? projectStore.members?.[projectId.toString()] : undefined;
const options =
members?.map(({ member }) => ({
@@ -104,4 +107,4 @@ export const ChangeIssueAssignee: FC<Props> = ({ setIsPaletteOpen, issue, user }
))}
</>
);
};
});
+8 -4
View File
@@ -3,7 +3,7 @@ import React from "react";
import Image from "next/image";
// ui
import { PrimaryButton } from "components/ui";
import { Button } from "@plane/ui";
type Props = {
title: string;
@@ -33,10 +33,14 @@ export const EmptyState: React.FC<Props> = ({
{description && <p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>}
<div className="flex items-center gap-4">
{primaryButton && (
<PrimaryButton className="flex items-center gap-1.5" onClick={primaryButton.onClick} disabled={disabled}>
{primaryButton.icon}
<Button
variant="primary"
prependIcon={primaryButton.icon}
onClick={primaryButton.onClick}
disabled={disabled}
>
{primaryButton.text}
</PrimaryButton>
</Button>
)}
{secondaryButton}
</div>
+1
View File
@@ -89,6 +89,7 @@ const LabelPill = ({ labelId }: { labelId: string }) => {
/>
);
};
const EstimatePoint = ({ point }: { point: string }) => {
const { estimateValue, isEstimateActive } = useEstimateOption(Number(point));
const currentPoint = Number(point) + 1;
@@ -1,326 +0,0 @@
import React from "react";
import { X } from "lucide-react";
// ui
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
filters: Partial<IIssueFilterOptions>;
setFilters: (updatedFilter: Partial<IIssueFilterOptions>) => void;
clearAllFilters: (...args: any) => void;
labels: IIssueLabels[] | undefined;
members: IUserLite[] | undefined;
states: IState[] | undefined;
};
export const FiltersList: React.FC<Props> = ({ filters, setFilters, clearAllFilters, labels, members, states }) => {
if (!filters) return <></>;
const nullFilters = Object.keys(filters).filter((key) => filters[key as keyof IIssueFilterOptions] === null);
return (
<div className="flex flex-1 flex-wrap items-center gap-2 text-xs">
{Object.keys(filters).map((filterKey) => {
const key = filterKey as keyof typeof filters;
if (filters[key] === null) return null;
return (
<div
key={key}
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
>
<span className="capitalize text-custom-text-200">
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
</span>
{filters[key] === null || (filters[key]?.length ?? 0) <= 0 ? (
<span className="inline-flex items-center px-2 py-0.5 font-medium">None</span>
) : Array.isArray(filters[key]) ? (
<div className="space-x-2">
<div className="flex flex-wrap items-center gap-1">
{key === "state"
? filters.state?.map((stateId: string) => {
const state = states?.find((s) => s.id === stateId);
return (
<p
key={state?.id}
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium"
style={{
color: state?.color,
backgroundColor: `${state?.color}20`,
}}
>
<span>
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} />
</span>
<span>{state?.name ?? ""}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
state: filters.state?.filter((s: any) => s !== stateId),
})
}
>
<X className="h-3 w-3" />
</span>
</p>
);
})
: key === "state_group"
? filters.state_group?.map((stateGroup) => {
const group = stateGroup as TStateGroups;
return (
<p
key={group}
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
style={{
color: STATE_GROUP_COLORS[group],
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
}}
>
<span>
<StateGroupIcon stateGroup={group} color={undefined} />
</span>
<span>{group}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
state_group: filters.state_group?.filter((g) => g !== group),
})
}
>
<X className="h-3 w-3" />
</span>
</p>
);
})
: key === "priority"
? filters.priority?.map((priority: any) => (
<p
key={priority}
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
priority === "urgent"
? "bg-red-500/20 text-red-500"
: priority === "high"
? "bg-orange-500/20 text-orange-500"
: priority === "medium"
? "bg-yellow-500/20 text-yellow-500"
: priority === "low"
? "bg-green-500/20 text-green-500"
: "bg-custom-background-90 text-custom-text-200"
}`}
>
<span>
<PriorityIcon priority={priority} className="h-3 w-3" />
</span>
<span>{priority === "null" ? "None" : priority}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
priority: filters.priority?.filter((p: any) => p !== priority),
})
}
>
<X className="h-3 w-3" />
</span>
</p>
))
: key === "assignees"
? filters.assignees?.map((memberId: string) => {
const member = members?.find((m) => m.id === memberId);
return (
<div
key={memberId}
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
>
<Avatar name={member?.display_name} src={member?.avatar} showTooltip={false} />
<span>{member?.display_name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
subscriber: filters.subscriber?.filter((p: any) => p !== memberId),
})
}
>
<X className="h-3 w-3" />
</span>
</div>
);
})
: key === "created_by"
? filters.created_by?.map((memberId: string) => {
const member = members?.find((m) => m.id === memberId);
return (
<div
key={`${memberId}-${key}`}
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
>
<Avatar name={member?.display_name} src={member?.avatar} />
<span>{member?.display_name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
created_by: filters.created_by?.filter((p: any) => p !== memberId),
})
}
>
<X className="h-3 w-3" />
</span>
</div>
);
})
: key === "labels"
? filters.labels?.map((labelId: string) => {
const label = labels?.find((l) => l.id === labelId);
if (!label) return null;
const color = label.color !== "" ? label.color : "#0f172a";
return (
<div
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5"
style={{
color: color,
backgroundColor: `${color}20`, // add 20% opacity
}}
key={labelId}
>
<div
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: color,
}}
/>
<span>{label.name}</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
labels: filters.labels?.filter((l: any) => l !== labelId),
})
}
>
<X
className="h-3 w-3"
style={{
color: color,
}}
/>
</span>
</div>
);
})
: key === "start_date"
? filters.start_date?.map((date: string) => {
if (filters.start_date && filters.start_date.length <= 0) return null;
const splitDate = date.split(";");
return (
<div
key={date}
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
>
<div className="h-1.5 w-1.5 rounded-full" />
<span className="capitalize">
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
start_date: filters.start_date?.filter((d: any) => d !== date),
})
}
>
<X className="h-3 w-3" />
</span>
</div>
);
})
: key === "target_date"
? filters.target_date?.map((date: string) => {
if (filters.target_date && filters.target_date.length <= 0) return null;
const splitDate = date.split(";");
return (
<div
key={date}
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
>
<div className="h-1.5 w-1.5 rounded-full" />
<span className="capitalize">
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
target_date: filters.target_date?.filter((d: any) => d !== date),
})
}
>
<X className="h-3 w-3" />
</span>
</div>
);
})
: (filters[key] as any)?.join(", ")}
<button
type="button"
onClick={() =>
setFilters({
[key]: null,
})
}
>
<X className="h-3 w-3" />
</button>
</div>
</div>
) : (
<div className="flex items-center gap-x-1 capitalize">
{filters[key as keyof typeof filters]}
<button
type="button"
onClick={() =>
setFilters({
[key]: null,
})
}
>
<X className="h-3 w-3" />
</button>
</div>
)}
</div>
);
})}
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
<button
type="button"
onClick={clearAllFilters}
className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
>
<span>Clear all filters</span>
<X className="h-3 w-3" />
</button>
)}
</div>
);
};
-1
View File
@@ -1,3 +1,2 @@
export * from "./date-filter-modal";
export * from "./date-filter-select";
export * from "./filters-list";
-1
View File
@@ -2,7 +2,6 @@ export * from "./filters";
export * from "./modals";
export * from "./sidebar";
export * from "./theme";
export * from "./views";
export * from "./activity";
export * from "./reaction-selector";
export * from "./image-picker-popover";
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import useSWR from "swr";
// react hook form
import { SubmitHandler, useForm } from "react-hook-form";
// headless ui
@@ -9,7 +9,6 @@ import { Combobox, Dialog, Transition } from "@headlessui/react";
import { IssueService } from "services/issue";
// hooks
import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view";
// ui
import { Button, LayersIcon } from "@plane/ui";
// icons
@@ -17,15 +16,7 @@ import { Search } from "lucide-react";
// types
import { IUser, IIssue } from "types";
// fetch keys
import {
CYCLE_DETAILS,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST,
PROJECT_ISSUES_LIST_WITH_PARAMS,
VIEW_ISSUES,
} from "constants/fetch-keys";
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
type FormInput = {
delete_issue_ids: string[];
@@ -43,7 +34,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = (props) => {
const { isOpen, onClose, user } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { workspaceSlug, projectId } = router.query;
// states
const [query, setQuery] = useState("");
// fetching project issues.
@@ -53,9 +44,6 @@ export const BulkDeleteIssuesModal: React.FC<Props> = (props) => {
);
const { setToastAlert } = useToast();
const { displayFilters, params } = useIssuesView();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { order_by, group_by, ...viewGanttParams } = params;
const {
handleSubmit,
@@ -89,14 +77,6 @@ export const BulkDeleteIssuesModal: React.FC<Props> = (props) => {
if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids];
const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString())
: viewId
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "");
await issueService
.bulkDeleteIssues(
workspaceSlug as string,
@@ -113,17 +93,6 @@ export const BulkDeleteIssuesModal: React.FC<Props> = (props) => {
message: "Issues deleted successfully!",
});
if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
else {
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
mutate(CYCLE_DETAILS(cycleId.toString()));
} else if (moduleId) {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
} else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params));
}
handleClose();
})
.catch(() =>
@@ -1,30 +1,16 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
import { Rocket, Search, X } from "lucide-react";
// services
import { ProjectService } from "services/project";
// hooks
import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view";
import useDebounce from "hooks/use-debounce";
// ui
import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
// icons
import { Rocket, Search, X } from "lucide-react";
// types
import { ISearchIssueResponse, TProjectIssuesSearchParams } from "types";
// fetch-keys
import {
CYCLE_DETAILS,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS,
} from "constants/fetch-keys";
type Props = {
isOpen: boolean;
@@ -53,12 +39,10 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const { params } = useIssuesView();
const handleClose = () => {
onClose();
setSearchTerm("");
@@ -81,16 +65,6 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
await handleOnSubmit(selectedIssues).finally(() => setIsSubmitting(false));
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
mutate(CYCLE_DETAILS(cycleId as string));
}
if (moduleId) {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
}
handleClose();
setToastAlert({
@@ -15,6 +15,7 @@ type Props = {
export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEditLink, userAuth }) => {
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
<>
{links.map((link) => (
@@ -5,7 +5,6 @@ import Image from "next/image";
import { Tab } from "@headlessui/react";
// hooks
import useLocalStorage from "hooks/use-local-storage";
import useIssuesView from "hooks/use-issues-view";
// images
import emptyLabel from "public/empty-state/empty_label.svg";
import emptyMembers from "public/empty-state/empty_members.svg";
@@ -47,8 +46,6 @@ export const SidebarProgressStats: React.FC<Props> = ({
noBackground,
isPeekView = false,
}) => {
const { filters, setFilters } = useIssuesView();
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
const currentValue = (tab: string | null) => {
@@ -145,16 +142,17 @@ export const SidebarProgressStats: React.FC<Props> = ({
total={assignee.total_issues}
{...(!isPeekView && {
onClick: () => {
if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
setFilters({
assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id),
});
else
setFilters({
assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""],
});
// TODO: set filters here
// if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
// setFilters({
// assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id),
// });
// else
// setFilters({
// assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""],
// });
},
selected: filters?.assignees?.includes(assignee.assignee_id ?? ""),
// selected: filters?.assignees?.includes(assignee.assignee_id ?? ""),
})}
/>
);
@@ -203,14 +201,15 @@ export const SidebarProgressStats: React.FC<Props> = ({
completed={label.completed_issues}
total={label.total_issues}
{...(!isPeekView && {
// TODO: set filters here
onClick: () => {
if (filters.labels?.includes(label.label_id ?? ""))
setFilters({
labels: filters?.labels?.filter((l) => l !== label.label_id),
});
else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] });
// if (filters.labels?.includes(label.label_id ?? ""))
// setFilters({
// labels: filters?.labels?.filter((l) => l !== label.label_id),
// });
// else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] });
},
selected: filters?.labels?.includes(label.label_id ?? ""),
// selected: filters?.labels?.includes(label.label_id ?? ""),
})}
/>
))
@@ -1,62 +0,0 @@
import { useEffect } from "react";
// react hook form
import { useFormContext } from "react-hook-form";
// hooks
import useProjectDetails from "hooks/use-project-details";
// components
import { InlineCreateIssueFormWrapper } from "components/core";
// types
import { IIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
};
const InlineInput = () => {
const { projectDetails } = useProjectDetails();
const { register, setFocus } = useFormContext();
useEffect(() => {
setFocus("name");
}, [setFocus]);
return (
<>
<div className="w-[14px] h-[14px] rounded-full border border-custom-border-1000 flex-shrink-0" />
<h4 className="text-sm text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full px-2 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</>
);
};
export const GanttInlineCreateIssueForm: React.FC<Props> = (props) => (
<>
<InlineCreateIssueFormWrapper
className="flex py-3 px-4 border-[0.5px] border-custom-border-100 mr-2.5 items-center rounded gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm"
{...props}
>
<InlineInput />
</InlineCreateIssueFormWrapper>
{props.isOpen && (
<p className="text-xs ml-3 mt-3 italic text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
</p>
)}
</>
);
-1
View File
@@ -1 +0,0 @@
export * from "./inline-issue-create-wrapper";
@@ -1,238 +0,0 @@
import { useEffect, useRef } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import { useForm, FormProvider } from "react-hook-form";
import { Transition } from "@headlessui/react";
// services
import { ModuleService } from "services/module.service";
import { IssueService, IssueDraftService } from "services/issue";
// hooks
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
import useKeypress from "hooks/use-keypress";
import useIssuesView from "hooks/use-issues-view";
import useMyIssues from "hooks/my-issues/use-my-issues";
import useGanttChartIssues from "hooks/gantt-chart/issue-view";
// import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
// helpers
import { getFetchKeysForIssueMutation } from "helpers/string.helper";
// fetch-keys
import {
USER_ISSUE,
SUB_ISSUES,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
CYCLE_DETAILS,
MODULE_DETAILS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys";
// types
import { IIssue } from "types";
const defaultValues: Partial<IIssue> = {
name: "",
};
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
className?: string;
children?: React.ReactNode;
};
const issueService = new IssueService();
const issueDraftService = new IssueDraftService();
const moduleService = new ModuleService();
export const addIssueToCycle = async (
workspaceSlug: string,
projectId: string,
issueId: string,
cycleId: string,
user: any,
params: any
) => {
if (!workspaceSlug || !projectId) return;
await issueService
.addIssueToCycle(
workspaceSlug as string,
projectId.toString(),
cycleId,
{
issues: [issueId],
},
user
)
.then(() => {
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params));
mutate(CYCLE_DETAILS(cycleId as string));
}
});
};
export const addIssueToModule = async (
workspaceSlug: string,
projectId: string,
issueId: string,
moduleId: string,
user: any,
params: any
) => {
await moduleService
.addIssuesToModule(
workspaceSlug as string,
projectId.toString(),
moduleId as string,
{
issues: [issueId],
},
user
)
.then(() => {
if (moduleId) {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
}
});
};
export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
const { isOpen, handleClose, onSuccess, prePopulatedData, children, className } = props;
const ref = useRef<HTMLFormElement>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
const { user } = useUser();
const { setToastAlert } = useToast();
const { displayFilters, params } = useIssuesView();
// const { params: calendarParams } = useCalendarIssuesView();
const { ...viewGanttParams } = params;
// const { params: spreadsheetParams } = useSpreadsheetIssuesView();
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { params: ganttParams } = useGanttChartIssues(workspaceSlug?.toString(), projectId?.toString());
const method = useForm<IIssue>({ defaultValues });
const {
reset,
handleSubmit,
getValues,
formState: { errors, isSubmitting },
} = method;
useOutsideClickDetector(ref, handleClose);
useKeypress("Escape", handleClose);
useEffect(() => {
const values = getValues();
if (prePopulatedData) reset({ ...defaultValues, ...values, ...prePopulatedData });
}, [reset, prePopulatedData, getValues]);
useEffect(() => {
if (!isOpen) reset({ ...defaultValues });
}, [isOpen, reset]);
useEffect(() => {
if (!errors) return;
Object.keys(errors).forEach((key) => {
const error = errors[key as keyof IIssue];
setToastAlert({
type: "error",
title: "Error!",
message: error?.message?.toString() || "Some error occurred. Please try again.",
});
});
}, [errors, setToastAlert]);
const { ganttFetchKey } = getFetchKeysForIssueMutation({
cycleId: cycleId,
moduleId: moduleId,
viewId: viewId,
projectId: projectId?.toString() ?? "",
viewGanttParams,
ganttParams,
});
const onSubmitHandler = async (formData: IIssue) => {
if (!workspaceSlug || !projectId || !user || isSubmitting) return;
reset({ ...defaultValues });
await (!isDraftIssues
? issueService.createIssue(workspaceSlug.toString(), projectId.toString(), formData, user)
: issueDraftService.createDraftIssue(workspaceSlug.toString(), projectId.toString(), formData)
)
.then(async (res) => {
await mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params));
if (formData.cycle && formData.cycle !== "")
await addIssueToCycle(workspaceSlug.toString(), projectId.toString(), res.id, formData.cycle, user, params);
if (formData.module && formData.module !== "")
await addIssueToModule(workspaceSlug.toString(), projectId.toString(), res.id, formData.module, user, params);
if (isDraftIssues) await mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId.toString() ?? "", params));
if (displayFilters.layout === "gantt_chart") await mutate(ganttFetchKey);
if (groupedIssues) await mutateMyIssues();
setToastAlert({
type: "success",
title: "Success!",
message: "Issue created successfully.",
});
if (onSuccess) await onSuccess(res);
if (formData.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string));
if (formData.parent && formData.parent !== "") mutate(SUB_ISSUES(formData.parent));
})
.catch((err) => {
Object.keys(err || {}).forEach((key) => {
const error = err?.[key];
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
setToastAlert({
type: "error",
title: "Error!",
message: errorTitle || "Some error occurred. Please try again.",
});
});
});
};
return (
<>
<Transition
show={isOpen}
enter="transition ease-in-out duration-200 transform"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in-out duration-200 transform"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<FormProvider {...method}>
<form ref={ref} className={className} onSubmit={handleSubmit(onSubmitHandler)}>
{children}
</form>
</FormProvider>
</Transition>
</>
);
};

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