Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 608eea67f7 | |||
| badb190c25 | |||
| 7e1b90c97a | |||
| 46a6a599a1 | |||
| 8ae1e3cff2 | |||
| ffdd515cf8 | |||
| 5d9393cfa6 | |||
| 0fb531e4b7 | |||
| cf10e3445d | |||
| f90595ca31 | |||
| 1ce7f20c2d | |||
| 84d3d34e14 | |||
| 76e55bee95 | |||
| f05b8de91d | |||
| bc694bb742 | |||
| 65d2a2546d | |||
| cd81ec1002 | |||
| 6325f97c8e | |||
| fe505e6b31 | |||
| d57d91e530 | |||
| 984f7ed6b8 | |||
| 90bcdeccf4 | |||
| 1d88de472e | |||
| 4af5fef210 | |||
| f1ded8540e | |||
| b38a352c98 | |||
| a77839a942 | |||
| f63a04c1ab | |||
| dfb6b2b247 | |||
| 088cc8c659 | |||
| 05e4311e06 | |||
| 5674acd985 | |||
| 8e590f6f60 |
@@ -1,238 +0,0 @@
|
||||
# All the python scripts that are used for back migrations
|
||||
import uuid
|
||||
import random
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from plane.db.models import ProjectIdentifier
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueComment,
|
||||
User,
|
||||
Project,
|
||||
ProjectMember,
|
||||
Label,
|
||||
Integration,
|
||||
)
|
||||
|
||||
|
||||
# Update description and description html values for old descriptions
|
||||
def update_description():
|
||||
try:
|
||||
issues = Issue.objects.all()
|
||||
updated_issues = []
|
||||
|
||||
for issue in issues:
|
||||
issue.description_html = f"<p>{issue.description}</p>"
|
||||
issue.description_stripped = issue.description
|
||||
updated_issues.append(issue)
|
||||
|
||||
Issue.objects.bulk_update(
|
||||
updated_issues,
|
||||
["description_html", "description_stripped"],
|
||||
batch_size=100,
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def update_comments():
|
||||
try:
|
||||
issue_comments = IssueComment.objects.all()
|
||||
updated_issue_comments = []
|
||||
|
||||
for issue_comment in issue_comments:
|
||||
issue_comment.comment_html = (
|
||||
f"<p>{issue_comment.comment_stripped}</p>"
|
||||
)
|
||||
updated_issue_comments.append(issue_comment)
|
||||
|
||||
IssueComment.objects.bulk_update(
|
||||
updated_issue_comments, ["comment_html"], batch_size=100
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def update_project_identifiers():
|
||||
try:
|
||||
project_identifiers = ProjectIdentifier.objects.filter(
|
||||
workspace_id=None
|
||||
).select_related("project", "project__workspace")
|
||||
updated_identifiers = []
|
||||
|
||||
for identifier in project_identifiers:
|
||||
identifier.workspace_id = identifier.project.workspace_id
|
||||
updated_identifiers.append(identifier)
|
||||
|
||||
ProjectIdentifier.objects.bulk_update(
|
||||
updated_identifiers, ["workspace_id"], batch_size=50
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def update_user_empty_password():
|
||||
try:
|
||||
users = User.objects.filter(password="")
|
||||
updated_users = []
|
||||
|
||||
for user in users:
|
||||
user.password = make_password(uuid.uuid4().hex)
|
||||
user.is_password_autoset = True
|
||||
updated_users.append(user)
|
||||
|
||||
User.objects.bulk_update(updated_users, ["password"], batch_size=50)
|
||||
print("Success")
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def updated_issue_sort_order():
|
||||
try:
|
||||
issues = Issue.objects.all()
|
||||
updated_issues = []
|
||||
|
||||
for issue in issues:
|
||||
issue.sort_order = issue.sequence_id * random.randint(100, 500)
|
||||
updated_issues.append(issue)
|
||||
|
||||
Issue.objects.bulk_update(
|
||||
updated_issues, ["sort_order"], batch_size=100
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def update_project_cover_images():
|
||||
try:
|
||||
project_cover_images = [
|
||||
"https://images.unsplash.com/photo-1677432658720-3d84f9d657b4?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
|
||||
"https://images.unsplash.com/photo-1661107564401-57497d8fe86f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
|
||||
"https://images.unsplash.com/photo-1677352241429-dc90cfc7a623?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
|
||||
"https://images.unsplash.com/photo-1677196728306-eeafea692454?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1331&q=80",
|
||||
"https://images.unsplash.com/photo-1660902179734-c94c944f7830?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1255&q=80",
|
||||
"https://images.unsplash.com/photo-1672243775941-10d763d9adef?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
|
||||
"https://images.unsplash.com/photo-1677040628614-53936ff66632?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
|
||||
"https://images.unsplash.com/photo-1676920410907-8d5f8dd4b5ba?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
|
||||
"https://images.unsplash.com/photo-1676846328604-ce831c481346?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1155&q=80",
|
||||
"https://images.unsplash.com/photo-1676744843212-09b7e64c3a05?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
|
||||
"https://images.unsplash.com/photo-1676798531090-1608bedeac7b?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
|
||||
"https://images.unsplash.com/photo-1597088758740-56fd7ec8a3f0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1169&q=80",
|
||||
"https://images.unsplash.com/photo-1676638392418-80aad7c87b96?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
|
||||
"https://images.unsplash.com/photo-1649639194967-2fec0b4ea7bc?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
|
||||
"https://images.unsplash.com/photo-1675883086902-b453b3f8146e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
|
||||
"https://images.unsplash.com/photo-1675887057159-40fca28fdc5d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1173&q=80",
|
||||
"https://images.unsplash.com/photo-1675373980203-f84c5a672aa5?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
|
||||
"https://images.unsplash.com/photo-1675191475318-d2bf6bad1200?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
|
||||
"https://images.unsplash.com/photo-1675456230532-2194d0c4bcc0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
|
||||
"https://images.unsplash.com/photo-1675371788315-60fa0ef48267?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
|
||||
]
|
||||
|
||||
projects = Project.objects.all()
|
||||
updated_projects = []
|
||||
for project in projects:
|
||||
project.cover_image = project_cover_images[random.randint(0, 19)]
|
||||
updated_projects.append(project)
|
||||
|
||||
Project.objects.bulk_update(
|
||||
updated_projects, ["cover_image"], batch_size=100
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def update_user_view_property():
|
||||
try:
|
||||
project_members = ProjectMember.objects.all()
|
||||
updated_project_members = []
|
||||
for project_member in project_members:
|
||||
project_member.default_props = {
|
||||
"filters": {"type": None},
|
||||
"orderBy": "-created_at",
|
||||
"collapsed": True,
|
||||
"issueView": "list",
|
||||
"filterIssue": None,
|
||||
"groupByProperty": None,
|
||||
"showEmptyGroups": True,
|
||||
}
|
||||
updated_project_members.append(project_member)
|
||||
|
||||
ProjectMember.objects.bulk_update(
|
||||
updated_project_members, ["default_props"], batch_size=100
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def update_label_color():
|
||||
try:
|
||||
labels = Label.objects.filter(color="")
|
||||
updated_labels = []
|
||||
for label in labels:
|
||||
label.color = "#" + "%06x" % random.randint(0, 0xFFFFFF)
|
||||
updated_labels.append(label)
|
||||
|
||||
Label.objects.bulk_update(updated_labels, ["color"], batch_size=100)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def create_slack_integration():
|
||||
try:
|
||||
_ = Integration.objects.create(
|
||||
provider="slack", network=2, title="Slack"
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def update_integration_verified():
|
||||
try:
|
||||
integrations = Integration.objects.all()
|
||||
updated_integrations = []
|
||||
for integration in integrations:
|
||||
integration.verified = True
|
||||
updated_integrations.append(integration)
|
||||
|
||||
Integration.objects.bulk_update(
|
||||
updated_integrations, ["verified"], batch_size=10
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def update_start_date():
|
||||
try:
|
||||
issues = Issue.objects.filter(
|
||||
state__group__in=["started", "completed"]
|
||||
)
|
||||
updated_issues = []
|
||||
for issue in issues:
|
||||
issue.start_date = issue.created_at.date()
|
||||
updated_issues.append(issue)
|
||||
Issue.objects.bulk_update(
|
||||
updated_issues, ["start_date"], batch_size=500
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
@@ -36,9 +36,8 @@ from .project import (
|
||||
)
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
from .view import (
|
||||
GlobalViewSerializer,
|
||||
IssueViewSerializer,
|
||||
IssueViewFavoriteSerializer,
|
||||
ViewSerializer,
|
||||
ViewFavoriteSerializer,
|
||||
)
|
||||
from .cycle import (
|
||||
CycleSerializer,
|
||||
|
||||
@@ -3,69 +3,33 @@ from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer, DynamicBaseSerializer
|
||||
from .workspace import WorkspaceLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
||||
from plane.db.models import View, ViewFavorite
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
class GlobalViewSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
source="workspace", read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = GlobalView
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"query",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
query_params = validated_data.get("query_data", {})
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
validated_data["query"] = dict()
|
||||
return GlobalView.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
query_params = validated_data.get("query_data", {})
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
validated_data["query"] = dict()
|
||||
validated_data["query"] = issue_filters(query_params, "PATCH")
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class IssueViewSerializer(DynamicBaseSerializer):
|
||||
class ViewSerializer(DynamicBaseSerializer):
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
source="workspace", read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueView
|
||||
model = View
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"query",
|
||||
"access",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
query_params = validated_data.get("query_data", {})
|
||||
query_params = validated_data.get("filters", {})
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
validated_data["query"] = {}
|
||||
return IssueView.objects.create(**validated_data)
|
||||
return View.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
query_params = validated_data.get("query_data", {})
|
||||
query_params = validated_data.get("filters", {})
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
@@ -74,11 +38,10 @@ class IssueViewSerializer(DynamicBaseSerializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class IssueViewFavoriteSerializer(BaseSerializer):
|
||||
view_detail = IssueViewSerializer(source="issue_view", read_only=True)
|
||||
class ViewFavoriteSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IssueViewFavorite
|
||||
model = ViewFavorite
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
|
||||
@@ -14,6 +14,8 @@ from plane.app.views import (
|
||||
UserActivityGraphEndpoint,
|
||||
UserIssueCompletedGraphEndpoint,
|
||||
UserWorkspaceDashboardEndpoint,
|
||||
UserWorkspaceViewViewSet,
|
||||
UserProjectViewViewSet,
|
||||
## End Workspaces
|
||||
)
|
||||
|
||||
@@ -85,6 +87,7 @@ urlpatterns = [
|
||||
UserIssueCompletedGraphEndpoint.as_view(),
|
||||
name="completed-graph",
|
||||
),
|
||||
## End User Graph
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/dashboard/",
|
||||
UserWorkspaceDashboardEndpoint.as_view(),
|
||||
@@ -95,5 +98,83 @@ urlpatterns = [
|
||||
SetUserPasswordEndpoint.as_view(),
|
||||
name="set-password",
|
||||
),
|
||||
## End User Graph
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/views/",
|
||||
UserWorkspaceViewViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="user-workspace-views",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/views/<uuid:pk>/",
|
||||
UserWorkspaceViewViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="user-workspace-views",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/views/<uuid:pk>/duplicate/",
|
||||
UserWorkspaceViewViewSet.as_view(
|
||||
{
|
||||
"post": "duplicate",
|
||||
}
|
||||
),
|
||||
name="user-workspace-views",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/views/<uuid:pk>/lock/",
|
||||
UserWorkspaceViewViewSet.as_view(
|
||||
{
|
||||
"post": "toggle_lock",
|
||||
}
|
||||
),
|
||||
name="user-workspace-views-lock",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/projects/<uuid:project_id>/views/",
|
||||
UserProjectViewViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="user-project-views",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
|
||||
UserProjectViewViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="user-project-views",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/lock/",
|
||||
UserProjectViewViewSet.as_view(
|
||||
{
|
||||
"post": "toggle_lock",
|
||||
}
|
||||
),
|
||||
name="user-project-lock-views",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/duplicate/",
|
||||
UserWorkspaceViewViewSet.as_view(
|
||||
{
|
||||
"post": "duplicate",
|
||||
}
|
||||
),
|
||||
name="user-project-duplicate-views",
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
@@ -2,17 +2,85 @@ from django.urls import path
|
||||
|
||||
|
||||
from plane.app.views import (
|
||||
IssueViewViewSet,
|
||||
GlobalViewViewSet,
|
||||
GlobalViewIssuesViewSet,
|
||||
IssueViewFavoriteViewSet,
|
||||
ProjectViewViewSet,
|
||||
WorkspaceViewViewSet,
|
||||
WorkspaceViewFavoriteViewSet,
|
||||
ProjectViewFavoriteViewSet,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/issues/",
|
||||
WorkspaceViewViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
name="workspace-view-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/",
|
||||
WorkspaceViewViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="workspace-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/<uuid:pk>/",
|
||||
WorkspaceViewViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="workspace-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/<uuid:view_id>/duplicate/",
|
||||
WorkspaceViewFavoriteViewSet.as_view(
|
||||
{
|
||||
"post": "duplicate",
|
||||
}
|
||||
),
|
||||
name="workspace-duplicate-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/<uuid:view_id>/favorite/",
|
||||
WorkspaceViewFavoriteViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="workspace-favorite-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/<uuid:pk>/visibility/",
|
||||
WorkspaceViewViewSet.as_view(
|
||||
{
|
||||
"post": "visibility",
|
||||
}
|
||||
),
|
||||
name="workspace-duplicate-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/lock/",
|
||||
WorkspaceViewViewSet.as_view(
|
||||
{
|
||||
"post": "toggle_lock",
|
||||
}
|
||||
),
|
||||
name="project-lock-views",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
|
||||
IssueViewViewSet.as_view(
|
||||
ProjectViewViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
@@ -22,7 +90,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
|
||||
IssueViewViewSet.as_view(
|
||||
ProjectViewViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
@@ -33,53 +101,41 @@ urlpatterns = [
|
||||
name="project-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/",
|
||||
GlobalViewViewSet.as_view(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/duplicate/",
|
||||
ProjectViewViewSet.as_view(
|
||||
{
|
||||
"post": "duplicate",
|
||||
}
|
||||
),
|
||||
name="project-duplicate-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/visibility/",
|
||||
ProjectViewViewSet.as_view(
|
||||
{
|
||||
"post": "visibility",
|
||||
}
|
||||
),
|
||||
name="project-duplicate-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/lock/",
|
||||
ProjectViewViewSet.as_view(
|
||||
{
|
||||
"post": "toggle_lock",
|
||||
}
|
||||
),
|
||||
name="project-lock-views",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:view_id>/favorite/",
|
||||
ProjectViewFavoriteViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="global-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/<uuid:pk>/",
|
||||
GlobalViewViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="global-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/issues/",
|
||||
GlobalViewIssuesViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
name="global-view-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
|
||||
IssueViewFavoriteViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="user-favorite-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
|
||||
IssueViewFavoriteViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="user-favorite-view",
|
||||
name="project-favorite-view",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -52,10 +52,13 @@ from .workspace import (
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import (
|
||||
GlobalViewViewSet,
|
||||
GlobalViewIssuesViewSet,
|
||||
IssueViewViewSet,
|
||||
IssueViewFavoriteViewSet,
|
||||
WorkspaceViewViewSet,
|
||||
ProjectViewViewSet,
|
||||
WorkspaceViewFavoriteViewSet,
|
||||
ProjectViewFavoriteViewSet,
|
||||
UserWorkspaceViewViewSet,
|
||||
UserProjectViewViewSet,
|
||||
ProjectViewViewSet,
|
||||
)
|
||||
from .cycle import (
|
||||
CycleViewSet,
|
||||
|
||||
@@ -17,7 +17,7 @@ from plane.db.models import (
|
||||
Cycle,
|
||||
Module,
|
||||
Page,
|
||||
IssueView,
|
||||
View,
|
||||
)
|
||||
from plane.utils.issue_search import search_issues
|
||||
|
||||
@@ -161,7 +161,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
for field in fields:
|
||||
q |= Q(**{f"{field}__icontains": query})
|
||||
|
||||
issue_views = IssueView.objects.filter(
|
||||
issue_views = View.objects.filter(
|
||||
q,
|
||||
project__project_projectmember__member=self.request.user,
|
||||
workspace__slug=slug,
|
||||
|
||||
+388
-106
@@ -1,6 +1,5 @@
|
||||
# Django imports
|
||||
from django.db.models import (
|
||||
Prefetch,
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
@@ -10,68 +9,370 @@ from django.db.models import (
|
||||
When,
|
||||
Exists,
|
||||
Max,
|
||||
Q,
|
||||
)
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.db.models import Prefetch, OuterRef, Exists
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
# Module imports
|
||||
from . import BaseViewSet, BaseAPIView
|
||||
from . import BaseViewSet
|
||||
from plane.app.serializers import (
|
||||
GlobalViewSerializer,
|
||||
IssueViewSerializer,
|
||||
ViewSerializer,
|
||||
IssueSerializer,
|
||||
IssueViewFavoriteSerializer,
|
||||
ViewFavoriteSerializer,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
WorkspaceEntityPermission,
|
||||
ProjectEntityPermission,
|
||||
WorkspaceViewerPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
GlobalView,
|
||||
IssueView,
|
||||
View,
|
||||
Issue,
|
||||
IssueViewFavorite,
|
||||
IssueReaction,
|
||||
ViewFavorite,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
IssueSubscriber,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.grouper import group_results
|
||||
|
||||
|
||||
class GlobalViewViewSet(BaseViewSet):
|
||||
serializer_class = IssueViewSerializer
|
||||
model = IssueView
|
||||
class UserWorkspaceViewViewSet(BaseViewSet):
|
||||
serializer_class = ViewSerializer
|
||||
model = View
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
|
||||
serializer.save(workspace_id=workspace.id)
|
||||
serializer.save(
|
||||
workspace_id=workspace.id, access=0, owned_by=self.request.user
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
subquery = ViewFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
view_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
# .filter(project__isnull=True)
|
||||
.filter(Q(owned_by=self.request.user) & Q(access=0))
|
||||
.select_related("workspace")
|
||||
.annotate(is_favorite=Exists(subquery))
|
||||
.order_by(self.request.GET.get("order_by", "-is_pinned"))
|
||||
.order_by("-is_pinned", "-created_at")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, pk):
|
||||
view = View.objects.get(pk=pk, workspace__slug=slug)
|
||||
if view.owned_by == self.request.user and not view.is_locked:
|
||||
serializer = ViewSerializer(view, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(
|
||||
{"error": "You cannot update the view"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
def list(self, request, slug):
|
||||
type = request.GET.get("type", None)
|
||||
views = self.get_queryset()
|
||||
|
||||
if type == "workspace":
|
||||
views = views.filter(project__isnull=True)
|
||||
|
||||
if type == "project":
|
||||
views = views.filter(project__isnull=False)
|
||||
|
||||
serializer = ViewSerializer(views, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def toggle_lock(self, request, slug, pk):
|
||||
view = View.objects.get(pk=pk, workspace__slug=slug)
|
||||
lock = request.data.get("lock", view.is_locked)
|
||||
if view.owned_by != self.request.user:
|
||||
return Response(
|
||||
{"error": "You cannot lock the view"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
view.is_locked = lock
|
||||
view.save(update_fields=["is_locked"])
|
||||
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def duplicate(self, request, slug, pk):
|
||||
view = View.objects.get(workspace__slug=slug, pk=pk)
|
||||
# Create a shallow copy of the original view object
|
||||
new_view = view
|
||||
|
||||
# Set the primary key of the new view to None to ensure it gets a new primary key
|
||||
new_view.pk = None
|
||||
|
||||
# Modify the name of the new view to indicate that it's a copy
|
||||
new_view.name = f"{view.name} (Copy)"
|
||||
new_view.save(owned_by=request.user)
|
||||
return Response(ViewSerializer(new_view).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
def destroy(self, request, slug, pk):
|
||||
view = View.objects.get(workspace__slug=slug, pk=pk)
|
||||
if view.owned_by != self.request.user:
|
||||
return Response(
|
||||
{"error": "You cannot delete the view"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
view.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class WorkspaceViewViewSet(BaseViewSet):
|
||||
serializer_class = ViewSerializer
|
||||
model = View
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
|
||||
serializer.save(workspace_id=workspace.id, owned_by=self.request.user)
|
||||
|
||||
def get_queryset(self):
|
||||
subquery = ViewFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
view_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project__isnull=True)
|
||||
.filter(Q(access=1))
|
||||
.select_related("workspace")
|
||||
.annotate(is_favorite=Exists(subquery))
|
||||
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def toggle_lock(self, request, slug, pk):
|
||||
view = View.objects.get(pk=pk, workspace__slug=slug)
|
||||
lock = request.data.get("lock", view.is_locked)
|
||||
view.is_locked = lock
|
||||
view.save(update_fields=["is_locked"])
|
||||
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
|
||||
|
||||
def duplicate(self, request, slug, pk):
|
||||
view = View.objects.get(workspace__slug=slug, pk=pk)
|
||||
# Create a shallow copy of the original view object
|
||||
new_view = view
|
||||
|
||||
# Set the primary key of the new view to None to ensure it gets a new primary key
|
||||
new_view.pk = None
|
||||
|
||||
# Modify the name of the new view to indicate that it's a copy
|
||||
new_view.name = f"{view.name} (Copy)"
|
||||
new_view.save(owned_by=request.user)
|
||||
return Response(ViewSerializer(new_view).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
def visibility(self, request, slug, pk):
|
||||
view = (
|
||||
self.get_queryset()
|
||||
.filter(pk=pk, workspace__slug=slug)
|
||||
.first()
|
||||
)
|
||||
if view.owned_by != self.request.user:
|
||||
return Response(
|
||||
{"error": "You cannot update the view"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
view.access = request.data.get("access", view.access)
|
||||
view.save(update_fields=["access"])
|
||||
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class UserProjectViewViewSet(BaseViewSet):
|
||||
serializer_class = ViewSerializer
|
||||
model = View
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
|
||||
serializer.save(
|
||||
workspace_id=workspace.id,
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
access=0,
|
||||
owned_by=self.request.user,
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
subquery = ViewFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
view_id=OuterRef("pk"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.filter(Q(owned_by=self.request.user) & Q(access=0))
|
||||
.select_related("workspace")
|
||||
.annotate(is_favorite=Exists(subquery))
|
||||
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
view = View.objects.get(
|
||||
pk=pk, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
if view.owned_by == self.request.user and not view.is_locked:
|
||||
serializer = ViewSerializer(view, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(
|
||||
{"error": "You cannot update the view"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
def toggle_lock(self, request, slug, project_id, pk):
|
||||
view = View.objects.get(
|
||||
pk=pk, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
lock = request.data.get("lock", view.is_locked)
|
||||
if view.owned_by != self.request.user:
|
||||
return Response(
|
||||
{"error": "You cannot lock the view"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
view.is_locked = lock
|
||||
view.save(update_fields=["is_locked"])
|
||||
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
|
||||
|
||||
def duplicate(self, request, slug, project_id, pk):
|
||||
view = View.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
# Create a shallow copy of the original view object
|
||||
new_view = view
|
||||
|
||||
# Set the primary key of the new view to None to ensure it gets a new primary key
|
||||
new_view.pk = None
|
||||
|
||||
# Modify the name of the new view to indicate that it's a copy
|
||||
new_view.name = f"{view.name} (Copy)"
|
||||
new_view.save(owned_by=request.user)
|
||||
return Response(ViewSerializer(new_view).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
view = View.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if view.owned_by != self.request.user:
|
||||
return Response(
|
||||
{"error": "You cannot delete the view"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
view.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class ProjectViewViewSet(BaseViewSet):
|
||||
serializer_class = ViewSerializer
|
||||
model = View
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
|
||||
serializer.save(
|
||||
workspace_id=workspace.id,
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
owned_by=self.request.user,
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
subquery = ViewFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
view_id=OuterRef("pk"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.filter(Q(access=1))
|
||||
.select_related("workspace")
|
||||
.annotate(is_favorite=Exists(subquery))
|
||||
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def toggle_lock(self, request, slug, project_id, pk):
|
||||
view = (
|
||||
self.get_queryset()
|
||||
.filter(pk=pk, project_id=project_id, workspace__slug=slug)
|
||||
.first()
|
||||
)
|
||||
lock = request.data.get("lock", view.is_locked)
|
||||
view.is_locked = lock
|
||||
view.save(update_fields=["is_locked"])
|
||||
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
|
||||
|
||||
def duplicate(self, request, slug, project_id, pk):
|
||||
view = View.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
# Create a shallow copy of the original view object
|
||||
new_view = view
|
||||
|
||||
# Set the primary key of the new view to None to ensure it gets a new primary key
|
||||
new_view.pk = None
|
||||
|
||||
# Modify the name of the new view to indicate that it's a copy
|
||||
new_view.name = f"{view.name} (Copy)"
|
||||
new_view.save(owned_by=request.user)
|
||||
return Response(ViewSerializer(new_view).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def visibility(self, request, slug, project_id, pk):
|
||||
view = (
|
||||
self.get_queryset()
|
||||
.filter(pk=pk, project_id=project_id, workspace__slug=slug)
|
||||
.first()
|
||||
)
|
||||
if view.owned_by != self.request.user:
|
||||
return Response(
|
||||
{"error": "You cannot update the view"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
view.access = request.data.get("access", view.access)
|
||||
view.save(update_fields=["access"])
|
||||
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
@@ -87,41 +388,9 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
fields = [
|
||||
field
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
|
||||
# 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)
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
@@ -147,6 +416,29 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
fields = [
|
||||
field
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
|
||||
# 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":
|
||||
priority_order = (
|
||||
@@ -213,52 +505,9 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueViewViewSet(BaseViewSet):
|
||||
serializer_class = IssueViewSerializer
|
||||
model = IssueView
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def get_queryset(self):
|
||||
subquery = IssueViewFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
view_id=OuterRef("pk"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.annotate(is_favorite=Exists(subquery))
|
||||
.order_by("-is_favorite", "name")
|
||||
.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
|
||||
model = IssueViewFavorite
|
||||
class WorkspaceViewFavoriteViewSet(BaseViewSet):
|
||||
serializer_class = ViewFavoriteSerializer
|
||||
model = ViewFavorite
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
@@ -269,19 +518,52 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
||||
.select_related("view")
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
serializer = IssueViewFavoriteSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(user=request.user, project_id=project_id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
def create(self, request, slug, view_id):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
view = ViewFavorite.objects.create(
|
||||
view_id=view_id, user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
return Response(
|
||||
ViewFavoriteSerializer(view).data, status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, view_id):
|
||||
view_favorite = ViewFavorite.objects.get(
|
||||
user=request.user,
|
||||
workspace__slug=slug,
|
||||
view_id=view_id,
|
||||
)
|
||||
view_favorite.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class ProjectViewFavoriteViewSet(BaseViewSet):
|
||||
serializer_class = ViewFavoriteSerializer
|
||||
model = ViewFavorite
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(user=self.request.user)
|
||||
.select_related("view")
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id, view_id):
|
||||
view = ViewFavorite.objects.create(
|
||||
view_id=view_id, user=request.user, project_id=project_id
|
||||
)
|
||||
return Response(
|
||||
ViewFavoriteSerializer(view).data, status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, view_id):
|
||||
view_favourite = IssueViewFavorite.objects.get(
|
||||
view_favorite = ViewFavorite.objects.get(
|
||||
project=project_id,
|
||||
user=request.user,
|
||||
workspace__slug=slug,
|
||||
view_id=view_id,
|
||||
)
|
||||
view_favourite.delete()
|
||||
view_favorite.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -1516,11 +1516,9 @@ class WorkspaceUserPropertiesEndpoint(BaseAPIView):
|
||||
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
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
workspace_properties, _ = WorkspaceUserProperties.objects.get_or_create(
|
||||
user=request.user, workspace=workspace
|
||||
)
|
||||
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-02 13:15
|
||||
|
||||
from plane.db.models import WorkspaceUserProperties, ProjectMember, IssueView
|
||||
from plane.db.models import ProjectMember
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def workspace_user_properties(apps, schema_editor):
|
||||
WorkspaceMember = apps.get_model("db", "WorkspaceMember")
|
||||
WorkspaceUserProperties = apps.get_model("db", "WorkspaceUserProperties")
|
||||
updated_workspace_user_properties = []
|
||||
for workspace_members in WorkspaceMember.objects.all():
|
||||
updated_workspace_user_properties.append(
|
||||
@@ -49,6 +50,7 @@ def project_user_properties(apps, schema_editor):
|
||||
|
||||
def issue_view(apps, schema_editor):
|
||||
GlobalView = apps.get_model("db", "GlobalView")
|
||||
IssueView = apps.get_model("db", "IssueView")
|
||||
updated_issue_views = []
|
||||
|
||||
for global_view in GlobalView.objects.all():
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-30 07:49
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db.models import F
|
||||
|
||||
def views_owned_by(apps, schema_editor):
|
||||
View = apps.get_model("db", "View")
|
||||
View.objects.update(owned_by=F('created_by'))
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0060_cycle_progress_snapshot'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='IssueView',
|
||||
new_name='View',
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='view',
|
||||
table='views',
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='IssueViewFavorite',
|
||||
new_name='ViewFavorite',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='viewfavorite',
|
||||
name='project',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name='workspaceuserproperties',
|
||||
table='workspace_user_properties',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='view',
|
||||
options={'ordering': ('-created_at',), 'verbose_name': 'View', 'verbose_name_plural': 'Views'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='view',
|
||||
name='is_locked',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='view',
|
||||
name='is_pinned',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='view',
|
||||
name='owned_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='views', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='view',
|
||||
name='access',
|
||||
field=models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public'), (2, 'Shared')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='viewfavorite',
|
||||
name='view',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='view_favorites', to='db.view'),
|
||||
),
|
||||
migrations.RunPython(views_owned_by)
|
||||
]
|
||||
@@ -52,7 +52,7 @@ from .state import State
|
||||
|
||||
from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
|
||||
|
||||
from .view import GlobalView, IssueView, IssueViewFavorite
|
||||
from .view import View, ViewFavorite
|
||||
|
||||
from .module import (
|
||||
Module,
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
# Module import
|
||||
from . import ProjectBaseModel, BaseModel, WorkspaceBaseModel
|
||||
from . import BaseModel, WorkspaceBaseModel
|
||||
|
||||
|
||||
def get_default_filters():
|
||||
@@ -84,7 +84,7 @@ class GlobalView(BaseModel):
|
||||
return f"{self.name} <{self.workspace.name}>"
|
||||
|
||||
|
||||
class IssueView(WorkspaceBaseModel):
|
||||
class View(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")
|
||||
@@ -94,29 +94,44 @@ class IssueView(WorkspaceBaseModel):
|
||||
default=get_default_display_properties
|
||||
)
|
||||
access = models.PositiveSmallIntegerField(
|
||||
default=1, choices=((0, "Private"), (1, "Public"))
|
||||
default=1, choices=((0, "Private"), (1, "Public"), (2, "Shared"))
|
||||
)
|
||||
owned_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="views", null=True, blank=True
|
||||
)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
is_locked = models.BooleanField(default=False)
|
||||
is_pinned = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue View"
|
||||
verbose_name_plural = "Issue Views"
|
||||
db_table = "issue_views"
|
||||
verbose_name = "View"
|
||||
verbose_name_plural = "Views"
|
||||
db_table = "views"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
largest_sort_order = View.objects.filter(
|
||||
workspace=self.workspace
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
|
||||
super(View, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the View"""
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
||||
|
||||
class IssueViewFavorite(ProjectBaseModel):
|
||||
class ViewFavorite(WorkspaceBaseModel):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="user_view_favorites",
|
||||
)
|
||||
view = models.ForeignKey(
|
||||
"db.IssueView", on_delete=models.CASCADE, related_name="view_favorites"
|
||||
"db.View", on_delete=models.CASCADE, related_name="view_favorites"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -326,7 +326,7 @@ class WorkspaceUserProperties(BaseModel):
|
||||
unique_together = ["workspace", "user"]
|
||||
verbose_name = "Workspace User Property"
|
||||
verbose_name_plural = "Workspace User Property"
|
||||
db_table = "Workspace_user_properties"
|
||||
db_table = "workspace_user_properties"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
Vendored
-72
@@ -1,72 +0,0 @@
|
||||
import { TIssue } from "./issues/base";
|
||||
import type { IProjectLite } from "./projects";
|
||||
|
||||
export type TInboxIssueExtended = {
|
||||
completed_at: string | null;
|
||||
start_date: string | null;
|
||||
target_date: string | null;
|
||||
};
|
||||
|
||||
export interface IInboxIssue extends TIssue, TInboxIssueExtended {
|
||||
issue_inbox: {
|
||||
duplicate_to: string | null;
|
||||
id: string;
|
||||
snoozed_till: Date | null;
|
||||
source: string;
|
||||
status: -2 | -1 | 0 | 1 | 2;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IInbox {
|
||||
id: string;
|
||||
project_detail: IProjectLite;
|
||||
pending_issue_count: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
name: string;
|
||||
description: string;
|
||||
is_default: boolean;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
view_props: { filters: IInboxFilterOptions };
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
interface StatePending {
|
||||
readonly status: -2;
|
||||
}
|
||||
interface StatusReject {
|
||||
status: -1;
|
||||
}
|
||||
|
||||
interface StatusSnoozed {
|
||||
status: 0;
|
||||
snoozed_till: Date;
|
||||
}
|
||||
|
||||
interface StatusAccepted {
|
||||
status: 1;
|
||||
}
|
||||
|
||||
interface StatusDuplicate {
|
||||
status: 2;
|
||||
duplicate_to: string;
|
||||
}
|
||||
|
||||
export type TInboxStatus =
|
||||
| StatusReject
|
||||
| StatusSnoozed
|
||||
| StatusAccepted
|
||||
| StatusDuplicate
|
||||
| StatePending;
|
||||
|
||||
export interface IInboxFilterOptions {
|
||||
priority?: string[] | null;
|
||||
inbox_status?: number[] | null;
|
||||
}
|
||||
|
||||
export interface IInboxQueryParams {
|
||||
priority: string | null;
|
||||
inbox_status: string | null;
|
||||
}
|
||||
Vendored
+1
-4
@@ -13,11 +13,7 @@ export * from "./pages";
|
||||
export * from "./ai";
|
||||
export * from "./estimate";
|
||||
export * from "./importer";
|
||||
|
||||
// FIXME: Remove this after development and the refactor/mobx-store-issue branch is stable
|
||||
export * from "./inbox";
|
||||
export * from "./inbox/root";
|
||||
|
||||
export * from "./analytics";
|
||||
export * from "./calendar";
|
||||
export * from "./notifications";
|
||||
@@ -31,6 +27,7 @@ export * from "./auth";
|
||||
export * from "./api_token";
|
||||
export * from "./instance";
|
||||
export * from "./app";
|
||||
export * from "./view/root";
|
||||
|
||||
export type NestedKeyOf<ObjectType extends object> = {
|
||||
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
||||
|
||||
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
TViewFilters,
|
||||
TViewDisplayFilters,
|
||||
TViewDisplayProperties,
|
||||
} from "./filter";
|
||||
|
||||
export type TViewTypes =
|
||||
| "WORKSPACE_PRIVATE_VIEWS"
|
||||
| "WORKSPACE_PUBLIC_VIEWS"
|
||||
| "PROJECT_PRIVATE_VIEWS"
|
||||
| "PROJECT_PUBLIC_VIEWS";
|
||||
|
||||
declare enum EViewAccess {
|
||||
"public" = 0,
|
||||
"private" = 1,
|
||||
"shared" = 2,
|
||||
}
|
||||
|
||||
export type TViewAccess =
|
||||
| EViewAccess.public
|
||||
| EViewAccess.private
|
||||
| EViewAccess.shared;
|
||||
|
||||
export type TView = {
|
||||
id: string | undefined;
|
||||
workspace: string | undefined;
|
||||
project: string | undefined;
|
||||
name: string | undefined;
|
||||
description: string | undefined;
|
||||
query: string | undefined;
|
||||
filters: TViewFilters;
|
||||
display_filters: TViewDisplayFilters;
|
||||
display_properties: TViewDisplayProperties;
|
||||
access: TViewAccess | undefined;
|
||||
owned_by: string | undefined;
|
||||
sort_order: number | undefined;
|
||||
is_locked: boolean;
|
||||
is_pinned: boolean;
|
||||
is_favorite: boolean;
|
||||
created_by: string | undefined;
|
||||
updated_by: string | undefined;
|
||||
created_at: Date | undefined;
|
||||
updated_at: Date | undefined;
|
||||
// local view variables
|
||||
is_local_view: boolean;
|
||||
is_create: boolean;
|
||||
is_editable: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateView = {
|
||||
name: string | undefined;
|
||||
description: string | undefined;
|
||||
filters: TViewFilters;
|
||||
display_filters: TViewDisplayFilters;
|
||||
display_properties: TViewDisplayProperties;
|
||||
};
|
||||
Vendored
+124
@@ -0,0 +1,124 @@
|
||||
export type TViewLayouts =
|
||||
| "list"
|
||||
| "kanban"
|
||||
| "calendar"
|
||||
| "spreadsheet"
|
||||
| "gantt";
|
||||
|
||||
export type TViewDisplayFiltersGrouped =
|
||||
| "project"
|
||||
| "state_detail.group"
|
||||
| "state"
|
||||
| "priority"
|
||||
| "labels"
|
||||
| "created_by"
|
||||
| "assignees"
|
||||
| "mentions"
|
||||
| "modules"
|
||||
| "cycles";
|
||||
|
||||
export type TViewDisplayFiltersOrderBy =
|
||||
| "sort_order"
|
||||
| "created_at"
|
||||
| "-created_at"
|
||||
| "updated_at"
|
||||
| "-updated_at"
|
||||
| "start_date"
|
||||
| "-start_date"
|
||||
| "target_date"
|
||||
| "-target_date"
|
||||
| "state__name"
|
||||
| "-state__name"
|
||||
| "priority"
|
||||
| "-priority"
|
||||
| "labels__name"
|
||||
| "-labels__name"
|
||||
| "assignees__first_name"
|
||||
| "-assignees__first_name"
|
||||
| "estimate_point"
|
||||
| "-estimate_point"
|
||||
| "link_count"
|
||||
| "-link_count"
|
||||
| "attachment_count"
|
||||
| "-attachment_count"
|
||||
| "sub_issues_count"
|
||||
| "-sub_issues_count";
|
||||
|
||||
export type TViewDisplayFiltersType = "active" | "backlog";
|
||||
|
||||
export type TViewCalendarLayouts = "month" | "week";
|
||||
|
||||
export type TViewFilters = {
|
||||
project: string[];
|
||||
module: string[];
|
||||
cycle: string[];
|
||||
priority: string[];
|
||||
state: string[];
|
||||
state_group: string[];
|
||||
assignees: string[];
|
||||
mentions: string[];
|
||||
subscriber: string[];
|
||||
created_by: string[];
|
||||
labels: string[];
|
||||
start_date: string[];
|
||||
target_date: string[];
|
||||
};
|
||||
|
||||
export type TViewDisplayFilters = {
|
||||
layout: TViewLayouts;
|
||||
group_by: TViewDisplayFiltersGrouped | undefined;
|
||||
sub_group_by: TViewDisplayFiltersGrouped | undefined;
|
||||
order_by: TViewDisplayFiltersOrderBy | string;
|
||||
type: TViewDisplayFiltersType | undefined;
|
||||
sub_issue: boolean;
|
||||
show_empty_groups: boolean;
|
||||
calendar: {
|
||||
show_weekends: boolean;
|
||||
layout: TViewCalendarLayouts;
|
||||
};
|
||||
};
|
||||
|
||||
export type TViewDisplayProperties = {
|
||||
assignee: boolean;
|
||||
start_date: boolean;
|
||||
due_date: boolean;
|
||||
labels: boolean;
|
||||
key: boolean;
|
||||
priority: boolean;
|
||||
state: boolean;
|
||||
sub_issue_count: boolean;
|
||||
link: boolean;
|
||||
attachment_count: boolean;
|
||||
estimate: boolean;
|
||||
created_on: boolean;
|
||||
updated_on: boolean;
|
||||
};
|
||||
|
||||
export type TViewFilterProps = {
|
||||
filters: TViewFilters;
|
||||
display_filters: TViewDisplayFilters;
|
||||
display_properties: TViewDisplayProperties;
|
||||
};
|
||||
|
||||
export type TViewFilterPartialProps = {
|
||||
filters: Partial<TViewFilters>;
|
||||
display_filters: Partial<TViewDisplayFilters>;
|
||||
display_properties: Partial<TViewDisplayProperties>;
|
||||
};
|
||||
|
||||
export type TViewFilterQueryParams =
|
||||
| "project"
|
||||
| "module"
|
||||
| "cycle"
|
||||
| "priority"
|
||||
| "state"
|
||||
| "state_group"
|
||||
| "assignees"
|
||||
| "mentions"
|
||||
| "subscriber"
|
||||
| "created_by"
|
||||
| "labels"
|
||||
| "start_date"
|
||||
| "target_date"
|
||||
| "type"
|
||||
| "sub_issue";
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export * from "./filter";
|
||||
export * from "./base";
|
||||
export * from "./user-base";
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
TViewFilters,
|
||||
TViewDisplayFilters,
|
||||
TViewDisplayProperties,
|
||||
} from "./filter";
|
||||
|
||||
export type TUserView = {
|
||||
id: string | undefined;
|
||||
workspace: string | undefined;
|
||||
user: string | undefined;
|
||||
filters: TViewFilters;
|
||||
display_filters: TViewDisplayFilters;
|
||||
display_properties: TViewDisplayProperties;
|
||||
created_by: string | undefined;
|
||||
updated_by: string | undefined;
|
||||
created_at: Date | undefined;
|
||||
updated_at: Date | undefined;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { mutate } from "swr";
|
||||
@@ -138,14 +138,18 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds;
|
||||
|
||||
|
||||
return (
|
||||
<div className={cn("relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-custom-sidebar-background-100", !isProjectLevel ? "flex-col" : "")}
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-custom-sidebar-background-100",
|
||||
!isProjectLevel ? "flex-col" : ""
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
||||
<LayersIcon height={14} width={14} />
|
||||
{analytics ? analytics.total : "..."} <div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
|
||||
{analytics ? analytics.total : "..."}{" "}
|
||||
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
|
||||
</div>
|
||||
{isProjectLevel && (
|
||||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
||||
@@ -154,8 +158,8 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||
(cycleId
|
||||
? cycleDetails?.created_at
|
||||
: moduleId
|
||||
? moduleDetails?.created_at
|
||||
: projectDetails?.created_at) ?? ""
|
||||
? moduleDetails?.created_at
|
||||
: projectDetails?.created_at) ?? ""
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,13 @@
|
||||
import { FC } from "react";
|
||||
|
||||
type TGlobalViewsRootProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
viewId: string;
|
||||
};
|
||||
|
||||
export const GlobalViewsRoot: FC<TGlobalViewsRootProps> = (props) => {
|
||||
const { viewId } = props;
|
||||
|
||||
return <div>GlobalViewsRoot {viewId}</div>;
|
||||
};
|
||||
@@ -18,7 +18,7 @@ import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react";
|
||||
// types
|
||||
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
|
||||
import type { TInboxDetailedStatus } from "@plane/types";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { ISSUE_DELETED } from "constants/event-tracker";
|
||||
|
||||
@@ -30,7 +30,7 @@ type TInboxIssueActionsHeader = {
|
||||
};
|
||||
|
||||
type TInboxIssueOperations = {
|
||||
updateInboxIssueStatus: (data: TInboxStatus) => Promise<void>;
|
||||
updateInboxIssueStatus: (data: TInboxDetailedStatus) => Promise<void>;
|
||||
removeInboxIssue: () => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -162,12 +162,13 @@ export const ProfileSidebar = observer(() => {
|
||||
{project.assigned_issues > 0 && (
|
||||
<Tooltip tooltipContent="Completion percentage" position="left">
|
||||
<div
|
||||
className={`rounded px-1 py-0.5 text-xs font-medium ${completedIssuePercentage <= 35
|
||||
? "bg-red-500/10 text-red-500"
|
||||
: completedIssuePercentage <= 70
|
||||
className={`rounded px-1 py-0.5 text-xs font-medium ${
|
||||
completedIssuePercentage <= 35
|
||||
? "bg-red-500/10 text-red-500"
|
||||
: completedIssuePercentage <= 70
|
||||
? "bg-yellow-500/10 text-yellow-500"
|
||||
: "bg-green-500/10 text-green-500"
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{completedIssuePercentage}%
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { FC } from "react";
|
||||
import { ImagePlus, X } from "lucide-react";
|
||||
// hooks
|
||||
import { useViewDetail, useViewFilter } from "hooks/store";
|
||||
// types
|
||||
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewAppliedFiltersItem = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
filterKey: keyof TViewFilters;
|
||||
propertyId: string;
|
||||
};
|
||||
|
||||
export const ViewAppliedFiltersItem: FC<TViewAppliedFiltersItem> = (props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, filterKey, propertyId } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
|
||||
|
||||
const propertyDetail = viewFilterHelper?.propertyDetails(filterKey, propertyId) || undefined;
|
||||
|
||||
const removeFilterOption = () => {
|
||||
viewDetailStore?.setFilters(filterKey, propertyId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`filter_value_${filterKey}_${propertyId}`}
|
||||
className="bg-custom-background-80 rounded relative flex items-center gap-1.5 p-1"
|
||||
>
|
||||
<div className="flex-shrink-0 w-4 h-4 relative flex justify-center items-center overflow-hidden">
|
||||
{propertyDetail?.icon || <ImagePlus size={14} />}
|
||||
</div>
|
||||
<div className="text-xs">{propertyDetail?.label || propertyId}</div>
|
||||
<div
|
||||
className="flex-shrink-0 w-3.5 h-3.5 relative flex justify-center items-center overflow-hidden rounded-full transition-all cursor-pointer bg-custom-background-80 hover:bg-custom-background-90 text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={removeFilterOption}
|
||||
>
|
||||
<X size={10} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { X } from "lucide-react";
|
||||
// hooks
|
||||
import { useViewDetail, useViewFilter } from "hooks/store";
|
||||
// components
|
||||
import { ViewAppliedFiltersItem } from "./filter-item";
|
||||
// types
|
||||
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewAppliedFilters = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
filterKey: keyof TViewFilters;
|
||||
propertyVisibleCount?: number | undefined;
|
||||
};
|
||||
|
||||
export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, filterKey, propertyVisibleCount } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
const viewFilterStore = useViewFilter(workspaceSlug, projectId);
|
||||
|
||||
const currentDefaultFilterDetails = viewFilterStore?.propertyDefaultDetails(filterKey);
|
||||
|
||||
const propertyValues =
|
||||
viewDetailStore?.appliedFilters?.filters && !isEmpty(viewDetailStore?.appliedFilters?.filters)
|
||||
? viewDetailStore?.appliedFilters?.filters?.[filterKey] || undefined
|
||||
: undefined;
|
||||
|
||||
const clearPropertyFilter = () => viewDetailStore?.setFilters(filterKey, "clear_all");
|
||||
|
||||
if (!propertyValues || propertyValues.length <= 0) return <></>;
|
||||
return (
|
||||
<div className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1 px-2 min-h-[32px]">
|
||||
<div className="flex-shrink-0 text-xs text-custom-text-200 capitalize">{filterKey.replaceAll("_", " ")}</div>
|
||||
<div className="relative flex items-center gap-1.5 flex-wrap">
|
||||
{propertyVisibleCount && propertyValues.length >= propertyVisibleCount ? (
|
||||
<div className="text-xs font-medium bg-custom-primary-100/20 rounded relative flex items-center gap-1 p-1 px-2">
|
||||
<div className="flex-shrink-0 w-4-h-4">{currentDefaultFilterDetails?.icon}</div>
|
||||
<div className="whitespace-nowrap">
|
||||
{propertyValues.length} {currentDefaultFilterDetails?.label}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{propertyValues.map((propertyId) => (
|
||||
<Fragment key={propertyId}>
|
||||
<ViewAppliedFiltersItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
filterKey={filterKey}
|
||||
propertyId={propertyId}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 rounded-full cursor-pointer transition-all bg-custom-background-80 hover:bg-custom-background-90 text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={clearPropertyFilter}
|
||||
>
|
||||
<X size={10} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { X } from "lucide-react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
// hooks
|
||||
import { useViewDetail } from "hooks/store";
|
||||
// components
|
||||
import { ViewAppliedFilters } from "./filter";
|
||||
// types
|
||||
import { TViewTypes, TViewFilters } from "@plane/types";
|
||||
|
||||
type TViewAppliedFiltersRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
propertyVisibleCount?: number | undefined;
|
||||
showClearAll?: boolean;
|
||||
};
|
||||
|
||||
export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, propertyVisibleCount = undefined, showClearAll = false } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
|
||||
const filterKeys =
|
||||
viewDetailStore?.filtersToUpdate && !isEmpty(viewDetailStore?.filtersToUpdate?.filters)
|
||||
? Object.keys(viewDetailStore?.filtersToUpdate?.filters)
|
||||
: undefined;
|
||||
|
||||
const clearAllFilters = () => viewDetailStore?.setFilters(undefined, "clear_all");
|
||||
|
||||
if (!filterKeys || !viewDetailStore?.isFiltersApplied)
|
||||
return (
|
||||
<div className="relative w-full text-sm text-custom-text-200 inline-block truncate line-clamp-1 pt-1.5">
|
||||
No filters applied. Apply filters to create views.
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="relative flex items-center gap-2 flex-wrap">
|
||||
{filterKeys.map((key) => {
|
||||
const filterKey = key as keyof TViewFilters;
|
||||
return (
|
||||
<Fragment key={filterKey}>
|
||||
<ViewAppliedFilters
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
filterKey={filterKey}
|
||||
propertyVisibleCount={propertyVisibleCount}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{showClearAll && (
|
||||
<div
|
||||
className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1.5 px-2 cursor-pointer transition-all hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100 min-h-[36px]"
|
||||
onClick={clearAllFilters}
|
||||
>
|
||||
<div className="text-xs">Clear All</div>
|
||||
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4">
|
||||
<X size={10} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { FC, Fragment, useCallback, useEffect, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import { TViewOperations } from "../types";
|
||||
|
||||
type TViewDeleteConfirmationModal = {
|
||||
viewId: string;
|
||||
viewOperations: TViewOperations;
|
||||
};
|
||||
|
||||
export const ViewDeleteConfirmationModal: FC<TViewDeleteConfirmationModal> = (props) => {
|
||||
const { viewId, viewOperations } = props;
|
||||
// state
|
||||
const [modalToggle, setModalToggle] = useState(false);
|
||||
const [loader, setLoader] = useState(false);
|
||||
|
||||
const modalOpen = useCallback(() => setModalToggle(true), [setModalToggle]);
|
||||
const modalClose = useCallback(() => {
|
||||
setModalToggle(false);
|
||||
}, [setModalToggle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewId) modalOpen();
|
||||
}, [viewId, modalOpen, modalClose]);
|
||||
|
||||
const onContinue = async () => {
|
||||
setLoader(true);
|
||||
setLoader(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={modalToggle} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={modalClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem] py-5 border-[0.1px] border-custom-border-100">
|
||||
<div className="p-3 px-5 relative flex items-center gap-2">Content</div>
|
||||
|
||||
<div className="p-3 px-5 relative flex justify-end items-center gap-2">
|
||||
<Button variant="neutral-primary" onClick={modalClose} disabled={loader}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={onContinue} disabled={loader}>
|
||||
{loader ? `Duplicating` : `Duplicate View`}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { FC, Fragment, useCallback, useEffect, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import { TViewOperations } from "../types";
|
||||
|
||||
type TViewDuplicateConfirmationModal = {
|
||||
viewId: string;
|
||||
viewOperations: TViewOperations;
|
||||
};
|
||||
|
||||
export const ViewDuplicateConfirmationModal: FC<TViewDuplicateConfirmationModal> = (props) => {
|
||||
const { viewId, viewOperations } = props;
|
||||
// state
|
||||
const [modalToggle, setModalToggle] = useState(false);
|
||||
const [loader, setLoader] = useState(false);
|
||||
|
||||
const modalOpen = useCallback(() => setModalToggle(true), [setModalToggle]);
|
||||
const modalClose = useCallback(() => {
|
||||
setModalToggle(false);
|
||||
}, [setModalToggle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewId) modalOpen();
|
||||
}, [viewId, modalOpen, modalClose]);
|
||||
|
||||
const onContinue = async () => {
|
||||
setLoader(true);
|
||||
setLoader(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={modalToggle} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={modalClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem] py-5 border-[0.1px] border-custom-border-100">
|
||||
<div className="p-3 px-5 relative flex items-center gap-2">Content</div>
|
||||
|
||||
<div className="p-3 px-5 relative flex justify-end items-center gap-2">
|
||||
<Button variant="neutral-primary" onClick={modalClose} disabled={loader}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={onContinue} disabled={loader}>
|
||||
{loader ? `Duplicating` : `Duplicate View`}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import { FC, Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { MonitorDot } from "lucide-react";
|
||||
// hooks
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { ViewDisplayPropertiesRoot } from "../";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewDisplayFiltersDropdown = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
children?: ReactNode;
|
||||
displayDropdownText?: boolean;
|
||||
dropdownPlacement?: Placement;
|
||||
};
|
||||
|
||||
export const ViewDisplayFiltersDropdown: FC<TViewDisplayFiltersDropdown> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
viewId,
|
||||
viewType,
|
||||
children,
|
||||
displayDropdownText = true,
|
||||
dropdownPlacement = "bottom-start",
|
||||
} = props;
|
||||
// state
|
||||
const [dropdownToggle, setDropdownToggle] = useState(false);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
// popper-js init
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: dropdownPlacement,
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 10],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const handleDropdownOpen = () => setDropdownToggle(true);
|
||||
const handleDropdownClose = () => setDropdownToggle(false);
|
||||
const handleDropdownToggle = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!dropdownToggle) handleDropdownOpen();
|
||||
else handleDropdownClose();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleDropdownClose);
|
||||
|
||||
return (
|
||||
<Combobox as="div" ref={dropdownRef}>
|
||||
<Combobox.Button as={Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={"block h-full w-full outline-none"}
|
||||
onClick={handleDropdownToggle}
|
||||
>
|
||||
{children ? (
|
||||
<div className="relative inline-block">{children}</div>
|
||||
) : (
|
||||
<Tooltip tooltipContent={"Display"} position="bottom">
|
||||
<div
|
||||
className={`relative flex items-center gap-1 h-8 rounded px-2 transition-all
|
||||
${
|
||||
displayDropdownText
|
||||
? `border border-custom-border-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80`
|
||||
: `hover:bg-custom-background-80`
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="w-4 h-4 relative flex justify-center items-center overflow-hidden">
|
||||
<MonitorDot size={14} />
|
||||
</div>
|
||||
{displayDropdownText && <div className="text-sm whitespace-nowrap">Display</div>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
|
||||
{dropdownToggle && (
|
||||
<Combobox.Options className="fixed z-10" static>
|
||||
<div
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
className="my-1 w-72 p-2 space-y-2 rounded bg-custom-background-100 border-[0.5px] border-custom-border-300 shadow-custom-shadow-rg focus:outline-none"
|
||||
>
|
||||
<div className="max-h-96 space-y-1 overflow-y-scroll">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium text-custom-text-200">Properties</div>
|
||||
<ViewDisplayPropertiesRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border border-red-500">Content</div>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { FC } from "react";
|
||||
|
||||
type TViewDisplayFiltersRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
};
|
||||
|
||||
export const ViewDisplayFiltersRoot: FC<TViewDisplayFiltersRoot> = (props) => {
|
||||
const { workspaceSlug, projectId, viewId } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>ViewDisplayFiltersRoot</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useViewDetail } from "hooks/store";
|
||||
// types
|
||||
import { TViewDisplayProperties, TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewDisplayPropertySelection = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
property: keyof TViewDisplayProperties;
|
||||
};
|
||||
|
||||
export const ViewDisplayPropertySelection: FC<TViewDisplayPropertySelection> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, property } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
|
||||
const propertyIsSelected = viewDetailStore?.appliedFilters?.display_properties?.[property];
|
||||
|
||||
const handlePropertySelection = () => viewDetailStore?.setDisplayProperties(property);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative flex items-center gap-1 text-xs rounded p-0.5 px-2 border transition-all capitalize cursor-pointer
|
||||
${
|
||||
propertyIsSelected
|
||||
? `border-custom-primary-100 bg-custom-primary-100`
|
||||
: `border-custom-border-300 hover:bg-custom-background-80`
|
||||
}`}
|
||||
onClick={handlePropertySelection}
|
||||
>
|
||||
{["key"].includes(property) ? "ID" : property.replaceAll("_", " ")}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { ViewDisplayPropertySelection } from "../";
|
||||
// types
|
||||
import { TViewDisplayProperties, TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewDisplayPropertiesRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
};
|
||||
|
||||
export const ViewDisplayPropertiesRoot: FC<TViewDisplayPropertiesRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType } = props;
|
||||
|
||||
const displayProperties: Partial<keyof TViewDisplayProperties>[] = [
|
||||
"key",
|
||||
"state",
|
||||
"labels",
|
||||
"priority",
|
||||
"assignee",
|
||||
"start_date",
|
||||
"due_date",
|
||||
"sub_issue_count",
|
||||
"attachment_count",
|
||||
"estimate",
|
||||
"link",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center flex-wrap gap-2">
|
||||
{displayProperties.map((property) => (
|
||||
<Fragment key={property}>
|
||||
<ViewDisplayPropertySelection
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
property={property}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import { FC, Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { ListFilter, Search } from "lucide-react";
|
||||
// hooks
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { ViewFiltersRoot } from "../";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewFiltersDropdown = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
children?: ReactNode;
|
||||
displayDropdownText?: boolean;
|
||||
dropdownPlacement?: Placement;
|
||||
};
|
||||
|
||||
export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
viewId,
|
||||
viewType,
|
||||
children,
|
||||
displayDropdownText = true,
|
||||
dropdownPlacement = "bottom-start",
|
||||
} = props;
|
||||
// state
|
||||
const [dropdownToggle, setDropdownToggle] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const [dateCustomFilterToggle, setDateCustomFilterToggle] = useState<string | undefined>(undefined);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
// popper-js init
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: dropdownPlacement,
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 10],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const handleDropdownOpen = () => setDropdownToggle(true);
|
||||
const handleDropdownClose = () => setDropdownToggle(false);
|
||||
const handleDropdownToggle = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!dropdownToggle) handleDropdownOpen();
|
||||
else handleDropdownClose();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, () => dateCustomFilterToggle === undefined && handleDropdownClose());
|
||||
|
||||
return (
|
||||
<Combobox as="div" ref={dropdownRef}>
|
||||
<Combobox.Button as={Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={"block h-full w-full outline-none"}
|
||||
onClick={handleDropdownToggle}
|
||||
>
|
||||
{children ? (
|
||||
<span className="relative inline-block">{children}</span>
|
||||
) : (
|
||||
<Tooltip tooltipContent={"Filters"} position="bottom">
|
||||
<div
|
||||
className={`relative flex items-center gap-1 h-8 rounded px-2 transition-all
|
||||
${
|
||||
displayDropdownText
|
||||
? `border border-custom-border-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80`
|
||||
: `hover:bg-custom-background-80`
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="w-4 h-4 relative flex justify-center items-center overflow-hidden">
|
||||
<ListFilter size={14} />
|
||||
</div>
|
||||
{displayDropdownText && <div className="text-sm whitespace-nowrap">Filters</div>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
|
||||
{dropdownToggle && (
|
||||
<Combobox.Options className="fixed z-10" static>
|
||||
<div
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
className="my-1 w-72 p-2 space-y-2 rounded bg-custom-background-100 border-[0.5px] border-custom-border-300 shadow-custom-shadow-rg focus:outline-none"
|
||||
>
|
||||
<div className="relative p-0.5 px-2 text-sm flex items-center gap-2 rounded border border-custom-border-100 bg-custom-background-90">
|
||||
<Search className="h-3 w-3 text-custom-text-300" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search for a view..."
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[500px] space-y-0.5 overflow-y-scroll mb-2">
|
||||
<ViewFiltersRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
dateCustomFilterToggle={dateCustomFilterToggle}
|
||||
setDateCustomFilterToggle={setDateCustomFilterToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { FC, Fragment, useMemo, useRef, useState } from "react";
|
||||
import { ChevronDown, ChevronUp, RotateCcw } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import { useViewDetail } from "hooks/store";
|
||||
// ui
|
||||
import { PhotoFilterIcon } from "@plane/ui";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
import { TViewFilterEditDropdownOptions, TViewOperations } from "../types";
|
||||
|
||||
type TViewFiltersEditDropdown = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewOperations: TViewOperations;
|
||||
};
|
||||
|
||||
export const ViewFiltersEditDropdown: FC<TViewFiltersEditDropdown> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
// popper-js init
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-end",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 10],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// dropdown options
|
||||
const dropdownOptions: TViewFilterEditDropdownOptions[] = useMemo(
|
||||
() => [
|
||||
// {
|
||||
// icon: PhotoFilterIcon,
|
||||
// key: "save_as_new",
|
||||
// label: "Save as new view",
|
||||
// onClick: () => {
|
||||
// viewOperations.localViewCreateEdit(undefined, viewDetailStore?.filtersToUpdate);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
icon: RotateCcw,
|
||||
key: "reset_changes",
|
||||
label: "Reset changes",
|
||||
onClick: () => viewDetailStore?.resetChanges(),
|
||||
},
|
||||
],
|
||||
[viewOperations, viewDetailStore]
|
||||
);
|
||||
|
||||
if (!viewDetailStore?.isFiltersUpdateEnabled) return <></>;
|
||||
return (
|
||||
<Menu as="div" className="relative flex-shrink-0" ref={dropdownRef}>
|
||||
<div className=" relative flex items-center rounded h-8 transition-all cursor-pointer bg-custom-primary-100/20 text-custom-primary-100">
|
||||
<button
|
||||
className="text-sm px-3 font-medium h-full border-r border-white/50 flex justify-center items-center rounded-l transition-all hover:bg-custom-primary-100/30"
|
||||
disabled={viewDetailStore?.loader === "updating"}
|
||||
onClick={() => viewOperations.update()}
|
||||
>
|
||||
{viewDetailStore?.loader === "updating" ? "updating..." : "Update"}
|
||||
</button>
|
||||
<Menu.Button
|
||||
as="div"
|
||||
className="flex-shrink-0 px-1.5 hover:bg-custom-primary-100/30 h-full flex justify-center items-center rounded-r transition-all outline-none"
|
||||
ref={setReferenceElement}
|
||||
>
|
||||
{({ open }) => (!open ? <ChevronDown size={16} /> : <ChevronUp size={16} />)}
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute right-0 z-20 mt-1.5 flex w-52 flex-col rounded border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 text-xs shadow-lg outline-none p-1"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{dropdownOptions &&
|
||||
dropdownOptions.length > 0 &&
|
||||
dropdownOptions.map((option) => (
|
||||
<Menu.Item
|
||||
key={option.key}
|
||||
as="button"
|
||||
type="button"
|
||||
className="relative flex items-center gap-2 p-1 py-1.5 rounded transition-all hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100 cursor-pointer"
|
||||
onClick={option.onClick}
|
||||
>
|
||||
<div className="flex-shrink-0 w-4 h-4 relative flex justify-center items-center">
|
||||
<option.icon size={12} className="w-3 h-3" />
|
||||
</div>
|
||||
<div className="text-xs whitespace-nowrap">{option.label}</div>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useViewDetail, useViewFilter } from "hooks/store";
|
||||
// components
|
||||
import { ViewFiltersItem, ViewFilterSelection } from "../";
|
||||
import { DateFilterModal } from "components/core";
|
||||
// types
|
||||
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewFiltersItemRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
filterKey: keyof TViewFilters;
|
||||
dateCustomFilterToggle: string | undefined;
|
||||
setDateCustomFilterToggle: (value: string | undefined) => void;
|
||||
};
|
||||
|
||||
export const ViewFiltersItemRoot: FC<TViewFiltersItemRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, filterKey, dateCustomFilterToggle, setDateCustomFilterToggle } =
|
||||
props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
|
||||
// state
|
||||
const [viewAll, setViewAll] = useState(false);
|
||||
|
||||
const propertyIds = viewFilterHelper?.filterIdsWithKey(filterKey) || [];
|
||||
|
||||
const filterPropertyIds = propertyIds.length > 5 ? (viewAll ? propertyIds : propertyIds.slice(0, 5)) : propertyIds;
|
||||
|
||||
const handlePropertySelection = (_propertyId: string) => {
|
||||
if (["start_date", "target_date"].includes(filterKey)) {
|
||||
if (_propertyId === "custom") {
|
||||
const _propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
|
||||
const selectedDates = _propertyIds.filter((id) => id.includes("-"));
|
||||
if (selectedDates.length > 0)
|
||||
selectedDates.forEach((date: string) => viewDetailStore?.setFilters(filterKey, date));
|
||||
else setDateCustomFilterToggle(filterKey);
|
||||
} else viewDetailStore?.setFilters(filterKey, _propertyId);
|
||||
} else viewDetailStore?.setFilters(filterKey, _propertyId);
|
||||
};
|
||||
|
||||
const handleCustomDateSelection = (selectedDates: string[]) => {
|
||||
selectedDates.forEach((date: string) => {
|
||||
viewDetailStore?.setFilters(filterKey, date);
|
||||
setDateCustomFilterToggle(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
if (propertyIds.length <= 0)
|
||||
return <div className="text-xs italic py-1 text-custom-text-300">No items are available.</div>;
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
{filterPropertyIds.map((propertyId) => (
|
||||
<button
|
||||
key={`filterKey_${propertyId}`}
|
||||
className="relative w-full flex items-center overflow-hidden gap-2.5 cursor-pointer p-1 py-1.5 rounded hover:bg-custom-background-80 transition-all group"
|
||||
onClick={() => handlePropertySelection(propertyId)}
|
||||
>
|
||||
<ViewFilterSelection
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
filterKey={filterKey}
|
||||
propertyId={propertyId}
|
||||
/>
|
||||
<ViewFiltersItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
filterKey={filterKey}
|
||||
propertyId={propertyId}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{propertyIds.length > 5 && (
|
||||
<div
|
||||
className="text-xs transition-all text-custom-primary-100/90 hover:text-custom-primary-100 font-medium pl-8 cursor-pointer py-1"
|
||||
onClick={() => setViewAll((prevData) => !prevData)}
|
||||
>
|
||||
{viewAll ? "View less" : "View all"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dateCustomFilterToggle === filterKey && (
|
||||
<DateFilterModal
|
||||
handleClose={() => setDateCustomFilterToggle(undefined)}
|
||||
isOpen={dateCustomFilterToggle === filterKey ? true : false}
|
||||
onSelect={handleCustomDateSelection}
|
||||
title="Start date"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { ImagePlus } from "lucide-react";
|
||||
// hooks
|
||||
import { useViewFilter } from "hooks/store";
|
||||
// types
|
||||
import { TViewFilters } from "@plane/types";
|
||||
|
||||
type TViewFiltersItem = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
|
||||
filterKey: keyof TViewFilters;
|
||||
propertyId: string;
|
||||
};
|
||||
|
||||
export const ViewFiltersItem: FC<TViewFiltersItem> = (props) => {
|
||||
const { workspaceSlug, projectId, filterKey, propertyId } = props;
|
||||
// hooks
|
||||
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
|
||||
|
||||
const propertyDetail = viewFilterHelper?.propertyDetails(filterKey, propertyId) || undefined;
|
||||
|
||||
if (!propertyDetail) return <></>;
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="flex-shrink-0 w-4 h-4 flex justify-center items-center">
|
||||
{propertyDetail?.icon || <ImagePlus size={14} />}
|
||||
</div>
|
||||
<div className="text-xs block truncate line-clamp-1 text-custom-text-200 group-hover:text-custom-text-100">
|
||||
{propertyDetail?.label || propertyId}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { FC } from "react";
|
||||
import { Check } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useViewDetail } from "hooks/store";
|
||||
// types
|
||||
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewFilterSelection = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
filterKey: keyof TViewFilters;
|
||||
propertyId: string;
|
||||
};
|
||||
|
||||
export const ViewFilterSelection: FC<TViewFilterSelection> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, filterKey, propertyId } = props;
|
||||
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
|
||||
const propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
|
||||
|
||||
const isSelected = ["start_date", "target_date"].includes(filterKey)
|
||||
? propertyId === "custom"
|
||||
? propertyIds.filter((id) => id.includes("-")).length > 0
|
||||
? true
|
||||
: false
|
||||
: propertyIds?.includes(propertyId)
|
||||
: propertyIds?.includes(propertyId) || false;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex-shrink-0 w-3 h-3 flex justify-center items-center border rounded text-bold ${
|
||||
isSelected
|
||||
? "border-custom-primary-100 bg-custom-primary-100"
|
||||
: "border-custom-border-400 bg-custom-background-100"
|
||||
}`}
|
||||
>
|
||||
{isSelected && <Check size={14} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import concat from "lodash/concat";
|
||||
import uniq from "lodash/uniq";
|
||||
import filter from "lodash/filter";
|
||||
// hooks
|
||||
import { useViewDetail } from "hooks/store";
|
||||
// components
|
||||
import { ViewFiltersItemRoot } from "../";
|
||||
// types
|
||||
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||
import { VIEW_DEFAULT_FILTER_PARAMETERS } from "constants/view";
|
||||
|
||||
type TViewFiltersRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
dateCustomFilterToggle: string | undefined;
|
||||
setDateCustomFilterToggle: (value: string | undefined) => void;
|
||||
};
|
||||
|
||||
export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, dateCustomFilterToggle, setDateCustomFilterToggle } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
// state
|
||||
const [filterVisibility, setFilterVisibility] = useState<Partial<keyof TViewFilters>[]>([]);
|
||||
const handleFilterVisibility = (key: keyof TViewFilters) => {
|
||||
setFilterVisibility((prevData = []) => {
|
||||
if (prevData.includes(key)) return filter(prevData, (item) => item !== key);
|
||||
return uniq(concat(prevData, [key]));
|
||||
});
|
||||
};
|
||||
|
||||
const layout = viewDetailStore?.appliedFilters?.display_filters?.layout || "spreadsheet";
|
||||
|
||||
const filtersProperties = VIEW_DEFAULT_FILTER_PARAMETERS?.["all"]?.["spreadsheet"]?.filters || [];
|
||||
|
||||
if (!layout || filtersProperties.length <= 0) return <></>;
|
||||
return (
|
||||
<div className="space-y-1 divide-y divide-custom-border-300">
|
||||
{filtersProperties.map((filterKey) => (
|
||||
<div key={filterKey} className="relative py-1 first:pt-0 last:pb-0">
|
||||
<div className="sticky top-0 z-20 flex justify-between items-center gap-2 bg-custom-background-100 select-none">
|
||||
<div className="font-medium text-xs text-custom-text-300 capitalize py-1">
|
||||
{filterKey.replace("_", " ")}
|
||||
</div>
|
||||
<div
|
||||
className="flex-shrink-0 relative overflow-hidden w-5 h-5 rounded flex justify-center items-center cursor-pointer hover:bg-custom-background-80"
|
||||
onClick={() => handleFilterVisibility(filterKey)}
|
||||
>
|
||||
{!filterVisibility.includes(filterKey) ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||
</div>
|
||||
</div>
|
||||
{!filterVisibility.includes(filterKey) && (
|
||||
<ViewFiltersItemRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
filterKey={filterKey}
|
||||
dateCustomFilterToggle={dateCustomFilterToggle}
|
||||
setDateCustomFilterToggle={setDateCustomFilterToggle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
export * from "./root";
|
||||
|
||||
// views
|
||||
export * from "./views/root";
|
||||
export * from "./views/view-item";
|
||||
export * from "./views/view-dropdown";
|
||||
export * from "./views/view-dropdown-item";
|
||||
export * from "./views/create-edit-form";
|
||||
export * from "./views/edit-dropdown";
|
||||
|
||||
// layouts
|
||||
export * from "./layout";
|
||||
|
||||
// view filters
|
||||
export * from "./filters/dropdown";
|
||||
export * from "./filters/root";
|
||||
export * from "./filters/filter-item-root";
|
||||
export * from "./filters/filter-item";
|
||||
export * from "./filters/filter-selection";
|
||||
export * from "./filters/edit-dropdown";
|
||||
|
||||
// view display filters
|
||||
export * from "./display-filters/dropdown";
|
||||
export * from "./display-filters/root";
|
||||
|
||||
// view display properties
|
||||
export * from "./display-properties/root";
|
||||
export * from "./display-properties/property-selection";
|
||||
|
||||
// view applied filters
|
||||
export * from "./applied-filters/root";
|
||||
|
||||
// confirmation modals
|
||||
export * from "./confirmation-modals/duplicate";
|
||||
export * from "./confirmation-modals/delete";
|
||||
@@ -0,0 +1,54 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { LucideIcon, List, Kanban, Calendar, Sheet, GanttChartSquare } from "lucide-react";
|
||||
// hooks
|
||||
import { useViewDetail } from "hooks/store";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { TViewLayouts, TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewLayoutRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
};
|
||||
|
||||
const LAYOUTS_DATA: { key: TViewLayouts; title: string; icon: LucideIcon }[] = [
|
||||
{ key: "list", title: "List Layout", icon: List },
|
||||
{ key: "kanban", title: "Kanban Layout", icon: Kanban },
|
||||
{ key: "calendar", title: "Calendar Layout", icon: Calendar },
|
||||
{ key: "spreadsheet", title: "Spreadsheet Layout", icon: Sheet },
|
||||
{ key: "gantt", title: "Gantt Chart layout", icon: GanttChartSquare },
|
||||
];
|
||||
|
||||
export const ViewLayoutRoot: FC<TViewLayoutRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
|
||||
if (!viewDetailStore) return <></>;
|
||||
return (
|
||||
<div className="relative flex gap-0.5 items-center bg-custom-background-80 rounded p-1 shadow-custom-shadow-2xs">
|
||||
{LAYOUTS_DATA.map((layout) => (
|
||||
<Fragment key={layout.key}>
|
||||
<Tooltip tooltipContent={layout.title} position="bottom">
|
||||
<div
|
||||
className={`relative h-6 w-7 flex justify-center items-center overflow-hidden rounded transition-all cursor-pointer
|
||||
${
|
||||
viewDetailStore?.filtersToUpdate?.display_filters?.layout === layout.key
|
||||
? `bg-custom-background-100 shadow-custom-shadow-2xs`
|
||||
: `hover:bg-custom-background-100`
|
||||
}
|
||||
`}
|
||||
onClick={() => viewDetailStore.setDisplayFilters({ layout: layout.key })}
|
||||
>
|
||||
<layout.icon size={12} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,300 @@
|
||||
import { FC, Fragment, useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { CheckCircle } from "lucide-react";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
// hooks
|
||||
import { useView, useViewDetail } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import {
|
||||
ViewRoot,
|
||||
ViewCreateEditForm,
|
||||
ViewEditDropdown,
|
||||
ViewLayoutRoot,
|
||||
ViewFiltersDropdown,
|
||||
ViewFiltersEditDropdown,
|
||||
ViewDisplayFiltersDropdown,
|
||||
ViewAppliedFiltersRoot,
|
||||
ViewDuplicateConfirmationModal,
|
||||
ViewDeleteConfirmationModal,
|
||||
} from ".";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// constants
|
||||
import { viewLocalPayload } from "constants/view";
|
||||
// types
|
||||
import { TViewOperations } from "./types";
|
||||
import { TView, TViewTypes } from "@plane/types";
|
||||
|
||||
type TGlobalViewRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
baseRoute: string;
|
||||
workspaceViewTabOptions: { key: TViewTypes; title: string; href: string }[];
|
||||
};
|
||||
|
||||
type TViewOperationsToggle = {
|
||||
type: "CREATE" | "EDIT" | "DUPLICATE" | "DELETE" | undefined;
|
||||
viewId: string | undefined;
|
||||
};
|
||||
|
||||
export const GlobalViewRoot: FC<TGlobalViewRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, baseRoute, workspaceViewTabOptions } = props;
|
||||
// hooks
|
||||
const viewStore = useView(workspaceSlug, projectId, viewType);
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
const { setToastAlert } = useToast();
|
||||
// states
|
||||
const [viewOperationsToggle, setViewOperationsToggle] = useState<TViewOperationsToggle>({
|
||||
type: undefined,
|
||||
viewId: undefined,
|
||||
});
|
||||
const handleViewOperationsToggle = (type: TViewOperationsToggle["type"], viewId: string | undefined) =>
|
||||
setViewOperationsToggle({ type, viewId });
|
||||
|
||||
const viewDetailCreateEditStore = useViewDetail(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
viewOperationsToggle?.viewId || viewId,
|
||||
viewType
|
||||
);
|
||||
|
||||
const viewOperations: TViewOperations = useMemo(
|
||||
() => ({
|
||||
localViewCreateEdit: (viewId: string | undefined, currentView = undefined) => {
|
||||
if (viewId === undefined) {
|
||||
if (currentView !== undefined) {
|
||||
// creating new view
|
||||
const currentViewPayload = cloneDeep({ ...currentView, id: uuidV4() });
|
||||
handleViewOperationsToggle("CREATE", currentViewPayload.id);
|
||||
viewStore?.localViewCreate(currentViewPayload as TView);
|
||||
} else {
|
||||
// if current view is available, create a new view with the same data
|
||||
const viewPayload = viewLocalPayload;
|
||||
handleViewOperationsToggle("CREATE", viewPayload.id);
|
||||
viewStore?.localViewCreate(viewPayload as TView);
|
||||
}
|
||||
} else {
|
||||
handleViewOperationsToggle("EDIT", viewId);
|
||||
viewDetailCreateEditStore?.setIsEditable(true);
|
||||
}
|
||||
},
|
||||
localViewCreateEditClear: async (viewId: string | undefined) => {
|
||||
if (viewDetailCreateEditStore?.is_create && viewId) viewStore?.remove(viewId);
|
||||
if (viewDetailCreateEditStore?.is_editable && viewId) viewDetailCreateEditStore.resetChanges();
|
||||
handleViewOperationsToggle(undefined, undefined);
|
||||
},
|
||||
|
||||
fetch: async () => {
|
||||
try {
|
||||
await viewStore?.fetch();
|
||||
} catch {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again later or contact the support team.",
|
||||
});
|
||||
}
|
||||
},
|
||||
create: async (data: Partial<TView>) => {
|
||||
try {
|
||||
await viewStore?.create(data);
|
||||
handleViewOperationsToggle(undefined, undefined);
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "View created successfully.",
|
||||
});
|
||||
} catch {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again later or contact the support team.",
|
||||
});
|
||||
}
|
||||
},
|
||||
remove: async (viewId: string) => {
|
||||
try {
|
||||
await viewStore?.remove(viewId);
|
||||
handleViewOperationsToggle(undefined, undefined);
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "View removed successfully.",
|
||||
});
|
||||
} catch {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again later or contact the support team.",
|
||||
});
|
||||
}
|
||||
},
|
||||
update: async () => {
|
||||
try {
|
||||
await viewDetailStore?.saveChanges();
|
||||
handleViewOperationsToggle(undefined, undefined);
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "View updated successfully.",
|
||||
});
|
||||
} catch {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again later or contact the support team.",
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
[viewStore, viewDetailStore, setToastAlert, viewDetailCreateEditStore]
|
||||
);
|
||||
|
||||
// fetch all views
|
||||
useEffect(() => {
|
||||
const fetchViews = async () => {
|
||||
await viewStore?.fetch(viewStore?.viewIds.length > 0 ? "mutation-loader" : "init-loader");
|
||||
};
|
||||
if (workspaceSlug && viewType && viewStore) fetchViews();
|
||||
}, [workspaceSlug, projectId, viewType, viewStore]);
|
||||
|
||||
// fetch view by id
|
||||
useEffect(() => {
|
||||
const fetchViews = async () => {
|
||||
viewId && (await viewStore?.fetchById(viewId));
|
||||
};
|
||||
if (workspaceSlug && viewId && viewType && viewStore) fetchViews();
|
||||
}, [workspaceSlug, projectId, viewId, viewType, viewStore]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<div className="relative flex items-center gap-2 px-5 py-4">
|
||||
<div className="relative flex items-center gap-2 overflow-hidden">
|
||||
<div className="flex-shrink-0 w-6 h-6 rounded relative flex justify-center items-center bg-custom-background-80">
|
||||
<CheckCircle size={12} />
|
||||
</div>
|
||||
<div className="font-medium inline-block whitespace-nowrap overflow-hidden truncate line-clamp-1">
|
||||
All Issues
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto relative flex items-center gap-3">
|
||||
<div className="relative flex items-center rounded border border-custom-border-200 bg-custom-background-80">
|
||||
{workspaceViewTabOptions.map((tab) => (
|
||||
<Link
|
||||
key={tab.key}
|
||||
href={tab.href}
|
||||
className={`p-4 py-1.5 rounded text-sm transition-all cursor-pointer font-medium
|
||||
${
|
||||
viewType === tab.key
|
||||
? "text-custom-text-100 bg-custom-background-100"
|
||||
: "text-custom-text-200 bg-custom-background-80 hover:text-custom-text-100"
|
||||
}`}
|
||||
>
|
||||
{tab.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewStore?.loader && viewStore?.loader === "init-loader" ? (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="border-b border-custom-border-200">
|
||||
<ViewRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
viewOperations={viewOperations}
|
||||
baseRoute={baseRoute}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-5 py-2 border-b border-custom-border-200 relative flex items-start gap-1">
|
||||
<div className="w-full overflow-hidden">
|
||||
<ViewAppliedFiltersRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
propertyVisibleCount={5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<ViewLayoutRoot workspaceSlug={workspaceSlug} projectId={projectId} viewId={viewId} viewType={viewType} />
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<ViewFiltersDropdown
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
displayDropdownText={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<ViewDisplayFiltersDropdown
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
displayDropdownText={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ViewEditDropdown
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewOperations={viewOperations}
|
||||
/>
|
||||
|
||||
<ViewFiltersEditDropdown
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
viewOperations={viewOperations}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* create edit modal */}
|
||||
{viewOperationsToggle.type && viewOperationsToggle.viewId && (
|
||||
<Fragment>
|
||||
{["CREATE", "EDIT"].includes(viewOperationsToggle.type) && (
|
||||
<ViewCreateEditForm
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewOperationsToggle.viewId}
|
||||
viewType={viewType}
|
||||
viewOperations={viewOperations}
|
||||
/>
|
||||
)}
|
||||
|
||||
{["DUPLICATE"].includes(viewOperationsToggle.type) && (
|
||||
<ViewDuplicateConfirmationModal viewId={viewOperationsToggle.viewId} viewOperations={viewOperations} />
|
||||
)}
|
||||
|
||||
{["DELETE"].includes(viewOperationsToggle.type) && (
|
||||
<ViewDeleteConfirmationModal viewId={viewOperationsToggle.viewId} viewOperations={viewOperations} />
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
// types
|
||||
import { TView, TUpdateView } from "@plane/types";
|
||||
|
||||
export type TViewOperations = {
|
||||
localViewCreateEdit: (viewId: string | undefined, currentView?: TUpdateView | undefined) => void;
|
||||
localViewCreateEditClear: (viewId: string | undefined) => Promise<void>;
|
||||
|
||||
fetch: () => Promise<void>;
|
||||
create: (data: Partial<TView>) => Promise<void>;
|
||||
update: () => Promise<void>;
|
||||
remove: (viewId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
// view and view filter edit dropdowns
|
||||
export type TViewEditDropdownOptions = {
|
||||
icon: LucideIcon;
|
||||
key: string;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
children: TViewEditDropdownOptions[] | undefined;
|
||||
};
|
||||
|
||||
export type TViewFilterEditDropdownOptions = {
|
||||
icon: LucideIcon | any;
|
||||
key: string;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
@@ -0,0 +1,178 @@
|
||||
import { FC, Fragment, useCallback, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Briefcase, Globe2, Plus, X } from "lucide-react";
|
||||
// hooks
|
||||
import { useViewDetail, useProject } from "hooks/store";
|
||||
// components
|
||||
import { ViewAppliedFiltersRoot, ViewFiltersDropdown } from "../";
|
||||
// ui
|
||||
import { Input, Button } from "@plane/ui";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
import { TViewOperations } from "../types";
|
||||
|
||||
type TViewCreateEditForm = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewOperations: TViewOperations;
|
||||
};
|
||||
|
||||
export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
const { getProjectById } = useProject();
|
||||
// states
|
||||
const [modalToggle, setModalToggle] = useState(false);
|
||||
const [loader, setLoader] = useState(false);
|
||||
|
||||
const modalOpen = useCallback(() => setModalToggle(true), [setModalToggle]);
|
||||
const modalClose = useCallback(() => {
|
||||
setModalToggle(false);
|
||||
setTimeout(() => {
|
||||
viewOperations.localViewCreateEditClear(viewId);
|
||||
}, 200);
|
||||
}, [viewId, setModalToggle, viewOperations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewId) modalOpen();
|
||||
}, [viewId, modalOpen, modalClose]);
|
||||
|
||||
const onContinue = async () => {
|
||||
setLoader(true);
|
||||
if (viewDetailStore?.is_create) {
|
||||
const payload = viewDetailStore?.filtersToUpdate;
|
||||
await viewOperations.create(payload);
|
||||
modalClose();
|
||||
} else {
|
||||
const payload = viewDetailStore?.filtersToUpdate;
|
||||
if (!payload) return;
|
||||
await viewOperations.update();
|
||||
modalClose();
|
||||
}
|
||||
setLoader(false);
|
||||
};
|
||||
|
||||
const projectDetails = projectId ? getProjectById(projectId) : undefined;
|
||||
|
||||
if (!viewDetailStore) return <></>;
|
||||
return (
|
||||
<Transition.Root show={modalToggle} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={modalClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem] py-5 border-[0.1px] border-custom-border-100">
|
||||
<div className="p-3 px-5 relative flex items-center gap-2">
|
||||
{projectId && projectDetails ? (
|
||||
<div className="relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
|
||||
<div className="flex-shrink-0 relative flex justify-center items-center w-5 h-5 overflow-hidden">
|
||||
<Briefcase className="w-3.5 h-3.5" />
|
||||
</div>
|
||||
<div className="text-xs uppercase font-medium">{projectDetails?.identifier || "Project"}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
|
||||
<div className="flex-shrink-0 relative flex justify-center items-center w-5 h-5 overflow-hidden">
|
||||
<Globe2 className="w-3.5 h-3.5" />
|
||||
</div>
|
||||
<div className="text-xs uppercase font-medium">Workspace</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="font-medium text-lg">Save View</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 px-5">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={viewDetailStore?.filtersToUpdate?.name || ""}
|
||||
onChange={(e) => {
|
||||
viewDetailStore?.setName(e.target.value);
|
||||
}}
|
||||
placeholder="What do you want to call this view?"
|
||||
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-3 px-5 relative flex justify-between items-center gap-2">
|
||||
<ViewFiltersDropdown
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
dropdownPlacement="right"
|
||||
>
|
||||
<div className="cursor-pointer relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
|
||||
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden">
|
||||
<Plus className="w-3 h-3" />
|
||||
</div>
|
||||
<div className="text-xs">Filters</div>
|
||||
</div>
|
||||
</ViewFiltersDropdown>
|
||||
{viewDetailStore?.isFiltersApplied && (
|
||||
<div
|
||||
className="cursor-pointer relative rounded p-1.5 px-2 flex items-center gap-1 border border-dashed border-custom-border-100 bg-custom-background-80"
|
||||
onClick={() => {
|
||||
viewDetailStore.setFilters(undefined, "clear_all");
|
||||
}}
|
||||
>
|
||||
<div className="text-xs">Clear all filters</div>
|
||||
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden">
|
||||
<X className="w-3 h-3" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-3 px-5 relative bg-custom-background-90 max-h-36 overflow-hidden overflow-y-auto">
|
||||
<ViewAppliedFiltersRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
propertyVisibleCount={undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-3 px-5 relative flex justify-end items-center gap-2">
|
||||
<Button variant="neutral-primary" onClick={modalClose} disabled={loader}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={onContinue} disabled={loader}>
|
||||
{loader ? `Saving...` : `${viewDetailStore?.is_create ? `Create` : `Update`} View`}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import { FC, Fragment, useMemo, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Copy, Eye, Globe2, Link2, Pencil, Trash } from "lucide-react";
|
||||
// types
|
||||
import { TViewEditDropdownOptions, TViewOperations } from "../types";
|
||||
|
||||
type TViewEditDropdown = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewOperations: TViewOperations;
|
||||
};
|
||||
|
||||
export const ViewEditDropdown: FC<TViewEditDropdown> = observer((props) => {
|
||||
const { viewId, viewOperations } = props;
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
// popper-js init
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-end",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 10],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// dropdown options
|
||||
const dropdownOptions: TViewEditDropdownOptions[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: Pencil,
|
||||
key: "rename",
|
||||
label: "Rename",
|
||||
onClick: () => viewOperations.localViewCreateEdit(viewId),
|
||||
children: undefined,
|
||||
},
|
||||
// {
|
||||
// icon: Eye,
|
||||
// key: "accessability",
|
||||
// label: "Change Accessability",
|
||||
// onClick: () => {},
|
||||
// children: [
|
||||
// {
|
||||
// icon: Eye,
|
||||
// key: "private",
|
||||
// label: "Private",
|
||||
// onClick: () => viewOperations.create({}),
|
||||
// children: undefined,
|
||||
// },
|
||||
// {
|
||||
// icon: Globe2,
|
||||
// key: "public",
|
||||
// label: "Public",
|
||||
// onClick: () => viewOperations.create({}),
|
||||
// children: undefined,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// icon: Copy,
|
||||
// key: "duplicate",
|
||||
// label: "Duplicate view",
|
||||
// onClick: () => viewOperations.remove(viewId),
|
||||
// children: undefined,
|
||||
// },
|
||||
// {
|
||||
// icon: Link2,
|
||||
// key: "copy_link",
|
||||
// label: "Copy view link",
|
||||
// onClick: () => viewOperations.remove(viewId),
|
||||
// children: undefined,
|
||||
// },
|
||||
// {
|
||||
// icon: Trash,
|
||||
// key: "delete",
|
||||
// label: "Delete view",
|
||||
// onClick: () => viewOperations.remove(viewId),
|
||||
// children: undefined,
|
||||
// },
|
||||
],
|
||||
[viewOperations, viewId]
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative flex-shrink-0" ref={dropdownRef}>
|
||||
<Menu.Button
|
||||
className="relative flex items-center gap-1 rounded px-2 h-8 transition-all hover:bg-custom-background-80 cursor-pointer outline-none"
|
||||
ref={setReferenceElement}
|
||||
>
|
||||
<div className="w-4 h-4 relative flex justify-center items-center overflow-hidden">
|
||||
<Pencil size={12} />
|
||||
</div>
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute right-0 z-20 mt-1.5 flex w-52 flex-col rounded border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 text-xs shadow-lg outline-none p-1"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{dropdownOptions &&
|
||||
dropdownOptions.length > 0 &&
|
||||
dropdownOptions.map((option) => (
|
||||
<Menu.Item
|
||||
key={option.key}
|
||||
as="button"
|
||||
type="button"
|
||||
className="relative w-full flex items-center gap-2 p-1 py-1.5 rounded transition-all hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100 cursor-pointer"
|
||||
onClick={option.onClick}
|
||||
>
|
||||
<div className="flex-shrink-0 w-4 h-4 relative flex justify-center items-center">
|
||||
<option.icon size={12} />
|
||||
</div>
|
||||
<div className="text-xs whitespace-nowrap">{option.label}</div>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { FC, Fragment, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
// hooks
|
||||
import { useView } from "hooks/store";
|
||||
// components
|
||||
import { ViewItem, ViewDropdown } from "../";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import { TViewOperations } from "../types";
|
||||
import { TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewOperations: TViewOperations;
|
||||
baseRoute: string;
|
||||
};
|
||||
|
||||
export const ViewRoot: FC<TViewRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute } = props;
|
||||
// hooks
|
||||
const viewStore = useView(workspaceSlug, projectId, viewType);
|
||||
// state
|
||||
const [itemsToRenderViewsCount, setItemsToRenderViewCount] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleViewTabsVisibility = () => {
|
||||
const tabContainer = document.getElementById("tab-container");
|
||||
const tabItemViewMore = document.getElementById("tab-item-view-more");
|
||||
const itemWidth = 116;
|
||||
if (!tabContainer || !tabItemViewMore) return;
|
||||
|
||||
const containerWidth = tabContainer.clientWidth;
|
||||
const itemViewMoreLeftOffset = tabItemViewMore.offsetLeft + (tabItemViewMore.clientWidth + 10);
|
||||
const itemViewMoreRightOffset = containerWidth - itemViewMoreLeftOffset;
|
||||
|
||||
const itemsToRenderLeft = Math.floor(itemViewMoreLeftOffset / itemWidth) || 0;
|
||||
const itemsToRenderRight = Math.floor(itemViewMoreRightOffset / itemWidth) || 0;
|
||||
setItemsToRenderViewCount(itemsToRenderLeft + itemsToRenderRight);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", () => handleViewTabsVisibility());
|
||||
handleViewTabsVisibility();
|
||||
|
||||
return () => window.removeEventListener("resize", () => handleViewTabsVisibility());
|
||||
}, [viewStore?.viewIds]);
|
||||
|
||||
const viewIds = viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length) || [];
|
||||
|
||||
if (!viewIds.includes(viewId)) {
|
||||
viewIds.pop();
|
||||
viewIds.push(viewId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex justify-between px-5 gap-2">
|
||||
<div className="w-full">
|
||||
{viewStore?.viewIds && viewStore?.viewIds.length > 0 && (
|
||||
<div id="tab-container" className="relative flex items-center w-full overflow-hidden">
|
||||
{viewIds.map((_viewId) => (
|
||||
<Fragment key={_viewId}>
|
||||
<ViewItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
viewItemId={_viewId}
|
||||
baseRoute={baseRoute}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
<div id="tab-item-view-more" className="min-w-[90px]">
|
||||
{viewStore?.viewIds.length <= (itemsToRenderViewsCount || viewStore?.viewIds.length) ? null : (
|
||||
<ViewDropdown
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
viewOperations={viewOperations}
|
||||
baseRoute={baseRoute}
|
||||
>
|
||||
<div className="text-sm font-semibold mb-1 p-2 px-2.5 text-custom-text-200 cursor-pointer hover:bg-custom-background-80 whitespace-nowrap rounded relative flex items-center gap-1">
|
||||
<span>
|
||||
<Plus size={12} />
|
||||
</span>
|
||||
<span>
|
||||
{viewStore?.viewIds.length - (itemsToRenderViewsCount || viewStore?.viewIds.length)} More...
|
||||
</span>
|
||||
</div>
|
||||
</ViewDropdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 my-auto pb-1">
|
||||
<Button size="sm" prependIcon={<Plus />} onClick={() => viewOperations?.localViewCreateEdit(undefined)}>
|
||||
New View
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { GripVertical, MoreVertical } from "lucide-react";
|
||||
// hooks
|
||||
import { useViewDetail } from "hooks/store";
|
||||
// ui
|
||||
import { PhotoFilterIcon, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewDropdownItem = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
currentViewId: string;
|
||||
searchQuery: string;
|
||||
baseRoute: string;
|
||||
};
|
||||
|
||||
export const ViewDropdownItem: FC<TViewDropdownItem> = (props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, currentViewId, searchQuery, baseRoute } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||
|
||||
const isDragEnabled = false;
|
||||
const isEditable = !viewDetailStore?.is_local_view || false;
|
||||
|
||||
if (!viewDetailStore) return <></>;
|
||||
if (!searchQuery || (searchQuery && viewDetailStore?.name?.toLowerCase().includes(searchQuery.toLowerCase())))
|
||||
return (
|
||||
<Combobox.Option
|
||||
value={undefined}
|
||||
className={`w-full px-1 pl-2 py-1.5 truncate flex items-center justify-between gap-1 rounded cursor-pointer select-none group
|
||||
${currentViewId === viewDetailStore?.id ? `bg-custom-primary-100/10` : `hover:bg-custom-background-80`}
|
||||
`}
|
||||
>
|
||||
<Tooltip tooltipContent={viewDetailStore?.name} position="left">
|
||||
<div className="relative w-full flex items-center gap-1 overflow-hidden">
|
||||
{isDragEnabled && (
|
||||
<div className="flex-shrink-0 w-5 h-5 relative rounded flex justify-center items-center hover:bg-custom-background-100">
|
||||
<GripVertical className="w-3.5 h-3.5 text-custom-text-200 group-hover:text-custom-text-100" />
|
||||
</div>
|
||||
)}
|
||||
<Link
|
||||
href={`${baseRoute}/${viewDetailStore?.id}`}
|
||||
className={`w-full h-full overflow-hidden relative flex items-center gap-1
|
||||
${
|
||||
currentViewId === viewDetailStore?.id
|
||||
? `text-custom-text-100`
|
||||
: `text-custom-text-200 group-hover:text-custom-text-100`
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex-shrink-0 w-5 h-5 relative flex justify-center items-center">
|
||||
<PhotoFilterIcon className="w-3 h-3 " />
|
||||
</div>
|
||||
|
||||
<div className="w-full line-clamp-1 truncate overflow-hidden inline-block whitespace-nowrap text-sm font-medium">
|
||||
{viewDetailStore?.name}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{isEditable && (
|
||||
<div className="flex-shrink-0 w-5 h-5 relative rounded flex justify-center items-center hover:bg-custom-background-100">
|
||||
<MoreVertical className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
);
|
||||
return <></>;
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { FC, Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { Plus, Search } from "lucide-react";
|
||||
// hooks
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
import { useView } from "hooks/store";
|
||||
// components
|
||||
import { ViewDropdownItem } from "..";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
import { TViewOperations } from "../types";
|
||||
|
||||
type TViewDropdown = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewOperations: TViewOperations;
|
||||
children?: ReactNode;
|
||||
baseRoute: string;
|
||||
dropdownPlacement?: Placement;
|
||||
};
|
||||
|
||||
export const ViewDropdown: FC<TViewDropdown> = (props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
viewId: currentViewId,
|
||||
viewType,
|
||||
viewOperations,
|
||||
children,
|
||||
baseRoute,
|
||||
dropdownPlacement = "bottom-start",
|
||||
} = props;
|
||||
// hooks
|
||||
const viewStore = useView(workspaceSlug, projectId, viewType);
|
||||
// states
|
||||
const [dropdownToggle, setDropdownToggle] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
// refs
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
// popper-js refs
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
// popper-js init
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: dropdownPlacement,
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 10],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const handleDropdownOpen = () => setDropdownToggle(true);
|
||||
const handleDropdownClose = () => setDropdownToggle(false);
|
||||
const handleDropdownToggle = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!dropdownToggle) handleDropdownOpen();
|
||||
else handleDropdownClose();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, handleDropdownClose);
|
||||
|
||||
return (
|
||||
<Combobox as="div" ref={dropdownRef}>
|
||||
<Combobox.Button as={Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={"block h-full w-full outline-none"}
|
||||
onClick={handleDropdownToggle}
|
||||
>
|
||||
{children ? (
|
||||
<span className="relative inline-block">{children}</span>
|
||||
) : (
|
||||
<span className="whitespace-nowrap">More...</span>
|
||||
)}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
|
||||
{dropdownToggle && (
|
||||
<Combobox.Options className="fixed z-10" static>
|
||||
<div
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
className="w-64 p-2 space-y-2 rounded bg-custom-background-100 border-[0.5px] border-custom-border-300 shadow-custom-shadow-rg focus:outline-none"
|
||||
>
|
||||
<div className="relative p-0.5 px-2 text-sm flex items-center gap-2 rounded border border-custom-border-100 bg-custom-background-90">
|
||||
<Search className="h-3 w-3 text-custom-text-300" strokeWidth={1.5} />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search for a view..."
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-h-60 space-y-0.5 overflow-y-scroll">
|
||||
{viewStore?.viewIds &&
|
||||
viewStore?.viewIds.length > 0 &&
|
||||
viewStore?.viewIds.map((viewId) => (
|
||||
<Fragment key={viewId}>
|
||||
<ViewDropdownItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
viewId={viewId}
|
||||
viewType={viewType}
|
||||
currentViewId={currentViewId}
|
||||
searchQuery={query}
|
||||
baseRoute={baseRoute}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative flex justify-center items-center gap-1 rounded p-1 py-1.5 transition-all border border-custom-border-200 bg-custom-background-90 hover:bg-custom-background-80 text-custom-text-300 hover:text-custom-text-200 cursor-pointer"
|
||||
onClick={() => viewOperations?.localViewCreateEdit(undefined)}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
<div className="text-sm">New view</div>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useViewDetail } from "hooks/store";
|
||||
// ui
|
||||
import { PhotoFilterIcon, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
|
||||
type TViewItem = {
|
||||
workspaceSlug: string;
|
||||
projectId: string | undefined;
|
||||
viewId: string;
|
||||
viewType: TViewTypes;
|
||||
viewItemId: string;
|
||||
baseRoute: string;
|
||||
};
|
||||
|
||||
export const ViewItem: FC<TViewItem> = observer((props) => {
|
||||
const { workspaceSlug, projectId, viewId, viewType, viewItemId, baseRoute } = props;
|
||||
// hooks
|
||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewItemId, viewType);
|
||||
|
||||
if (!viewDetailStore) return <></>;
|
||||
return (
|
||||
<div className="space-y-0.5 relative h-full flex flex-col justify-between">
|
||||
<Tooltip tooltipContent={viewDetailStore?.name} position="top">
|
||||
<Link
|
||||
href={`${baseRoute}/${viewItemId}`}
|
||||
className={`cursor-pointer relative p-2 px-2.5 flex justify-center items-center gap-1.5 rounded transition-all hover:bg-custom-background-80
|
||||
${viewItemId === viewId ? `text-custom-primary-100 bg-custom-primary-100/10` : `border-transparent`}
|
||||
`}
|
||||
onClick={(e) => viewItemId === viewId && e.preventDefault()}
|
||||
>
|
||||
<div className={`flex-shrink-0 rounded-sm relative w-3 h-3 flex justify-center items-center overflow-hidden`}>
|
||||
<PhotoFilterIcon className="w-3 h-3" />
|
||||
</div>
|
||||
<div className="w-full max-w-[80px] inline-block text-sm line-clamp-1 truncate overflow-hidden font-medium">
|
||||
{viewDetailStore?.name}
|
||||
</div>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<div
|
||||
className={`border-b-2 rounded-t-sm ${
|
||||
viewItemId === viewId ? `border-custom-primary-100` : `border-transparent`
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -270,7 +270,7 @@ export const SIDEBAR_MENU_ITEMS: {
|
||||
{
|
||||
key: "all-issues",
|
||||
label: "All Issues",
|
||||
href: `/workspace-views/all-issues`,
|
||||
href: `/views/public/all-issues`,
|
||||
access: EUserWorkspaceRoles.GUEST,
|
||||
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views`),
|
||||
Icon: CheckCircle,
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
// types
|
||||
import { TStateGroups, TIssuePriorities, TViewFilters, TViewDisplayFilters, TViewLayouts } from "@plane/types";
|
||||
|
||||
// filters constants
|
||||
export const STATE_GROUP_PROPERTY: {
|
||||
[key in TStateGroups]: {
|
||||
label: string;
|
||||
color: string;
|
||||
};
|
||||
} = {
|
||||
backlog: {
|
||||
label: "Backlog",
|
||||
color: "#d9d9d9",
|
||||
},
|
||||
unstarted: {
|
||||
label: "Unstarted",
|
||||
color: "#3f76ff",
|
||||
},
|
||||
started: {
|
||||
label: "Started",
|
||||
color: "#f59e0b",
|
||||
},
|
||||
completed: {
|
||||
label: "Completed",
|
||||
color: "#16a34a",
|
||||
},
|
||||
cancelled: {
|
||||
label: "Canceled",
|
||||
color: "#dc2626",
|
||||
},
|
||||
};
|
||||
|
||||
export const PRIORITIES_PROPERTY: {
|
||||
[key in TIssuePriorities]: {
|
||||
label: string;
|
||||
};
|
||||
} = {
|
||||
urgent: { label: "Urgent" },
|
||||
high: { label: "High" },
|
||||
medium: { label: "Medium" },
|
||||
low: { label: "Low" },
|
||||
none: { label: "None" },
|
||||
};
|
||||
|
||||
export const DATE_PROPERTY: {
|
||||
[key in string]: {
|
||||
label: string;
|
||||
};
|
||||
} = {
|
||||
"1_weeks;after;fromnow": { label: "1 week from now" },
|
||||
"2_weeks;after;fromnow": { label: "2 weeks from now" },
|
||||
"1_months;after;fromnow": { label: "1 month from now" },
|
||||
"2_months;after;fromnow": { label: "2 months from now" },
|
||||
custom: { label: "Custom" },
|
||||
};
|
||||
|
||||
// display filter constants
|
||||
|
||||
// layout, filter, display filter and display properties permissions for views
|
||||
type TViewLayoutFilterProperties = {
|
||||
filters: Partial<keyof TViewFilters>[];
|
||||
readonlyFilters?: Partial<keyof TViewFilters>[];
|
||||
display_filters: Partial<keyof TViewDisplayFilters>[];
|
||||
extra_options: ("sub_issue" | "show_empty_groups")[];
|
||||
display_properties: boolean;
|
||||
};
|
||||
|
||||
type TViewLayoutFilters = {
|
||||
list: TViewLayoutFilterProperties;
|
||||
kanban: TViewLayoutFilterProperties;
|
||||
calendar: TViewLayoutFilterProperties;
|
||||
spreadsheet: TViewLayoutFilterProperties;
|
||||
gantt: TViewLayoutFilterProperties;
|
||||
};
|
||||
|
||||
type TFilterPermissions = {
|
||||
all: Omit<TViewLayoutFilters, "list" | "kanban" | "calendar" | "gantt"> & {
|
||||
layouts: Omit<TViewLayouts, "list" | "kanban" | "calendar" | "gantt">[];
|
||||
};
|
||||
profile: Omit<TViewLayoutFilters, "spreadsheet" | "calendar" | "gantt"> & {
|
||||
layouts: Omit<TViewLayouts, "spreadsheet" | "calendar" | "gantt">[];
|
||||
};
|
||||
project: TViewLayoutFilters & {
|
||||
layouts: TViewLayouts[];
|
||||
};
|
||||
archived: Omit<TViewLayoutFilters, "kanban" | "spreadsheet" | "calendar" | "gantt"> & {
|
||||
layouts: Omit<TViewLayouts, "kanban" | "spreadsheet" | "calendar" | "gantt">[];
|
||||
};
|
||||
draft: Omit<TViewLayoutFilters, "spreadsheet" | "calendar" | "gantt"> & {
|
||||
layouts: Omit<TViewLayouts, "kanban" | "spreadsheet" | "calendar" | "gantt">[];
|
||||
};
|
||||
};
|
||||
|
||||
const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = {
|
||||
layouts: ["spreadsheet"],
|
||||
spreadsheet: {
|
||||
// filters: ["project", "priority", "state_group", "assignees", "created_by", "labels", "start_date", "target_date"],
|
||||
filters: [
|
||||
"project",
|
||||
"module",
|
||||
"cycle",
|
||||
"priority",
|
||||
"state",
|
||||
"state_group",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"subscriber",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
// display_filters: ["type"],
|
||||
display_filters: ["group_by", "sub_group_by", "order_by", "type"],
|
||||
extra_options: [],
|
||||
display_properties: true,
|
||||
},
|
||||
};
|
||||
|
||||
const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = {
|
||||
layouts: ["list", "kanban"],
|
||||
list: {
|
||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
||||
display_filters: ["group_by", "order_by", "type"],
|
||||
extra_options: [],
|
||||
display_properties: true,
|
||||
},
|
||||
kanban: {
|
||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
||||
display_filters: ["group_by", "order_by", "type"],
|
||||
extra_options: [],
|
||||
display_properties: true,
|
||||
},
|
||||
};
|
||||
|
||||
const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = {
|
||||
layouts: ["list", "kanban", "spreadsheet", "calendar", "gantt"],
|
||||
list: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"module",
|
||||
"cycle",
|
||||
],
|
||||
display_filters: ["group_by", "order_by", "type"],
|
||||
extra_options: ["sub_issue", "show_empty_groups"],
|
||||
display_properties: true,
|
||||
},
|
||||
kanban: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"module",
|
||||
"cycle",
|
||||
],
|
||||
display_filters: ["group_by", "sub_group_by", "order_by", "type"],
|
||||
extra_options: ["sub_issue", "show_empty_groups"],
|
||||
display_properties: true,
|
||||
},
|
||||
calendar: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"module",
|
||||
"cycle",
|
||||
],
|
||||
display_filters: ["type"],
|
||||
extra_options: ["sub_issue"],
|
||||
display_properties: true,
|
||||
},
|
||||
spreadsheet: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"module",
|
||||
"cycle",
|
||||
],
|
||||
display_filters: ["order_by", "type"],
|
||||
extra_options: [],
|
||||
display_properties: true,
|
||||
},
|
||||
|
||||
gantt: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"module",
|
||||
"cycle",
|
||||
],
|
||||
display_filters: ["order_by", "type"],
|
||||
extra_options: ["sub_issue"],
|
||||
display_properties: false,
|
||||
},
|
||||
};
|
||||
|
||||
const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = {
|
||||
layouts: ["list"],
|
||||
list: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"module",
|
||||
"cycle",
|
||||
],
|
||||
display_filters: ["group_by", "order_by"],
|
||||
extra_options: [],
|
||||
display_properties: true,
|
||||
},
|
||||
};
|
||||
|
||||
const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = {
|
||||
layouts: ["list", "kanban"],
|
||||
list: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"module",
|
||||
"cycle",
|
||||
],
|
||||
display_filters: ["group_by", "order_by", "type"],
|
||||
extra_options: ["sub_issue", "show_empty_groups"],
|
||||
display_properties: true,
|
||||
},
|
||||
kanban: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state",
|
||||
"assignees",
|
||||
"mentions",
|
||||
"created_by",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"module",
|
||||
"cycle",
|
||||
],
|
||||
display_filters: ["group_by", "sub_group_by", "order_by", "type"],
|
||||
extra_options: ["sub_issue", "show_empty_groups"],
|
||||
display_properties: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const VIEW_DEFAULT_FILTER_PARAMETERS: TFilterPermissions = {
|
||||
all: ALL_FILTER_PERMISSIONS,
|
||||
profile: PROFILE_FILTER_PERMISSIONS,
|
||||
project: PROJECT_FILTER_PERMISSIONS,
|
||||
archived: ARCHIVED_FILTER_PERMISSIONS,
|
||||
draft: DRAFT_FILTER_PERMISSIONS,
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./root";
|
||||
export * from "./filters";
|
||||
@@ -0,0 +1,21 @@
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
// types
|
||||
import { TViewTypes, TView } from "@plane/types";
|
||||
|
||||
export const VIEW_TYPES: Record<TViewTypes, TViewTypes> = {
|
||||
WORKSPACE_PRIVATE_VIEWS: "WORKSPACE_PRIVATE_VIEWS",
|
||||
WORKSPACE_PUBLIC_VIEWS: "WORKSPACE_PUBLIC_VIEWS",
|
||||
PROJECT_PRIVATE_VIEWS: "PROJECT_PRIVATE_VIEWS",
|
||||
PROJECT_PUBLIC_VIEWS: "PROJECT_PUBLIC_VIEWS",
|
||||
};
|
||||
|
||||
export const viewLocalPayload: Partial<TView> = {
|
||||
id: uuidV4(),
|
||||
name: "",
|
||||
description: "",
|
||||
filters: undefined,
|
||||
display_filters: undefined,
|
||||
display_properties: undefined,
|
||||
is_local_view: false,
|
||||
is_create: true,
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from "./use-application";
|
||||
export * from "./use-event-tracker"
|
||||
export * from "./use-event-tracker";
|
||||
export * from "./use-calendar-view";
|
||||
export * from "./use-cycle";
|
||||
export * from "./use-dashboard";
|
||||
@@ -22,3 +22,8 @@ export * from "./use-kanban-view";
|
||||
export * from "./use-issue-detail";
|
||||
export * from "./use-inbox";
|
||||
export * from "./use-inbox-issues";
|
||||
|
||||
// new store
|
||||
export * from "./views/use-view";
|
||||
export * from "./views/use-view-detail";
|
||||
export * from "./views/use-view-filters";
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "contexts/store-context";
|
||||
// store
|
||||
import { TViewStore } from "store/view/view.store";
|
||||
// types
|
||||
import { TViewTypes } from "@plane/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
|
||||
export const useViewDetail = (
|
||||
workspaceSlug: string,
|
||||
projectId: string | undefined,
|
||||
viewId: string,
|
||||
viewType: TViewTypes | undefined
|
||||
): TViewStore | undefined => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useViewDetail must be used within StoreProvider");
|
||||
|
||||
if (!workspaceSlug || !viewId) return undefined;
|
||||
|
||||
switch (viewType) {
|
||||
case VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS:
|
||||
return context.view.workspacePrivateViewStore.viewById(viewId);
|
||||
case VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS:
|
||||
return context.view.workspacePublicViewStore.viewById(viewId);
|
||||
case VIEW_TYPES.PROJECT_PRIVATE_VIEWS:
|
||||
if (!projectId) return undefined;
|
||||
return context.view.projectPrivateViewStore.viewById(viewId);
|
||||
case VIEW_TYPES.PROJECT_PUBLIC_VIEWS:
|
||||
if (!projectId) return undefined;
|
||||
return context.view.projectPublicViewStore.viewById(viewId);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,334 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Briefcase, CalendarDays, CircleUser, Tag } from "lucide-react";
|
||||
// hooks
|
||||
import { useProject, useModule, useCycle, useProjectState, useMember, useLabel } from "hooks/store";
|
||||
// ui
|
||||
import {
|
||||
Avatar,
|
||||
ContrastIcon,
|
||||
CycleGroupIcon,
|
||||
DiceIcon,
|
||||
DoubleCircleIcon,
|
||||
PriorityIcon,
|
||||
StateGroupIcon,
|
||||
} from "@plane/ui";
|
||||
// types
|
||||
import { TIssuePriorities, TStateGroups, TViewFilters } from "@plane/types";
|
||||
// constants
|
||||
import { STATE_GROUP_PROPERTY, PRIORITIES_PROPERTY, DATE_PROPERTY } from "constants/view/filters";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
|
||||
type TFilterPropertyDetails = {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type TFilterPropertyDefaultDetails = {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const useViewFilter = (workspaceSlug: string, projectId: string | undefined) => {
|
||||
const { projectMap, getProjectById } = useProject();
|
||||
const { getProjectModuleIds, getModuleById } = useModule();
|
||||
const { getProjectCycleIds, getCycleById } = useCycle();
|
||||
const { getProjectStates, getStateById } = useProjectState();
|
||||
const {
|
||||
getUserDetails,
|
||||
workspace: { workspaceMemberIds },
|
||||
project: { getProjectMemberIds },
|
||||
} = useMember();
|
||||
const { workspaceLabels, getProjectLabels, getLabelById } = useLabel();
|
||||
|
||||
if (!workspaceSlug) return undefined;
|
||||
|
||||
const filterIdsWithKey = (filterKey: keyof TViewFilters): string[] | undefined => {
|
||||
if (!filterKey) return undefined;
|
||||
|
||||
switch (filterKey) {
|
||||
case "project":
|
||||
return Object.keys(projectMap) || undefined;
|
||||
case "module":
|
||||
if (!projectId) return undefined;
|
||||
return getProjectModuleIds(projectId) || undefined;
|
||||
case "cycle":
|
||||
if (!projectId) return undefined;
|
||||
return getProjectCycleIds(projectId) || undefined;
|
||||
case "priority":
|
||||
return Object.keys(PRIORITIES_PROPERTY) || undefined;
|
||||
case "state":
|
||||
if (!projectId) return undefined;
|
||||
return getProjectStates(projectId)?.map((state) => state.id) || undefined;
|
||||
case "state_group":
|
||||
return Object.keys(STATE_GROUP_PROPERTY) || undefined;
|
||||
case "assignees":
|
||||
if (projectId) return getProjectMemberIds(projectId) || undefined;
|
||||
return workspaceMemberIds || undefined;
|
||||
case "mentions":
|
||||
if (projectId) return getProjectMemberIds(projectId) || undefined;
|
||||
return workspaceMemberIds || undefined;
|
||||
case "subscriber":
|
||||
if (projectId) return getProjectMemberIds(projectId) || undefined;
|
||||
return workspaceMemberIds || undefined;
|
||||
case "created_by":
|
||||
if (projectId) return getProjectMemberIds(projectId) || undefined;
|
||||
return workspaceMemberIds || undefined;
|
||||
case "labels":
|
||||
if (projectId) return getProjectLabels(projectId)?.map((label) => label.id) || undefined;
|
||||
return workspaceLabels?.map((label) => label.id) || undefined;
|
||||
case "start_date":
|
||||
return Object.keys(DATE_PROPERTY) || undefined;
|
||||
case "target_date":
|
||||
return Object.keys(DATE_PROPERTY) || undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const propertyDefaultDetails = (filterKey: keyof TViewFilters): TFilterPropertyDefaultDetails | undefined => {
|
||||
if (!filterKey) return undefined;
|
||||
|
||||
switch (filterKey) {
|
||||
case "project":
|
||||
return {
|
||||
icon: <Briefcase size={12} />,
|
||||
label: "Projects",
|
||||
};
|
||||
case "module":
|
||||
return {
|
||||
icon: <DiceIcon className="w-3 h-3" />,
|
||||
label: "Modules",
|
||||
};
|
||||
case "cycle":
|
||||
return {
|
||||
icon: <ContrastIcon className="w-3 h-3" />,
|
||||
label: "Cycles",
|
||||
};
|
||||
case "priority":
|
||||
return {
|
||||
icon: <PriorityIcon priority="high" withContainer size={10} />,
|
||||
label: "Priorities",
|
||||
};
|
||||
case "state":
|
||||
return {
|
||||
icon: <DoubleCircleIcon className="w-3 h-3" />,
|
||||
label: "States",
|
||||
};
|
||||
case "state_group":
|
||||
return {
|
||||
icon: <DoubleCircleIcon className="w-3 h-3" />,
|
||||
label: "State Groups",
|
||||
};
|
||||
case "assignees":
|
||||
return {
|
||||
icon: <CircleUser size={12} />,
|
||||
label: "Assignees",
|
||||
};
|
||||
case "mentions":
|
||||
return {
|
||||
icon: <CircleUser size={12} />,
|
||||
label: "Mentions",
|
||||
};
|
||||
case "subscriber":
|
||||
return {
|
||||
icon: <CircleUser size={12} />,
|
||||
label: "Subscribers",
|
||||
};
|
||||
case "created_by":
|
||||
return {
|
||||
icon: <CircleUser size={12} />,
|
||||
label: "Creators",
|
||||
};
|
||||
case "labels":
|
||||
return {
|
||||
icon: <Tag size={12} />,
|
||||
label: "Labels",
|
||||
};
|
||||
case "start_date":
|
||||
return {
|
||||
icon: <CalendarDays size={12} />,
|
||||
label: "Start Dates",
|
||||
};
|
||||
case "target_date":
|
||||
return {
|
||||
icon: <CalendarDays size={12} />,
|
||||
label: "Target Dates",
|
||||
};
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const propertyDetails = (filterKey: keyof TViewFilters, propertyId: string): TFilterPropertyDetails | undefined => {
|
||||
if (!filterKey || !propertyId) return undefined;
|
||||
|
||||
switch (filterKey) {
|
||||
case "project":
|
||||
const projectPropertyDetail = getProjectById(propertyId);
|
||||
if (!projectPropertyDetail) return undefined;
|
||||
return {
|
||||
icon: (
|
||||
<>
|
||||
{projectPropertyDetail.emoji ? (
|
||||
<div className="text-xs">{renderEmoji(projectPropertyDetail.emoji)}</div>
|
||||
) : projectPropertyDetail.icon_prop ? (
|
||||
<div className="text-xs">{renderEmoji(projectPropertyDetail.icon_prop)}</div>
|
||||
) : (
|
||||
<Briefcase size={12} />
|
||||
)}
|
||||
</>
|
||||
),
|
||||
label: projectPropertyDetail.name,
|
||||
};
|
||||
case "module":
|
||||
const modulePropertyDetail = getModuleById(propertyId);
|
||||
if (!modulePropertyDetail) return undefined;
|
||||
return {
|
||||
icon: <DiceIcon className="w-3 h-3" />,
|
||||
label: modulePropertyDetail.name,
|
||||
};
|
||||
case "cycle":
|
||||
const cyclePropertyDetail = getCycleById(propertyId);
|
||||
if (!cyclePropertyDetail) return undefined;
|
||||
return {
|
||||
icon: <CycleGroupIcon cycleGroup={cyclePropertyDetail.status} height="14px" width="14px" />,
|
||||
label: cyclePropertyDetail.name,
|
||||
};
|
||||
case "priority":
|
||||
const priorityPropertyDetail = PRIORITIES_PROPERTY?.[propertyId as TIssuePriorities];
|
||||
if (!priorityPropertyDetail) return undefined;
|
||||
return {
|
||||
icon: <PriorityIcon priority={propertyId as TIssuePriorities} size={10} withContainer />,
|
||||
label: priorityPropertyDetail.label,
|
||||
};
|
||||
case "state":
|
||||
const statePropertyDetail = getStateById(propertyId);
|
||||
if (!statePropertyDetail) return undefined;
|
||||
return {
|
||||
icon: <StateGroupIcon stateGroup={statePropertyDetail.group} />,
|
||||
label: statePropertyDetail.name,
|
||||
};
|
||||
case "state_group":
|
||||
const stateGroupPropertyDetail = STATE_GROUP_PROPERTY?.[propertyId as TStateGroups];
|
||||
if (!stateGroupPropertyDetail) return undefined;
|
||||
return {
|
||||
icon: <StateGroupIcon stateGroup={propertyId as TStateGroups} />,
|
||||
label: stateGroupPropertyDetail.label,
|
||||
};
|
||||
case "assignees":
|
||||
const assigneePropertyDetail = getUserDetails(propertyId);
|
||||
if (!assigneePropertyDetail) return undefined;
|
||||
return {
|
||||
icon: (
|
||||
<Avatar
|
||||
name={assigneePropertyDetail.display_name}
|
||||
src={assigneePropertyDetail.avatar}
|
||||
size={"sm"}
|
||||
showTooltip={false}
|
||||
/>
|
||||
),
|
||||
label: assigneePropertyDetail.display_name,
|
||||
};
|
||||
case "mentions":
|
||||
const mentionPropertyDetail = getUserDetails(propertyId);
|
||||
if (!mentionPropertyDetail) return undefined;
|
||||
return {
|
||||
icon: (
|
||||
<Avatar
|
||||
name={mentionPropertyDetail.display_name}
|
||||
src={mentionPropertyDetail.avatar}
|
||||
size={"sm"}
|
||||
showTooltip={false}
|
||||
/>
|
||||
),
|
||||
label: mentionPropertyDetail.display_name,
|
||||
};
|
||||
case "subscriber":
|
||||
const subscribedPropertyDetail = getUserDetails(propertyId);
|
||||
if (!subscribedPropertyDetail) return undefined;
|
||||
return {
|
||||
icon: (
|
||||
<Avatar
|
||||
name={subscribedPropertyDetail.display_name}
|
||||
src={subscribedPropertyDetail.avatar}
|
||||
size={"sm"}
|
||||
showTooltip={false}
|
||||
/>
|
||||
),
|
||||
label: subscribedPropertyDetail.display_name,
|
||||
};
|
||||
case "created_by":
|
||||
const createdByPropertyDetail = getUserDetails(propertyId);
|
||||
if (!createdByPropertyDetail) return undefined;
|
||||
return {
|
||||
icon: (
|
||||
<Avatar
|
||||
name={createdByPropertyDetail.display_name}
|
||||
src={createdByPropertyDetail.avatar}
|
||||
size={"sm"}
|
||||
showTooltip={false}
|
||||
/>
|
||||
),
|
||||
label: createdByPropertyDetail.display_name,
|
||||
};
|
||||
case "labels":
|
||||
const labelPropertyDetail = getLabelById(propertyId);
|
||||
if (!labelPropertyDetail) return undefined;
|
||||
return {
|
||||
icon: (
|
||||
<div
|
||||
className="w-2.5 h-2.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: labelPropertyDetail.color,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
label: labelPropertyDetail.name,
|
||||
};
|
||||
case "start_date":
|
||||
if (propertyId.includes("-")) {
|
||||
const customDateString = propertyId.split(";");
|
||||
return {
|
||||
icon: <CalendarDays size={12} />,
|
||||
label: `${customDateString[1].charAt(0).toUpperCase()}${customDateString[1].slice(1)} ${renderFormattedDate(
|
||||
customDateString[0]
|
||||
)}`,
|
||||
};
|
||||
} else {
|
||||
const startDatePropertyDetail = DATE_PROPERTY?.[propertyId];
|
||||
if (!startDatePropertyDetail) return undefined;
|
||||
return {
|
||||
icon: <CalendarDays size={12} />,
|
||||
label: startDatePropertyDetail.label,
|
||||
};
|
||||
}
|
||||
case "target_date":
|
||||
if (propertyId.includes("-")) {
|
||||
const customDateString = propertyId.split(";");
|
||||
return {
|
||||
icon: <CalendarDays size={12} />,
|
||||
label: `${customDateString[1].charAt(0).toUpperCase()}${customDateString[1].slice(1)} ${renderFormattedDate(
|
||||
customDateString[0]
|
||||
)}`,
|
||||
};
|
||||
} else {
|
||||
const targetDatePropertyDetail = DATE_PROPERTY?.[propertyId];
|
||||
if (!targetDatePropertyDetail) return undefined;
|
||||
return {
|
||||
icon: <CalendarDays size={12} />,
|
||||
label: targetDatePropertyDetail.label,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
filterIdsWithKey,
|
||||
propertyDefaultDetails,
|
||||
propertyDetails,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "contexts/store-context";
|
||||
// types
|
||||
import { ViewRootStore } from "store/view/view-root.store";
|
||||
import { TViewTypes } from "@plane/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
|
||||
export const useView = (
|
||||
workspaceSlug: string,
|
||||
projectId: string | undefined,
|
||||
viewType: TViewTypes | undefined
|
||||
): ViewRootStore | undefined => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useView must be used within StoreProvider");
|
||||
|
||||
if (!workspaceSlug || !viewType) return undefined;
|
||||
|
||||
switch (viewType) {
|
||||
case VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS:
|
||||
return context.view.workspacePrivateViewStore;
|
||||
case VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS:
|
||||
return context.view.workspacePublicViewStore;
|
||||
case VIEW_TYPES.PROJECT_PRIVATE_VIEWS:
|
||||
if (!projectId) return undefined;
|
||||
return context.view.projectPrivateViewStore;
|
||||
case VIEW_TYPES.PROJECT_PUBLIC_VIEWS:
|
||||
if (!projectId) return undefined;
|
||||
return context.view.projectPublicViewStore;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
type TUseViewsProps = {};
|
||||
|
||||
export const useViews = (issueId: string | undefined): TUseViewsProps => {
|
||||
console.log("issueId", issueId);
|
||||
|
||||
return {
|
||||
issueId,
|
||||
};
|
||||
};
|
||||
@@ -20,6 +20,7 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
||||
const {
|
||||
workspace: { fetchWorkspaceMembers },
|
||||
} = useMember();
|
||||
const { fetchWorkspaceLabels } = useLabel();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@@ -38,6 +39,11 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
||||
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null
|
||||
);
|
||||
// fetch workspace labels
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null
|
||||
);
|
||||
// fetch workspace user projects role
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null,
|
||||
|
||||
@@ -48,7 +48,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
||||
<InstanceLayout>
|
||||
<StoreWrapper>
|
||||
<CrispWrapper user={currentUser}>
|
||||
<PostHogProvider
|
||||
{/* <PostHogProvider
|
||||
user={currentUser}
|
||||
currentWorkspaceId= {currentWorkspace?.id}
|
||||
workspaceRole={currentWorkspaceRole}
|
||||
@@ -57,7 +57,8 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
||||
posthogHost={envConfig?.posthog_host || null}
|
||||
>
|
||||
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
||||
</PostHogProvider>
|
||||
</PostHogProvider> */}
|
||||
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
||||
</CrispWrapper>
|
||||
</StoreWrapper>
|
||||
</InstanceLayout>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ReactElement, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { GlobalViewRoot } from "components/view";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
|
||||
const ProjectPrivateViewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
|
||||
const workspaceViewTabOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: VIEW_TYPES.PROJECT_PRIVATE_VIEWS,
|
||||
title: "Private",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/views/private`,
|
||||
},
|
||||
{
|
||||
key: VIEW_TYPES.PROJECT_PUBLIC_VIEWS,
|
||||
title: "Public",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/views/public`,
|
||||
},
|
||||
],
|
||||
[workspaceSlug, projectId]
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||
return (
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
<GlobalViewRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
viewId={viewId.toString()}
|
||||
viewType={VIEW_TYPES.PROJECT_PRIVATE_VIEWS}
|
||||
baseRoute={`/${workspaceSlug?.toString()}/projects/${projectId}/views/private`}
|
||||
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectPrivateViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||
};
|
||||
|
||||
export default ProjectPrivateViewPage;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ReactElement, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { GlobalViewRoot } from "components/view";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
|
||||
const ProjectPrivateViewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
|
||||
const workspaceViewTabOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
|
||||
title: "Private",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/views/private`,
|
||||
},
|
||||
{
|
||||
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
|
||||
title: "Public",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/views/public`,
|
||||
},
|
||||
],
|
||||
[workspaceSlug, projectId]
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||
return (
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
<GlobalViewRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
viewId={viewId.toString()}
|
||||
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
|
||||
baseRoute={`/${workspaceSlug?.toString()}/projects/${projectId}/views/private`}
|
||||
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectPrivateViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||
};
|
||||
|
||||
export default ProjectPrivateViewPage;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ReactElement, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { GlobalViewRoot } from "components/view";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
|
||||
const ProjectPublicViewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
|
||||
const workspaceViewTabOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: VIEW_TYPES.PROJECT_PRIVATE_VIEWS,
|
||||
title: "Private",
|
||||
href: `/${workspaceSlug}/views/private/assigned`,
|
||||
},
|
||||
{
|
||||
key: VIEW_TYPES.PROJECT_PUBLIC_VIEWS,
|
||||
title: "Public",
|
||||
href: `/${workspaceSlug}/views/public/all-issues`,
|
||||
},
|
||||
],
|
||||
[workspaceSlug]
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !viewId) return <></>;
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden bg-custom-background-100 relative flex flex-col">
|
||||
<div className="flex-shrink-0 w-full">
|
||||
<GlobalViewRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={undefined}
|
||||
viewId={viewId.toString()}
|
||||
viewType={VIEW_TYPES.PROJECT_PUBLIC_VIEWS}
|
||||
baseRoute={`/${workspaceSlug?.toString()}/views/public`}
|
||||
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full h-full overflow-hidden">Issues render</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectPublicViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||
};
|
||||
|
||||
export default ProjectPublicViewPage;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ReactElement, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { GlobalViewRoot } from "components/view";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
|
||||
const WorkspacePrivateViewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, viewId } = router.query;
|
||||
|
||||
const workspaceViewTabOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
|
||||
title: "Private",
|
||||
href: `/${workspaceSlug}/views/private/assigned`,
|
||||
},
|
||||
{
|
||||
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
|
||||
title: "Public",
|
||||
href: `/${workspaceSlug}/views/public/all-issues`,
|
||||
},
|
||||
],
|
||||
[workspaceSlug]
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !viewId) return <></>;
|
||||
return (
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
<GlobalViewRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={undefined}
|
||||
viewId={viewId.toString()}
|
||||
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
|
||||
baseRoute={`/${workspaceSlug?.toString()}/views/private`}
|
||||
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WorkspacePrivateViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||
};
|
||||
|
||||
export default WorkspacePrivateViewPage;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ReactElement, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { GlobalViewRoot } from "components/view";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { VIEW_TYPES } from "constants/view";
|
||||
|
||||
const WorkspacePublicViewPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, viewId } = router.query;
|
||||
|
||||
const workspaceViewTabOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
|
||||
title: "Private",
|
||||
href: `/${workspaceSlug}/views/private/assigned`,
|
||||
},
|
||||
{
|
||||
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
|
||||
title: "Public",
|
||||
href: `/${workspaceSlug}/views/public/all-issues`,
|
||||
},
|
||||
],
|
||||
[workspaceSlug]
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !viewId) return <></>;
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden bg-custom-background-100 relative flex flex-col">
|
||||
<div className="flex-shrink-0 w-full">
|
||||
<GlobalViewRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={undefined}
|
||||
viewId={viewId.toString()}
|
||||
viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS}
|
||||
baseRoute={`/${workspaceSlug?.toString()}/views/public`}
|
||||
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full h-full overflow-hidden">Issues render</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WorkspacePublicViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||
};
|
||||
|
||||
export default WorkspacePublicViewPage;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
@@ -8,14 +9,20 @@ import { GlobalIssuesHeader } from "components/headers";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const GlobalViewIssuesPage: NextPageWithLayout = () => (
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
<GlobalViewsHeader />
|
||||
<AllIssueLayoutRoot />
|
||||
const GlobalViewIssuesPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, globalViewId: viewId } = router.query;
|
||||
|
||||
if (!workspaceSlug || !viewId) return <></>;
|
||||
return (
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
<GlobalViewsHeader />
|
||||
<AllIssueLayoutRoot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import type { IInboxIssue, IInbox, TInboxStatus, IInboxQueryParams } from "@plane/types";
|
||||
|
||||
export class InboxService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getInboxes(workspaceSlug: string, projectId: string): Promise<IInbox[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getInboxById(workspaceSlug: string, projectId: string, inboxId: string): Promise<IInbox> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchInbox(workspaceSlug: string, projectId: string, inboxId: string, data: Partial<IInbox>): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getInboxIssues(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
params?: IInboxQueryParams
|
||||
): Promise<IInboxIssue[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getInboxIssueById(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string
|
||||
): Promise<IInboxIssue> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteInboxIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async markInboxStatus(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string,
|
||||
data: TInboxStatus
|
||||
): Promise<IInboxIssue> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchInboxIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string,
|
||||
data: { issue: Partial<IInboxIssue> }
|
||||
): Promise<any> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createInboxIssue(workspaceSlug: string, projectId: string, inboxId: string, data: any): Promise<IInboxIssue> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,23 +10,24 @@ export class IssueFiltersService extends APIService {
|
||||
}
|
||||
|
||||
// // workspace issue filters
|
||||
// async fetchWorkspaceFilters(workspaceSlug: string): Promise<IIssueFiltersResponse> {
|
||||
// return this.get(`/api/workspaces/${workspaceSlug}/user-properties/`)
|
||||
// .then((response) => response?.data)
|
||||
// .catch((error) => {
|
||||
// throw error?.response?.data;
|
||||
// });
|
||||
// }
|
||||
// async patchWorkspaceFilters(
|
||||
// workspaceSlug: string,
|
||||
// data: Partial<IIssueFiltersResponse>
|
||||
// ): Promise<IIssueFiltersResponse> {
|
||||
// return this.patch(`/api/workspaces/${workspaceSlug}/user-properties/`, data)
|
||||
// .then((response) => response?.data)
|
||||
// .catch((error) => {
|
||||
// throw error?.response?.data;
|
||||
// });
|
||||
// }
|
||||
async fetchWorkspaceFilters(workspaceSlug: string): Promise<IIssueFiltersResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-properties/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchWorkspaceFilters(
|
||||
workspaceSlug: string,
|
||||
data: Partial<IIssueFiltersResponse>
|
||||
): Promise<IIssueFiltersResponse> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/user-properties/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
// project issue filters
|
||||
async fetchProjectIssueFilters(workspaceSlug: string, projectId: string): Promise<IIssueFiltersResponse> {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// view services
|
||||
export * from "./workspace_private.service";
|
||||
export * from "./workspace_public.service";
|
||||
export * from "./project_private.service";
|
||||
export * from "./project_public.service";
|
||||
|
||||
// user view services
|
||||
export * from "./user/workspace.service";
|
||||
export * from "./user/project.service";
|
||||
export * from "./user/module.service";
|
||||
export * from "./user/cycle.service";
|
||||
@@ -0,0 +1,139 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { TView } from "@plane/types";
|
||||
import { TViewService } from "./types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class ProjectPrivateViewService extends APIService implements TViewService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetch(workspaceSlug: string, projectId: string | undefined = undefined): Promise<TView[] | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.get(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchById(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.get(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async create(
|
||||
workspaceSlug: string,
|
||||
data: Partial<TView>,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
data: Partial<TView>,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.patch(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async remove(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<void | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.delete(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async lock(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/lock/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async unlock(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.delete(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/lock/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async duplicate(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/duplicate/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async makeFavorite(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/favorite/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async removeFavorite(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.delete(`/api/users/me/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/favorite/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { TView } from "@plane/types";
|
||||
import { TViewService } from "./types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class ProjectPublicViewService extends APIService implements TViewService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetch(workspaceSlug: string, projectId: string | undefined = undefined): Promise<TView[] | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchById(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async create(
|
||||
workspaceSlug: string,
|
||||
data: Partial<TView>,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
data: Partial<TView>,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async remove(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<void | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async lock(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/lock/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async unlock(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/lock/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async duplicate(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/duplicate/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async makeFavorite(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/favorite/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async removeFavorite(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
projectId: string | undefined = undefined
|
||||
): Promise<TView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/favorite/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
import { TView, TUserView } from "@plane/types";
|
||||
|
||||
export type TUserViewService = {
|
||||
// featureId represents moduleId/cycleId
|
||||
fetch: (workspaceSlug: string, projectId?: string, featureId?: string) => Promise<TUserView | undefined>;
|
||||
update: (
|
||||
workspaceSlug: string,
|
||||
data: Partial<TView>,
|
||||
projectId?: string,
|
||||
featureId?: string
|
||||
) => Promise<TUserView | undefined>;
|
||||
};
|
||||
|
||||
export type TViewService = {
|
||||
fetch: (workspaceSlug: string, projectId?: string) => Promise<TView[] | undefined>;
|
||||
fetchById: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||
create: (workspaceSlug: string, data: Partial<TView>, projectId?: string) => Promise<TView | undefined>;
|
||||
update: (
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
data: Partial<TView>,
|
||||
projectId?: string
|
||||
) => Promise<TView | undefined>;
|
||||
remove: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<void> | undefined;
|
||||
lock: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||
unlock: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||
duplicate: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||
makeFavorite: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||
removeFavorite: (workspaceSlug: string, viewId: string, projectId?: string) => Promise<TView | undefined>;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { TViewFilterProps, TUserView } from "@plane/types";
|
||||
import { TUserViewService } from "../types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class CycleFiltersService extends APIService implements TUserViewService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetch(workspaceSlug: string, projectId?: string, cycleId?: string): Promise<TUserView | undefined> {
|
||||
if (!projectId || !cycleId) return undefined;
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}user-properties/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceSlug: string,
|
||||
data: Partial<TViewFilterProps>,
|
||||
projectId?: string,
|
||||
cycleId?: string
|
||||
): Promise<TUserView | undefined> {
|
||||
if (!projectId || !cycleId) return undefined;
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}user-properties/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { TViewFilterProps, TUserView } from "@plane/types";
|
||||
import { TUserViewService } from "../types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class ModuleFiltersService extends APIService implements TUserViewService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetch(workspaceSlug: string, projectId?: string, moduleId?: string): Promise<TUserView | undefined> {
|
||||
if (!projectId || !moduleId) return undefined;
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}user-properties/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceSlug: string,
|
||||
data: Partial<TViewFilterProps>,
|
||||
projectId?: string,
|
||||
moduleId?: string
|
||||
): Promise<TUserView | undefined> {
|
||||
if (!projectId || !moduleId) return undefined;
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}user-properties/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { TViewFilterProps, TUserView } from "@plane/types";
|
||||
import { TUserViewService } from "../types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class ProjectFiltersService extends APIService implements TUserViewService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetch(workspaceSlug: string, projectId?: string): Promise<TUserView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceSlug: string,
|
||||
data: Partial<TViewFilterProps>,
|
||||
projectId?: string
|
||||
): Promise<TUserView | undefined> {
|
||||
if (!projectId) return undefined;
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/user-properties/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { TViewFilterProps, TUserView } from "@plane/types";
|
||||
import { TUserViewService } from "../types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class WorkspaceFiltersService extends APIService implements TUserViewService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetch(workspaceSlug: string): Promise<TUserView | undefined> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/user-properties/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async update(workspaceSlug: string, data: Partial<TViewFilterProps>): Promise<TUserView | undefined> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/user-properties/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { TView } from "@plane/types";
|
||||
import { TViewService } from "./types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class WorkspacePrivateViewService extends APIService implements TViewService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetch(workspaceSlug: string): Promise<TView[]> {
|
||||
return this.get(`/api/users/me/workspaces/${workspaceSlug}/views/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchById(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.get(`/api/users/me/workspaces/${workspaceSlug}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async create(workspaceSlug: string, data: Partial<TView>): Promise<TView> {
|
||||
return this.post(`/api/users/me/workspaces/${workspaceSlug}/views/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async update(workspaceSlug: string, viewId: string, data: Partial<TView>): Promise<TView> {
|
||||
return this.patch(`/api/users/me/workspaces/${workspaceSlug}/views/${viewId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async remove(workspaceSlug: string, viewId: string): Promise<void> {
|
||||
return this.delete(`/api/users/me/workspaces/${workspaceSlug}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async lock(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.post(`/api/users/me/workspaces/${workspaceSlug}/views/${viewId}/lock/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async unlock(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.delete(`/api/users/me/workspaces/${workspaceSlug}/views/${viewId}/lock/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async duplicate(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.post(`/api/users/me/workspaces/${workspaceSlug}/views/${viewId}/duplicate/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async makeFavorite(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.post(`/api/users/me/workspaces/${workspaceSlug}/views/${viewId}/favorite/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async removeFavorite(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.delete(`/api/users/me/workspaces/${workspaceSlug}/views/${viewId}/favorite/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import { TView } from "@plane/types";
|
||||
import { TViewService } from "./types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class WorkspacePublicViewService extends APIService implements TViewService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetch(workspaceSlug: string): Promise<TView[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/views/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchById(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async create(workspaceSlug: string, data: Partial<TView>): Promise<TView> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/views/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async update(workspaceSlug: string, viewId: string, data: Partial<TView>): Promise<TView> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/views/${viewId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async remove(workspaceSlug: string, viewId: string): Promise<void> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/views/${viewId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async lock(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/views/${viewId}/lock/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async unlock(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/views/${viewId}/lock/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async duplicate(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/views/${viewId}/duplicate/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async makeFavorite(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/views/${viewId}/favorite/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async removeFavorite(workspaceSlug: string, viewId: string): Promise<TView> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/views/${viewId}/favorite/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { observable, action, makeObservable, runInAction, computed } from "mobx"
|
||||
import set from "lodash/set";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
// services
|
||||
import { InboxService } from "services/inbox.service";
|
||||
import { InboxService } from "services/inbox/inbox.service";
|
||||
// types
|
||||
import { RootStore } from "store/root.store";
|
||||
import { TInboxIssueFilterOptions, TInboxIssueFilters, TInboxIssueQueryParams, TInbox } from "@plane/types";
|
||||
|
||||
@@ -18,6 +18,8 @@ import { IMentionStore, MentionStore } from "./mention.store";
|
||||
import { DashboardStore, IDashboardStore } from "./dashboard.store";
|
||||
import { IProjectPageStore, ProjectPageStore } from "./project-page.store";
|
||||
import { ILabelStore, LabelStore } from "./label.store";
|
||||
// new stores
|
||||
import { GlobalViewRootStore } from "./view/root.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
@@ -40,8 +42,10 @@ export class RootStore {
|
||||
mention: IMentionStore;
|
||||
dashboard: IDashboardStore;
|
||||
projectPages: IProjectPageStore;
|
||||
view: GlobalViewRootStore;
|
||||
|
||||
constructor() {
|
||||
// old store structure
|
||||
this.app = new AppRootStore(this);
|
||||
this.eventTracker = new EventTrackerStore(this);
|
||||
this.user = new UserRootStore(this);
|
||||
@@ -61,6 +65,7 @@ export class RootStore {
|
||||
this.mention = new MentionStore(this);
|
||||
this.projectPages = new ProjectPageStore(this);
|
||||
this.dashboard = new DashboardStore(this);
|
||||
this.view = new GlobalViewRootStore(this);
|
||||
}
|
||||
|
||||
resetOnSignout() {
|
||||
@@ -80,5 +85,6 @@ export class RootStore {
|
||||
this.mention = new MentionStore(this);
|
||||
this.projectPages = new ProjectPageStore(this);
|
||||
this.dashboard = new DashboardStore(this);
|
||||
this.view = new GlobalViewRootStore(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import get from "lodash/get";
|
||||
// types
|
||||
import {
|
||||
TViewFilters,
|
||||
TViewDisplayFilters,
|
||||
TViewDisplayProperties,
|
||||
TViewFilterProps,
|
||||
TViewFilterQueryParams,
|
||||
} from "@plane/types";
|
||||
|
||||
export class FiltersHelper {
|
||||
// computed filters
|
||||
computedFilters = (filters: TViewFilters, defaultValues?: Partial<TViewFilters>): TViewFilters => ({
|
||||
project: get(defaultValues, "project", get(filters, "project", [])),
|
||||
module: get(defaultValues, "module", get(filters, "module", [])),
|
||||
cycle: get(defaultValues, "cycle", get(filters, "cycle", [])),
|
||||
priority: get(defaultValues, "priority", get(filters, "priority", [])),
|
||||
state: get(defaultValues, "state", get(filters, "state", [])),
|
||||
state_group: get(defaultValues, "state_group", get(filters, "state_group", [])),
|
||||
assignees: get(defaultValues, "assignees", get(filters, "assignees", [])),
|
||||
mentions: get(defaultValues, "mentions", get(filters, "mentions", [])),
|
||||
subscriber: get(defaultValues, "subscriber", get(filters, "subscriber", [])),
|
||||
created_by: get(defaultValues, "created_by", get(filters, "created_by", [])),
|
||||
labels: get(defaultValues, "labels", get(filters, "labels", [])),
|
||||
start_date: get(defaultValues, "start_date", get(filters, "start_date", [])),
|
||||
target_date: get(defaultValues, "target_date", get(filters, "target_date", [])),
|
||||
});
|
||||
|
||||
// computed display filters
|
||||
computedDisplayFilters = (
|
||||
displayFilters: TViewDisplayFilters,
|
||||
defaultValues?: Partial<TViewDisplayFilters>
|
||||
): TViewDisplayFilters => ({
|
||||
layout: defaultValues?.layout || displayFilters?.layout || "list",
|
||||
group_by: defaultValues?.group_by || displayFilters?.group_by || undefined,
|
||||
sub_group_by: defaultValues?.sub_group_by || displayFilters?.sub_group_by || undefined,
|
||||
order_by: defaultValues?.order_by || displayFilters?.order_by || "sort_order",
|
||||
type: defaultValues?.type || displayFilters?.type || undefined,
|
||||
sub_issue: defaultValues?.sub_issue || displayFilters?.sub_issue || false,
|
||||
show_empty_groups: defaultValues?.show_empty_groups || displayFilters?.show_empty_groups || false,
|
||||
calendar: {
|
||||
show_weekends: defaultValues?.calendar?.show_weekends || displayFilters?.calendar?.show_weekends || false,
|
||||
layout: defaultValues?.calendar?.layout || displayFilters?.calendar?.layout || "month",
|
||||
},
|
||||
});
|
||||
|
||||
// computed display properties
|
||||
computedDisplayProperties = (
|
||||
displayProperties: TViewDisplayProperties,
|
||||
defaultValues?: Partial<TViewDisplayProperties>
|
||||
): TViewDisplayProperties => ({
|
||||
assignee: get(defaultValues, "assignee", get(displayProperties, "assignee", true)),
|
||||
start_date: get(defaultValues, "start_date", get(displayProperties, "start_date", true)),
|
||||
due_date: get(defaultValues, "due_date", get(displayProperties, "due_date", true)),
|
||||
labels: get(defaultValues, "labels", get(displayProperties, "labels", true)),
|
||||
priority: get(defaultValues, "priority", get(displayProperties, "priority", true)),
|
||||
state: get(defaultValues, "state", get(displayProperties, "state", true)),
|
||||
sub_issue_count: get(defaultValues, "sub_issue_count", get(displayProperties, "sub_issue_count", true)),
|
||||
attachment_count: get(defaultValues, "attachment_count", get(displayProperties, "attachment_count", true)),
|
||||
link: get(defaultValues, "link", get(displayProperties, "link", true)),
|
||||
estimate: get(defaultValues, "estimate", get(displayProperties, "estimate", true)),
|
||||
key: get(defaultValues, "key", get(displayProperties, "key", true)),
|
||||
created_on: get(defaultValues, "created_on", get(displayProperties, "created_on", true)),
|
||||
updated_on: get(defaultValues, "updated_on", get(displayProperties, "updated_on", true)),
|
||||
});
|
||||
|
||||
// compute filters and display_filters issue query parameters
|
||||
computeAppliedFiltersQueryParameters = (
|
||||
filters: TViewFilterProps,
|
||||
acceptableParamsByLayout: string[]
|
||||
): { params: any; query: string } => {
|
||||
const paramsObject: Partial<Record<TViewFilterQueryParams, string | boolean>> = {};
|
||||
let paramsString = "";
|
||||
|
||||
const filteredParams: Partial<Record<TViewFilterQueryParams, undefined | string[] | boolean | string>> = {
|
||||
// issue filters
|
||||
priority: filters.filters?.priority || undefined,
|
||||
state_group: filters.filters?.state_group || undefined,
|
||||
state: filters.filters?.state || undefined,
|
||||
assignees: filters.filters?.assignees || undefined,
|
||||
mentions: filters.filters?.mentions || undefined,
|
||||
created_by: filters.filters?.created_by || undefined,
|
||||
labels: filters.filters?.labels || undefined,
|
||||
start_date: filters.filters?.start_date || undefined,
|
||||
target_date: filters.filters?.target_date || undefined,
|
||||
project: filters.filters?.project || undefined,
|
||||
subscriber: filters.filters?.subscriber || undefined,
|
||||
// display filters
|
||||
type: filters?.display_filters?.type || undefined,
|
||||
sub_issue: filters?.display_filters?.sub_issue || true,
|
||||
};
|
||||
|
||||
Object.keys(filteredParams).forEach((key) => {
|
||||
const _key = key as TViewFilterQueryParams;
|
||||
const _value: string | boolean | string[] | undefined = filteredParams[_key];
|
||||
if (_value != undefined && acceptableParamsByLayout.includes(_key))
|
||||
paramsObject[_key] = Array.isArray(_value) ? _value.join(",") : _value;
|
||||
});
|
||||
|
||||
if (paramsObject && !isEmpty(paramsObject)) {
|
||||
paramsString = Object.keys(paramsObject)
|
||||
.map((key) => {
|
||||
const _key = key as TViewFilterQueryParams;
|
||||
const _value: string | boolean | undefined = paramsObject[_key];
|
||||
if (!undefined) return `${_key}=${_value}`;
|
||||
})
|
||||
.join("&");
|
||||
}
|
||||
|
||||
return { params: paramsObject, query: paramsString };
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// stores
|
||||
import { ViewRootStore } from "./view-root.store";
|
||||
// services
|
||||
import {
|
||||
WorkspacePrivateViewService,
|
||||
WorkspacePublicViewService,
|
||||
ProjectPublicViewService,
|
||||
ProjectPrivateViewService,
|
||||
WorkspaceFiltersService,
|
||||
ProjectFiltersService,
|
||||
} from "services/view";
|
||||
// types
|
||||
import { RootStore } from "store/root.store";
|
||||
|
||||
export class GlobalViewRootStore {
|
||||
workspacePrivateViewStore: ViewRootStore;
|
||||
workspacePublicViewStore: ViewRootStore;
|
||||
projectPrivateViewStore: ViewRootStore;
|
||||
projectPublicViewStore: ViewRootStore;
|
||||
|
||||
constructor(private store: RootStore) {
|
||||
const workspacePrivateDefaultViews: any[] = [
|
||||
{
|
||||
id: "assigned",
|
||||
name: "Assigned",
|
||||
filters: {
|
||||
assignees: store?.user?.currentUser?.id ? [store?.user?.currentUser?.id] : [],
|
||||
},
|
||||
is_local_view: true,
|
||||
},
|
||||
{
|
||||
id: "created",
|
||||
name: "Created",
|
||||
filters: {
|
||||
created_by: store?.user?.currentUser?.id ? [store?.user?.currentUser?.id] : [],
|
||||
},
|
||||
is_local_view: true,
|
||||
},
|
||||
{
|
||||
id: "subscribed",
|
||||
name: "Subscribed",
|
||||
filters: {
|
||||
subscriber: store?.user?.currentUser?.id ? [store?.user?.currentUser?.id] : [],
|
||||
},
|
||||
is_local_view: true,
|
||||
},
|
||||
];
|
||||
|
||||
const workspacePublicDefaultViews: any[] = [
|
||||
{
|
||||
id: "all-issues",
|
||||
name: "All Issues",
|
||||
filters: {},
|
||||
is_local_view: true,
|
||||
},
|
||||
];
|
||||
|
||||
this.workspacePrivateViewStore = new ViewRootStore(
|
||||
this.store,
|
||||
workspacePrivateDefaultViews,
|
||||
new WorkspacePrivateViewService(),
|
||||
new WorkspaceFiltersService()
|
||||
);
|
||||
this.workspacePublicViewStore = new ViewRootStore(
|
||||
this.store,
|
||||
workspacePublicDefaultViews,
|
||||
new WorkspacePublicViewService(),
|
||||
new WorkspaceFiltersService()
|
||||
);
|
||||
this.projectPrivateViewStore = new ViewRootStore(
|
||||
this.store,
|
||||
undefined,
|
||||
new ProjectPrivateViewService(),
|
||||
new ProjectFiltersService()
|
||||
);
|
||||
this.projectPublicViewStore = new ViewRootStore(
|
||||
this.store,
|
||||
undefined,
|
||||
new ProjectPublicViewService(),
|
||||
new ProjectFiltersService()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import set from "lodash/set";
|
||||
import sortBy from "lodash/sortBy";
|
||||
import reverse from "lodash/reverse";
|
||||
// stores
|
||||
import { RootStore } from "store/root.store";
|
||||
import { ViewStore } from "./view.store";
|
||||
// types
|
||||
import { TUserViewService, TViewService } from "services/view/types";
|
||||
import { TView } from "@plane/types";
|
||||
|
||||
export type TLoader = "init-loader" | "mutation-loader" | "submitting" | undefined;
|
||||
|
||||
type TViewRootStore = {
|
||||
// observables
|
||||
loader: TLoader;
|
||||
viewMap: Record<string, ViewStore>;
|
||||
// computed
|
||||
viewIds: string[];
|
||||
viewById: (viewId: string) => ViewStore | undefined;
|
||||
// actions
|
||||
localViewCreate: (view: TView) => Promise<void>;
|
||||
fetch: (_loader?: TLoader) => Promise<void>;
|
||||
fetchById: (viewId: string) => Promise<void>;
|
||||
create: (view: Partial<TView>) => Promise<void>;
|
||||
remove: (viewId: string) => Promise<void>;
|
||||
duplicate: (viewId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export class ViewRootStore implements TViewRootStore {
|
||||
// observables
|
||||
loader: TLoader = "init-loader";
|
||||
viewMap: Record<string, ViewStore> = {};
|
||||
|
||||
constructor(
|
||||
private store: RootStore,
|
||||
private defaultViews: TView[] = [],
|
||||
private service: TViewService,
|
||||
private userService: TUserViewService
|
||||
) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
loader: observable.ref,
|
||||
viewMap: observable,
|
||||
// computed
|
||||
viewIds: computed,
|
||||
// actions
|
||||
localViewCreate: action,
|
||||
fetch: action,
|
||||
fetchById: action,
|
||||
create: action,
|
||||
remove: action,
|
||||
duplicate: action,
|
||||
});
|
||||
}
|
||||
|
||||
// computed
|
||||
get viewIds() {
|
||||
const views = Object.values(this.viewMap);
|
||||
const localViews = views.filter((view) => view.is_local_view);
|
||||
let apiViews = views.filter((view) => !view.is_local_view && !view.is_create);
|
||||
apiViews = reverse(sortBy(apiViews, "sort_order"));
|
||||
const _viewIds = [...localViews.map((view) => view.id), ...apiViews.map((view) => view.id)];
|
||||
return _viewIds as string[];
|
||||
}
|
||||
|
||||
viewById = computedFn((viewId: string) => this.viewMap?.[viewId] || undefined);
|
||||
|
||||
// actions
|
||||
localViewCreate = async (view: TView) => {
|
||||
runInAction(() => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
});
|
||||
};
|
||||
|
||||
fetch = async (_loader: TLoader = "init-loader") => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
this.loader = _loader;
|
||||
|
||||
if (this.defaultViews && this.defaultViews.length > 0)
|
||||
runInAction(() => {
|
||||
this.defaultViews?.forEach((view) => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
});
|
||||
});
|
||||
|
||||
const views = await this.service.fetch(workspaceSlug, projectId);
|
||||
if (!views) return;
|
||||
|
||||
runInAction(() => {
|
||||
views.forEach((view) => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
});
|
||||
this.loader = undefined;
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
|
||||
fetchById = async (viewId: string) => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug || !viewId) return;
|
||||
|
||||
const userView = await this.userService.fetch(workspaceSlug, projectId);
|
||||
if (!userView) return;
|
||||
|
||||
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) {
|
||||
const view = { ...this.viewById(viewId) };
|
||||
if (!view) return;
|
||||
|
||||
runInAction(() => {
|
||||
view.display_filters = userView.display_filters;
|
||||
view.display_properties = userView.display_properties;
|
||||
});
|
||||
} else {
|
||||
const view = await this.service.fetchById(workspaceSlug, viewId, projectId);
|
||||
if (!view) return;
|
||||
|
||||
view?.display_filters && (view.display_filters = userView.display_filters);
|
||||
view?.display_properties && (view.display_properties = userView.display_properties);
|
||||
|
||||
runInAction(() => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
create = async (data: Partial<TView>) => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
const view = await this.service.create(workspaceSlug, data, projectId);
|
||||
if (!view) return;
|
||||
|
||||
runInAction(() => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
});
|
||||
|
||||
if (data.id) this.remove(data.id);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
remove = async (viewId: string) => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug || !viewId) return;
|
||||
|
||||
if (this.viewMap?.[viewId] != undefined && !this.viewMap?.[viewId]?.is_create)
|
||||
await this.service.remove?.(workspaceSlug, viewId, projectId);
|
||||
|
||||
runInAction(() => {
|
||||
delete this.viewMap[viewId];
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
|
||||
duplicate = async (viewId: string) => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug || !this.service.duplicate) return;
|
||||
|
||||
const view = await this.service.duplicate(workspaceSlug, viewId, projectId);
|
||||
if (!view) return;
|
||||
|
||||
runInAction(() => {
|
||||
if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService));
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import set from "lodash/set";
|
||||
import update from "lodash/update";
|
||||
import concat from "lodash/concat";
|
||||
import pull from "lodash/pull";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
// store
|
||||
import { RootStore } from "store/root.store";
|
||||
// types
|
||||
import { TUserViewService, TViewService } from "services/view/types";
|
||||
import {
|
||||
TView,
|
||||
TUpdateView,
|
||||
TViewFilters,
|
||||
TViewDisplayFilters,
|
||||
TViewDisplayProperties,
|
||||
TViewFilterProps,
|
||||
TViewAccess,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { FiltersHelper } from "./helpers/filters_helpers";
|
||||
|
||||
type TLoader = "updating" | undefined;
|
||||
|
||||
export type TViewStore = TView & {
|
||||
// observables
|
||||
loader: TLoader;
|
||||
filtersToUpdate: TUpdateView;
|
||||
// computed
|
||||
appliedFilters: TViewFilterProps | undefined;
|
||||
appliedFiltersQueryParams: string | undefined;
|
||||
isFiltersApplied: boolean;
|
||||
isFiltersUpdateEnabled: boolean;
|
||||
// helper actions
|
||||
setName: (name: string) => void;
|
||||
setDescription: (description: string) => void;
|
||||
setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => void;
|
||||
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
|
||||
setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => void;
|
||||
setIsEditable: (id_editable: boolean) => void;
|
||||
resetChanges: () => void;
|
||||
saveChanges: () => Promise<void>;
|
||||
// actions
|
||||
update: (viewData: TUpdateView) => Promise<void>;
|
||||
lockView: () => Promise<void>;
|
||||
unlockView: () => Promise<void>;
|
||||
makeFavorite: () => Promise<void>;
|
||||
removeFavorite: () => Promise<void>;
|
||||
};
|
||||
|
||||
export class ViewStore extends FiltersHelper implements TViewStore {
|
||||
id: string | undefined;
|
||||
workspace: string | undefined;
|
||||
project: string | undefined;
|
||||
name: string | undefined;
|
||||
description: string | undefined;
|
||||
query: string | undefined;
|
||||
filters: TViewFilters;
|
||||
display_filters: TViewDisplayFilters;
|
||||
display_properties: TViewDisplayProperties;
|
||||
access: TViewAccess | undefined;
|
||||
owned_by: string | undefined;
|
||||
sort_order: number | undefined;
|
||||
is_locked: boolean = false;
|
||||
is_pinned: boolean = false;
|
||||
is_favorite: boolean = false;
|
||||
created_by: string | undefined;
|
||||
updated_by: string | undefined;
|
||||
created_at: Date | undefined;
|
||||
updated_at: Date | undefined;
|
||||
is_local_view: boolean = false;
|
||||
is_create: boolean = false;
|
||||
is_editable: boolean = false;
|
||||
loader: TLoader = undefined;
|
||||
filtersToUpdate: TUpdateView;
|
||||
|
||||
constructor(
|
||||
private store: RootStore,
|
||||
_view: TView,
|
||||
private service: TViewService,
|
||||
private userService: TUserViewService
|
||||
) {
|
||||
super();
|
||||
this.id = _view.id;
|
||||
this.workspace = _view.workspace;
|
||||
this.project = _view.project;
|
||||
this.name = _view.name;
|
||||
this.description = _view.description;
|
||||
this.query = _view.query;
|
||||
this.filters = this.computedFilters(_view.filters);
|
||||
this.display_filters = this.computedDisplayFilters(_view.display_filters);
|
||||
this.display_properties = this.computedDisplayProperties(_view.display_properties);
|
||||
this.access = _view.access;
|
||||
this.owned_by = _view.owned_by;
|
||||
this.sort_order = _view.sort_order;
|
||||
this.is_locked = _view.is_locked;
|
||||
this.is_pinned = _view.is_pinned;
|
||||
this.is_favorite = _view.is_favorite;
|
||||
this.created_by = _view.created_by;
|
||||
this.updated_by = _view.updated_by;
|
||||
this.created_at = _view.created_at;
|
||||
this.updated_at = _view.updated_at;
|
||||
this.is_local_view = _view.is_local_view;
|
||||
this.is_create = _view.is_create;
|
||||
this.is_editable = _view.is_editable;
|
||||
this.filtersToUpdate = {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
filters: this.computedFilters(_view.filters),
|
||||
display_filters: this.computedDisplayFilters(_view.display_filters),
|
||||
display_properties: this.computedDisplayProperties(_view.display_properties),
|
||||
};
|
||||
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
id: observable.ref,
|
||||
workspace: observable.ref,
|
||||
project: observable.ref,
|
||||
name: observable.ref,
|
||||
description: observable.ref,
|
||||
query: observable.ref,
|
||||
filters: observable,
|
||||
display_filters: observable,
|
||||
display_properties: observable,
|
||||
access: observable.ref,
|
||||
owned_by: observable.ref,
|
||||
sort_order: observable.ref,
|
||||
is_locked: observable.ref,
|
||||
is_pinned: observable.ref,
|
||||
is_favorite: observable.ref,
|
||||
created_by: observable.ref,
|
||||
updated_by: observable.ref,
|
||||
created_at: observable.ref,
|
||||
updated_at: observable.ref,
|
||||
is_local_view: observable.ref,
|
||||
is_create: observable.ref,
|
||||
is_editable: observable.ref,
|
||||
loader: observable.ref,
|
||||
filtersToUpdate: observable,
|
||||
// computed
|
||||
appliedFilters: computed,
|
||||
appliedFiltersQueryParams: computed,
|
||||
isFiltersApplied: computed,
|
||||
isFiltersUpdateEnabled: computed,
|
||||
// helper actions
|
||||
setName: action,
|
||||
setFilters: action,
|
||||
setDisplayFilters: action,
|
||||
setDisplayProperties: action,
|
||||
setIsEditable: action,
|
||||
resetChanges: action,
|
||||
saveChanges: action,
|
||||
// actions
|
||||
update: action,
|
||||
lockView: action,
|
||||
unlockView: action,
|
||||
makeFavorite: action,
|
||||
removeFavorite: action,
|
||||
});
|
||||
}
|
||||
|
||||
// computed
|
||||
get appliedFilters() {
|
||||
return {
|
||||
filters: this.computedFilters(this.filters, this.filtersToUpdate.filters),
|
||||
display_filters: this.computedDisplayFilters(this.display_filters, this.filtersToUpdate.display_filters),
|
||||
display_properties: this.computedDisplayProperties(
|
||||
this.display_properties,
|
||||
this.filtersToUpdate.display_properties
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
get appliedFiltersQueryParams() {
|
||||
const filters = this.appliedFilters;
|
||||
if (!filters) return undefined;
|
||||
return this.computeAppliedFiltersQueryParameters(filters, [])?.query || undefined;
|
||||
}
|
||||
|
||||
get isFiltersApplied() {
|
||||
const filters = this.appliedFilters?.filters;
|
||||
let isFiltersApplied = false;
|
||||
Object.keys(filters).forEach((key) => {
|
||||
const _key = key as keyof TViewFilters;
|
||||
if (filters[_key]?.length > 0) isFiltersApplied = true;
|
||||
});
|
||||
return isFiltersApplied;
|
||||
}
|
||||
|
||||
get isFiltersUpdateEnabled() {
|
||||
const _filters = this.filters;
|
||||
const _appliedFilters = this.appliedFilters?.filters;
|
||||
|
||||
let isFiltersUpdateEnabled = false;
|
||||
Object.keys(_appliedFilters).forEach((key) => {
|
||||
const _key = key as keyof TViewFilters;
|
||||
if (!isEqual(_appliedFilters[_key].slice().sort(), _filters[_key].slice().sort())) isFiltersUpdateEnabled = true;
|
||||
});
|
||||
return isFiltersUpdateEnabled;
|
||||
}
|
||||
|
||||
// helper actions
|
||||
setName = (name: string) => {
|
||||
runInAction(() => {
|
||||
this.filtersToUpdate.name = name;
|
||||
});
|
||||
};
|
||||
|
||||
setDescription = (description: string) => {
|
||||
runInAction(() => {
|
||||
this.filtersToUpdate.description = description;
|
||||
});
|
||||
};
|
||||
|
||||
setFilters = (filterKey: keyof TViewFilters | undefined = undefined, filterValue: "clear_all" | string) => {
|
||||
runInAction(() => {
|
||||
if (filterKey === undefined) {
|
||||
if (filterValue === "clear_all") set(this.filtersToUpdate, ["filters"], {});
|
||||
} else
|
||||
update(this.filtersToUpdate, ["filters", filterKey], (_values = []) => {
|
||||
if (filterValue === "clear_all") return [];
|
||||
if (_values.includes(filterValue)) return pull(_values, filterValue);
|
||||
return concat(_values, filterValue);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
setDisplayFilters = async (display_filters: Partial<TViewDisplayFilters>) => {
|
||||
const appliedFilters = this.appliedFilters;
|
||||
|
||||
const layout = appliedFilters?.display_filters?.layout;
|
||||
const sub_group_by = appliedFilters?.display_filters?.sub_group_by;
|
||||
const group_by = appliedFilters?.display_filters?.group_by;
|
||||
const sub_issue = appliedFilters?.display_filters?.sub_issue;
|
||||
|
||||
if (group_by === undefined && display_filters.sub_group_by) display_filters.sub_group_by = undefined;
|
||||
if (layout === "kanban") {
|
||||
if (sub_group_by === group_by) display_filters.group_by = undefined;
|
||||
if (group_by === null) display_filters.group_by = "state";
|
||||
}
|
||||
if (layout === "spreadsheet" && sub_issue === true) display_filters.sub_issue = false;
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(display_filters).forEach((key) => {
|
||||
const _key = key as keyof TViewDisplayFilters;
|
||||
set(this.filtersToUpdate, ["display_filters", _key], display_filters[_key]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
setDisplayProperties = async (displayPropertyKey: keyof TViewDisplayProperties) => {
|
||||
runInAction(() => {
|
||||
update(this.filtersToUpdate, ["display_properties", displayPropertyKey], (_value: boolean = true) => !_value);
|
||||
});
|
||||
};
|
||||
|
||||
setIsEditable = (is_editable: boolean) => {
|
||||
runInAction(() => {
|
||||
this.is_editable = is_editable;
|
||||
});
|
||||
};
|
||||
|
||||
resetChanges = () => {
|
||||
runInAction(() => {
|
||||
const _view = cloneDeep(this);
|
||||
this.filtersToUpdate = {
|
||||
name: _view.name,
|
||||
description: _view.description,
|
||||
filters: _view.filters,
|
||||
display_filters: _view.display_filters,
|
||||
display_properties: _view.display_properties,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
saveChanges = async () => {
|
||||
try {
|
||||
if (this.filtersToUpdate) await this.update(this.filtersToUpdate);
|
||||
} catch {
|
||||
Object.keys(this.filtersToUpdate).forEach((key) => {
|
||||
const _key = key as keyof TUpdateView;
|
||||
set(this, _key, this.filtersToUpdate[_key]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// actions
|
||||
update = async (viewData: TUpdateView) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.loader = "updating";
|
||||
});
|
||||
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug || !this.id) return;
|
||||
|
||||
const view = await this.service.update(workspaceSlug, this.id, viewData, projectId);
|
||||
if (!view) return;
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(view).forEach((key) => {
|
||||
const _key = key as keyof TView;
|
||||
set(this, _key, view[_key]);
|
||||
});
|
||||
this.loader = undefined;
|
||||
});
|
||||
} catch {
|
||||
this.resetChanges();
|
||||
}
|
||||
};
|
||||
|
||||
lockView = async () => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug || !this.id || !this.service.lock) return;
|
||||
|
||||
const view = await this.service.lock(workspaceSlug, this.id, projectId);
|
||||
if (!view) return;
|
||||
|
||||
runInAction(() => {
|
||||
this.is_locked = view.is_locked;
|
||||
});
|
||||
} catch {
|
||||
this.is_locked = this.is_locked;
|
||||
}
|
||||
};
|
||||
|
||||
unlockView = async () => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug || !this.id || !this.service.unlock) return;
|
||||
|
||||
const view = await this.service.unlock(workspaceSlug, this.id, projectId);
|
||||
if (!view) return;
|
||||
|
||||
runInAction(() => {
|
||||
this.is_locked = view.is_locked;
|
||||
});
|
||||
} catch {
|
||||
this.is_locked = this.is_locked;
|
||||
}
|
||||
};
|
||||
|
||||
makeFavorite = async () => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug || !this.id || !this.service.makeFavorite) return;
|
||||
|
||||
const view = await this.service.makeFavorite(workspaceSlug, this.id, projectId);
|
||||
if (!view) return;
|
||||
|
||||
runInAction(() => {
|
||||
this.is_favorite = view.is_locked;
|
||||
});
|
||||
} catch {
|
||||
this.is_favorite = this.is_favorite;
|
||||
}
|
||||
};
|
||||
|
||||
removeFavorite = async () => {
|
||||
try {
|
||||
const { workspaceSlug, projectId } = this.store.app.router;
|
||||
if (!workspaceSlug || !this.id || !this.service.removeFavorite) return;
|
||||
|
||||
const view = await this.service.removeFavorite(workspaceSlug, this.id, projectId);
|
||||
if (!view) return;
|
||||
|
||||
runInAction(() => {
|
||||
this.is_favorite = view.is_locked;
|
||||
});
|
||||
} catch {
|
||||
this.is_favorite = this.is_favorite;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -5012,7 +5012,7 @@ fault@^2.0.0:
|
||||
dependencies:
|
||||
format "^0.2.0"
|
||||
|
||||
fflate@^0.4.1:
|
||||
fflate@^0.4.8:
|
||||
version "0.4.8"
|
||||
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
|
||||
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
|
||||
@@ -7171,12 +7171,18 @@ postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.29:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
posthog-js@^1.88.4:
|
||||
version "1.96.1"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.96.1.tgz#4f9719a24e4e14037b0e72d430194d7cdb576447"
|
||||
integrity sha512-kv1vQqYMt2BV3YHS+wxsbGuP+tz+M3y1AzNhz8TfkpY1HT8W/ONT0i0eQpeRr9Y+d4x/fZ6M4cXG5GMvi9lRCA==
|
||||
posthog-js@^1.105.0:
|
||||
version "1.105.6"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.105.6.tgz#3544de4389d5c7743fa420178bd127e49c4dc825"
|
||||
integrity sha512-5ITXsh29XIuNohHLy21nawGnfFZDpyt+yfnWge9sJl5yv0nNuoUmLiDgw1tJafoqGrfd5CUasKyzSI21HxsSeQ==
|
||||
dependencies:
|
||||
fflate "^0.4.1"
|
||||
fflate "^0.4.8"
|
||||
preact "^10.19.3"
|
||||
|
||||
preact@^10.19.3:
|
||||
version "10.19.4"
|
||||
resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.4.tgz#735d331d5b1bd2182cc36f2ba481fd6f0da3fe3b"
|
||||
integrity sha512-dwaX5jAh0Ga8uENBX1hSOujmKWgx9RtL80KaKUFLc6jb4vCEAc3EeZ0rnQO/FO4VgjfPMfoLFWnNG8bHuZ9VLw==
|
||||
|
||||
prebuild-install@^7.1.1:
|
||||
version "7.1.1"
|
||||
|
||||
Reference in New Issue
Block a user