mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
13 Commits
fix/active
...
feat/view_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb5966da79 | ||
|
|
d98b688342 | ||
|
|
ce21630388 | ||
|
|
0927fa150c | ||
|
|
eec411baaf | ||
|
|
ecc8fbd79b | ||
|
|
c9b628e578 | ||
|
|
b522de99ba | ||
|
|
b58d7a715a | ||
|
|
87cd44bcd2 | ||
|
|
804b7d8663 | ||
|
|
1539340113 | ||
|
|
d9ee692ce9 |
15
.github/workflows/create-sync-pr.yml
vendored
15
.github/workflows/create-sync-pr.yml
vendored
@@ -3,7 +3,7 @@ name: Create Sync Action
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- preview
|
||||
- develop # Change this to preview
|
||||
types:
|
||||
- closed
|
||||
env:
|
||||
@@ -33,14 +33,23 @@ jobs:
|
||||
sudo apt update
|
||||
sudo apt install gh -y
|
||||
|
||||
- name: Push Changes to Target Repo
|
||||
- name: Create Pull Request
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
run: |
|
||||
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
|
||||
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
|
||||
TARGET_BASE_BRANCH="${{ secrets.SYNC_TARGET_BASE_BRANCH_NAME }}"
|
||||
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
||||
|
||||
git checkout $SOURCE_BRANCH
|
||||
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
||||
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
||||
|
||||
PR_TITLE=${{secrets.SYNC_PR_TITLE}}
|
||||
|
||||
gh pr create \
|
||||
--base $TARGET_BASE_BRANCH \
|
||||
--head $TARGET_BRANCH \
|
||||
--title "$PR_TITLE" \
|
||||
--repo $TARGET_REPO
|
||||
|
||||
@@ -39,7 +39,6 @@ OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
||||
OPENAI_API_KEY="sk-" # deprecated
|
||||
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||
|
||||
|
||||
# Settings related to Docker
|
||||
DOCKERIZED=1 # deprecated
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
exp_serializer = expansion[expand](
|
||||
getattr(instance, expand)
|
||||
)
|
||||
response[expand] = exp_serializer.data
|
||||
response[expand] = exp_serializer.data
|
||||
else:
|
||||
# You might need to handle this case differently
|
||||
response[expand] = getattr(instance, f"{expand}_id", None)
|
||||
|
||||
@@ -17,6 +17,7 @@ from .workspace import (
|
||||
WorkspaceThemeSerializer,
|
||||
WorkspaceMemberAdminSerializer,
|
||||
WorkspaceMemberMeSerializer,
|
||||
WorkspaceUserPropertiesSerializer,
|
||||
)
|
||||
from .project import (
|
||||
ProjectSerializer,
|
||||
@@ -31,6 +32,7 @@ from .project import (
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectPublicMemberSerializer,
|
||||
ProjectMemberRoleSerializer,
|
||||
)
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
||||
@@ -39,6 +41,7 @@ from .cycle import (
|
||||
CycleIssueSerializer,
|
||||
CycleFavoriteSerializer,
|
||||
CycleWriteSerializer,
|
||||
CycleUserPropertiesSerializer,
|
||||
)
|
||||
from .asset import FileAssetSerializer
|
||||
from .issue import (
|
||||
@@ -61,6 +64,8 @@ from .issue import (
|
||||
IssueRelationSerializer,
|
||||
RelatedIssueSerializer,
|
||||
IssuePublicSerializer,
|
||||
IssueRelationLiteSerializer,
|
||||
|
||||
)
|
||||
|
||||
from .module import (
|
||||
@@ -69,6 +74,7 @@ from .module import (
|
||||
ModuleIssueSerializer,
|
||||
ModuleLinkSerializer,
|
||||
ModuleFavoriteSerializer,
|
||||
ModuleUserPropertiesSerializer,
|
||||
)
|
||||
|
||||
from .api import APITokenSerializer, APITokenReadSerializer
|
||||
|
||||
@@ -9,11 +9,12 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# If 'fields' is provided in the arguments, remove it and store it separately.
|
||||
# This is done so as not to pass this custom argument up to the superclass.
|
||||
fields = kwargs.pop("fields", None)
|
||||
fields = kwargs.pop("fields", [])
|
||||
self.expand = kwargs.pop("expand", []) or []
|
||||
fields = self.expand
|
||||
|
||||
# Call the initialization of the superclass.
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# If 'fields' was provided, filter the fields of the serializer accordingly.
|
||||
if fields is not None:
|
||||
self.fields = self._filter_fields(fields)
|
||||
@@ -47,12 +48,91 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
elif isinstance(item, dict):
|
||||
allowed.append(list(item.keys())[0])
|
||||
|
||||
# Convert the current serializer's fields and the allowed fields to sets.
|
||||
existing = set(self.fields)
|
||||
allowed = set(allowed)
|
||||
for field in allowed:
|
||||
if field not in self.fields:
|
||||
from . import (
|
||||
WorkspaceLiteSerializer,
|
||||
ProjectLiteSerializer,
|
||||
UserLiteSerializer,
|
||||
StateLiteSerializer,
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
CycleIssueSerializer,
|
||||
IssueFlatSerializer,
|
||||
)
|
||||
|
||||
# Remove fields from the serializer that aren't in the 'allowed' list.
|
||||
for field_name in (existing - allowed):
|
||||
self.fields.pop(field_name)
|
||||
# Expansion mapper
|
||||
expansion = {
|
||||
"user": UserLiteSerializer,
|
||||
"workspace": WorkspaceLiteSerializer,
|
||||
"project": ProjectLiteSerializer,
|
||||
"default_assignee": UserLiteSerializer,
|
||||
"project_lead": UserLiteSerializer,
|
||||
"state": StateLiteSerializer,
|
||||
"created_by": UserLiteSerializer,
|
||||
"issue": IssueSerializer,
|
||||
"actor": UserLiteSerializer,
|
||||
"owned_by": UserLiteSerializer,
|
||||
"members": UserLiteSerializer,
|
||||
"assignees": UserLiteSerializer,
|
||||
"labels": LabelSerializer,
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
"parent": IssueFlatSerializer,
|
||||
}
|
||||
|
||||
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle"] else False)
|
||||
|
||||
return self.fields
|
||||
|
||||
|
||||
def to_representation(self, instance):
|
||||
response = super().to_representation(instance)
|
||||
|
||||
# Ensure 'expand' is iterable before processing
|
||||
if self.expand:
|
||||
for expand in self.expand:
|
||||
if expand in self.fields:
|
||||
# Import all the expandable serializers
|
||||
from . import (
|
||||
WorkspaceLiteSerializer,
|
||||
ProjectLiteSerializer,
|
||||
UserLiteSerializer,
|
||||
StateLiteSerializer,
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
CycleIssueSerializer,
|
||||
)
|
||||
|
||||
# Expansion mapper
|
||||
expansion = {
|
||||
"user": UserLiteSerializer,
|
||||
"workspace": WorkspaceLiteSerializer,
|
||||
"project": ProjectLiteSerializer,
|
||||
"default_assignee": UserLiteSerializer,
|
||||
"project_lead": UserLiteSerializer,
|
||||
"state": StateLiteSerializer,
|
||||
"created_by": UserLiteSerializer,
|
||||
"issue": IssueSerializer,
|
||||
"actor": UserLiteSerializer,
|
||||
"owned_by": UserLiteSerializer,
|
||||
"members": UserLiteSerializer,
|
||||
"assignees": UserLiteSerializer,
|
||||
"labels": LabelSerializer,
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
}
|
||||
# Check if field in expansion then expand the field
|
||||
if expand in expansion:
|
||||
if isinstance(response.get(expand), list):
|
||||
exp_serializer = expansion[expand](
|
||||
getattr(instance, expand), many=True
|
||||
)
|
||||
else:
|
||||
exp_serializer = expansion[expand](
|
||||
getattr(instance, expand)
|
||||
)
|
||||
response[expand] = exp_serializer.data
|
||||
else:
|
||||
# You might need to handle this case differently
|
||||
response[expand] = getattr(instance, f"{expand}_id", None)
|
||||
|
||||
return response
|
||||
|
||||
@@ -7,7 +7,7 @@ from .user import UserLiteSerializer
|
||||
from .issue import IssueStateSerializer
|
||||
from .workspace import WorkspaceLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from plane.db.models import Cycle, CycleIssue, CycleFavorite
|
||||
from plane.db.models import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
|
||||
|
||||
|
||||
class CycleWriteSerializer(BaseSerializer):
|
||||
@@ -106,3 +106,15 @@ class CycleFavoriteSerializer(BaseSerializer):
|
||||
"project",
|
||||
"user",
|
||||
]
|
||||
|
||||
|
||||
class CycleUserPropertiesSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = CycleUserProperties
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"cycle"
|
||||
"user",
|
||||
]
|
||||
@@ -49,7 +49,6 @@ class IssueStateInboxSerializer(BaseSerializer):
|
||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
bridge_id = serializers.UUIDField(read_only=True)
|
||||
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -278,17 +278,28 @@ class IssueLabelSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class IssueRelationLiteSerializer(DynamicBaseSerializer):
|
||||
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
]
|
||||
|
||||
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||
issue_detail = IssueRelationLiteSerializer(read_only=True, source="related_issue")
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"issue_detail",
|
||||
"relation_type",
|
||||
"related_issue",
|
||||
"issue",
|
||||
"id"
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
@@ -296,16 +307,12 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
||||
issue_detail = IssueRelationLiteSerializer(read_only=True, source="issue")
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"issue_detail",
|
||||
"relation_type",
|
||||
"related_issue",
|
||||
"issue",
|
||||
"id"
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
@@ -512,7 +519,6 @@ class IssueStateSerializer(DynamicBaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
bridge_id = serializers.UUIDField(read_only=True)
|
||||
attachment_count = serializers.IntegerField(read_only=True)
|
||||
link_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -521,32 +527,58 @@ class IssueStateSerializer(DynamicBaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IssueSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
state_detail = StateSerializer(read_only=True, source="state")
|
||||
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
||||
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
|
||||
class IssueSerializer(DynamicBaseSerializer):
|
||||
# ids
|
||||
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
module_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
|
||||
# Many to many
|
||||
label_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="labels")
|
||||
assignee_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="assignees")
|
||||
|
||||
# Count items
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
|
||||
attachment_count = serializers.IntegerField(read_only=True)
|
||||
link_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
# is
|
||||
is_subscribed = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"description_html",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_id",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"sub_issues_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"attachment_count",
|
||||
"link_count",
|
||||
"is_subscribed",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class IssueLiteSerializer(DynamicBaseSerializer):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .base import BaseSerializer, DynamicBaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from .workspace import WorkspaceLiteSerializer
|
||||
@@ -14,6 +14,7 @@ from plane.db.models import (
|
||||
ModuleIssue,
|
||||
ModuleLink,
|
||||
ModuleFavorite,
|
||||
ModuleUserProperties,
|
||||
)
|
||||
|
||||
|
||||
@@ -159,7 +160,7 @@ class ModuleLinkSerializer(BaseSerializer):
|
||||
return ModuleLink.objects.create(**validated_data)
|
||||
|
||||
|
||||
class ModuleSerializer(BaseSerializer):
|
||||
class ModuleSerializer(DynamicBaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
||||
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
|
||||
@@ -196,3 +197,14 @@ class ModuleFavoriteSerializer(BaseSerializer):
|
||||
"project",
|
||||
"user",
|
||||
]
|
||||
|
||||
class ModuleUserPropertiesSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = ModuleUserProperties
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"module",
|
||||
"user"
|
||||
]
|
||||
@@ -159,6 +159,11 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
||||
model = ProjectMember
|
||||
fields = "__all__"
|
||||
|
||||
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = ("id", "role", "member", "project")
|
||||
|
||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||
project = ProjectLiteSerializer(read_only=True)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .base import BaseSerializer, DynamicBaseSerializer
|
||||
from .workspace import WorkspaceLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
||||
@@ -38,7 +38,7 @@ class GlobalViewSerializer(BaseSerializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class IssueViewSerializer(BaseSerializer):
|
||||
class IssueViewSerializer(DynamicBaseSerializer):
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||
@@ -80,4 +80,4 @@ class IssueViewFavoriteSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"project",
|
||||
"user",
|
||||
]
|
||||
]
|
||||
@@ -2,7 +2,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .base import BaseSerializer, DynamicBaseSerializer
|
||||
from .user import UserLiteSerializer, UserAdminLiteSerializer
|
||||
|
||||
from plane.db.models import (
|
||||
@@ -13,10 +13,11 @@ from plane.db.models import (
|
||||
TeamMember,
|
||||
WorkspaceMemberInvite,
|
||||
WorkspaceTheme,
|
||||
WorkspaceUserProperties,
|
||||
)
|
||||
|
||||
|
||||
class WorkSpaceSerializer(BaseSerializer):
|
||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
owner = UserLiteSerializer(read_only=True)
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
@@ -62,7 +63,7 @@ class WorkspaceLiteSerializer(BaseSerializer):
|
||||
|
||||
|
||||
|
||||
class WorkSpaceMemberSerializer(BaseSerializer):
|
||||
class WorkSpaceMemberSerializer(DynamicBaseSerializer):
|
||||
member = UserLiteSerializer(read_only=True)
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
|
||||
@@ -78,7 +79,7 @@ class WorkspaceMemberMeSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class WorkspaceMemberAdminSerializer(BaseSerializer):
|
||||
class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
|
||||
member = UserAdminLiteSerializer(read_only=True)
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
|
||||
@@ -161,3 +162,13 @@ class WorkspaceThemeSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"actor",
|
||||
]
|
||||
|
||||
|
||||
class WorkspaceUserPropertiesSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = WorkspaceUserProperties
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"user",
|
||||
]
|
||||
@@ -7,6 +7,7 @@ from plane.app.views import (
|
||||
CycleDateCheckEndpoint,
|
||||
CycleFavoriteViewSet,
|
||||
TransferCycleIssueEndpoint,
|
||||
CycleUserPropertiesEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -44,7 +45,7 @@ urlpatterns = [
|
||||
name="project-issue-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
|
||||
CycleIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
@@ -84,4 +85,9 @@ urlpatterns = [
|
||||
TransferCycleIssueEndpoint.as_view(),
|
||||
name="transfer-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/user-properties/",
|
||||
CycleUserPropertiesEndpoint.as_view(),
|
||||
name="cycle-user-filters",
|
||||
)
|
||||
]
|
||||
|
||||
@@ -40,7 +40,7 @@ urlpatterns = [
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:issue_id>/",
|
||||
InboxIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
|
||||
@@ -235,7 +235,7 @@ urlpatterns = [
|
||||
## End Comment Reactions
|
||||
## IssueProperty
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
|
||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||
name="project-issue-display-properties",
|
||||
),
|
||||
@@ -275,16 +275,17 @@ urlpatterns = [
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||
IssueRelationViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="issue-relation",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
|
||||
IssueRelationViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
"post": "remove_relation",
|
||||
}
|
||||
),
|
||||
name="issue-relation",
|
||||
|
||||
@@ -7,6 +7,7 @@ from plane.app.views import (
|
||||
ModuleLinkViewSet,
|
||||
ModuleFavoriteViewSet,
|
||||
BulkImportModulesEndpoint,
|
||||
ModuleUserPropertiesEndpoint
|
||||
)
|
||||
|
||||
|
||||
@@ -44,7 +45,7 @@ urlpatterns = [
|
||||
name="project-module-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
|
||||
ModuleIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
@@ -101,4 +102,9 @@ urlpatterns = [
|
||||
BulkImportModulesEndpoint.as_view(),
|
||||
name="bulk-modules-create",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/user-properties/",
|
||||
ModuleUserPropertiesEndpoint.as_view(),
|
||||
name="cycle-user-filters",
|
||||
)
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@ from plane.app.views import (
|
||||
IssueViewViewSet,
|
||||
GlobalViewViewSet,
|
||||
GlobalViewIssuesViewSet,
|
||||
IssueViewFavoriteViewSet,
|
||||
IssueViewFavoriteViewSet,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ from plane.app.views import (
|
||||
WorkspaceUserProfileEndpoint,
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceProjectMemberEndpoint,
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -92,6 +94,11 @@ urlpatterns = [
|
||||
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
||||
name="workspace-member",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-members/",
|
||||
WorkspaceProjectMemberEndpoint.as_view(),
|
||||
name="workspace-member-roles",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/<uuid:pk>/",
|
||||
WorkSpaceMemberViewSet.as_view(
|
||||
@@ -195,4 +202,9 @@ urlpatterns = [
|
||||
WorkspaceLabelsEndpoint.as_view(),
|
||||
name="workspace-labels",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/user-properties/",
|
||||
WorkspaceUserPropertiesEndpoint.as_view(),
|
||||
name="workspace-user-filters",
|
||||
)
|
||||
]
|
||||
|
||||
@@ -45,6 +45,8 @@ from .workspace import (
|
||||
WorkspaceUserProfileEndpoint,
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceProjectMemberEndpoint,
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import (
|
||||
@@ -59,6 +61,7 @@ from .cycle import (
|
||||
CycleDateCheckEndpoint,
|
||||
CycleFavoriteViewSet,
|
||||
TransferCycleIssueEndpoint,
|
||||
CycleUserPropertiesEndpoint,
|
||||
)
|
||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
||||
from .issue import (
|
||||
@@ -103,6 +106,7 @@ from .module import (
|
||||
ModuleIssueViewSet,
|
||||
ModuleLinkViewSet,
|
||||
ModuleFavoriteViewSet,
|
||||
ModuleUserPropertiesEndpoint,
|
||||
)
|
||||
|
||||
from .api import ApiTokenEndpoint
|
||||
|
||||
@@ -159,6 +159,21 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||
if resolve(self.request.path_info).url_name == "project":
|
||||
return self.kwargs.get("pk", None)
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
fields = [
|
||||
field for field in self.request.GET.get("fields", "").split(",") if field
|
||||
]
|
||||
return fields if fields else None
|
||||
|
||||
@property
|
||||
def expand(self):
|
||||
expand = [
|
||||
expand for expand in self.request.GET.get("expand", "").split(",") if expand
|
||||
]
|
||||
return expand if expand else None
|
||||
|
||||
|
||||
|
||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
permission_classes = [
|
||||
@@ -239,3 +254,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
@property
|
||||
def project_id(self):
|
||||
return self.kwargs.get("project_id", None)
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
fields = [
|
||||
field for field in self.request.GET.get("fields", "").split(",") if field
|
||||
]
|
||||
return fields if fields else None
|
||||
|
||||
@property
|
||||
def expand(self):
|
||||
expand = [
|
||||
expand for expand in self.request.GET.get("expand", "").split(",") if expand
|
||||
]
|
||||
return expand if expand else None
|
||||
|
||||
@@ -20,7 +20,6 @@ class ConfigurationEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
|
||||
# Get all the configuration
|
||||
(
|
||||
GOOGLE_CLIENT_ID,
|
||||
@@ -90,8 +89,12 @@ class ConfigurationEndpoint(BaseAPIView):
|
||||
|
||||
data = {}
|
||||
# Authentication
|
||||
data["google_client_id"] = GOOGLE_CLIENT_ID if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != "\"\"" else None
|
||||
data["github_client_id"] = GITHUB_CLIENT_ID if GITHUB_CLIENT_ID and GITHUB_CLIENT_ID != "\"\"" else None
|
||||
data["google_client_id"] = (
|
||||
GOOGLE_CLIENT_ID if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != '""' else None
|
||||
)
|
||||
data["github_client_id"] = (
|
||||
GITHUB_CLIENT_ID if GITHUB_CLIENT_ID and GITHUB_CLIENT_ID != '""' else None
|
||||
)
|
||||
data["github_app_name"] = GITHUB_APP_NAME
|
||||
data["magic_login"] = (
|
||||
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
||||
@@ -114,7 +117,9 @@ class ConfigurationEndpoint(BaseAPIView):
|
||||
# File size settings
|
||||
data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||
|
||||
# is self managed
|
||||
data["is_self_managed"] = bool(int(os.environ.get("IS_SELF_MANAGED", "1")))
|
||||
# is smtp configured
|
||||
data["is_smtp_configured"] = not (
|
||||
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
||||
)
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -14,7 +14,7 @@ from django.db.models import (
|
||||
Case,
|
||||
When,
|
||||
Value,
|
||||
CharField
|
||||
CharField,
|
||||
)
|
||||
from django.core import serializers
|
||||
from django.utils import timezone
|
||||
@@ -33,8 +33,9 @@ from plane.app.serializers import (
|
||||
CycleFavoriteSerializer,
|
||||
IssueStateSerializer,
|
||||
CycleWriteSerializer,
|
||||
CycleUserPropertiesSerializer,
|
||||
)
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission
|
||||
from plane.db.models import (
|
||||
User,
|
||||
Cycle,
|
||||
@@ -44,6 +45,7 @@ from plane.db.models import (
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
Label,
|
||||
CycleUserProperties,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.grouper import group_results
|
||||
@@ -164,23 +166,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT")
|
||||
),
|
||||
When(
|
||||
start_date__gt=timezone.now(),
|
||||
then=Value("UPCOMING")
|
||||
),
|
||||
When(
|
||||
end_date__lt=timezone.now(),
|
||||
then=Value("COMPLETED")
|
||||
Q(start_date__lte=timezone.now())
|
||||
& Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
|
||||
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||
When(
|
||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||
then=Value("DRAFT")
|
||||
then=Value("DRAFT"),
|
||||
),
|
||||
default=Value("DRAFT"),
|
||||
output_field=CharField(),
|
||||
default=Value("DRAFT"),
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
@@ -202,6 +199,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset()
|
||||
cycle_view = request.GET.get("cycle_view", "all")
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
|
||||
queryset = queryset.order_by("-is_favorite", "-created_at")
|
||||
|
||||
@@ -307,44 +305,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
||||
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
# Upcoming Cycles
|
||||
if cycle_view == "upcoming":
|
||||
queryset = queryset.filter(start_date__gt=timezone.now())
|
||||
return Response(
|
||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
# Completed Cycles
|
||||
if cycle_view == "completed":
|
||||
queryset = queryset.filter(end_date__lt=timezone.now())
|
||||
return Response(
|
||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
# Draft Cycles
|
||||
if cycle_view == "draft":
|
||||
queryset = queryset.filter(
|
||||
end_date=None,
|
||||
start_date=None,
|
||||
)
|
||||
|
||||
return Response(
|
||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
# Incomplete Cycles
|
||||
if cycle_view == "incomplete":
|
||||
queryset = queryset.filter(
|
||||
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
|
||||
)
|
||||
return Response(
|
||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
# If no matching view is found return all cycles
|
||||
return Response(
|
||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
cycles = CycleSerializer(queryset, many=True).data
|
||||
return Response(cycles, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
if (
|
||||
@@ -576,7 +538,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(bridge_id=F("issue_cycle__id"))
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project")
|
||||
@@ -600,12 +561,10 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
issues = IssueStateSerializer(
|
||||
serializer = IssueStateSerializer(
|
||||
issues, many=True, fields=fields if fields else None
|
||||
).data
|
||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id, cycle_id):
|
||||
issues = request.data.get("issues", [])
|
||||
@@ -698,11 +657,13 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, cycle_id, pk):
|
||||
def destroy(self, request, slug, project_id, cycle_id, issue_id):
|
||||
cycle_issue = CycleIssue.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
|
||||
issue_id=issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
cycle_id=cycle_id,
|
||||
)
|
||||
issue_id = cycle_issue.issue_id
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
@@ -712,7 +673,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
}
|
||||
),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(cycle_issue.issue_id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@@ -834,3 +795,39 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
def patch(self, request, slug, project_id, cycle_id):
|
||||
cycle_properties = CycleUserProperties.objects.get(
|
||||
user=request.user,
|
||||
cycle_id=cycle_id,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
cycle_properties.filters = request.data.get("filters", cycle_properties.filters)
|
||||
cycle_properties.display_filters = request.data.get(
|
||||
"display_filters", cycle_properties.display_filters
|
||||
)
|
||||
cycle_properties.display_properties = request.data.get(
|
||||
"display_properties", cycle_properties.display_properties
|
||||
)
|
||||
cycle_properties.save()
|
||||
|
||||
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def get(self, request, slug, project_id, cycle_id):
|
||||
cycle_properties, _ = CycleUserProperties.objects.get_or_create(
|
||||
user=request.user,
|
||||
project_id=project_id,
|
||||
cycle_id=cycle_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -107,7 +107,6 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
project_id=project_id,
|
||||
)
|
||||
.filter(**filters)
|
||||
.annotate(bridge_id=F("issue_inbox__id"))
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||
@@ -204,9 +203,9 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
serializer = IssueStateInboxSerializer(issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
||||
def partial_update(self, request, slug, project_id, inbox_id, issue_id):
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
)
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
@@ -316,19 +315,16 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
)
|
||||
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
|
||||
issue = Issue.objects.get(
|
||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = IssueStateInboxSerializer(issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, slug, project_id, inbox_id, pk):
|
||||
def destroy(self, request, slug, project_id, inbox_id, issue_id):
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
)
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
@@ -350,7 +346,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
).delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
|
||||
@@ -52,6 +52,7 @@ from plane.app.serializers import (
|
||||
IssueRelationSerializer,
|
||||
RelatedIssueSerializer,
|
||||
IssuePublicSerializer,
|
||||
IssueRelationLiteSerializer,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
@@ -129,22 +130,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, project_id):
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
@@ -159,7 +144,26 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
is_subscribed=Exists(
|
||||
IssueSubscriber.objects.filter(
|
||||
subscriber=self.request.user, issue_id=OuterRef("id")
|
||||
)
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, project_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = self.get_queryset().filter(**filters)
|
||||
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
@@ -217,9 +221,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
|
||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
||||
issues = IssueSerializer(
|
||||
issue_queryset, many=True, fields=self.fields, expand=self.expand
|
||||
).data
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
@@ -256,7 +261,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
).get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
@@ -590,16 +598,19 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
issue_property, created = IssueProperty.objects.get_or_create(
|
||||
def patch(self, request, slug, project_id):
|
||||
issue_property = IssueProperty.objects.get(
|
||||
user=request.user,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
if not created:
|
||||
issue_property.properties = request.data.get("properties", {})
|
||||
issue_property.save()
|
||||
issue_property.properties = request.data.get("properties", {})
|
||||
issue_property.filters = request.data.get("filters", issue_property.filters)
|
||||
issue_property.display_filters = request.data.get(
|
||||
"display_filters", issue_property.display_filters
|
||||
)
|
||||
issue_property.display_properties = request.data.get(
|
||||
"display_properties", issue_property.display_properties
|
||||
)
|
||||
issue_property.save()
|
||||
serializer = IssuePropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
@@ -708,6 +719,13 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
is_subscribed=Exists(
|
||||
IssueSubscriber.objects.filter(
|
||||
subscriber=self.request.user, issue_id=OuterRef("id")
|
||||
)
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
@@ -728,7 +746,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
item["state_group"]: item["state_count"] for item in state_distribution
|
||||
}
|
||||
|
||||
serializer = IssueLiteSerializer(
|
||||
serializer = IssueSerializer(
|
||||
sub_issues,
|
||||
many=True,
|
||||
)
|
||||
@@ -775,7 +793,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
return Response(
|
||||
IssueFlatSerializer(updated_sub_issues, many=True).data,
|
||||
IssueSerializer(updated_sub_issues, many=True).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -1062,9 +1080,10 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
else issue_queryset.filter(parent__isnull=True)
|
||||
)
|
||||
|
||||
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
|
||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
||||
issues = IssueLiteSerializer(
|
||||
issue_queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
@@ -1365,23 +1384,62 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id, issue_id):
|
||||
issue_relations = (
|
||||
IssueRelation.objects.filter(Q(issue_id=issue_id) | Q(related_issue=issue_id))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("issue")
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
blocking_issues = issue_relations.filter(relation_type="blocked_by", related_issue_id=issue_id)
|
||||
blocked_by_issues = issue_relations.filter(relation_type="blocked_by", issue_id=issue_id)
|
||||
duplicate_issues = issue_relations.filter(issue_id=issue_id, relation_type="duplicate")
|
||||
duplicate_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="duplicate")
|
||||
relates_to_issues = issue_relations.filter(issue_id=issue_id, relation_type="relates_to")
|
||||
relates_to_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="relates_to")
|
||||
|
||||
blocked_by_issues_serialized = IssueRelationSerializer(blocked_by_issues, many=True).data
|
||||
duplicate_issues_serialized = IssueRelationSerializer(duplicate_issues, many=True).data
|
||||
relates_to_issues_serialized = IssueRelationSerializer(relates_to_issues, many=True).data
|
||||
|
||||
# revere relation for blocked by issues
|
||||
blocking_issues_serialized = RelatedIssueSerializer(blocking_issues, many=True).data
|
||||
# reverse relation for duplicate issues
|
||||
duplicate_issues_related_serialized = RelatedIssueSerializer(duplicate_issues_related, many=True).data
|
||||
# reverse relation for related issues
|
||||
relates_to_issues_related_serialized = RelatedIssueSerializer(relates_to_issues_related, many=True).data
|
||||
|
||||
response_data = {
|
||||
'blocking': blocking_issues_serialized,
|
||||
'blocked_by': blocked_by_issues_serialized,
|
||||
'duplicate': duplicate_issues_serialized + duplicate_issues_related_serialized,
|
||||
'relates_to': relates_to_issues_serialized + relates_to_issues_related_serialized,
|
||||
}
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
related_list = request.data.get("related_list", [])
|
||||
relation = request.data.get("relation", None)
|
||||
relation_type = request.data.get("relation_type", None)
|
||||
issues = request.data.get("issues", [])
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
issue_relation = IssueRelation.objects.bulk_create(
|
||||
[
|
||||
IssueRelation(
|
||||
issue_id=related_issue["issue"],
|
||||
related_issue_id=related_issue["related_issue"],
|
||||
relation_type=related_issue["relation_type"],
|
||||
issue_id=issue if relation_type == "blocking" else issue_id,
|
||||
related_issue_id=issue_id if relation_type == "blocking" else issue,
|
||||
relation_type="blocked_by" if relation_type == "blocking" else relation_type,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
for related_issue in related_list
|
||||
for issue in issues
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
@@ -1397,7 +1455,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
|
||||
if relation == "blocking":
|
||||
if relation_type == "blocking":
|
||||
return Response(
|
||||
RelatedIssueSerializer(issue_relation, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
@@ -1408,10 +1466,18 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, issue_id, pk):
|
||||
issue_relation = IssueRelation.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
def remove_relation(self, request, slug, project_id, issue_id):
|
||||
relation_type = request.data.get("relation_type", None)
|
||||
related_issue = request.data.get("related_issue", None)
|
||||
|
||||
if relation_type == "blocking":
|
||||
issue_relation = IssueRelation.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=related_issue, related_issue_id=issue_id
|
||||
)
|
||||
else:
|
||||
issue_relation = IssueRelation.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, related_issue_id=related_issue
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueRelationSerializer(issue_relation).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
@@ -1419,7 +1485,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
issue_relation.delete()
|
||||
issue_activity.delay(
|
||||
type="issue_relation.activity.deleted",
|
||||
requested_data=json.dumps({"related_list": None}),
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
@@ -1547,9 +1613,10 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
|
||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
||||
issues = IssueLiteSerializer(
|
||||
issue_queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
@@ -1626,4 +1693,4 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -21,8 +21,9 @@ from plane.app.serializers import (
|
||||
ModuleLinkSerializer,
|
||||
ModuleFavoriteSerializer,
|
||||
IssueStateSerializer,
|
||||
ModuleUserPropertiesSerializer,
|
||||
)
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission
|
||||
from plane.db.models import (
|
||||
Module,
|
||||
ModuleIssue,
|
||||
@@ -32,6 +33,7 @@ from plane.db.models import (
|
||||
ModuleFavorite,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
ModuleUserProperties,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.grouper import group_results
|
||||
@@ -54,7 +56,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
subquery = ModuleFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
module_id=OuterRef("pk"),
|
||||
@@ -136,7 +137,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("-is_favorite","-created_at")
|
||||
.order_by("-is_favorite", "-created_at")
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
@@ -153,6 +154,14 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset()
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
modules = ModuleSerializer(
|
||||
queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
return Response(modules, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
queryset = self.get_queryset().get(pk=pk)
|
||||
|
||||
@@ -289,7 +298,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
webhook_event = "module_issue"
|
||||
bulk = True
|
||||
|
||||
|
||||
filterset_fields = [
|
||||
"issue__labels__id",
|
||||
"issue__assignees__id",
|
||||
@@ -335,7 +343,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(bridge_id=F("issue_module__id"))
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project")
|
||||
@@ -359,9 +366,10 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
|
||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
||||
serializer = IssueStateSerializer(
|
||||
issues, many=True, fields=fields if fields else None
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id, module_id):
|
||||
issues = request.data.get("issues", [])
|
||||
@@ -444,20 +452,23 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, module_id, pk):
|
||||
def destroy(self, request, slug, project_id, module_id, issue_id):
|
||||
module_issue = ModuleIssue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
module_id=module_id,
|
||||
issue_id=issue_id,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"module_id": str(module_id),
|
||||
"issues": [str(module_issue.issue_id)],
|
||||
"issues": [str(issue_id)],
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(module_issue.issue_id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@@ -521,4 +532,42 @@ class ModuleFavoriteViewSet(BaseViewSet):
|
||||
module_id=module_id,
|
||||
)
|
||||
module_favorite.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class ModuleUserPropertiesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
def patch(self, request, slug, project_id, module_id):
|
||||
module_properties = ModuleUserProperties.objects.get(
|
||||
user=request.user,
|
||||
module_id=module_id,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
module_properties.filters = request.data.get(
|
||||
"filters", module_properties.filters
|
||||
)
|
||||
module_properties.display_filters = request.data.get(
|
||||
"display_filters", module_properties.display_filters
|
||||
)
|
||||
module_properties.display_properties = request.data.get(
|
||||
"display_properties", module_properties.display_properties
|
||||
)
|
||||
module_properties.save()
|
||||
|
||||
serializer = ModuleUserPropertiesSerializer(module_properties)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def get(self, request, slug, project_id, module_id):
|
||||
module_properties, _ = ModuleUserProperties.objects.get_or_create(
|
||||
user=request.user,
|
||||
project_id=project_id,
|
||||
module_id=module_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
serializer = ModuleUserPropertiesSerializer(module_properties)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -157,9 +157,8 @@ class PageViewSet(BaseViewSet):
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||
return Response(
|
||||
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
pages = PageSerializer(queryset, many=True).data
|
||||
return Response(pages, status=status.HTTP_200_OK)
|
||||
|
||||
def archive(self, request, slug, project_id, page_id):
|
||||
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
|
||||
@@ -210,9 +209,9 @@ class PageViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
).filter(archived_at__isnull=False)
|
||||
|
||||
return Response(
|
||||
PageSerializer(pages, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
pages = PageSerializer(pages, many=True).data
|
||||
return Response(pages, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||
|
||||
@@ -36,6 +36,7 @@ from plane.app.serializers import (
|
||||
ProjectFavoriteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectMemberRoleSerializer,
|
||||
)
|
||||
|
||||
from plane.app.permissions import (
|
||||
@@ -180,12 +181,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
projects, many=True
|
||||
).data,
|
||||
)
|
||||
projects = ProjectListSerializer(projects, many=True, fields=fields if fields else None).data
|
||||
return Response(projects, status=status.HTTP_200_OK)
|
||||
|
||||
return Response(
|
||||
ProjectListSerializer(
|
||||
projects, many=True, fields=fields if fields else None
|
||||
).data
|
||||
)
|
||||
|
||||
def create(self, request, slug):
|
||||
try:
|
||||
@@ -713,13 +711,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
project_member = ProjectMember.objects.get(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Get the list of project members for the project
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
@@ -727,10 +719,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
is_active=True,
|
||||
).select_related("project", "member", "workspace")
|
||||
|
||||
if project_member.role > 10:
|
||||
serializer = ProjectMemberAdminSerializer(project_members, many=True)
|
||||
else:
|
||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||
serializer = ProjectMemberRoleSerializer(project_members, fields=("id", "member", "role"), many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
@@ -1010,18 +999,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
files = []
|
||||
s3_client_params = {
|
||||
"service_name": "s3",
|
||||
"aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
|
||||
"aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY,
|
||||
}
|
||||
|
||||
# Use AWS_S3_ENDPOINT_URL if it is present in the settings
|
||||
if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL:
|
||||
s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
|
||||
|
||||
s3 = boto3.client(**s3_client_params)
|
||||
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
params = {
|
||||
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||
"Prefix": "static/project-cover/",
|
||||
@@ -1034,19 +1016,9 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
if not content["Key"].endswith(
|
||||
"/"
|
||||
): # This line ensures we're only getting files, not "sub-folders"
|
||||
if (
|
||||
hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
|
||||
and settings.AWS_S3_CUSTOM_DOMAIN
|
||||
and hasattr(settings, "AWS_S3_URL_PROTOCOL")
|
||||
and settings.AWS_S3_URL_PROTOCOL
|
||||
):
|
||||
files.append(
|
||||
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}"
|
||||
)
|
||||
else:
|
||||
files.append(
|
||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
)
|
||||
files.append(
|
||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
)
|
||||
|
||||
return Response(files, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -27,7 +27,12 @@ from plane.app.serializers import (
|
||||
IssueLiteSerializer,
|
||||
IssueViewFavoriteSerializer,
|
||||
)
|
||||
from plane.app.permissions import WorkspaceEntityPermission, ProjectEntityPermission
|
||||
from plane.app.permissions import (
|
||||
WorkspaceEntityPermission,
|
||||
ProjectEntityPermission,
|
||||
WorkspaceViewerPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
GlobalView,
|
||||
@@ -43,8 +48,8 @@ from plane.utils.grouper import group_results
|
||||
|
||||
|
||||
class GlobalViewViewSet(BaseViewSet):
|
||||
serializer_class = GlobalViewSerializer
|
||||
model = GlobalView
|
||||
serializer_class = IssueViewSerializer
|
||||
model = IssueView
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
@@ -58,6 +63,7 @@ class GlobalViewViewSet(BaseViewSet):
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project__isnull=True)
|
||||
.select_related("workspace")
|
||||
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||
.distinct()
|
||||
@@ -179,12 +185,10 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
|
||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
||||
return Response(
|
||||
issue_dict,
|
||||
status=status.HTTP_200_OK,
|
||||
serializer = IssueLiteSerializer(
|
||||
issue_queryset, many=True, fields=fields if fields else None
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueViewViewSet(BaseViewSet):
|
||||
@@ -217,6 +221,14 @@ class IssueViewViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset()
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
views = IssueViewSerializer(
|
||||
queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
return Response(views, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueViewFavoriteViewSet(BaseViewSet):
|
||||
serializer_class = IssueViewFavoriteSerializer
|
||||
@@ -246,4 +258,4 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
||||
view_id=view_id,
|
||||
)
|
||||
view_favourite.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -44,6 +44,8 @@ from plane.app.serializers import (
|
||||
IssueLiteSerializer,
|
||||
WorkspaceMemberAdminSerializer,
|
||||
WorkspaceMemberMeSerializer,
|
||||
ProjectMemberRoleSerializer,
|
||||
WorkspaceUserPropertiesSerializer,
|
||||
)
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from . import BaseViewSet
|
||||
@@ -64,6 +66,7 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
CycleIssue,
|
||||
IssueReaction,
|
||||
WorkspaceUserProperties
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
WorkSpaceBasePermission,
|
||||
@@ -71,11 +74,13 @@ from plane.app.permissions import (
|
||||
WorkspaceEntityPermission,
|
||||
WorkspaceViewerPermission,
|
||||
WorkspaceUserPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.event_tracking_task import workspace_invite_event
|
||||
|
||||
|
||||
class WorkSpaceViewSet(BaseViewSet):
|
||||
model = Workspace
|
||||
serializer_class = WorkSpaceSerializer
|
||||
@@ -173,6 +178,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
member_count = (
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace=OuterRef("id"),
|
||||
@@ -208,9 +214,12 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
workspaces = WorkSpaceSerializer(
|
||||
self.filter_queryset(workspace),
|
||||
fields=fields if fields else None,
|
||||
many=True,
|
||||
).data
|
||||
return Response(workspaces, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||
@@ -407,7 +416,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
|
||||
|
||||
# Delete the invitation
|
||||
workspace_invite.delete()
|
||||
|
||||
|
||||
# Send event
|
||||
workspace_invite_event.delay(
|
||||
user=user.id if user is not None else None,
|
||||
@@ -537,10 +546,15 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
workspace_members = self.get_queryset()
|
||||
|
||||
if workspace_member.role > 10:
|
||||
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
|
||||
serializer = WorkspaceMemberAdminSerializer(
|
||||
workspace_members,
|
||||
fields=("id", "member", "role"),
|
||||
many=True,
|
||||
)
|
||||
else:
|
||||
serializer = WorkSpaceMemberSerializer(
|
||||
workspace_members,
|
||||
fields=("id", "member", "role"),
|
||||
many=True,
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -705,6 +719,43 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class WorkspaceProjectMemberEndpoint(BaseAPIView):
|
||||
serializer_class = ProjectMemberRoleSerializer
|
||||
model = ProjectMember
|
||||
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
# Fetch all project IDs where the user is involved
|
||||
project_ids = ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
member__is_bot=False,
|
||||
is_active=True,
|
||||
).values_list('project_id', flat=True).distinct()
|
||||
|
||||
# Get all the project members in which the user is involved
|
||||
project_members = ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member__is_bot=False,
|
||||
project_id__in=project_ids,
|
||||
is_active=True,
|
||||
).select_related("project", "member", "workspace")
|
||||
project_members = ProjectMemberRoleSerializer(project_members, many=True).data
|
||||
|
||||
project_members_dict = dict()
|
||||
|
||||
# Construct a dictionary with project_id as key and project_members as value
|
||||
for project_member in project_members:
|
||||
project_id = project_member.pop("project")
|
||||
if str(project_id) not in project_members_dict:
|
||||
project_members_dict[str(project_id)] = []
|
||||
project_members_dict[str(project_id)].append(project_member)
|
||||
|
||||
return Response(project_members_dict, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class TeamMemberViewSet(BaseViewSet):
|
||||
serializer_class = TeamSerializer
|
||||
model = Team
|
||||
@@ -1334,8 +1385,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
issues = IssueLiteSerializer(
|
||||
issue_queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkspaceLabelsEndpoint(BaseAPIView):
|
||||
@@ -1349,3 +1399,30 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
|
||||
project__project_projectmember__member=request.user,
|
||||
).values("parent", "name", "color", "id", "project_id", "workspace__slug")
|
||||
return Response(labels, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkspaceUserPropertiesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceViewerPermission,
|
||||
]
|
||||
|
||||
def patch(self, request, slug):
|
||||
workspace_properties = WorkspaceUserProperties.objects.get(
|
||||
user=request.user,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
workspace_properties.filters = request.data.get("filters", workspace_properties.filters)
|
||||
workspace_properties.display_filters = request.data.get("display_filters", workspace_properties.display_filters)
|
||||
workspace_properties.display_properties = request.data.get("display_properties", workspace_properties.display_properties)
|
||||
workspace_properties.save()
|
||||
|
||||
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def get(self, request, slug):
|
||||
workspace_properties, _ = WorkspaceUserProperties.objects.get_or_create(
|
||||
user=request.user, workspace__slug=slug
|
||||
)
|
||||
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -112,8 +112,16 @@ def track_parent(
|
||||
epoch,
|
||||
):
|
||||
if current_instance.get("parent") != requested_data.get("parent"):
|
||||
old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first() if current_instance.get("parent") is not None else None
|
||||
new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first() if requested_data.get("parent") is not None else None
|
||||
old_parent = (
|
||||
Issue.objects.filter(pk=current_instance.get("parent")).first()
|
||||
if current_instance.get("parent") is not None
|
||||
else None
|
||||
)
|
||||
new_parent = (
|
||||
Issue.objects.filter(pk=requested_data.get("parent")).first()
|
||||
if requested_data.get("parent") is not None
|
||||
else None
|
||||
)
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -714,7 +722,9 @@ def create_cycle_issue_activity(
|
||||
cycle = Cycle.objects.filter(
|
||||
pk=created_record.get("fields").get("cycle")
|
||||
).first()
|
||||
issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first()
|
||||
issue = Issue.objects.filter(
|
||||
pk=created_record.get("fields").get("issue")
|
||||
).first()
|
||||
if issue:
|
||||
issue.updated_at = timezone.now()
|
||||
issue.save(update_fields=["updated_at"])
|
||||
@@ -830,7 +840,9 @@ def create_module_issue_activity(
|
||||
module = Module.objects.filter(
|
||||
pk=created_record.get("fields").get("module")
|
||||
).first()
|
||||
issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first()
|
||||
issue = Issue.objects.filter(
|
||||
pk=created_record.get("fields").get("issue")
|
||||
).first()
|
||||
if issue:
|
||||
issue.updated_at = timezone.now()
|
||||
issue.save(update_fields=["updated_at"])
|
||||
@@ -1276,40 +1288,42 @@ def create_issue_relation_activity(
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance is None and requested_data.get("related_list") is not None:
|
||||
for issue_relation in requested_data.get("related_list"):
|
||||
if issue_relation.get("relation_type") == "blocked_by":
|
||||
relation_type = "blocking"
|
||||
else:
|
||||
relation_type = issue_relation.get("relation_type")
|
||||
issue = Issue.objects.get(pk=issue_relation.get("issue"))
|
||||
if current_instance is None and requested_data.get("issues") is not None:
|
||||
for related_issue in requested_data.get("issues"):
|
||||
issue = Issue.objects.get(pk=related_issue)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_relation.get("related_issue"),
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
field=relation_type,
|
||||
field=requested_data.get("relation_type"),
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"added {relation_type} relation",
|
||||
old_identifier=issue_relation.get("issue"),
|
||||
comment=f"added {requested_data.get('relation_type')} relation",
|
||||
old_identifier=related_issue,
|
||||
)
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_relation.get("issue"),
|
||||
issue_id=related_issue,
|
||||
actor_id=actor_id,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
field=f'{issue_relation.get("relation_type")}',
|
||||
field="blocking"
|
||||
if requested_data.get("relation_type") == "blocked_by"
|
||||
else (
|
||||
"blocked_by"
|
||||
if requested_data.get("relation_type") == "blocking"
|
||||
else requested_data.get("relation_type")
|
||||
),
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f'added {issue_relation.get("relation_type")} relation',
|
||||
old_identifier=issue_relation.get("related_issue"),
|
||||
comment=f'added {"blocking" if requested_data.get("relation_type") == "blocked_by" else ("blocked_by" if requested_data.get("relation_type") == "blocking" else requested_data.get("relation_type")),} relation',
|
||||
old_identifier=issue_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
@@ -1329,44 +1343,44 @@ def delete_issue_relation_activity(
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance is not None and requested_data.get("related_list") is None:
|
||||
if current_instance.get("relation_type") == "blocked_by":
|
||||
relation_type = "blocking"
|
||||
else:
|
||||
relation_type = current_instance.get("relation_type")
|
||||
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=current_instance.get("related_issue"),
|
||||
actor_id=actor_id,
|
||||
verb="deleted",
|
||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field=relation_type,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"deleted {relation_type} relation",
|
||||
old_identifier=current_instance.get("issue"),
|
||||
epoch=epoch,
|
||||
)
|
||||
issue = Issue.objects.get(pk=requested_data.get("related_issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb="deleted",
|
||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field=requested_data.get("relation_type"),
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"deleted {requested_data.get('relation_type')} relation",
|
||||
old_identifier=requested_data.get("related_issue"),
|
||||
epoch=epoch,
|
||||
)
|
||||
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=current_instance.get("issue"),
|
||||
actor_id=actor_id,
|
||||
verb="deleted",
|
||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field=f'{current_instance.get("relation_type")}',
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f'deleted {current_instance.get("relation_type")} relation',
|
||||
old_identifier=current_instance.get("related_issue"),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=requested_data.get("related_issue"),
|
||||
actor_id=actor_id,
|
||||
verb="deleted",
|
||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocking"
|
||||
if requested_data.get("relation_type") == "blocked_by"
|
||||
else (
|
||||
"blocked_by"
|
||||
if requested_data.get("relation_type") == "blocking"
|
||||
else requested_data.get("relation_type")
|
||||
),
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f'deleted {requested_data.get("relation_type")} relation',
|
||||
old_identifier=requested_data.get("related_issue"),
|
||||
epoch=epoch,
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
def create_draft_issue_activity(
|
||||
requested_data,
|
||||
|
||||
@@ -291,6 +291,9 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
|
||||
sender = "in_app:issue_activities:assigned"
|
||||
|
||||
for issue_activity in issue_activities_created:
|
||||
# Do not send notification for description update
|
||||
if issue_activity.get("field") == "description":
|
||||
continue;
|
||||
issue_comment = issue_activity.get("issue_comment")
|
||||
if issue_comment is not None:
|
||||
issue_comment = IssueComment.objects.get(
|
||||
@@ -341,7 +344,7 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
|
||||
for mention_id in comment_mentions:
|
||||
|
||||
136
apiserver/plane/db/migrations/0052_auto_20231220_1141.py
Normal file
136
apiserver/plane/db/migrations/0052_auto_20231220_1141.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-20 11:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import plane.db.models.cycle
|
||||
import plane.db.models.issue
|
||||
import plane.db.models.module
|
||||
import plane.db.models.view
|
||||
import plane.db.models.workspace
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0051_cycle_external_id_cycle_external_source_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='issueview',
|
||||
old_name='query_data',
|
||||
new_name='filters',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='issueproperty',
|
||||
old_name='properties',
|
||||
new_name='display_properties',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issueproperty',
|
||||
name='display_properties',
|
||||
field=models.JSONField(default=plane.db.models.issue.get_default_display_properties),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueproperty',
|
||||
name='display_filters',
|
||||
field=models.JSONField(default=plane.db.models.issue.get_default_display_filters),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueproperty',
|
||||
name='filters',
|
||||
field=models.JSONField(default=plane.db.models.issue.get_default_filters),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueview',
|
||||
name='display_filters',
|
||||
field=models.JSONField(default=plane.db.models.view.get_default_display_filters),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueview',
|
||||
name='display_properties',
|
||||
field=models.JSONField(default=plane.db.models.view.get_default_display_properties),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueview',
|
||||
name='sort_order',
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issueview',
|
||||
name='project',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WorkspaceUserProperties',
|
||||
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)),
|
||||
('filters', models.JSONField(default=plane.db.models.workspace.get_default_filters)),
|
||||
('display_filters', models.JSONField(default=plane.db.models.workspace.get_default_display_filters)),
|
||||
('display_properties', models.JSONField(default=plane.db.models.workspace.get_default_display_properties)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to=settings.AUTH_USER_MODEL)),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Workspace User Property',
|
||||
'verbose_name_plural': 'Workspace User Property',
|
||||
'db_table': 'Workspace_user_properties',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('workspace', 'user')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ModuleUserProperties',
|
||||
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)),
|
||||
('filters', models.JSONField(default=plane.db.models.module.get_default_filters)),
|
||||
('display_filters', models.JSONField(default=plane.db.models.module.get_default_display_filters)),
|
||||
('display_properties', models.JSONField(default=plane.db.models.module.get_default_display_properties)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to='db.module')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to=settings.AUTH_USER_MODEL)),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Module User Property',
|
||||
'verbose_name_plural': 'Module User Property',
|
||||
'db_table': 'module_user_properties',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('module', 'user')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CycleUserProperties',
|
||||
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)),
|
||||
('filters', models.JSONField(default=plane.db.models.cycle.get_default_filters)),
|
||||
('display_filters', models.JSONField(default=plane.db.models.cycle.get_default_display_filters)),
|
||||
('display_properties', models.JSONField(default=plane.db.models.cycle.get_default_display_properties)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to='db.cycle')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to=settings.AUTH_USER_MODEL)),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Cycle User Property',
|
||||
'verbose_name_plural': 'Cycle User Properties',
|
||||
'db_table': 'cycle_user_properties',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('cycle', 'user')},
|
||||
},
|
||||
),
|
||||
]
|
||||
65
apiserver/plane/db/migrations/0053_auto_20240102_1315.py
Normal file
65
apiserver/plane/db/migrations/0053_auto_20240102_1315.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-02 13:15
|
||||
|
||||
from plane.db.models import WorkspaceUserProperties, ProjectMember, IssueView
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def workspace_user_properties(apps, schema_editor):
|
||||
WorkspaceMember = apps.get_model("db", "WorkspaceMember")
|
||||
updated_workspace_user_properties = []
|
||||
for workspace_members in WorkspaceMember.objects.all():
|
||||
updated_workspace_user_properties.append(
|
||||
WorkspaceUserProperties(
|
||||
user_id=workspace_members.member_id,
|
||||
display_filters=workspace_members.view_props.get("display_filters"),
|
||||
display_properties=workspace_members.view_props.get("display_properties"),
|
||||
workspace_id=workspace_members.workspace_id,
|
||||
)
|
||||
)
|
||||
WorkspaceUserProperties.objects.bulk_create(updated_workspace_user_properties, batch_size=2000)
|
||||
|
||||
|
||||
def project_user_properties(apps, schema_editor):
|
||||
IssueProperty = apps.get_model("db", "IssueProperty")
|
||||
updated_issue_user_properties = []
|
||||
for issue_property in IssueProperty.objects.all():
|
||||
project_member = ProjectMember.objects.filter(project_id=issue_property.project_id, member_id=issue_property.user_id).first()
|
||||
if project_member:
|
||||
issue_property.filters = project_member.view_props.get("filters")
|
||||
issue_property.display_filters = project_member.view_props.get("display_filters")
|
||||
updated_issue_user_properties.append(issue_property)
|
||||
|
||||
IssueProperty.objects.bulk_update(updated_issue_user_properties, ["filters", "display_filters"], batch_size=2000)
|
||||
|
||||
|
||||
def issue_view(apps, schema_editor):
|
||||
GlobalView = apps.get_model("db", "GlobalView")
|
||||
updated_issue_views = []
|
||||
|
||||
for global_view in GlobalView.objects.all():
|
||||
updated_issue_views.append(
|
||||
IssueView(
|
||||
workspace_id=global_view.workspace_id,
|
||||
name=global_view.name,
|
||||
description=global_view.description,
|
||||
query=global_view.query,
|
||||
access=global_view.access,
|
||||
filters=global_view.query_data.get("filters", {}),
|
||||
sort_order=global_view.sort_order,
|
||||
created_by_id=global_view.created_by_id,
|
||||
updated_by_id=global_view.updated_by_id,
|
||||
)
|
||||
)
|
||||
IssueView.objects.bulk_create(updated_issue_views, batch_size=100)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0052_auto_20231220_1141'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(workspace_user_properties),
|
||||
migrations.RunPython(project_user_properties),
|
||||
migrations.RunPython(issue_view),
|
||||
]
|
||||
@@ -9,6 +9,8 @@ from .workspace import (
|
||||
WorkspaceMemberInvite,
|
||||
TeamMember,
|
||||
WorkspaceTheme,
|
||||
WorkspaceUserProperties,
|
||||
WorkspaceBaseModel,
|
||||
)
|
||||
|
||||
from .project import (
|
||||
@@ -48,11 +50,11 @@ from .social_connection import SocialLoginConnection
|
||||
|
||||
from .state import State
|
||||
|
||||
from .cycle import Cycle, CycleIssue, CycleFavorite
|
||||
from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
|
||||
|
||||
from .view import GlobalView, IssueView, IssueViewFavorite
|
||||
|
||||
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
|
||||
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite, ModuleUserProperties
|
||||
|
||||
from .api import APIToken, APIActivityLog
|
||||
|
||||
|
||||
@@ -6,6 +6,47 @@ from django.conf import settings
|
||||
from . import ProjectBaseModel
|
||||
|
||||
|
||||
def get_default_filters():
|
||||
return {
|
||||
"priority": None,
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"assignees": None,
|
||||
"created_by": None,
|
||||
"labels": None,
|
||||
"start_date": None,
|
||||
"target_date": None,
|
||||
"subscriber": None,
|
||||
}
|
||||
|
||||
def get_default_display_filters():
|
||||
return {
|
||||
"group_by": None,
|
||||
"order_by": "-created_at",
|
||||
"type": None,
|
||||
"sub_issue": True,
|
||||
"show_empty_groups": True,
|
||||
"layout": "list",
|
||||
"calendar_date_range": "",
|
||||
}
|
||||
|
||||
def get_default_display_properties():
|
||||
return {
|
||||
"assignee": True,
|
||||
"attachment_count": True,
|
||||
"created_on": True,
|
||||
"due_date": True,
|
||||
"estimate": True,
|
||||
"key": True,
|
||||
"labels": True,
|
||||
"link": True,
|
||||
"priority": True,
|
||||
"start_date": True,
|
||||
"state": True,
|
||||
"sub_issue_count": True,
|
||||
"updated_on": True,
|
||||
}
|
||||
|
||||
class Cycle(ProjectBaseModel):
|
||||
name = models.CharField(max_length=255, verbose_name="Cycle Name")
|
||||
description = models.TextField(verbose_name="Cycle Description", blank=True)
|
||||
@@ -89,3 +130,28 @@ class CycleFavorite(ProjectBaseModel):
|
||||
def __str__(self):
|
||||
"""Return user and the cycle"""
|
||||
return f"{self.user.email} <{self.cycle.name}>"
|
||||
|
||||
|
||||
class CycleUserProperties(ProjectBaseModel):
|
||||
cycle = models.ForeignKey(
|
||||
"db.Cycle", on_delete=models.CASCADE, related_name="cycle_user_properties"
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="cycle_user_properties",
|
||||
)
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = ["cycle", "user"]
|
||||
verbose_name = "Cycle User Property"
|
||||
verbose_name_plural = "Cycle User Properties"
|
||||
db_table = "cycle_user_properties"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.cycle.name} {self.user.email}"
|
||||
@@ -33,6 +33,48 @@ def get_default_properties():
|
||||
}
|
||||
|
||||
|
||||
def get_default_filters():
|
||||
return {
|
||||
"priority": None,
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"assignees": None,
|
||||
"created_by": None,
|
||||
"labels": None,
|
||||
"start_date": None,
|
||||
"target_date": None,
|
||||
"subscriber": None,
|
||||
}
|
||||
|
||||
def get_default_display_filters():
|
||||
return {
|
||||
"group_by": None,
|
||||
"order_by": "-created_at",
|
||||
"type": None,
|
||||
"sub_issue": True,
|
||||
"show_empty_groups": True,
|
||||
"layout": "list",
|
||||
"calendar_date_range": "",
|
||||
}
|
||||
|
||||
def get_default_display_properties():
|
||||
return {
|
||||
"assignee": True,
|
||||
"attachment_count": True,
|
||||
"created_on": True,
|
||||
"due_date": True,
|
||||
"estimate": True,
|
||||
"key": True,
|
||||
"labels": True,
|
||||
"link": True,
|
||||
"priority": True,
|
||||
"start_date": True,
|
||||
"state": True,
|
||||
"sub_issue_count": True,
|
||||
"updated_on": True,
|
||||
}
|
||||
|
||||
|
||||
# TODO: Handle identifiers for Bulk Inserts - nk
|
||||
class IssueManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
@@ -394,7 +436,9 @@ class IssueProperty(ProjectBaseModel):
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_property_user",
|
||||
)
|
||||
properties = models.JSONField(default=get_default_properties)
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Property"
|
||||
|
||||
@@ -6,6 +6,47 @@ from django.conf import settings
|
||||
from . import ProjectBaseModel
|
||||
|
||||
|
||||
def get_default_filters():
|
||||
return {
|
||||
"priority": None,
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"assignees": None,
|
||||
"created_by": None,
|
||||
"labels": None,
|
||||
"start_date": None,
|
||||
"target_date": None,
|
||||
"subscriber": None,
|
||||
},
|
||||
|
||||
def get_default_display_filters():
|
||||
return {
|
||||
"group_by": None,
|
||||
"order_by": "-created_at",
|
||||
"type": None,
|
||||
"sub_issue": True,
|
||||
"show_empty_groups": True,
|
||||
"layout": "list",
|
||||
"calendar_date_range": "",
|
||||
}
|
||||
|
||||
def get_default_display_properties():
|
||||
return {
|
||||
"assignee": True,
|
||||
"attachment_count": True,
|
||||
"created_on": True,
|
||||
"due_date": True,
|
||||
"estimate": True,
|
||||
"key": True,
|
||||
"labels": True,
|
||||
"link": True,
|
||||
"priority": True,
|
||||
"start_date": True,
|
||||
"state": True,
|
||||
"sub_issue_count": True,
|
||||
"updated_on": True,
|
||||
}
|
||||
|
||||
class Module(ProjectBaseModel):
|
||||
name = models.CharField(max_length=255, verbose_name="Module Name")
|
||||
description = models.TextField(verbose_name="Module Description", blank=True)
|
||||
@@ -141,3 +182,28 @@ class ModuleFavorite(ProjectBaseModel):
|
||||
def __str__(self):
|
||||
"""Return user and the module"""
|
||||
return f"{self.user.email} <{self.module.name}>"
|
||||
|
||||
|
||||
class ModuleUserProperties(ProjectBaseModel):
|
||||
module = models.ForeignKey(
|
||||
"db.Module", on_delete=models.CASCADE, related_name="module_user_properties"
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="module_user_properties",
|
||||
)
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = ["module", "user"]
|
||||
verbose_name = "Module User Property"
|
||||
verbose_name_plural = "Module User Property"
|
||||
db_table = "module_user_properties"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.module.name} {self.user.email}"
|
||||
@@ -3,9 +3,50 @@ from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
# Module import
|
||||
from . import ProjectBaseModel, BaseModel
|
||||
from . import ProjectBaseModel, BaseModel, WorkspaceBaseModel
|
||||
|
||||
|
||||
def get_default_filters():
|
||||
return {
|
||||
"priority": None,
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"assignees": None,
|
||||
"created_by": None,
|
||||
"labels": None,
|
||||
"start_date": None,
|
||||
"target_date": None,
|
||||
"subscriber": None,
|
||||
}
|
||||
|
||||
def get_default_display_filters():
|
||||
return {
|
||||
"group_by": None,
|
||||
"order_by": "-created_at",
|
||||
"type": None,
|
||||
"sub_issue": True,
|
||||
"show_empty_groups": True,
|
||||
"layout": "list",
|
||||
"calendar_date_range": "",
|
||||
}
|
||||
|
||||
def get_default_display_properties():
|
||||
return {
|
||||
"assignee": True,
|
||||
"attachment_count": True,
|
||||
"created_on": True,
|
||||
"due_date": True,
|
||||
"estimate": True,
|
||||
"key": True,
|
||||
"labels": True,
|
||||
"link": True,
|
||||
"priority": True,
|
||||
"start_date": True,
|
||||
"state": True,
|
||||
"sub_issue_count": True,
|
||||
"updated_on": True,
|
||||
}
|
||||
|
||||
class GlobalView(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
|
||||
@@ -40,14 +81,20 @@ class GlobalView(BaseModel):
|
||||
return f"{self.name} <{self.workspace.name}>"
|
||||
|
||||
|
||||
class IssueView(ProjectBaseModel):
|
||||
class IssueView(WorkspaceBaseModel):
|
||||
name = models.CharField(max_length=255, verbose_name="View Name")
|
||||
description = models.TextField(verbose_name="View Description", blank=True)
|
||||
query = models.JSONField(verbose_name="View Query")
|
||||
filters = models.JSONField(default=dict)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
access = models.PositiveSmallIntegerField(
|
||||
default=1, choices=((0, "Private"), (1, "Public"))
|
||||
)
|
||||
query_data = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
cycle = models.ForeignKey("db.Cycle", on_delete=models.CASCADE, related_name="views")
|
||||
module = models.ForeignKey("db.Module", on_delete=models.CASCADE, related_name="views")
|
||||
layouts = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue View"
|
||||
|
||||
@@ -54,6 +54,51 @@ def get_default_props():
|
||||
},
|
||||
}
|
||||
|
||||
def get_default_filters():
|
||||
return {
|
||||
"priority": None,
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"assignees": None,
|
||||
"created_by": None,
|
||||
"labels": None,
|
||||
"start_date": None,
|
||||
"target_date": None,
|
||||
"subscriber": None,
|
||||
}
|
||||
|
||||
def get_default_display_filters():
|
||||
return {
|
||||
"display_filters": {
|
||||
"group_by": None,
|
||||
"order_by": "-created_at",
|
||||
"type": None,
|
||||
"sub_issue": True,
|
||||
"show_empty_groups": True,
|
||||
"layout": "list",
|
||||
"calendar_date_range": "",
|
||||
},
|
||||
}
|
||||
|
||||
def get_default_display_properties():
|
||||
return {
|
||||
"display_properties": {
|
||||
"assignee": True,
|
||||
"attachment_count": True,
|
||||
"created_on": True,
|
||||
"due_date": True,
|
||||
"estimate": True,
|
||||
"key": True,
|
||||
"labels": True,
|
||||
"link": True,
|
||||
"priority": True,
|
||||
"start_date": True,
|
||||
"state": True,
|
||||
"sub_issue_count": True,
|
||||
"updated_on": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_issue_props():
|
||||
return {
|
||||
@@ -103,6 +148,22 @@ class Workspace(BaseModel):
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class WorkspaceBaseModel(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", models.CASCADE, related_name="workspace_%(class)s"
|
||||
)
|
||||
project = models.ForeignKey(
|
||||
"db.Project", models.CASCADE, related_name="project_%(class)s", null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.project:
|
||||
self.workspace = self.project.workspace
|
||||
super(WorkspaceBaseModel, self).save(*args, **kwargs)
|
||||
|
||||
class WorkspaceMember(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member"
|
||||
@@ -218,3 +279,28 @@ class WorkspaceTheme(BaseModel):
|
||||
verbose_name_plural = "Workspace Themes"
|
||||
db_table = "workspace_themes"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class WorkspaceUserProperties(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_user_properties"
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="workspace_user_properties",
|
||||
)
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "user"]
|
||||
verbose_name = "Workspace User Property"
|
||||
verbose_name_plural = "Workspace User Property"
|
||||
db_table = "Workspace_user_properties"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.workspace.name} {self.user.email}"
|
||||
@@ -30,7 +30,7 @@ openpyxl==3.1.2
|
||||
beautifulsoup4==4.12.2
|
||||
dj-database-url==2.1.0
|
||||
posthog==3.0.2
|
||||
cryptography==41.0.6
|
||||
cryptography==41.0.5
|
||||
lxml==4.9.3
|
||||
boto3==1.28.40
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ function download(){
|
||||
echo ""
|
||||
echo "Latest version is now available for you to use"
|
||||
echo ""
|
||||
echo "In case of Upgrade, your new setting file is available as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
|
||||
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
|
||||
echo ""
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"packages/eslint-config-custom",
|
||||
"packages/tailwind-config-custom",
|
||||
"packages/tsconfig",
|
||||
"packages/ui"
|
||||
"packages/ui",
|
||||
"packages/types"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h } from "jsx-dom-cjs";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { Node as ProseMirrorNode, ResolvedPos } from "@tiptap/pm/model";
|
||||
import { Decoration, NodeView } from "@tiptap/pm/view";
|
||||
import tippy, { Instance, Props } from "tippy.js";
|
||||
|
||||
@@ -8,6 +8,12 @@ import { CellSelection, TableMap, updateColumnsOnResize } from "@tiptap/pm/table
|
||||
|
||||
import { icons } from "src/ui/extensions/table/table/icons";
|
||||
|
||||
type ToolboxItem = {
|
||||
label: string;
|
||||
icon: string;
|
||||
action: (args: any) => void;
|
||||
};
|
||||
|
||||
export function updateColumns(
|
||||
node: ProseMirrorNode,
|
||||
colgroup: HTMLElement,
|
||||
@@ -75,7 +81,7 @@ const defaultTippyOptions: Partial<Props> = {
|
||||
placement: "right",
|
||||
};
|
||||
|
||||
function setCellsBackgroundColor(editor: Editor, backgroundColor) {
|
||||
function setCellsBackgroundColor(editor: Editor, backgroundColor: string) {
|
||||
return editor
|
||||
.chain()
|
||||
.focus()
|
||||
@@ -88,7 +94,7 @@ function setCellsBackgroundColor(editor: Editor, backgroundColor) {
|
||||
.run();
|
||||
}
|
||||
|
||||
const columnsToolboxItems = [
|
||||
const columnsToolboxItems: ToolboxItem[] = [
|
||||
{
|
||||
label: "Add Column Before",
|
||||
icon: icons.insertLeftTableIcon,
|
||||
@@ -109,7 +115,7 @@ const columnsToolboxItems = [
|
||||
}: {
|
||||
editor: Editor;
|
||||
triggerButton: HTMLElement;
|
||||
controlsContainer;
|
||||
controlsContainer: Element;
|
||||
}) => {
|
||||
createColorPickerToolbox({
|
||||
triggerButton,
|
||||
@@ -127,7 +133,7 @@ const columnsToolboxItems = [
|
||||
},
|
||||
];
|
||||
|
||||
const rowsToolboxItems = [
|
||||
const rowsToolboxItems: ToolboxItem[] = [
|
||||
{
|
||||
label: "Add Row Above",
|
||||
icon: icons.insertTopTableIcon,
|
||||
@@ -172,11 +178,12 @@ function createToolbox({
|
||||
tippyOptions,
|
||||
onClickItem,
|
||||
}: {
|
||||
triggerButton: HTMLElement;
|
||||
items: { icon: string; label: string }[];
|
||||
triggerButton: Element | null;
|
||||
items: ToolboxItem[];
|
||||
tippyOptions: any;
|
||||
onClickItem: any;
|
||||
onClickItem: (item: ToolboxItem) => void;
|
||||
}): Instance<Props> {
|
||||
// @ts-expect-error
|
||||
const toolbox = tippy(triggerButton, {
|
||||
content: h(
|
||||
"div",
|
||||
@@ -278,14 +285,14 @@ export class TableView implements NodeView {
|
||||
decorations: Decoration[];
|
||||
editor: Editor;
|
||||
getPos: () => number;
|
||||
hoveredCell;
|
||||
hoveredCell: ResolvedPos | null = null;
|
||||
map: TableMap;
|
||||
root: HTMLElement;
|
||||
table: HTMLElement;
|
||||
colgroup: HTMLElement;
|
||||
table: HTMLTableElement;
|
||||
colgroup: HTMLTableColElement;
|
||||
tbody: HTMLElement;
|
||||
rowsControl?: HTMLElement;
|
||||
columnsControl?: HTMLElement;
|
||||
rowsControl?: HTMLElement | null;
|
||||
columnsControl?: HTMLElement | null;
|
||||
columnsToolbox?: Instance<Props>;
|
||||
rowsToolbox?: Instance<Props>;
|
||||
controls?: HTMLElement;
|
||||
@@ -398,13 +405,13 @@ export class TableView implements NodeView {
|
||||
this.render();
|
||||
}
|
||||
|
||||
update(node: ProseMirrorNode, decorations) {
|
||||
update(node: ProseMirrorNode, decorations: readonly Decoration[]) {
|
||||
if (node.type !== this.node.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.node = node;
|
||||
this.decorations = decorations;
|
||||
this.decorations = [...decorations];
|
||||
this.map = TableMap.get(this.node);
|
||||
|
||||
if (this.editor.isEditable) {
|
||||
@@ -430,19 +437,16 @@ export class TableView implements NodeView {
|
||||
}
|
||||
|
||||
updateControls() {
|
||||
const { hoveredTable: table, hoveredCell: cell } = Object.values(this.decorations).reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.spec.hoveredCell !== undefined) {
|
||||
acc["hoveredCell"] = curr.spec.hoveredCell;
|
||||
}
|
||||
const { hoveredTable: table, hoveredCell: cell } = Object.values(this.decorations).reduce((acc, curr) => {
|
||||
if (curr.spec.hoveredCell !== undefined) {
|
||||
acc["hoveredCell"] = curr.spec.hoveredCell;
|
||||
}
|
||||
|
||||
if (curr.spec.hoveredTable !== undefined) {
|
||||
acc["hoveredTable"] = curr.spec.hoveredTable;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, HTMLElement>
|
||||
) as any;
|
||||
if (curr.spec.hoveredTable !== undefined) {
|
||||
acc["hoveredTable"] = curr.spec.hoveredTable;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, HTMLElement>) as any;
|
||||
|
||||
if (table === undefined || cell === undefined) {
|
||||
return this.root.classList.add("controls--disabled");
|
||||
@@ -453,14 +457,21 @@ export class TableView implements NodeView {
|
||||
|
||||
const cellDom = this.editor.view.nodeDOM(cell.pos) as HTMLElement;
|
||||
|
||||
if (!this.table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tableRect = this.table.getBoundingClientRect();
|
||||
const cellRect = cellDom.getBoundingClientRect();
|
||||
|
||||
this.columnsControl.style.left = `${cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft}px`;
|
||||
this.columnsControl.style.width = `${cellRect.width}px`;
|
||||
|
||||
this.rowsControl.style.top = `${cellRect.top - tableRect.top}px`;
|
||||
this.rowsControl.style.height = `${cellRect.height}px`;
|
||||
if (this.columnsControl) {
|
||||
this.columnsControl.style.left = `${cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft}px`;
|
||||
this.columnsControl.style.width = `${cellRect.width}px`;
|
||||
}
|
||||
if (this.rowsControl) {
|
||||
this.rowsControl.style.top = `${cellRect.top - tableRect.top}px`;
|
||||
this.rowsControl.style.height = `${cellRect.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
selectColumn() {
|
||||
@@ -471,10 +482,7 @@ export class TableView implements NodeView {
|
||||
const headCellPos = this.map.map[colIndex + this.map.width * (this.map.height - 1)] + (this.getPos() + 1);
|
||||
|
||||
const cellSelection = CellSelection.create(this.editor.view.state.doc, anchorCellPos, headCellPos);
|
||||
this.editor.view.dispatch(
|
||||
// @ts-ignore
|
||||
this.editor.state.tr.setSelection(cellSelection)
|
||||
);
|
||||
this.editor.view.dispatch(this.editor.state.tr.setSelection(cellSelection));
|
||||
}
|
||||
|
||||
selectRow() {
|
||||
@@ -485,9 +493,6 @@ export class TableView implements NodeView {
|
||||
const headCellPos = this.map.map[anchorCellIndex + (this.map.width - 1)] + (this.getPos() + 1);
|
||||
|
||||
const cellSelection = CellSelection.create(this.editor.state.doc, anchorCellPos, headCellPos);
|
||||
this.editor.view.dispatch(
|
||||
// @ts-ignore
|
||||
this.editor.view.state.tr.setSelection(cellSelection)
|
||||
);
|
||||
this.editor.view.dispatch(this.editor.view.state.tr.setSelection(cellSelection));
|
||||
}
|
||||
}
|
||||
|
||||
7
packages/types/package.json
Normal file
7
packages/types/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.14.0",
|
||||
"private": true,
|
||||
"main": "./src/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IProjectLite, IWorkspaceLite } from "types";
|
||||
import { IProjectLite, IWorkspaceLite } from "@plane/types";
|
||||
|
||||
export interface IGptResponse {
|
||||
response: string;
|
||||
@@ -1,7 +1,3 @@
|
||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||
getLayout?: (page: ReactElement) => ReactNode;
|
||||
};
|
||||
|
||||
export interface IAppConfig {
|
||||
email_password_login: boolean;
|
||||
file_size_limit: number;
|
||||
@@ -14,5 +10,5 @@ export interface IAppConfig {
|
||||
posthog_host: string | null;
|
||||
has_openai_configured: boolean;
|
||||
has_unsplash_configured: boolean;
|
||||
is_self_managed: boolean;
|
||||
is_smtp_configured: boolean;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IUser, IIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "types";
|
||||
import type { IUser, TIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "@plane/types";
|
||||
|
||||
export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft";
|
||||
|
||||
@@ -68,7 +68,7 @@ export type TLabelsDistribution = {
|
||||
|
||||
export interface CycleIssueResponse {
|
||||
id: string;
|
||||
issue_detail: IIssue;
|
||||
issue_detail: TIssue;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
created_by: string;
|
||||
@@ -82,7 +82,7 @@ export interface CycleIssueResponse {
|
||||
|
||||
export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined;
|
||||
|
||||
export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | null;
|
||||
export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | null;
|
||||
|
||||
export type CycleDateCheckData = {
|
||||
start_date: string;
|
||||
@@ -1,24 +1,24 @@
|
||||
export interface IEstimate {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
name: string;
|
||||
description: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
points: IEstimatePoint[];
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
project: string;
|
||||
project_detail: IProject;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
points: IEstimatePoint[];
|
||||
workspace: string;
|
||||
workspace_detail: IWorkspace;
|
||||
}
|
||||
|
||||
export interface IEstimatePoint {
|
||||
id: string;
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
description: string;
|
||||
estimate: string;
|
||||
id: string;
|
||||
key: number;
|
||||
project: string;
|
||||
updated_at: string;
|
||||
@@ -1,9 +1,9 @@
|
||||
export * from "./github-importer";
|
||||
export * from "./jira-importer";
|
||||
|
||||
import { IProjectLite } from "types/projects";
|
||||
import { IProjectLite } from "../projects";
|
||||
// types
|
||||
import { IUserLite } from "types/users";
|
||||
import { IUserLite } from "../users";
|
||||
|
||||
export interface IImporterService {
|
||||
created_at: string;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IIssue } from "./issues";
|
||||
import { TIssue } from "./issues";
|
||||
import type { IProjectLite } from "./projects";
|
||||
|
||||
export interface IInboxIssue extends IIssue {
|
||||
export interface IInboxIssue extends TIssue {
|
||||
issue_inbox: {
|
||||
duplicate_to: string | null;
|
||||
id: string;
|
||||
@@ -21,6 +21,11 @@ export * from "./reaction";
|
||||
export * from "./view-props";
|
||||
export * from "./workspace-views";
|
||||
export * from "./webhook";
|
||||
export * from "./issues/base"; // TODO: Remove this after development and the refactor/mobx-store-issue branch is stable
|
||||
export * from "./auth";
|
||||
export * from "./api_token";
|
||||
export * from "./instance";
|
||||
export * from "./app";
|
||||
|
||||
export type NestedKeyOf<ObjectType extends object> = {
|
||||
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ReactElement } from "react";
|
||||
import { KeyedMutator } from "swr";
|
||||
import type {
|
||||
IState,
|
||||
@@ -10,7 +11,8 @@ import type {
|
||||
IStateLite,
|
||||
Properties,
|
||||
IIssueDisplayFilterOptions,
|
||||
} from "types";
|
||||
IIssueReaction,
|
||||
} from "@plane/types";
|
||||
|
||||
export interface IIssueCycle {
|
||||
id: string;
|
||||
@@ -83,6 +85,7 @@ export interface IIssue {
|
||||
attachment_count: number;
|
||||
attachments: any[];
|
||||
issue_relations: IssueRelation[];
|
||||
issue_reactions: IIssueReaction[];
|
||||
related_issues: IssueRelation[];
|
||||
bridge_id?: string | null;
|
||||
completed_at: Date;
|
||||
@@ -138,7 +141,7 @@ export interface ISubIssuesState {
|
||||
|
||||
export interface ISubIssueResponse {
|
||||
state_distribution: ISubIssuesState;
|
||||
sub_issues: IIssue[];
|
||||
sub_issues: TIssue[];
|
||||
}
|
||||
|
||||
export interface BlockeIssueDetail {
|
||||
@@ -240,13 +243,13 @@ export interface IIssueAttachment {
|
||||
}
|
||||
|
||||
export interface IIssueViewProps {
|
||||
groupedIssues: { [key: string]: IIssue[] } | undefined;
|
||||
groupedIssues: { [key: string]: TIssue[] } | undefined;
|
||||
displayFilters: IIssueDisplayFilterOptions | undefined;
|
||||
isEmpty: boolean;
|
||||
mutateIssues: KeyedMutator<
|
||||
| IIssue[]
|
||||
| TIssue[]
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
[key: string]: TIssue[];
|
||||
}
|
||||
>;
|
||||
params: any;
|
||||
@@ -254,3 +257,88 @@ export interface IIssueViewProps {
|
||||
}
|
||||
|
||||
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
|
||||
|
||||
export interface ViewFlags {
|
||||
enableQuickAdd: boolean;
|
||||
enableIssueCreation: boolean;
|
||||
enableInlineEditing: boolean;
|
||||
}
|
||||
|
||||
export type GroupByColumnTypes =
|
||||
| "project"
|
||||
| "state"
|
||||
| "state_detail.group"
|
||||
| "priority"
|
||||
| "labels"
|
||||
| "assignees"
|
||||
| "created_by";
|
||||
|
||||
export interface IGroupByColumn {
|
||||
id: string;
|
||||
name: string;
|
||||
Icon: ReactElement | undefined;
|
||||
payload: Partial<TIssue>;
|
||||
}
|
||||
|
||||
export interface IIssueMap {
|
||||
[key: string]: TIssue;
|
||||
}
|
||||
|
||||
// new issue structure types
|
||||
export type TIssue = {
|
||||
id: string;
|
||||
name: string;
|
||||
state_id: string;
|
||||
description_html: string;
|
||||
sort_order: number;
|
||||
completed_at: string | null;
|
||||
estimate_point: number | null;
|
||||
priority: TIssuePriorities;
|
||||
start_date: string | null;
|
||||
target_date: string | null;
|
||||
sequence_id: number;
|
||||
project_id: string;
|
||||
parent_id: string | null;
|
||||
cycle_id: string | null;
|
||||
module_id: string | null;
|
||||
label_ids: string[];
|
||||
assignee_ids: string[];
|
||||
sub_issues_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
attachment_count: number;
|
||||
link_count: number;
|
||||
is_subscribed: boolean;
|
||||
archived_at: boolean;
|
||||
is_draft: boolean;
|
||||
// tempId is used for optimistic updates. It is not a part of the API response.
|
||||
tempId?: string;
|
||||
// issue details
|
||||
related_issues: any;
|
||||
issue_reactions: any;
|
||||
issue_relations: any;
|
||||
issue_cycle: any;
|
||||
issue_module: any;
|
||||
parent_detail: any;
|
||||
issue_link: any;
|
||||
};
|
||||
|
||||
export type TIssueMap = {
|
||||
[issue_id: string]: TIssue;
|
||||
};
|
||||
|
||||
export type TLoader = "init-loader" | "mutation" | undefined;
|
||||
|
||||
export type TGroupedIssues = {
|
||||
[group_id: string]: string[];
|
||||
};
|
||||
|
||||
export type TSubGroupedIssues = {
|
||||
[sub_grouped_id: string]: {
|
||||
[group_id: string]: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TUnGroupedIssues = string[];
|
||||
23
packages/types/src/issues/base.d.ts
vendored
Normal file
23
packages/types/src/issues/base.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// issues
|
||||
export * from "./issue";
|
||||
export * from "./issue_reaction";
|
||||
export * from "./issue_link";
|
||||
export * from "./issue_attachment";
|
||||
export * from "./issue_relation";
|
||||
export * from "./issue_activity";
|
||||
export * from "./issue_comment_reaction";
|
||||
export * from "./issue_sub_issues";
|
||||
|
||||
export type TLoader = "init-loader" | "mutation" | undefined;
|
||||
|
||||
export type TGroupedIssues = {
|
||||
[group_id: string]: string[];
|
||||
};
|
||||
|
||||
export type TSubGroupedIssues = {
|
||||
[sub_grouped_id: string]: {
|
||||
[group_id: string]: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TUnGroupedIssues = string[];
|
||||
36
packages/types/src/issues/issue.d.ts
vendored
Normal file
36
packages/types/src/issues/issue.d.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
// new issue structure types
|
||||
export type TIssue = {
|
||||
id: string;
|
||||
name: string;
|
||||
state_id: string;
|
||||
description_html: string;
|
||||
sort_order: number;
|
||||
completed_at: string | null;
|
||||
estimate_point: number | null;
|
||||
priority: TIssuePriorities;
|
||||
start_date: string;
|
||||
target_date: string;
|
||||
sequence_id: number;
|
||||
project_id: string;
|
||||
parent_id: string | null;
|
||||
cycle_id: string | null;
|
||||
module_id: string | null;
|
||||
label_ids: string[];
|
||||
assignee_ids: string[];
|
||||
sub_issues_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
attachment_count: number;
|
||||
link_count: number;
|
||||
is_subscribed: boolean;
|
||||
archived_at: boolean;
|
||||
is_draft: boolean;
|
||||
// tempId is used for optimistic updates. It is not a part of the API response.
|
||||
tempId?: string;
|
||||
};
|
||||
|
||||
export type TIssueMap = {
|
||||
[issue_id: string]: TIssue;
|
||||
};
|
||||
41
packages/types/src/issues/issue_activity.d.ts
vendored
Normal file
41
packages/types/src/issues/issue_activity.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
export type TIssueActivity = {
|
||||
access?: "EXTERNAL" | "INTERNAL";
|
||||
actor: string;
|
||||
actor_detail: IUserLite;
|
||||
attachments: any[];
|
||||
comment?: string;
|
||||
comment_html?: string;
|
||||
comment_stripped?: string;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
field: string | null;
|
||||
id: string;
|
||||
issue: string | null;
|
||||
issue_comment?: string | null;
|
||||
issue_detail: {
|
||||
description_html: string;
|
||||
id: string;
|
||||
name: string;
|
||||
priority: string | null;
|
||||
sequence_id: string;
|
||||
} | null;
|
||||
new_identifier: string | null;
|
||||
new_value: string | null;
|
||||
old_identifier: string | null;
|
||||
old_value: string | null;
|
||||
project: string;
|
||||
project_detail: IProjectLite;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
verb: string;
|
||||
workspace: string;
|
||||
workspace_detail?: IWorkspaceLite;
|
||||
};
|
||||
|
||||
export type TIssueActivityMap = {
|
||||
[issue_id: string]: TIssueActivity;
|
||||
};
|
||||
|
||||
export type TIssueActivityIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
23
packages/types/src/issues/issue_attachment.d.ts
vendored
Normal file
23
packages/types/src/issues/issue_attachment.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
export type TIssueAttachment = {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
attributes: {
|
||||
name: string;
|
||||
size: number;
|
||||
};
|
||||
asset: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
workspace: string;
|
||||
issue: string;
|
||||
};
|
||||
|
||||
export type TIssueAttachmentMap = {
|
||||
[issue_id: string]: TIssueAttachment;
|
||||
};
|
||||
|
||||
export type TIssueAttachmentIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
20
packages/types/src/issues/issue_comment_reaction.d.ts
vendored
Normal file
20
packages/types/src/issues/issue_comment_reaction.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
export type TIssueCommentReaction = {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
reaction: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
workspace: string;
|
||||
actor: string;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
export type TIssueCommentReactionMap = {
|
||||
[issue_id: string]: TIssueCommentReaction;
|
||||
};
|
||||
|
||||
export type TIssueCommentReactionIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
20
packages/types/src/issues/issue_link.d.ts
vendored
Normal file
20
packages/types/src/issues/issue_link.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
export type TIssueLinkEditableFields = {
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type TIssueLink = TIssueLinkEditableFields & {
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
created_by_detail: IUserLite;
|
||||
id: string;
|
||||
metadata: any;
|
||||
};
|
||||
|
||||
export type TIssueLinkMap = {
|
||||
[issue_id: string]: TIssueLink;
|
||||
};
|
||||
|
||||
export type TIssueLinkIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
21
packages/types/src/issues/issue_reaction.d.ts
vendored
Normal file
21
packages/types/src/issues/issue_reaction.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
export type TIssueReaction = {
|
||||
actor: string;
|
||||
actor_detail: IUserLite;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
id: string;
|
||||
issue: string;
|
||||
project: string;
|
||||
reaction: string;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
workspace: string;
|
||||
};
|
||||
|
||||
export type TIssueReactionMap = {
|
||||
[issue_id: string]: TIssueReaction;
|
||||
};
|
||||
|
||||
export type TIssueReactionIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
20
packages/types/src/issues/issue_relation.d.ts
vendored
Normal file
20
packages/types/src/issues/issue_relation.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { TIssue } from "./issues";
|
||||
|
||||
export type TIssueRelationTypes =
|
||||
| "blocking"
|
||||
| "blocked_by"
|
||||
| "duplicate"
|
||||
| "relates_to";
|
||||
|
||||
export type TIssueRelationObject = { issue_detail: TIssue };
|
||||
|
||||
export type TIssueRelation = Record<
|
||||
TIssueRelationTypes,
|
||||
TIssueRelationObject[]
|
||||
>;
|
||||
|
||||
export type TIssueRelationMap = {
|
||||
[issue_id: string]: Record<TIssueRelationTypes, string[]>;
|
||||
};
|
||||
|
||||
export type TIssueRelationIdMap = Record<TIssueRelationTypes, string[]>;
|
||||
22
packages/types/src/issues/issue_sub_issues.d.ts
vendored
Normal file
22
packages/types/src/issues/issue_sub_issues.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { TIssue } from "./issue";
|
||||
|
||||
export type TSubIssuesStateDistribution = {
|
||||
backlog: number;
|
||||
unstarted: number;
|
||||
started: number;
|
||||
completed: number;
|
||||
cancelled: number;
|
||||
};
|
||||
|
||||
export type TIssueSubIssues = {
|
||||
state_distribution: TSubIssuesStateDistribution;
|
||||
sub_issues: TIssue[];
|
||||
};
|
||||
|
||||
export type TIssueSubIssuesStateDistributionMap = {
|
||||
[issue_id: string]: TSubIssuesStateDistribution;
|
||||
};
|
||||
|
||||
export type TIssueSubIssuesIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
0
packages/types/src/issues/issue_subscription.d.ts
vendored
Normal file
0
packages/types/src/issues/issue_subscription.d.ts
vendored
Normal file
@@ -1,14 +1,14 @@
|
||||
import type {
|
||||
IUser,
|
||||
IUserLite,
|
||||
IIssue,
|
||||
TIssue,
|
||||
IProject,
|
||||
IWorkspace,
|
||||
IWorkspaceLite,
|
||||
IProjectLite,
|
||||
IIssueFilterOptions,
|
||||
ILinkDetails,
|
||||
} from "types";
|
||||
} from "@plane/types";
|
||||
|
||||
export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled";
|
||||
|
||||
@@ -58,7 +58,7 @@ export interface ModuleIssueResponse {
|
||||
created_by: string;
|
||||
id: string;
|
||||
issue: string;
|
||||
issue_detail: IIssue;
|
||||
issue_detail: TIssue;
|
||||
module: string;
|
||||
module_detail: IModule;
|
||||
project: string;
|
||||
@@ -75,4 +75,4 @@ export type ModuleLink = {
|
||||
|
||||
export type SelectModuleType = (IModule & { actionType: "edit" | "delete" | "create-issue" }) | undefined;
|
||||
|
||||
export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | undefined;
|
||||
export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | undefined;
|
||||
@@ -1,5 +1,5 @@
|
||||
// types
|
||||
import { IIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "types";
|
||||
import { TIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "@plane/types";
|
||||
|
||||
export interface IPage {
|
||||
access: number;
|
||||
@@ -27,15 +27,11 @@ export interface IPage {
|
||||
}
|
||||
|
||||
export interface IRecentPages {
|
||||
today: IPage[];
|
||||
yesterday: IPage[];
|
||||
this_week: IPage[];
|
||||
older: IPage[];
|
||||
[key: string]: IPage[];
|
||||
}
|
||||
|
||||
export interface RecentPagesResponse {
|
||||
[key: string]: IPage[];
|
||||
today: string[];
|
||||
yesterday: string[];
|
||||
this_week: string[];
|
||||
older: string[];
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
export interface IPageBlock {
|
||||
@@ -47,7 +43,7 @@ export interface IPageBlock {
|
||||
description_stripped: any;
|
||||
id: string;
|
||||
issue: string | null;
|
||||
issue_detail: IIssue | null;
|
||||
issue_detail: TIssue | null;
|
||||
name: string;
|
||||
page: string;
|
||||
project: string;
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { IUserLite, IWorkspace, IWorkspaceLite, IUserMemberLite, TStateGroups, IProjectViewProps } from ".";
|
||||
|
||||
export type TUserProjectRole = 5 | 10 | 15 | 20;
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import type { IUser, IUserLite, IWorkspace, IWorkspaceLite, TStateGroups } from ".";
|
||||
|
||||
export interface IProject {
|
||||
archive_in: number;
|
||||
@@ -34,13 +33,10 @@ export interface IProject {
|
||||
is_deployed: boolean;
|
||||
is_favorite: boolean;
|
||||
is_member: boolean;
|
||||
member_role: TUserProjectRole | null;
|
||||
member_role: EUserProjectRoles | null;
|
||||
members: IProjectMemberLite[];
|
||||
issue_views_view: boolean;
|
||||
module_view: boolean;
|
||||
name: string;
|
||||
network: number;
|
||||
page_view: boolean;
|
||||
project_lead: IUserLite | string | null;
|
||||
sort_order: number | null;
|
||||
total_cycles: number;
|
||||
@@ -64,6 +60,10 @@ type ProjectPreferences = {
|
||||
};
|
||||
};
|
||||
|
||||
export interface IProjectMap {
|
||||
[id: string]: IProject;
|
||||
}
|
||||
|
||||
export interface IProjectMemberLite {
|
||||
id: string;
|
||||
member__avatar: string;
|
||||
@@ -77,7 +77,7 @@ export interface IProjectMember {
|
||||
project: IProjectLite;
|
||||
workspace: IWorkspaceLite;
|
||||
comment: string;
|
||||
role: TUserProjectRole;
|
||||
role: EUserProjectRoles;
|
||||
|
||||
preferences: ProjectPreferences;
|
||||
|
||||
@@ -90,27 +90,14 @@ export interface IProjectMember {
|
||||
updated_by: string;
|
||||
}
|
||||
|
||||
export interface IProjectMemberInvitation {
|
||||
export interface IProjectMembership {
|
||||
id: string;
|
||||
|
||||
project: IProject;
|
||||
workspace: IWorkspace;
|
||||
|
||||
email: string;
|
||||
accepted: boolean;
|
||||
token: string;
|
||||
message: string;
|
||||
responded_at: Date;
|
||||
role: TUserProjectRole;
|
||||
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
member: string;
|
||||
role: EUserProjectRoles;
|
||||
}
|
||||
|
||||
export interface IProjectBulkAddFormData {
|
||||
members: { role: TUserProjectRole; member_id: string }[];
|
||||
members: { role: EUserProjectRoles; member_id: string }[];
|
||||
}
|
||||
|
||||
export interface IGithubRepository {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IProject, IProjectLite, IWorkspaceLite } from "types";
|
||||
import { IProject, IProjectLite, IWorkspaceLite } from "@plane/types";
|
||||
|
||||
export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { IIssueActivity, IIssueLite, TStateGroups } from ".";
|
||||
|
||||
export interface IUser {
|
||||
@@ -61,11 +62,10 @@ export interface IUserTheme {
|
||||
|
||||
export interface IUserLite {
|
||||
avatar: string;
|
||||
created_at: Date;
|
||||
display_name: string;
|
||||
email?: string;
|
||||
first_name: string;
|
||||
readonly id: string;
|
||||
id: string;
|
||||
is_bot: boolean;
|
||||
last_name: string;
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export interface IUserProfileProjectSegregation {
|
||||
}
|
||||
|
||||
export interface IUserProjectsRole {
|
||||
[project_id: string]: number;
|
||||
[projectId: string]: EUserProjectRoles;
|
||||
}
|
||||
|
||||
// export interface ICurrentUser {
|
||||
@@ -108,6 +108,18 @@ export interface IIssueDisplayProperties {
|
||||
updated_on?: boolean;
|
||||
}
|
||||
|
||||
export interface IIssueFilters {
|
||||
filters: IIssueFilterOptions | undefined;
|
||||
displayFilters: IIssueDisplayFilterOptions | undefined;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
}
|
||||
|
||||
export interface IIssueFiltersResponse {
|
||||
filters: IIssueFilterOptions;
|
||||
display_filters: IIssueDisplayFilterOptions;
|
||||
display_properties: IIssueDisplayProperties;
|
||||
}
|
||||
|
||||
export interface IWorkspaceIssueFilterOptions {
|
||||
assignees?: string[] | null;
|
||||
created_by?: string[] | null;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IIssueFilterOptions } from "./view-props";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "./view-props";
|
||||
|
||||
export interface IProjectView {
|
||||
id: string;
|
||||
@@ -10,6 +10,9 @@ export interface IProjectView {
|
||||
updated_by: string;
|
||||
name: string;
|
||||
description: string;
|
||||
filters: IIssueFilterOptions;
|
||||
display_filters: IIssueDisplayFilterOptions;
|
||||
display_properties: IIssueDisplayProperties;
|
||||
query: IIssueFilterOptions;
|
||||
query_data: IIssueFilterOptions;
|
||||
project: string;
|
||||
@@ -1,4 +1,9 @@
|
||||
import { IWorkspaceViewProps } from "./view-props";
|
||||
import {
|
||||
IWorkspaceViewProps,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "./view-props";
|
||||
|
||||
export interface IWorkspaceView {
|
||||
id: string;
|
||||
@@ -10,6 +15,9 @@ export interface IWorkspaceView {
|
||||
updated_by: string;
|
||||
name: string;
|
||||
description: string;
|
||||
filters: IIssueIIFilterOptions;
|
||||
display_filters: IIssueDisplayFilterOptions;
|
||||
display_properties: IIssueDisplayProperties;
|
||||
query: any;
|
||||
query_data: IWorkspaceViewProps;
|
||||
project: string;
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { IProjectMember, IUser, IUserLite, IWorkspaceViewProps } from "types";
|
||||
|
||||
export type TUserWorkspaceRole = 5 | 10 | 15 | 20;
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import type { IProjectMember, IUser, IUserLite, IWorkspaceViewProps } from "@plane/types";
|
||||
|
||||
export interface IWorkspace {
|
||||
readonly id: string;
|
||||
@@ -27,18 +26,23 @@ export interface IWorkspaceLite {
|
||||
|
||||
export interface IWorkspaceMemberInvitation {
|
||||
accepted: boolean;
|
||||
readonly id: string;
|
||||
email: string;
|
||||
token: string;
|
||||
id: string;
|
||||
message: string;
|
||||
responded_at: Date;
|
||||
role: TUserWorkspaceRole;
|
||||
created_by_detail: IUser;
|
||||
workspace: IWorkspace;
|
||||
role: EUserWorkspaceRoles;
|
||||
token: string;
|
||||
workspace: string;
|
||||
workspace_detail: {
|
||||
id: string;
|
||||
logo: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IWorkspaceBulkInviteFormData {
|
||||
emails: { email: string; role: TUserWorkspaceRole }[];
|
||||
emails: { email: string; role: EUserWorkspaceRoles }[];
|
||||
}
|
||||
|
||||
export type Properties = {
|
||||
@@ -58,15 +62,9 @@ export type Properties = {
|
||||
};
|
||||
|
||||
export interface IWorkspaceMember {
|
||||
company_role: string | null;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
id: string;
|
||||
member: IUserLite;
|
||||
role: TUserWorkspaceRole;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
workspace: IWorkspaceLite;
|
||||
role: EUserWorkspaceRoles;
|
||||
}
|
||||
|
||||
export interface IWorkspaceMemberMe {
|
||||
@@ -76,7 +74,7 @@ export interface IWorkspaceMemberMe {
|
||||
default_props: IWorkspaceViewProps;
|
||||
id: string;
|
||||
member: string;
|
||||
role: TUserWorkspaceRole;
|
||||
role: EUserWorkspaceRoles;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
view_props: IWorkspaceViewProps;
|
||||
@@ -1,13 +1,16 @@
|
||||
import * as React from "react";
|
||||
|
||||
// icons
|
||||
import { AlertCircle, Ban, SignalHigh, SignalLow, SignalMedium } from "lucide-react";
|
||||
|
||||
// types
|
||||
import { IPriorityIcon } from "./type";
|
||||
type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
|
||||
|
||||
export const PriorityIcon: React.FC<IPriorityIcon> = ({ priority, className = "", transparentBg = false }) => {
|
||||
if (!className || className === "") className = "h-4 w-4";
|
||||
interface IPriorityIcon {
|
||||
className?: string;
|
||||
priority: TIssuePriorities;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
|
||||
const { priority, className = "", size = 14 } = props;
|
||||
|
||||
// Convert to lowercase for string comparison
|
||||
const lowercasePriority = priority?.toLowerCase();
|
||||
@@ -16,31 +19,17 @@ export const PriorityIcon: React.FC<IPriorityIcon> = ({ priority, className = ""
|
||||
const getPriorityIcon = (): React.ReactNode => {
|
||||
switch (lowercasePriority) {
|
||||
case "urgent":
|
||||
return <AlertCircle className={`text-red-500 ${transparentBg ? "" : "p-0.5"} ${className}`} />;
|
||||
return <AlertCircle size={size} className={`text-red-500 ${className}`} />;
|
||||
case "high":
|
||||
return <SignalHigh className={`text-orange-500 ${transparentBg ? "" : "pl-1"} ${className}`} />;
|
||||
return <SignalHigh size={size} strokeWidth={3} className={`text-orange-500 ${className}`} />;
|
||||
case "medium":
|
||||
return <SignalMedium className={`text-yellow-500 ${transparentBg ? "" : "ml-1.5"} ${className}`} />;
|
||||
return <SignalMedium size={size} strokeWidth={3} className={`text-yellow-500 ${className}`} />;
|
||||
case "low":
|
||||
return <SignalLow className={`text-green-500 ${transparentBg ? "" : "ml-2"} ${className}`} />;
|
||||
return <SignalLow size={size} strokeWidth={3} className={`text-custom-primary-100 ${className}`} />;
|
||||
default:
|
||||
return <Ban className={`text-custom-text-200 ${transparentBg ? "" : "p-0.5"} ${className}`} />;
|
||||
return <Ban size={size} className={`text-custom-text-200 ${className}`} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{transparentBg ? (
|
||||
getPriorityIcon()
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-5 w-5 place-items-center items-center rounded border ${
|
||||
lowercasePriority === "urgent" ? "border-red-500/20 bg-red-500/20" : "border-custom-border-200"
|
||||
}`}
|
||||
>
|
||||
{getPriorityIcon()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return <>{getPriorityIcon()}</>;
|
||||
};
|
||||
|
||||
8
packages/ui/src/icons/type.d.ts
vendored
8
packages/ui/src/icons/type.d.ts
vendored
@@ -1,11 +1,3 @@
|
||||
export interface ISvgIcons extends React.SVGAttributes<SVGElement> {
|
||||
className?: string | undefined;
|
||||
}
|
||||
|
||||
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
|
||||
|
||||
export interface IPriorityIcon {
|
||||
priority: TIssuePriorities | null;
|
||||
className?: string;
|
||||
transparentBg?: boolean | false;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useTheme } from "next-themes";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { mutate } from "swr";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
@@ -22,9 +22,7 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
// states
|
||||
const [isDeactivating, setIsDeactivating] = useState(false);
|
||||
|
||||
const {
|
||||
user: { deactivateAccount },
|
||||
} = useMobxStore();
|
||||
const { deactivateAccount } = useUser();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { XCircle } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useApplication } from "hooks/store";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// types
|
||||
import { IEmailCheckData } from "types/auth";
|
||||
import { IEmailCheckData } from "@plane/types";
|
||||
// constants
|
||||
import { ESignInSteps } from "components/account";
|
||||
|
||||
@@ -25,11 +27,13 @@ type TEmailFormValues = {
|
||||
|
||||
const authService = new AuthService();
|
||||
|
||||
export const EmailForm: React.FC<Props> = (props) => {
|
||||
export const EmailForm: React.FC<Props> = observer((props) => {
|
||||
const { handleStepChange, updateEmail } = props;
|
||||
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
config: { envConfig },
|
||||
} = useApplication();
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
@@ -54,9 +58,11 @@ export const EmailForm: React.FC<Props> = (props) => {
|
||||
await authService
|
||||
.emailCheck(payload)
|
||||
.then((res) => {
|
||||
// if the password has been autoset, send the user to magic sign-in
|
||||
if (res.is_password_autoset) handleStepChange(ESignInSteps.UNIQUE_CODE);
|
||||
// if the password has not been autoset, send them to password sign-in
|
||||
// if the password has been auto set, send the user to magic sign-in
|
||||
if (res.is_password_autoset && envConfig?.is_smtp_configured) {
|
||||
handleStepChange(ESignInSteps.UNIQUE_CODE);
|
||||
}
|
||||
// if the password has not been auto set, send them to password sign-in
|
||||
else handleStepChange(ESignInSteps.PASSWORD);
|
||||
})
|
||||
.catch((err) =>
|
||||
@@ -119,4 +125,4 @@ export const EmailForm: React.FC<Props> = (props) => {
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
import { useApplication } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { GitHubSignInButton, GoogleSignInButton } from "components/account";
|
||||
@@ -21,8 +20,8 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
||||
const { setToastAlert } = useToast();
|
||||
// mobx store
|
||||
const {
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
config: { envConfig },
|
||||
} = useApplication();
|
||||
|
||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||
try {
|
||||
|
||||
@@ -6,20 +6,23 @@ import { XCircle } from "lucide-react";
|
||||
import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useApplication } from "hooks/store";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// types
|
||||
import { IPasswordSignInData } from "types/auth";
|
||||
import { IPasswordSignInData } from "@plane/types";
|
||||
// constants
|
||||
import { ESignInSteps } from "components/account";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
updateEmail: (email: string) => void;
|
||||
handleStepChange: (step: ESignInSteps) => void;
|
||||
handleSignInRedirection: () => Promise<void>;
|
||||
handleEmailClear: () => void;
|
||||
};
|
||||
|
||||
type TPasswordFormValues = {
|
||||
@@ -34,13 +37,16 @@ const defaultValues: TPasswordFormValues = {
|
||||
|
||||
const authService = new AuthService();
|
||||
|
||||
export const PasswordForm: React.FC<Props> = (props) => {
|
||||
const { email, updateEmail, handleStepChange, handleSignInRedirection } = props;
|
||||
export const PasswordForm: React.FC<Props> = observer((props) => {
|
||||
const { email, updateEmail, handleStepChange, handleSignInRedirection, handleEmailClear } = props;
|
||||
// states
|
||||
const [isSendingUniqueCode, setIsSendingUniqueCode] = useState(false);
|
||||
const [isSendingResetPasswordLink, setIsSendingResetPasswordLink] = useState(false);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
const {
|
||||
config: { envConfig },
|
||||
} = useApplication();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
@@ -157,11 +163,12 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="orville.wright@frstflt.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||
disabled
|
||||
/>
|
||||
{value.length > 0 && (
|
||||
<XCircle
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => onChange("")}
|
||||
onClick={handleEmailClear}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -199,26 +206,28 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2.5 sm:grid-cols-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSendUniqueCode}
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
size="xl"
|
||||
loading={isSendingUniqueCode}
|
||||
>
|
||||
{isSendingUniqueCode ? "Sending code" : "Use unique code"}
|
||||
</Button>
|
||||
<div className="flex gap-4">
|
||||
{envConfig && envConfig.is_smtp_configured && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSendUniqueCode}
|
||||
variant="outline-primary"
|
||||
className="w-full"
|
||||
size="xl"
|
||||
loading={isSendingUniqueCode}
|
||||
>
|
||||
{isSendingUniqueCode ? "Sending code" : "Use unique code"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline-primary"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
size="xl"
|
||||
disabled={!isValid}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Go to workspace
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-onboarding-text-200">
|
||||
@@ -230,4 +239,4 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import { useApplication } from "hooks/store";
|
||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||
// components
|
||||
import { LatestFeatureBlock } from "components/common";
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
OAuthOptions,
|
||||
OptionalSetPasswordForm,
|
||||
CreatePasswordForm,
|
||||
SelfHostedSignInForm,
|
||||
} from "components/account";
|
||||
|
||||
export enum ESignInSteps {
|
||||
@@ -38,77 +36,81 @@ export const SignInRoot = observer(() => {
|
||||
const { handleRedirection } = useSignInRedirection();
|
||||
// mobx store
|
||||
const {
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
config: { envConfig },
|
||||
} = useApplication();
|
||||
|
||||
const isOAuthEnabled = envConfig && (envConfig.google_client_id || envConfig.github_client_id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto flex flex-col">
|
||||
{envConfig?.is_self_managed ? (
|
||||
<SelfHostedSignInForm
|
||||
email={email}
|
||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{signInStep === ESignInSteps.EMAIL && (
|
||||
<EmailForm
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||
/>
|
||||
)}
|
||||
{signInStep === ESignInSteps.PASSWORD && (
|
||||
<PasswordForm
|
||||
email={email}
|
||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
/>
|
||||
)}
|
||||
{signInStep === ESignInSteps.SET_PASSWORD_LINK && (
|
||||
<SetPasswordLink email={email} updateEmail={(newEmail) => setEmail(newEmail)} />
|
||||
)}
|
||||
{signInStep === ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD && (
|
||||
<UniqueCodeForm
|
||||
email={email}
|
||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
submitButtonLabel="Go to workspace"
|
||||
showTermsAndConditions
|
||||
updateUserOnboardingStatus={(value) => setIsOnboarded(value)}
|
||||
/>
|
||||
)}
|
||||
{signInStep === ESignInSteps.UNIQUE_CODE && (
|
||||
<UniqueCodeForm
|
||||
email={email}
|
||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
updateUserOnboardingStatus={(value) => setIsOnboarded(value)}
|
||||
/>
|
||||
)}
|
||||
{signInStep === ESignInSteps.OPTIONAL_SET_PASSWORD && (
|
||||
<OptionalSetPasswordForm
|
||||
email={email}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
isOnboarded={isOnboarded}
|
||||
/>
|
||||
)}
|
||||
{signInStep === ESignInSteps.CREATE_PASSWORD && (
|
||||
<CreatePasswordForm
|
||||
email={email}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
isOnboarded={isOnboarded}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{signInStep === ESignInSteps.EMAIL && (
|
||||
<EmailForm
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||
/>
|
||||
)}
|
||||
{signInStep === ESignInSteps.PASSWORD && (
|
||||
<PasswordForm
|
||||
email={email}
|
||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleEmailClear={() => {
|
||||
setEmail("");
|
||||
setSignInStep(ESignInSteps.EMAIL);
|
||||
}}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
/>
|
||||
)}
|
||||
{signInStep === ESignInSteps.SET_PASSWORD_LINK && (
|
||||
<SetPasswordLink email={email} updateEmail={(newEmail) => setEmail(newEmail)} />
|
||||
)}
|
||||
{signInStep === ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD && (
|
||||
<UniqueCodeForm
|
||||
email={email}
|
||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
submitButtonLabel="Go to workspace"
|
||||
showTermsAndConditions
|
||||
updateUserOnboardingStatus={(value) => setIsOnboarded(value)}
|
||||
handleEmailClear={() => {
|
||||
setEmail("");
|
||||
setSignInStep(ESignInSteps.EMAIL);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{signInStep === ESignInSteps.UNIQUE_CODE && (
|
||||
<UniqueCodeForm
|
||||
email={email}
|
||||
updateEmail={(newEmail) => setEmail(newEmail)}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
updateUserOnboardingStatus={(value) => setIsOnboarded(value)}
|
||||
handleEmailClear={() => {
|
||||
setEmail("");
|
||||
setSignInStep(ESignInSteps.EMAIL);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{signInStep === ESignInSteps.OPTIONAL_SET_PASSWORD && (
|
||||
<OptionalSetPasswordForm
|
||||
email={email}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
isOnboarded={isOnboarded}
|
||||
/>
|
||||
)}
|
||||
{signInStep === ESignInSteps.CREATE_PASSWORD && (
|
||||
<CreatePasswordForm
|
||||
email={email}
|
||||
handleStepChange={(step) => setSignInStep(step)}
|
||||
handleSignInRedirection={handleRedirection}
|
||||
isOnboarded={isOnboarded}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
{isOAuthEnabled && !OAUTH_HIDDEN_STEPS.includes(signInStep) && (
|
||||
<OAuthOptions handleSignInRedirection={handleRedirection} />
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// types
|
||||
import { IPasswordSignInData } from "types/auth";
|
||||
import { IPasswordSignInData } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// types
|
||||
import { IEmailCheckData } from "types/auth";
|
||||
import { IEmailCheckData } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// types
|
||||
import { IEmailCheckData, IMagicSignInData } from "types/auth";
|
||||
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
|
||||
// constants
|
||||
import { ESignInSteps } from "components/account";
|
||||
|
||||
@@ -25,6 +25,7 @@ type Props = {
|
||||
submitButtonLabel?: string;
|
||||
showTermsAndConditions?: boolean;
|
||||
updateUserOnboardingStatus: (value: boolean) => void;
|
||||
handleEmailClear: () => void;
|
||||
};
|
||||
|
||||
type TUniqueCodeFormValues = {
|
||||
@@ -50,6 +51,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
submitButtonLabel = "Continue",
|
||||
showTermsAndConditions = false,
|
||||
updateUserOnboardingStatus,
|
||||
handleEmailClear,
|
||||
} = props;
|
||||
// states
|
||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||
@@ -183,11 +185,12 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="orville.wright@frstflt.com"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||
disabled
|
||||
/>
|
||||
{value.length > 0 && (
|
||||
<XCircle
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => onChange("")}
|
||||
onClick={handleEmailClear}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -233,8 +236,8 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
||||
{resendTimerCode > 0
|
||||
? `Request new code in ${resendTimerCode}s`
|
||||
: isRequestingNewCode
|
||||
? "Requesting new code"
|
||||
: "Request new code"}
|
||||
? "Requesting new code"
|
||||
: "Request new code"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AnalyticsService } from "services/analytics.service";
|
||||
// components
|
||||
import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics";
|
||||
// types
|
||||
import { IAnalyticsParams } from "types";
|
||||
import { IAnalyticsParams } from "@plane/types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BarTooltipProps } from "@nivo/bar";
|
||||
import { DATE_KEYS } from "constants/analytics";
|
||||
import { renderMonthAndYear } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
datum: BarTooltipProps<any>;
|
||||
@@ -60,8 +60,8 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
|
||||
? "capitalize"
|
||||
: ""
|
||||
: params.x_axis === "priority" || params.x_axis === "state__group"
|
||||
? "capitalize"
|
||||
: ""
|
||||
? "capitalize"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}:
|
||||
|
||||
@@ -9,7 +9,7 @@ import { BarGraph } from "components/ui";
|
||||
import { findStringWithMostCharacters } from "helpers/array.helper";
|
||||
import { generateBarColor, generateDisplayName } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
analytics: IAnalyticsResponse;
|
||||
@@ -101,8 +101,8 @@ export const AnalyticsGraph: React.FC<Props> = ({ analytics, barGraphData, param
|
||||
? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase()
|
||||
: "?"
|
||||
: datum.value && datum.value !== "None"
|
||||
? `${datum.value}`.toUpperCase()[0]
|
||||
: "?"}
|
||||
? `${datum.value}`.toUpperCase()[0]
|
||||
: "?"}
|
||||
</text>
|
||||
</g>
|
||||
</Tooltip>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button, Loader } from "@plane/ui";
|
||||
// helpers
|
||||
import { convertResponseToBarGraphData } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
// components
|
||||
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics";
|
||||
// types
|
||||
import { IAnalyticsParams } from "types";
|
||||
import { IAnalyticsParams } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
control: Control<IAnalyticsParams, any>;
|
||||
@@ -20,12 +18,7 @@ type Props = {
|
||||
export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
|
||||
const { control, setValue, params, fullScreen, isProjectLevel } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
|
||||
const { workspaceProjectIds: workspaceProjectIds } = useProject();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -40,7 +33,11 @@ export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
|
||||
name="project"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectProject value={value ?? undefined} onChange={onChange} projects={projectsList ?? undefined} />
|
||||
<SelectProject
|
||||
value={value ?? undefined}
|
||||
onChange={onChange}
|
||||
projectIds={workspaceProjectIds ?? undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
// ui
|
||||
import { CustomSearchSelect } from "@plane/ui";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
|
||||
type Props = {
|
||||
value: string[] | undefined;
|
||||
onChange: (val: string[] | null) => void;
|
||||
projects: IProject[] | undefined;
|
||||
projectIds: string[] | undefined;
|
||||
};
|
||||
|
||||
export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) => {
|
||||
const options = projects?.map((project) => ({
|
||||
value: project.id,
|
||||
query: project.name + project.identifier,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[0.65rem] text-custom-text-200">{project.identifier}</span>
|
||||
{project.name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
export const SelectProject: React.FC<Props> = observer((props) => {
|
||||
const { value, onChange, projectIds } = props;
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
const options = projectIds?.map((projectId) => {
|
||||
const projectDetails = getProjectById(projectId);
|
||||
|
||||
return {
|
||||
value: projectDetails?.id,
|
||||
query: `${projectDetails?.name} ${projectDetails?.identifier}`,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[0.65rem] text-custom-text-200">{projectDetails?.identifier}</span>
|
||||
{projectDetails?.name}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<CustomSearchSelect
|
||||
@@ -28,9 +36,9 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
|
||||
options={options}
|
||||
label={
|
||||
value && value.length > 0
|
||||
? projects
|
||||
?.filter((p) => value.includes(p.id))
|
||||
.map((p) => p.identifier)
|
||||
? projectIds
|
||||
?.filter((p) => value.includes(p))
|
||||
.map((p) => getProjectById(p)?.name)
|
||||
.join(", ")
|
||||
: "All projects"
|
||||
}
|
||||
@@ -38,4 +46,4 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
|
||||
multiple
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
// ui
|
||||
import { CustomSelect } from "@plane/ui";
|
||||
// types
|
||||
import { IAnalyticsParams, TXAxisValues } from "types";
|
||||
import { IAnalyticsParams, TXAxisValues } from "@plane/types";
|
||||
// constants
|
||||
import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user