forked from github/plane
Merge pull request #222 from makeplane/stage-release
dev: promote to production (v0.2-dev)
This commit is contained in:
commit
2e9b77cbdc
10
.eslintrc.js
Normal file
10
.eslintrc.js
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
// This tells ESLint to load the config from the package `config`
|
||||
// extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["apps/*/"],
|
||||
},
|
||||
},
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
@ -49,7 +49,7 @@ USER root
|
||||
RUN apk --update --no-cache add "bash~=5.1"
|
||||
COPY ./bin ./bin/
|
||||
|
||||
RUN chmod +x ./bin/channel-worker ./bin/takeoff ./bin/worker
|
||||
RUN chmod +x ./bin/takeoff ./bin/worker
|
||||
|
||||
USER captain
|
||||
|
||||
|
@ -1,3 +1,2 @@
|
||||
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
|
||||
worker: python manage.py rqworker
|
||||
channel-worker: python manage.py runworker issue-activites
|
@ -1,5 +1,5 @@
|
||||
# All the python scripts that are used for back migrations
|
||||
|
||||
from plane.db.models import ProjectIdentifier
|
||||
from plane.db.models import Issue, IssueComment
|
||||
|
||||
# Update description and description html values for old descriptions
|
||||
@ -40,3 +40,21 @@ def update_comments():
|
||||
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")
|
||||
|
@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
python manage.py wait_for_db
|
||||
python manage.py migrate
|
||||
python manage.py runworker issue-activites
|
@ -1 +0,0 @@
|
||||
from .issue_consumer import IssueConsumer
|
@ -1,547 +0,0 @@
|
||||
from channels.generic.websocket import SyncConsumer
|
||||
import json
|
||||
from plane.db.models import IssueActivity, Project, User, Issue, State, Label
|
||||
|
||||
|
||||
class IssueConsumer(SyncConsumer):
|
||||
|
||||
# Track Chnages in name
|
||||
def track_name(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("name") != requested_data.get("name"):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("name"),
|
||||
new_value=requested_data.get("name"),
|
||||
field="name",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to {requested_data.get('name')}",
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in parent issue
|
||||
def track_parent(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("parent") != requested_data.get("parent"):
|
||||
|
||||
if requested_data.get("parent") == None:
|
||||
old_parent = Issue.objects.get(pk=current_instance.get("parent"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{old_parent.sequence_id}",
|
||||
new_value=None,
|
||||
field="parent",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the parent issue to None",
|
||||
old_identifier=old_parent.id,
|
||||
new_identifier=None,
|
||||
)
|
||||
)
|
||||
else:
|
||||
new_parent = Issue.objects.get(pk=requested_data.get("parent"))
|
||||
old_parent = Issue.objects.filter(
|
||||
pk=current_instance.get("parent")
|
||||
).first()
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{old_parent.sequence_id}"
|
||||
if old_parent is not None
|
||||
else None,
|
||||
new_value=f"{project.identifier}-{new_parent.sequence_id}",
|
||||
field="parent",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the parent issue to {new_parent.name}",
|
||||
old_identifier=old_parent.id
|
||||
if old_parent is not None
|
||||
else None,
|
||||
new_identifier=new_parent.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in priority
|
||||
def track_priority(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("priority") != requested_data.get("priority"):
|
||||
if requested_data.get("priority") == None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("parent"),
|
||||
new_value=requested_data.get("parent"),
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the priority to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("priority"),
|
||||
new_value=requested_data.get("priority"),
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the priority to {requested_data.get('priority')}",
|
||||
)
|
||||
)
|
||||
|
||||
# Track chnages in state of the issue
|
||||
def track_state(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("state") != requested_data.get("state"):
|
||||
|
||||
new_state = State.objects.get(pk=requested_data.get("state", None))
|
||||
old_state = State.objects.get(pk=current_instance.get("state", None))
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=old_state.name,
|
||||
new_value=new_state.name,
|
||||
field="state",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the state to {new_state.name}",
|
||||
old_identifier=old_state.id,
|
||||
new_identifier=new_state.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Track issue description
|
||||
def track_description(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("description_html") != requested_data.get("description_html"):
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("description_html"),
|
||||
new_value=requested_data.get("description_html"),
|
||||
field="description",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the description to {requested_data.get('description_html')}",
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in issue target date
|
||||
def track_target_date(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("target_date") != requested_data.get("target_date"):
|
||||
if requested_data.get("target_date") == None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("target_date"),
|
||||
new_value=requested_data.get("target_date"),
|
||||
field="target_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the target date to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("target_date"),
|
||||
new_value=requested_data.get("target_date"),
|
||||
field="target_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the target date to {requested_data.get('target_date')}",
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in issue start date
|
||||
def track_start_date(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("start_date") != requested_data.get("start_date"):
|
||||
if requested_data.get("start_date") == None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("start_date"),
|
||||
new_value=requested_data.get("start_date"),
|
||||
field="start_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("start_date"),
|
||||
new_value=requested_data.get("start_date"),
|
||||
field="start_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to {requested_data.get('start_date')}",
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in issue labels
|
||||
def track_labels(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
# Label Addition
|
||||
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
||||
|
||||
for label in requested_data.get("labels_list"):
|
||||
if label not in current_instance.get("labels"):
|
||||
label = Label.objects.get(pk=label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=label.name,
|
||||
field="labels",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added label {label.name}",
|
||||
new_identifier=label.id,
|
||||
old_identifier=None,
|
||||
)
|
||||
)
|
||||
|
||||
# Label Removal
|
||||
if len(requested_data.get("labels_list")) < len(current_instance.get("labels")):
|
||||
|
||||
for label in current_instance.get("labels"):
|
||||
if label not in requested_data.get("labels_list"):
|
||||
label = Label.objects.get(pk=label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=label.name,
|
||||
new_value="",
|
||||
field="labels",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed label {label.name}",
|
||||
old_identifier=label.id,
|
||||
new_identifier=None,
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in issue assignees
|
||||
def track_assignees(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
|
||||
# Assignee Addition
|
||||
if len(requested_data.get("assignees_list")) > len(
|
||||
current_instance.get("assignees")
|
||||
):
|
||||
|
||||
for assignee in requested_data.get("assignees_list"):
|
||||
if assignee not in current_instance.get("assignees"):
|
||||
assignee = User.objects.get(pk=assignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=assignee.email,
|
||||
field="assignees",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added assignee {assignee.email}",
|
||||
new_identifier=actor.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Assignee Removal
|
||||
if len(requested_data.get("assignees_list")) < len(
|
||||
current_instance.get("assignees")
|
||||
):
|
||||
|
||||
for assignee in current_instance.get("assignees"):
|
||||
if assignee not in requested_data.get("assignees_list"):
|
||||
assignee = User.objects.get(pk=assignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=assignee.email,
|
||||
new_value="",
|
||||
field="assignee",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed assignee {assignee.email}",
|
||||
old_identifier=actor.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in blocking issues
|
||||
def track_blocks(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if len(requested_data.get("blocks_list")) > len(
|
||||
current_instance.get("blocked_issues")
|
||||
):
|
||||
|
||||
for block in requested_data.get("blocks_list"):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
blocked
|
||||
for blocked in current_instance.get("blocked_issues")
|
||||
if blocked.get("block") == block
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
issue = Issue.objects.get(pk=block)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Blocked Issue Removal
|
||||
if len(requested_data.get("blocks_list")) < len(
|
||||
current_instance.get("blocked_issues")
|
||||
):
|
||||
|
||||
for blocked in current_instance.get("blocked_issues"):
|
||||
if blocked.get("block") not in requested_data.get("blocks_list"):
|
||||
issue = Issue.objects.get(pk=blocked.get("block"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Track changes in blocked_by issues
|
||||
def track_blockings(
|
||||
self,
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if len(requested_data.get("blockers_list")) > len(
|
||||
current_instance.get("blocker_issues")
|
||||
):
|
||||
|
||||
for block in requested_data.get("blockers_list"):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
blocked
|
||||
for blocked in current_instance.get("blocker_issues")
|
||||
if blocked.get("blocked_by") == block
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
issue = Issue.objects.get(pk=block)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Blocked Issue Removal
|
||||
if len(requested_data.get("blockers_list")) < len(
|
||||
current_instance.get("blocker_issues")
|
||||
):
|
||||
|
||||
for blocked in current_instance.get("blocker_issues"):
|
||||
if blocked.get("blocked_by") not in requested_data.get("blockers_list"):
|
||||
issue = Issue.objects.get(pk=blocked.get("blocked_by"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Receive message from room group
|
||||
def issue_activity(self, event):
|
||||
|
||||
issue_activities = []
|
||||
# Remove event type:
|
||||
event.pop("type")
|
||||
|
||||
requested_data = json.loads(event.get("requested_data"))
|
||||
current_instance = json.loads(event.get("current_instance"))
|
||||
issue_id = event.get("issue_id")
|
||||
actor_id = event.get("actor_id")
|
||||
project_id = event.get("project_id")
|
||||
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
ISSUE_ACTIVITY_MAPPER = {
|
||||
"name": self.track_name,
|
||||
"parent": self.track_parent,
|
||||
"priority": self.track_priority,
|
||||
"state": self.track_state,
|
||||
"description": self.track_description,
|
||||
"target_date": self.track_target_date,
|
||||
"start_date": self.track_start_date,
|
||||
"labels_list": self.track_labels,
|
||||
"assignees_list": self.track_assignees,
|
||||
"blocks_list": self.track_blocks,
|
||||
"blockers_list": self.track_blockings,
|
||||
}
|
||||
|
||||
for key in requested_data:
|
||||
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
||||
if func is not None:
|
||||
func(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
)
|
||||
|
||||
# Save all the values to database
|
||||
IssueActivity.objects.bulk_create(issue_activities)
|
@ -4,6 +4,12 @@ from rest_framework.permissions import BasePermission, SAFE_METHODS
|
||||
# Module import
|
||||
from plane.db.models import WorkspaceMember, ProjectMember
|
||||
|
||||
# Permission Mappings
|
||||
Admin = 20
|
||||
Member = 15
|
||||
Viewer = 10
|
||||
Guest = 5
|
||||
|
||||
|
||||
class ProjectBasePermission(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
@ -13,16 +19,24 @@ class ProjectBasePermission(BasePermission):
|
||||
|
||||
## Safe Methods -> Handle the filtering logic in queryset
|
||||
if request.method in SAFE_METHODS:
|
||||
return True
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug, member=request.user
|
||||
).exists()
|
||||
|
||||
## Only workspace owners or admins can create the projects
|
||||
if request.method == "POST":
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace=view.workspace, member=request.user, role__in=[15, 20]
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[Admin, Member],
|
||||
).exists()
|
||||
|
||||
## Only Project Admins can update project attributes
|
||||
return ProjectMember.objects.filter(
|
||||
workspace=view.workspace, member=request.user, role=20
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role=Admin,
|
||||
project_id=view.project_id,
|
||||
).exists()
|
||||
|
||||
|
||||
@ -34,16 +48,23 @@ class ProjectMemberPermission(BasePermission):
|
||||
|
||||
## Safe Methods -> Handle the filtering logic in queryset
|
||||
if request.method in SAFE_METHODS:
|
||||
return True
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug, member=request.user
|
||||
).exists()
|
||||
## Only workspace owners or admins can create the projects
|
||||
if request.method == "POST":
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace=view.workspace, member=request.user, role__in=[15, 20]
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[Admin, Member],
|
||||
).exists()
|
||||
|
||||
## Only Project Admins can update project attributes
|
||||
return ProjectMember.objects.filter(
|
||||
workspace=view.workspace, member=request.user, role__in=[15, 20]
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[Admin, Member],
|
||||
project_id=view.project_id,
|
||||
).exists()
|
||||
|
||||
|
||||
@ -55,9 +76,16 @@ class ProjectEntityPermission(BasePermission):
|
||||
|
||||
## Safe Methods -> Handle the filtering logic in queryset
|
||||
if request.method in SAFE_METHODS:
|
||||
return True
|
||||
## Only workspace owners or admins can create the projects
|
||||
|
||||
return ProjectMember.objects.filter(
|
||||
workspace=view.workspace, member=request.user, role__in=[15, 20]
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
project_id=view.project_id,
|
||||
).exists()
|
||||
|
||||
## Only project members or admins can create and edit the project attributes
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[Admin, Member],
|
||||
project_id=view.project_id,
|
||||
).exists()
|
||||
|
@ -2,7 +2,15 @@
|
||||
from rest_framework.permissions import BasePermission, SAFE_METHODS
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import WorkspaceMember, ProjectMember
|
||||
from plane.db.models import WorkspaceMember
|
||||
|
||||
|
||||
|
||||
# Permission Mappings
|
||||
Owner = 20
|
||||
Admin = 15
|
||||
Member = 10
|
||||
Guest = 5
|
||||
|
||||
|
||||
# TODO: Move the below logic to python match - python v3.10
|
||||
@ -22,13 +30,15 @@ class WorkSpaceBasePermission(BasePermission):
|
||||
# allow only admins and owners to update the workspace settings
|
||||
if request.method in ["PUT", "PATCH"]:
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user, workspace=view.workspace, role__in=[15, 20]
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role__in=[Owner, Admin],
|
||||
).exists()
|
||||
|
||||
# allow only owner to delete the workspace
|
||||
if request.method == "DELETE":
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user, workspace=view.workspace, role=20
|
||||
member=request.user, workspace__slug=view.workspace_slug, role=Owner
|
||||
).exists()
|
||||
|
||||
|
||||
@ -39,5 +49,7 @@ class WorkSpaceAdminPermission(BasePermission):
|
||||
return False
|
||||
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user, workspace=view.workspace, role__in=[15, 20]
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role__in=[Owner, Admin],
|
||||
).exists()
|
||||
|
@ -29,7 +29,6 @@ from .issue import (
|
||||
IssueCommentSerializer,
|
||||
TimeLineIssueSerializer,
|
||||
IssuePropertySerializer,
|
||||
IssueLabelSerializer,
|
||||
BlockerIssueSerializer,
|
||||
BlockedIssueSerializer,
|
||||
IssueAssigneeSerializer,
|
||||
@ -40,3 +39,5 @@ from .issue import (
|
||||
)
|
||||
|
||||
from .module import ModuleWriteSerializer, ModuleSerializer, ModuleIssueSerializer
|
||||
|
||||
from .api_token import APITokenSerializer
|
8
apiserver/plane/api/serializers/api_token.py
Normal file
8
apiserver/plane/api/serializers/api_token.py
Normal file
@ -0,0 +1,8 @@
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import APIToken
|
||||
|
||||
|
||||
class APITokenSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = APIToken
|
||||
fields = "__all__"
|
@ -1,3 +1,6 @@
|
||||
# Third party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
@ -22,6 +25,7 @@ class CycleSerializer(BaseSerializer):
|
||||
class CycleIssueSerializer(BaseSerializer):
|
||||
|
||||
issue_detail = IssueStateSerializer(read_only=True, source="issue")
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CycleIssue
|
||||
|
@ -432,6 +432,7 @@ class IssueSerializer(BaseSerializer):
|
||||
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
|
||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
|
@ -93,7 +93,7 @@ class ModuleWriteSerializer(BaseSerializer):
|
||||
links = validated_data.pop("links_list", None)
|
||||
|
||||
if members is not None:
|
||||
ModuleIssue.objects.filter(module=instance).delete()
|
||||
ModuleMember.objects.filter(module=instance).delete()
|
||||
ModuleMember.objects.bulk_create(
|
||||
[
|
||||
ModuleMember(
|
||||
@ -150,6 +150,7 @@ class ModuleIssueSerializer(BaseSerializer):
|
||||
|
||||
module_detail = ModuleFlatSerializer(read_only=True, source="module")
|
||||
issue_detail = IssueStateSerializer(read_only=True, source="issue")
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ModuleIssue
|
||||
|
@ -4,73 +4,98 @@ from django.urls import path
|
||||
# Create your urls here.
|
||||
|
||||
from plane.api.views import (
|
||||
# Authentication
|
||||
SignUpEndpoint,
|
||||
SignInEndpoint,
|
||||
SignOutEndpoint,
|
||||
MagicSignInEndpoint,
|
||||
MagicSignInGenerateEndpoint,
|
||||
OauthEndpoint,
|
||||
## End Authentication
|
||||
# Auth Extended
|
||||
ForgotPasswordEndpoint,
|
||||
PeopleEndpoint,
|
||||
UserEndpoint,
|
||||
VerifyEmailEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
RequestEmailVerificationEndpoint,
|
||||
OauthEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
)
|
||||
|
||||
from plane.api.views import (
|
||||
UserWorkspaceInvitationsEndpoint,
|
||||
## End Auth Extender
|
||||
# User
|
||||
UserEndpoint,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
## End User
|
||||
# Workspaces
|
||||
WorkSpaceViewSet,
|
||||
UserWorkspaceInvitationsEndpoint,
|
||||
UserWorkSpacesEndpoint,
|
||||
InviteWorkspaceEndpoint,
|
||||
JoinWorkspaceEndpoint,
|
||||
WorkSpaceMemberViewSet,
|
||||
WorkspaceInvitationsViewset,
|
||||
UserWorkspaceInvitationsEndpoint,
|
||||
WorkspaceMemberUserEndpoint,
|
||||
WorkspaceMemberUserViewsEndpoint,
|
||||
WorkSpaceAvailabilityCheckEndpoint,
|
||||
TeamMemberViewSet,
|
||||
AddTeamToProjectEndpoint,
|
||||
UserLastProjectWithWorkspaceEndpoint,
|
||||
UserWorkspaceInvitationEndpoint,
|
||||
## End Workspaces
|
||||
# File Assets
|
||||
FileAssetEndpoint,
|
||||
## End File Assets
|
||||
# Projects
|
||||
ProjectViewSet,
|
||||
InviteProjectEndpoint,
|
||||
ProjectMemberViewSet,
|
||||
ProjectMemberInvitationsViewset,
|
||||
StateViewSet,
|
||||
ShortCutViewSet,
|
||||
ViewViewSet,
|
||||
CycleViewSet,
|
||||
FileAssetEndpoint,
|
||||
ProjectMemberUserEndpoint,
|
||||
AddMemberToProjectEndpoint,
|
||||
ProjectJoinEndpoint,
|
||||
UserProjectInvitationsViewset,
|
||||
ProjectIdentifierEndpoint,
|
||||
## End Projects
|
||||
# Issues
|
||||
IssueViewSet,
|
||||
WorkSpaceIssuesEndpoint,
|
||||
IssueActivityEndpoint,
|
||||
IssueCommentViewSet,
|
||||
TeamMemberViewSet,
|
||||
TimeLineIssueViewSet,
|
||||
CycleIssueViewSet,
|
||||
IssuePropertyViewSet,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UserWorkspaceInvitationEndpoint,
|
||||
UserProjectInvitationsViewset,
|
||||
ProjectIdentifierEndpoint,
|
||||
LabelViewSet,
|
||||
AddMemberToProjectEndpoint,
|
||||
ProjectJoinEndpoint,
|
||||
UserWorkSpaceIssues,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
ProjectUserViewsEndpoint,
|
||||
TimeLineIssueViewSet,
|
||||
IssuePropertyViewSet,
|
||||
LabelViewSet,
|
||||
SubIssuesEndpoint,
|
||||
## End Issues
|
||||
# States
|
||||
StateViewSet,
|
||||
## End States
|
||||
# Shortcuts
|
||||
ShortCutViewSet,
|
||||
## End Shortcuts
|
||||
# Views
|
||||
ViewViewSet,
|
||||
## End Views
|
||||
# Cycles
|
||||
CycleViewSet,
|
||||
CycleIssueViewSet,
|
||||
## End Cycles
|
||||
# Modules
|
||||
ModuleViewSet,
|
||||
ModuleIssueViewSet,
|
||||
UserLastProjectWithWorkspaceEndpoint,
|
||||
UserWorkSpaceIssues,
|
||||
ProjectMemberUserEndpoint,
|
||||
WorkspaceMemberUserEndpoint,
|
||||
WorkspaceMemberUserViewsEndpoint,
|
||||
WorkSpaceAvailabilityCheckEndpoint,
|
||||
## End Modules
|
||||
# Api Tokens
|
||||
ApiTokenEndpoint,
|
||||
## End Api Tokens
|
||||
)
|
||||
|
||||
from plane.api.views.project import AddTeamToProjectEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# Social Auth
|
||||
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
|
||||
# Auth
|
||||
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
||||
path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"),
|
||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||
# Magic Sign In/Up
|
||||
path(
|
||||
@ -95,8 +120,6 @@ urlpatterns = [
|
||||
ForgotPasswordEndpoint.as_view(),
|
||||
name="forgot-password",
|
||||
),
|
||||
# List Users
|
||||
path("users/", PeopleEndpoint.as_view()),
|
||||
# User Profile
|
||||
path(
|
||||
"users/me/",
|
||||
@ -521,6 +544,11 @@ urlpatterns = [
|
||||
UserWorkSpaceIssues.as_view(),
|
||||
name="workspace-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
|
||||
SubIssuesEndpoint.as_view(),
|
||||
name="sub-issues",
|
||||
),
|
||||
## End Issues
|
||||
## Issue Activity
|
||||
path(
|
||||
@ -654,9 +682,8 @@ urlpatterns = [
|
||||
name="project-module-issues",
|
||||
),
|
||||
## End Modules
|
||||
# path(
|
||||
# "issues/<int:pk>/all/",
|
||||
# IssueViewSet.as_view({"get": "list_issue_history_comments"}),
|
||||
# name="Issue history and comments",
|
||||
# ),
|
||||
# API Tokens
|
||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-token"),
|
||||
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-token"),
|
||||
## End API Tokens
|
||||
]
|
||||
|
@ -13,7 +13,6 @@ from .project import (
|
||||
ProjectMemberUserEndpoint,
|
||||
)
|
||||
from .people import (
|
||||
PeopleEndpoint,
|
||||
UserEndpoint,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
)
|
||||
@ -52,6 +51,7 @@ from .issue import (
|
||||
LabelViewSet,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
UserWorkSpaceIssues,
|
||||
SubIssuesEndpoint,
|
||||
)
|
||||
|
||||
from .auth_extended import (
|
||||
@ -64,6 +64,7 @@ from .auth_extended import (
|
||||
|
||||
|
||||
from .authentication import (
|
||||
SignUpEndpoint,
|
||||
SignInEndpoint,
|
||||
SignOutEndpoint,
|
||||
MagicSignInEndpoint,
|
||||
@ -71,3 +72,5 @@ from .authentication import (
|
||||
)
|
||||
|
||||
from .module import ModuleViewSet, ModuleIssueViewSet
|
||||
|
||||
from .api_token import ApiTokenEndpoint
|
62
apiserver/plane/api/views/api_token.py
Normal file
62
apiserver/plane/api/views/api_token.py
Normal file
@ -0,0 +1,62 @@
|
||||
# Python import
|
||||
from uuid import uuid4
|
||||
|
||||
# Third party
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module import
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import APIToken
|
||||
from plane.api.serializers import APITokenSerializer
|
||||
|
||||
|
||||
class ApiTokenEndpoint(BaseAPIView):
|
||||
def post(self, request):
|
||||
try:
|
||||
|
||||
label = request.data.get("label", str(uuid4().hex))
|
||||
|
||||
api_token = APIToken.objects.create(
|
||||
label=label,
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
serializer = APITokenSerializer(api_token)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
api_tokens = APIToken.objects.filter(user=request.user)
|
||||
serializer = APITokenSerializer(api_tokens, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def delete(self, request, pk):
|
||||
try:
|
||||
api_token = APIToken.objects.get(pk=pk)
|
||||
api_token.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except APIToken.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Token does not exists"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
@ -6,7 +6,7 @@ from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import FileAsset, Workspace
|
||||
from plane.db.models import FileAsset
|
||||
from plane.api.serializers import FileAssetSerializer
|
||||
|
||||
|
||||
@ -18,8 +18,8 @@ class FileAssetEndpoint(BaseAPIView):
|
||||
A viewset for viewing and editing task instances.
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
files = FileAsset.objects.all()
|
||||
def get(self, request, slug):
|
||||
files = FileAsset.objects.filter(workspace__slug=slug)
|
||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Django imports
|
||||
from django.urls import resolve
|
||||
from django.conf import settings
|
||||
|
||||
# Third part imports
|
||||
from rest_framework import status
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
@ -39,32 +40,23 @@ class BaseViewSet(ModelViewSet, BasePaginator):
|
||||
return self.model.objects.all()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise APIException(
|
||||
"Please check the view", status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.db import connection
|
||||
print(f'# of Queries: {len(connection.queries)}')
|
||||
|
||||
print(
|
||||
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
||||
)
|
||||
return response
|
||||
|
||||
@property
|
||||
def workspace_slug(self):
|
||||
return self.kwargs.get("slug", None)
|
||||
|
||||
@property
|
||||
def workspace(self):
|
||||
if self.workspace_slug:
|
||||
try:
|
||||
return Workspace.objects.get(slug=self.workspace_slug)
|
||||
except Workspace.DoesNotExist:
|
||||
raise NotFound(detail="Workspace does not exist")
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
project_id = self.kwargs.get("project_id", None)
|
||||
@ -74,16 +66,6 @@ class BaseViewSet(ModelViewSet, BasePaginator):
|
||||
if resolve(self.request.path_info).url_name == "project":
|
||||
return self.kwargs.get("pk", None)
|
||||
|
||||
@property
|
||||
def project(self):
|
||||
if self.project_id:
|
||||
try:
|
||||
return Project.objects.get(pk=self.project_id)
|
||||
except Project.DoesNotExist:
|
||||
raise NotFound(detail="Project does not exist")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class BaseAPIView(APIView, BasePaginator):
|
||||
|
||||
@ -110,33 +92,16 @@ class BaseAPIView(APIView, BasePaginator):
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.db import connection
|
||||
print(f'# of Queries: {len(connection.queries)}')
|
||||
|
||||
print(
|
||||
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
||||
)
|
||||
return response
|
||||
|
||||
@property
|
||||
def workspace_slug(self):
|
||||
return self.kwargs.get("slug", None)
|
||||
|
||||
@property
|
||||
def workspace(self):
|
||||
if self.workspace_slug:
|
||||
try:
|
||||
return Workspace.objects.get(slug=self.workspace_slug)
|
||||
except Workspace.DoesNotExist:
|
||||
raise NotFound(detail="Workspace does not exist")
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
return self.kwargs.get("project_id", None)
|
||||
|
||||
@property
|
||||
def project(self):
|
||||
if self.project_id:
|
||||
try:
|
||||
return Project.objects.get(pk=self.project_id)
|
||||
except Project.DoesNotExist:
|
||||
raise NotFound(detail="Project does not exist")
|
||||
else:
|
||||
return None
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Django imports
|
||||
from django.db.models import OuterRef, Func, F
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
@ -32,6 +35,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("owned_by")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@ -55,6 +59,12 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.annotate(
|
||||
sub_issues_count=Issue.objects.filter(parent=OuterRef("issue_id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
@ -62,8 +72,8 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("cycle")
|
||||
.select_related("issue")
|
||||
.select_related("issue__state")
|
||||
.select_related("issue", "issue__state", "issue__project")
|
||||
.prefetch_related("issue__assignees", "issue__labels")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
@ -3,16 +3,13 @@ import json
|
||||
from itertools import groupby, chain
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import Count, Sum
|
||||
from django.db.models import Prefetch, OuterRef, Func, F
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from sentry_sdk import capture_exception
|
||||
from channels.layers import get_channel_layer
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
# Module imports
|
||||
from . import BaseViewSet, BaseAPIView
|
||||
@ -43,6 +40,7 @@ from plane.db.models import (
|
||||
CycleIssue,
|
||||
ModuleIssue,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
|
||||
class IssueViewSet(BaseViewSet):
|
||||
@ -73,12 +71,12 @@ class IssueViewSet(BaseViewSet):
|
||||
|
||||
def perform_update(self, serializer):
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
current_instance = Issue.objects.filter(pk=self.kwargs.get("pk", None)).first()
|
||||
current_instance = (
|
||||
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||
)
|
||||
if current_instance is not None:
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.send)(
|
||||
"issue-activites",
|
||||
issue_activity.delay(
|
||||
{
|
||||
"type": "issue.activity",
|
||||
"requested_data": requested_data,
|
||||
@ -94,9 +92,16 @@ class IssueViewSet(BaseViewSet):
|
||||
return super().perform_update(serializer)
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.annotate(
|
||||
sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
@ -126,7 +131,9 @@ class IssueViewSet(BaseViewSet):
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_module",
|
||||
queryset=ModuleIssue.objects.select_related("module", "issue"),
|
||||
queryset=ModuleIssue.objects.select_related(
|
||||
"module", "issue"
|
||||
).prefetch_related("module__members"),
|
||||
),
|
||||
)
|
||||
)
|
||||
@ -162,13 +169,22 @@ class IssueViewSet(BaseViewSet):
|
||||
|
||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
||||
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True).data,
|
||||
return Response(
|
||||
{
|
||||
"next_cursor": str(0),
|
||||
"prev_cursor": str(0),
|
||||
"next_page_results": False,
|
||||
"prev_page_results": False,
|
||||
"count": issue_queryset.count(),
|
||||
"total_pages": 1,
|
||||
"extra_stats": {},
|
||||
"results": IssueSerializer(issue_queryset, many=True).data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
@ -207,8 +223,48 @@ class IssueViewSet(BaseViewSet):
|
||||
class UserWorkSpaceIssues(BaseAPIView):
|
||||
def get(self, request, slug):
|
||||
try:
|
||||
issues = Issue.objects.filter(
|
||||
assignees__in=[request.user], workspace__slug=slug
|
||||
issues = (
|
||||
Issue.objects.filter(assignees__in=[request.user], workspace__slug=slug)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"blocked_issues",
|
||||
queryset=IssueBlocker.objects.select_related(
|
||||
"blocked_by", "block"
|
||||
),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"blocker_issues",
|
||||
queryset=IssueBlocker.objects.select_related(
|
||||
"block", "blocked_by"
|
||||
),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle",
|
||||
queryset=CycleIssue.objects.select_related("cycle", "issue"),
|
||||
),
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_module",
|
||||
queryset=ModuleIssue.objects.select_related("module", "issue"),
|
||||
),
|
||||
)
|
||||
)
|
||||
serializer = IssueSerializer(issues, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@ -468,3 +524,62 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class SubIssuesEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
try:
|
||||
|
||||
sub_issues = (
|
||||
Issue.objects.filter(
|
||||
parent_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"blocked_issues",
|
||||
queryset=IssueBlocker.objects.select_related(
|
||||
"blocked_by", "block"
|
||||
),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"blocker_issues",
|
||||
queryset=IssueBlocker.objects.select_related(
|
||||
"block", "blocked_by"
|
||||
),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle",
|
||||
queryset=CycleIssue.objects.select_related("cycle", "issue"),
|
||||
),
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_module",
|
||||
queryset=ModuleIssue.objects.select_related("module", "issue"),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
serializer = IssueSerializer(sub_issues, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Django Imports
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import Prefetch, F, OuterRef, Func
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
@ -15,7 +15,13 @@ from plane.api.serializers import (
|
||||
ModuleIssueSerializer,
|
||||
)
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import Module, ModuleIssue, Project, Issue, ModuleLink
|
||||
from plane.db.models import (
|
||||
Module,
|
||||
ModuleIssue,
|
||||
Project,
|
||||
Issue,
|
||||
ModuleLink,
|
||||
)
|
||||
|
||||
|
||||
class ModuleViewSet(BaseViewSet):
|
||||
@ -45,13 +51,15 @@ class ModuleViewSet(BaseViewSet):
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_module",
|
||||
queryset=ModuleIssue.objects.select_related("module", "issue"),
|
||||
queryset=ModuleIssue.objects.select_related(
|
||||
"module", "issue", "issue__state", "issue__project"
|
||||
).prefetch_related("issue__assignees", "issue__labels"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"link_module",
|
||||
queryset=ModuleLink.objects.select_related("module"),
|
||||
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -110,6 +118,12 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.annotate(
|
||||
sub_issues_count=Issue.objects.filter(parent=OuterRef("issue"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(module_id=self.kwargs.get("module_id"))
|
||||
@ -117,7 +131,9 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("module")
|
||||
.select_related("issue")
|
||||
.select_related("issue", "issue__state", "issue__project")
|
||||
.prefetch_related("issue__assignees", "issue__labels")
|
||||
.prefetch_related("module__members")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
@ -223,8 +223,8 @@ class OauthEndpoint(BaseAPIView):
|
||||
username=username,
|
||||
email=email,
|
||||
mobile_number=mobile_number,
|
||||
first_name=data["first_name"],
|
||||
last_name=data["last_name"],
|
||||
first_name=data.get("first_name", ""),
|
||||
last_name=data.get("last_name", ""),
|
||||
is_email_verified=email_verified,
|
||||
is_password_autoset=True,
|
||||
)
|
||||
|
@ -7,48 +7,11 @@ from sentry_sdk import capture_exception
|
||||
# Module imports
|
||||
from plane.api.serializers import (
|
||||
UserSerializer,
|
||||
WorkSpaceSerializer,
|
||||
)
|
||||
|
||||
from plane.api.views.base import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import User, Workspace
|
||||
|
||||
|
||||
class PeopleEndpoint(BaseAPIView):
|
||||
|
||||
filterset_fields = ("date_joined",)
|
||||
|
||||
search_fields = (
|
||||
"^first_name",
|
||||
"^last_name",
|
||||
"^email",
|
||||
"^username",
|
||||
)
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
users = User.objects.all().order_by("-date_joined")
|
||||
if (
|
||||
request.GET.get("search", None) is not None
|
||||
and len(request.GET.get("search")) < 3
|
||||
):
|
||||
return Response(
|
||||
{"message": "Search term must be at least 3 characters long"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=self.filter_queryset(users),
|
||||
on_results=lambda data: UserSerializer(data, many=True).data,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"message": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class UserEndpoint(BaseViewSet):
|
||||
serializer_class = UserSerializer
|
||||
model = User
|
||||
|
@ -67,7 +67,9 @@ class ProjectViewSet(BaseViewSet):
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
|
||||
.select_related("workspace", "workspace__owner")
|
||||
.select_related(
|
||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@ -294,7 +296,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(email=self.request.user.email)
|
||||
.select_related("workspace")
|
||||
.select_related("workspace", "workspace__owner", "project")
|
||||
)
|
||||
|
||||
def create(self, request):
|
||||
@ -349,6 +351,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.select_related("project")
|
||||
.select_related("member")
|
||||
.select_related("workspace", "workspace__owner")
|
||||
)
|
||||
|
||||
|
||||
@ -481,6 +484,7 @@ class ProjectMemberInvitationsViewset(BaseViewSet):
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.select_related("project")
|
||||
.select_related("workspace", "workspace__owner")
|
||||
)
|
||||
|
||||
|
||||
@ -496,7 +500,12 @@ class ProjectMemberInviteDetailViewSet(BaseViewSet):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(super().get_queryset().select_related("project"))
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.select_related("project")
|
||||
.select_related("workspace", "workspace__owner")
|
||||
)
|
||||
|
||||
|
||||
class ProjectIdentifierEndpoint(BaseAPIView):
|
||||
|
@ -10,7 +10,7 @@ from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.db.models import CharField, Count
|
||||
from django.db.models import CharField, Count, OuterRef, Func, F
|
||||
from django.db.models.functions import Cast
|
||||
|
||||
# Third party modules
|
||||
@ -111,6 +111,14 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
|
||||
member_count = (
|
||||
WorkspaceMember.objects.filter(workspace=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
|
||||
workspace = (
|
||||
Workspace.objects.prefetch_related(
|
||||
Prefetch("workspace_member", queryset=WorkspaceMember.objects.all())
|
||||
@ -119,7 +127,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
workspace_member__member=request.user,
|
||||
)
|
||||
.select_related("owner")
|
||||
).annotate(total_members=Count("workspace_member"))
|
||||
).annotate(total_members=member_count)
|
||||
|
||||
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@ -176,7 +184,7 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
||||
workspace_members = WorkspaceMember.objects.filter(
|
||||
workspace_id=workspace.id,
|
||||
member__email__in=[email.get("email") for email in emails],
|
||||
)
|
||||
).select_related("member", "workspace", "workspace__owner")
|
||||
|
||||
if len(workspace_members):
|
||||
return Response(
|
||||
@ -339,7 +347,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace")
|
||||
.select_related("workspace", "workspace__owner")
|
||||
)
|
||||
|
||||
|
||||
@ -353,7 +361,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(email=self.request.user.email)
|
||||
.select_related("workspace")
|
||||
.select_related("workspace", "workspace__owner")
|
||||
)
|
||||
|
||||
def create(self, request):
|
||||
@ -524,7 +532,7 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
||||
|
||||
project_member = ProjectMember.objects.filter(
|
||||
workspace_id=last_workspace_id, member=request.user
|
||||
).select_related("workspace", "project", "member")
|
||||
).select_related("workspace", "project", "member", "workspace__owner")
|
||||
|
||||
project_member_serializer = ProjectMemberSerializer(
|
||||
project_member, many=True
|
||||
|
@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
from channels.routing import ProtocolTypeRouter, ChannelNameRouter
|
||||
from channels.routing import ProtocolTypeRouter
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
django_asgi_app = get_asgi_application()
|
||||
@ -10,15 +10,9 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
|
||||
# Initialize Django ASGI application early to ensure the AppRegistry
|
||||
# is populated before importing code that may import ORM models.
|
||||
|
||||
from plane.api.consumers import IssueConsumer
|
||||
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
"http": get_asgi_application(),
|
||||
"channel": ChannelNameRouter(
|
||||
{
|
||||
"issue-activites": IssueConsumer.as_asgi(),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
553
apiserver/plane/bgtasks/issue_activites_task.py
Normal file
553
apiserver/plane/bgtasks/issue_activites_task.py
Normal file
@ -0,0 +1,553 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
# Third Party imports
|
||||
from django_rq import job
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import User, Issue, Project, Label, IssueActivity, State
|
||||
|
||||
|
||||
# Track Chnages in name
|
||||
def track_name(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("name") != requested_data.get("name"):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("name"),
|
||||
new_value=requested_data.get("name"),
|
||||
field="name",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to {requested_data.get('name')}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in parent issue
|
||||
def track_parent(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("parent") != requested_data.get("parent"):
|
||||
|
||||
if requested_data.get("parent") == None:
|
||||
old_parent = Issue.objects.get(pk=current_instance.get("parent"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{old_parent.sequence_id}",
|
||||
new_value=None,
|
||||
field="parent",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the parent issue to None",
|
||||
old_identifier=old_parent.id,
|
||||
new_identifier=None,
|
||||
)
|
||||
)
|
||||
else:
|
||||
new_parent = Issue.objects.get(pk=requested_data.get("parent"))
|
||||
old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first()
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{old_parent.sequence_id}"
|
||||
if old_parent is not None
|
||||
else None,
|
||||
new_value=f"{project.identifier}-{new_parent.sequence_id}",
|
||||
field="parent",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the parent issue to {new_parent.name}",
|
||||
old_identifier=old_parent.id if old_parent is not None else None,
|
||||
new_identifier=new_parent.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in priority
|
||||
def track_priority(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("priority") != requested_data.get("priority"):
|
||||
if requested_data.get("priority") == None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("priority"),
|
||||
new_value=None,
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the priority to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("priority"),
|
||||
new_value=requested_data.get("priority"),
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the priority to {requested_data.get('priority')}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track chnages in state of the issue
|
||||
def track_state(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("state") != requested_data.get("state"):
|
||||
|
||||
new_state = State.objects.get(pk=requested_data.get("state", None))
|
||||
old_state = State.objects.get(pk=current_instance.get("state", None))
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=old_state.name,
|
||||
new_value=new_state.name,
|
||||
field="state",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the state to {new_state.name}",
|
||||
old_identifier=old_state.id,
|
||||
new_identifier=new_state.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track issue description
|
||||
def track_description(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("description_html") != requested_data.get(
|
||||
"description_html"
|
||||
):
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("description_html"),
|
||||
new_value=requested_data.get("description_html"),
|
||||
field="description",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the description to {requested_data.get('description_html')}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in issue target date
|
||||
def track_target_date(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("target_date") != requested_data.get("target_date"):
|
||||
if requested_data.get("target_date") == None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("target_date"),
|
||||
new_value=requested_data.get("target_date"),
|
||||
field="target_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the target date to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("target_date"),
|
||||
new_value=requested_data.get("target_date"),
|
||||
field="target_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the target date to {requested_data.get('target_date')}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in issue start date
|
||||
def track_start_date(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if current_instance.get("start_date") != requested_data.get("start_date"):
|
||||
if requested_data.get("start_date") == None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("start_date"),
|
||||
new_value=requested_data.get("start_date"),
|
||||
field="start_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("start_date"),
|
||||
new_value=requested_data.get("start_date"),
|
||||
field="start_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to {requested_data.get('start_date')}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in issue labels
|
||||
def track_labels(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
# Label Addition
|
||||
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
||||
|
||||
for label in requested_data.get("labels_list"):
|
||||
if label not in current_instance.get("labels"):
|
||||
label = Label.objects.get(pk=label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=label.name,
|
||||
field="labels",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added label {label.name}",
|
||||
new_identifier=label.id,
|
||||
old_identifier=None,
|
||||
)
|
||||
)
|
||||
|
||||
# Label Removal
|
||||
if len(requested_data.get("labels_list")) < len(current_instance.get("labels")):
|
||||
|
||||
for label in current_instance.get("labels"):
|
||||
if label not in requested_data.get("labels_list"):
|
||||
label = Label.objects.get(pk=label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=label.name,
|
||||
new_value="",
|
||||
field="labels",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed label {label.name}",
|
||||
old_identifier=label.id,
|
||||
new_identifier=None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in issue assignees
|
||||
def track_assignees(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
|
||||
# Assignee Addition
|
||||
if len(requested_data.get("assignees_list")) > len(
|
||||
current_instance.get("assignees")
|
||||
):
|
||||
|
||||
for assignee in requested_data.get("assignees_list"):
|
||||
if assignee not in current_instance.get("assignees"):
|
||||
assignee = User.objects.get(pk=assignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=assignee.email,
|
||||
field="assignees",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added assignee {assignee.email}",
|
||||
new_identifier=actor.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Assignee Removal
|
||||
if len(requested_data.get("assignees_list")) < len(
|
||||
current_instance.get("assignees")
|
||||
):
|
||||
|
||||
for assignee in current_instance.get("assignees"):
|
||||
if assignee not in requested_data.get("assignees_list"):
|
||||
assignee = User.objects.get(pk=assignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=assignee.email,
|
||||
new_value="",
|
||||
field="assignee",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed assignee {assignee.email}",
|
||||
old_identifier=actor.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in blocking issues
|
||||
def track_blocks(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if len(requested_data.get("blocks_list")) > len(
|
||||
current_instance.get("blocked_issues")
|
||||
):
|
||||
|
||||
for block in requested_data.get("blocks_list"):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
blocked
|
||||
for blocked in current_instance.get("blocked_issues")
|
||||
if blocked.get("block") == block
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
issue = Issue.objects.get(pk=block)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Blocked Issue Removal
|
||||
if len(requested_data.get("blocks_list")) < len(
|
||||
current_instance.get("blocked_issues")
|
||||
):
|
||||
|
||||
for blocked in current_instance.get("blocked_issues"):
|
||||
if blocked.get("block") not in requested_data.get("blocks_list"):
|
||||
issue = Issue.objects.get(pk=blocked.get("block"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in blocked_by issues
|
||||
def track_blockings(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
if len(requested_data.get("blockers_list")) > len(
|
||||
current_instance.get("blocker_issues")
|
||||
):
|
||||
|
||||
for block in requested_data.get("blockers_list"):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
blocked
|
||||
for blocked in current_instance.get("blocker_issues")
|
||||
if blocked.get("blocked_by") == block
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
issue = Issue.objects.get(pk=block)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Blocked Issue Removal
|
||||
if len(requested_data.get("blockers_list")) < len(
|
||||
current_instance.get("blocker_issues")
|
||||
):
|
||||
|
||||
for blocked in current_instance.get("blocker_issues"):
|
||||
if blocked.get("blocked_by") not in requested_data.get("blockers_list"):
|
||||
issue = Issue.objects.get(pk=blocked.get("blocked_by"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Receive message from room group
|
||||
@job("default")
|
||||
def issue_activity(event):
|
||||
try:
|
||||
issue_activities = []
|
||||
|
||||
requested_data = json.loads(event.get("requested_data"))
|
||||
current_instance = json.loads(event.get("current_instance"))
|
||||
issue_id = event.get("issue_id")
|
||||
actor_id = event.get("actor_id")
|
||||
project_id = event.get("project_id")
|
||||
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
ISSUE_ACTIVITY_MAPPER = {
|
||||
"name": track_name,
|
||||
"parent": track_parent,
|
||||
"priority": track_priority,
|
||||
"state": track_state,
|
||||
"description": track_description,
|
||||
"target_date": track_target_date,
|
||||
"start_date": track_start_date,
|
||||
"labels_list": track_labels,
|
||||
"assignees_list": track_assignees,
|
||||
"blocks_list": track_blocks,
|
||||
"blockers_list": track_blockings,
|
||||
}
|
||||
|
||||
for key in requested_data:
|
||||
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
||||
if func is not None:
|
||||
func(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
)
|
||||
|
||||
# Save all the values to database
|
||||
_ = IssueActivity.objects.bulk_create(issue_activities)
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return
|
@ -1,52 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
from fieldsignals import post_save_changed
|
||||
|
||||
|
||||
class DbConfig(AppConfig):
|
||||
name = "plane.db"
|
||||
|
||||
# def ready(self):
|
||||
|
||||
# post_save_changed.connect(
|
||||
# self.model_activity,
|
||||
# sender=self.get_model("Issue"),
|
||||
# )
|
||||
|
||||
# def model_activity(self, sender, instance, changed_fields, **kwargs):
|
||||
|
||||
# verb = "created" if instance._state.adding else "changed"
|
||||
|
||||
# import inspect
|
||||
|
||||
# for frame_record in inspect.stack():
|
||||
# if frame_record[3] == "get_response":
|
||||
# request = frame_record[0].f_locals["request"]
|
||||
# REQUEST_METHOD = request.method
|
||||
|
||||
# if REQUEST_METHOD == "POST":
|
||||
|
||||
# self.get_model("IssueActivity").objects.create(
|
||||
# issue=instance, project=instance.project, actor=instance.created_by
|
||||
# )
|
||||
|
||||
# elif REQUEST_METHOD == "PATCH":
|
||||
|
||||
# try:
|
||||
# del changed_fields["updated_at"]
|
||||
# del changed_fields["updated_by"]
|
||||
# except KeyError as e:
|
||||
# pass
|
||||
|
||||
# for field_name, (old, new) in changed_fields.items():
|
||||
# field = field_name
|
||||
# old_value = old
|
||||
# new_value = new
|
||||
# self.get_model("IssueActivity").objects.create(
|
||||
# issue=instance,
|
||||
# verb=verb,
|
||||
# field=field,
|
||||
# old_value=old_value,
|
||||
# new_value=new_value,
|
||||
# project=instance.project,
|
||||
# actor=instance.updated_by,
|
||||
# )
|
||||
|
57
apiserver/plane/db/migrations/0018_auto_20230130_0119.py
Normal file
57
apiserver/plane/db/migrations/0018_auto_20230130_0119.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Generated by Django 3.2.16 on 2023-01-29 19:49
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import plane.db.models.api_token
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0017_alter_workspace_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='is_bot',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issue',
|
||||
name='description',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issue',
|
||||
name='description_html',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issue',
|
||||
name='description_stripped',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='APIToken',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('token', models.CharField(default=plane.db.models.api_token.generate_token, max_length=255, unique=True)),
|
||||
('label', models.CharField(default=plane.db.models.api_token.generate_label_token, max_length=255)),
|
||||
('user_type', models.PositiveSmallIntegerField(choices=[(0, 'Human'), (1, 'Bot')], default=0)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='apitoken_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='apitoken_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bot_tokens', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'API Token',
|
||||
'verbose_name_plural': 'API Tokems',
|
||||
'db_table': 'api_tokens',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
]
|
23
apiserver/plane/db/migrations/0019_auto_20230131_0049.py
Normal file
23
apiserver/plane/db/migrations/0019_auto_20230131_0049.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.16 on 2023-01-30 19:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0018_auto_20230130_0119'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='issueactivity',
|
||||
name='new_value',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='New Value'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issueactivity',
|
||||
name='old_value',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Old Value'),
|
||||
),
|
||||
]
|
@ -38,3 +38,5 @@ from .shortcut import Shortcut
|
||||
from .view import View
|
||||
|
||||
from .module import Module, ModuleMember, ModuleIssue, ModuleLink
|
||||
|
||||
from .api_token import APIToken
|
39
apiserver/plane/db/models/api_token.py
Normal file
39
apiserver/plane/db/models/api_token.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Python imports
|
||||
from uuid import uuid4
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
def generate_label_token():
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
def generate_token():
|
||||
return uuid4().hex + uuid4().hex
|
||||
|
||||
|
||||
class APIToken(BaseModel):
|
||||
|
||||
token = models.CharField(max_length=255, unique=True, default=generate_token)
|
||||
label = models.CharField(max_length=255, default=generate_label_token)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="bot_tokens",
|
||||
)
|
||||
user_type = models.PositiveSmallIntegerField(
|
||||
choices=((0, "Human"), (1, "Bot")), default=0
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "API Token"
|
||||
verbose_name_plural = "API Tokems"
|
||||
db_table = "api_tokens"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user.name)
|
@ -32,9 +32,9 @@ class Issue(ProjectBaseModel):
|
||||
related_name="state_issue",
|
||||
)
|
||||
name = models.CharField(max_length=255, verbose_name="Issue Name")
|
||||
description = models.JSONField(blank=True)
|
||||
description_html = models.TextField(blank=True)
|
||||
description_stripped = models.TextField(blank=True)
|
||||
description = models.JSONField(blank=True, null=True)
|
||||
description_html = models.TextField(blank=True, null=True)
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
priority = models.CharField(
|
||||
max_length=30,
|
||||
choices=PRIORITY_CHOICES,
|
||||
@ -87,7 +87,9 @@ class Issue(ProjectBaseModel):
|
||||
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
strip_tags(self.description_html) if self.description_html != "" else ""
|
||||
None
|
||||
if (self.description_html == "" or self.description_html is None)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(Issue, self).save(*args, **kwargs)
|
||||
|
||||
@ -143,12 +145,8 @@ class IssueActivity(ProjectBaseModel):
|
||||
field = models.CharField(
|
||||
max_length=255, verbose_name="Field Name", blank=True, null=True
|
||||
)
|
||||
old_value = models.CharField(
|
||||
max_length=255, verbose_name="Old Value", blank=True, null=True
|
||||
)
|
||||
new_value = models.CharField(
|
||||
max_length=255, verbose_name="New Value", blank=True, null=True
|
||||
)
|
||||
old_value = models.TextField(verbose_name="Old Value", blank=True, null=True)
|
||||
new_value = models.TextField(verbose_name="New Value", blank=True, null=True)
|
||||
|
||||
comment = models.TextField(verbose_name="Comment", blank=True)
|
||||
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
|
||||
@ -211,10 +209,11 @@ class IssueComment(ProjectBaseModel):
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.comment_stripped = strip_tags(self.comment_html) if self.comment_html != "" else ""
|
||||
self.comment_stripped = (
|
||||
strip_tags(self.comment_html) if self.comment_html != "" else ""
|
||||
)
|
||||
return super(IssueComment, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Comment"
|
||||
verbose_name_plural = "Issue Comments"
|
||||
|
@ -68,6 +68,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
last_workspace_id = models.UUIDField(null=True)
|
||||
my_issues_prop = models.JSONField(null=True)
|
||||
role = models.CharField(max_length=300, null=True, blank=True)
|
||||
is_bot = models.BooleanField(default=False)
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
|
||||
@ -101,7 +102,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
@receiver(post_save, sender=User)
|
||||
def send_welcome_email(sender, instance, created, **kwargs):
|
||||
try:
|
||||
if created:
|
||||
if created and not instance.is_bot:
|
||||
first_name = instance.first_name.capitalize()
|
||||
to_email = instance.email
|
||||
from_email_string = f"Team Plane <team@mailer.plane.so>"
|
||||
|
@ -34,9 +34,7 @@ INSTALLED_APPS = [
|
||||
"rest_framework_simplejwt.token_blacklist",
|
||||
"corsheaders",
|
||||
"taggit",
|
||||
"fieldsignals",
|
||||
"django_rq",
|
||||
"channels",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -66,11 +66,3 @@ RQ_QUEUES = {
|
||||
|
||||
WEB_URL = "http://localhost:3000"
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [(REDIS_HOST, REDIS_PORT)],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
"""Production settings and globals."""
|
||||
import ssl
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import dj_database_url
|
||||
from urllib.parse import urlparse
|
||||
from redis.asyncio.connection import Connection, RedisSSLContext
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
@ -186,64 +183,10 @@ RQ_QUEUES = {
|
||||
}
|
||||
|
||||
|
||||
class CustomSSLConnection(Connection):
|
||||
def __init__(
|
||||
self,
|
||||
ssl_context: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.ssl_context = RedisSSLContext(ssl_context)
|
||||
|
||||
|
||||
class RedisSSLContext:
|
||||
__slots__ = ("context",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ssl_context,
|
||||
):
|
||||
self.context = ssl_context
|
||||
|
||||
def get(self):
|
||||
return self.context
|
||||
|
||||
|
||||
url = urlparse(os.environ.get("REDIS_URL"))
|
||||
|
||||
DOCKERIZED = os.environ.get("DOCKERIZED", False) # Set the variable true if running in docker-compose environment
|
||||
|
||||
if not DOCKERIZED:
|
||||
|
||||
ssl_context = ssl.SSLContext()
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [
|
||||
{
|
||||
"host": url.hostname,
|
||||
"port": url.port,
|
||||
"username": url.username,
|
||||
"password": url.password,
|
||||
"connection_class": CustomSSLConnection,
|
||||
"ssl_context": ssl_context,
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
else:
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [(os.environ.get("REDIS_URL"))],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
DOCKERIZED = os.environ.get(
|
||||
"DOCKERIZED", False
|
||||
) # Set the variable true if running in docker-compose environment
|
||||
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
||||
|
@ -1,11 +1,8 @@
|
||||
"""Production settings and globals."""
|
||||
import ssl
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import dj_database_url
|
||||
from urllib.parse import urlparse
|
||||
from redis.asyncio.connection import Connection, RedisSSLContext
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
@ -14,7 +11,7 @@ from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from .common import * # noqa
|
||||
|
||||
# Database
|
||||
DEBUG = False
|
||||
DEBUG = True
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||
@ -186,52 +183,5 @@ RQ_QUEUES = {
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSSLConnection(Connection):
|
||||
def __init__(
|
||||
self,
|
||||
ssl_context: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.ssl_context = RedisSSLContext(ssl_context)
|
||||
|
||||
class RedisSSLContext:
|
||||
__slots__ = (
|
||||
"context",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ssl_context,
|
||||
):
|
||||
self.context = ssl_context
|
||||
|
||||
def get(self):
|
||||
return self.context
|
||||
|
||||
|
||||
url = urlparse(os.environ.get("REDIS_URL"))
|
||||
|
||||
ssl_context = ssl.SSLContext()
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
'hosts': [
|
||||
{
|
||||
'host': url.hostname,
|
||||
'port': url.port,
|
||||
'username': url.username,
|
||||
'password': url.password,
|
||||
'connection_class': CustomSSLConnection,
|
||||
'ssl_context': ssl_context,
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
||||
|
@ -6,7 +6,7 @@ django-taggit==2.1.0
|
||||
psycopg2==2.9.3
|
||||
django-oauth-toolkit==2.0.0
|
||||
mistune==2.0.3
|
||||
djangorestframework==3.13.1
|
||||
djangorestframework==3.14.0
|
||||
redis==4.2.2
|
||||
django-nested-admin==3.4.0
|
||||
django-cors-headers==3.11.0
|
||||
@ -16,16 +16,13 @@ faker==13.4.0
|
||||
django-filter==21.1
|
||||
jsonmodels==2.5.0
|
||||
djangorestframework-simplejwt==5.1.0
|
||||
sentry-sdk==1.5.12
|
||||
sentry-sdk==1.13.0
|
||||
django-s3-storage==0.13.6
|
||||
django-crum==0.7.9
|
||||
django-guardian==2.4.0
|
||||
django-fieldsignals==0.7.0
|
||||
dj_rest_auth==2.2.5
|
||||
google-auth==2.9.1
|
||||
google-api-python-client==2.55.0
|
||||
django-rq==2.5.1
|
||||
django-redis==5.2.0
|
||||
channels==4.0.0
|
||||
channels-redis==4.0.0
|
||||
uvicorn==0.20.0
|
1
apps/app/.eslintrc.js
Normal file
1
apps/app/.eslintrc.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require("config/.eslintrc");
|
@ -14,7 +14,7 @@ ENV PATH="${PATH}:./pnpm"
|
||||
|
||||
COPY ./apps ./apps
|
||||
COPY ./package.json ./package.json
|
||||
COPY ./.eslintrc.json ./.eslintrc.json
|
||||
COPY ./.eslintrc.js ./.eslintrc.js
|
||||
COPY ./turbo.json ./turbo.json
|
||||
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
|
||||
COPY ./pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
|
@ -1,28 +1,30 @@
|
||||
import React, { useState } from "react";
|
||||
// react hook form
|
||||
import { useForm } from "react-hook-form";
|
||||
// ui
|
||||
import { Button, Input } from "ui";
|
||||
import authenticationService from "lib/services/authentication.service";
|
||||
// icons
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { Button, Input } from "components/ui";
|
||||
// services
|
||||
import authenticationService from "services/authentication.service";
|
||||
import useToast from "hooks/use-toast";
|
||||
// icons
|
||||
|
||||
// types
|
||||
type SignIn = {
|
||||
type EmailCodeFormValues = {
|
||||
email: string;
|
||||
key?: string;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
export const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
const [codeSent, setCodeSent] = useState(false);
|
||||
const { setToastAlert } = useToast();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting, dirtyFields, isValid, isDirty },
|
||||
} = useForm<SignIn>({
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm<EmailCodeFormValues>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
key: "",
|
||||
@ -32,9 +34,8 @@ const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const onSubmit = ({ email }: SignIn) => {
|
||||
const onSubmit = ({ email }: EmailCodeFormValues) => {
|
||||
console.log(email);
|
||||
|
||||
authenticationService
|
||||
.emailCode({ email })
|
||||
.then((res) => {
|
||||
@ -46,15 +47,20 @@ const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignin = (formData: SignIn) => {
|
||||
const handleSignin = (formData: EmailCodeFormValues) => {
|
||||
authenticationService
|
||||
.magicSignIn(formData)
|
||||
.then(async (response) => {
|
||||
await onSuccess(response);
|
||||
.then((response) => {
|
||||
onSuccess(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setError("token" as keyof SignIn, {
|
||||
setToastAlert({
|
||||
title: "Oops!",
|
||||
type: "error",
|
||||
message: "Enter the correct code to sign in",
|
||||
});
|
||||
setError("token" as keyof EmailCodeFormValues, {
|
||||
type: "manual",
|
||||
message: error.error,
|
||||
});
|
||||
@ -127,5 +133,3 @@ const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailCodeForm;
|
@ -1,29 +1,28 @@
|
||||
import React from "react";
|
||||
// next
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// react hook form
|
||||
import { useForm } from "react-hook-form";
|
||||
// ui
|
||||
import { Button, Input } from "ui";
|
||||
import authenticationService from "lib/services/authentication.service";
|
||||
import { Button, Input } from "components/ui";
|
||||
import authenticationService from "services/authentication.service";
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
// types
|
||||
type SignIn = {
|
||||
type EmailPasswordFormValues = {
|
||||
email: string;
|
||||
password?: string;
|
||||
medium?: string;
|
||||
};
|
||||
|
||||
const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
export const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
const { setToastAlert } = useToast();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { errors, isSubmitting, dirtyFields, isValid, isDirty },
|
||||
} = useForm<SignIn>({
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm<EmailPasswordFormValues>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
@ -33,19 +32,24 @@ const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const onSubmit = (formData: SignIn) => {
|
||||
const onSubmit = (formData: EmailPasswordFormValues) => {
|
||||
authenticationService
|
||||
.emailLogin(formData)
|
||||
.then(async (response) => {
|
||||
await onSuccess(response);
|
||||
.then((response) => {
|
||||
onSuccess(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setToastAlert({
|
||||
title: "Oops!",
|
||||
type: "error",
|
||||
message: "Enter the correct email address and password to sign in",
|
||||
});
|
||||
if (!error?.response?.data) return;
|
||||
Object.keys(error.response.data).forEach((key) => {
|
||||
const err = error.response.data[key];
|
||||
console.log("err", err);
|
||||
setError(key as keyof SignIn, {
|
||||
setError(key as keyof EmailPasswordFormValues, {
|
||||
type: "manual",
|
||||
message: Array.isArray(err) ? err.join(", ") : err,
|
||||
});
|
||||
@ -85,8 +89,8 @@ const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="text-sm ml-auto">
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<div className="ml-auto text-sm">
|
||||
<Link href={"/forgot-password"}>
|
||||
<a className="font-medium text-theme hover:text-indigo-500">Forgot your password?</a>
|
||||
</Link>
|
||||
@ -105,5 +109,3 @@ const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailPasswordForm;
|
46
apps/app/components/account/email-signin-form.tsx
Normal file
46
apps/app/components/account/email-signin-form.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useState, FC } from "react";
|
||||
import { KeyIcon } from "@heroicons/react/24/outline";
|
||||
// components
|
||||
import { EmailCodeForm, EmailPasswordForm } from "components/account";
|
||||
|
||||
export interface EmailSignInFormProps {
|
||||
handleSuccess: () => void;
|
||||
}
|
||||
|
||||
export const EmailSignInForm: FC<EmailSignInFormProps> = (props) => {
|
||||
const { handleSuccess } = props;
|
||||
// states
|
||||
const [useCode, setUseCode] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{useCode ? (
|
||||
<EmailCodeForm onSuccess={handleSuccess} />
|
||||
) : (
|
||||
<EmailPasswordForm onSuccess={handleSuccess} />
|
||||
)}
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="bg-white px-2 text-gray-500">or</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="mt-6 flex w-full flex-col items-stretch gap-y-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center rounded border border-gray-300 px-3 py-2 text-sm duration-300 hover:bg-gray-100"
|
||||
onClick={() => setUseCode((prev) => !prev)}
|
||||
>
|
||||
<KeyIcon className="h-[25px] w-[25px]" />
|
||||
<span className="w-full text-center font-medium">
|
||||
{useCode ? "Continue with Password" : "Continue with Code"}
|
||||
</span>
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
51
apps/app/components/account/github-login-button.tsx
Normal file
51
apps/app/components/account/github-login-button.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { useEffect, useState, FC } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
// images
|
||||
import githubImage from "/public/logos/github.png";
|
||||
|
||||
const { NEXT_PUBLIC_GITHUB_ID } = process.env;
|
||||
|
||||
export interface GithubLoginButtonProps {
|
||||
handleSignIn: React.Dispatch<string>;
|
||||
}
|
||||
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
const { handleSignIn } = props;
|
||||
// router
|
||||
const {
|
||||
query: { code },
|
||||
} = useRouter();
|
||||
// states
|
||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (code) {
|
||||
handleSignIn(code.toString());
|
||||
}
|
||||
}, [code, handleSignIn]);
|
||||
|
||||
useEffect(() => {
|
||||
const origin =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
setLoginCallBackURL(`${origin}/signin` as any);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}`}
|
||||
>
|
||||
<button className="flex w-full items-center rounded bg-black px-3 py-2 text-sm text-white opacity-90 duration-300 hover:opacity-100">
|
||||
<Image
|
||||
src={githubImage}
|
||||
height={25}
|
||||
width={25}
|
||||
className="flex-shrink-0"
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
<span className="w-full text-center font-medium">Continue with GitHub</span>
|
||||
</button>
|
||||
</Link>
|
||||
);
|
||||
};
|
@ -4,12 +4,13 @@ import Script from "next/script";
|
||||
|
||||
export interface IGoogleLoginButton {
|
||||
text?: string;
|
||||
onSuccess?: (res: any) => void;
|
||||
onFailure?: (res: any) => void;
|
||||
handleSignIn: React.Dispatch<any>;
|
||||
styles?: CSSProperties;
|
||||
}
|
||||
|
||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
const { handleSignIn } = props;
|
||||
|
||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
|
||||
|
||||
@ -17,7 +18,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
if (!googleSignInButton.current || gsiScriptLoaded) return;
|
||||
window?.google?.accounts.id.initialize({
|
||||
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
|
||||
callback: props.onSuccess as any,
|
||||
callback: handleSignIn,
|
||||
});
|
||||
window?.google?.accounts.id.renderButton(
|
||||
googleSignInButton.current,
|
||||
@ -32,7 +33,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
);
|
||||
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
|
||||
setGsiScriptLoaded(true);
|
||||
}, [props.onSuccess, gsiScriptLoaded]);
|
||||
}, [handleSignIn, gsiScriptLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (window?.google?.accounts?.id) {
|
||||
@ -46,7 +47,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||
<div className="w-full" id="googleSignInButton" ref={googleSignInButton}></div>
|
||||
<div className="w-full" id="googleSignInButton" ref={googleSignInButton} />
|
||||
</>
|
||||
);
|
||||
};
|
5
apps/app/components/account/index.ts
Normal file
5
apps/app/components/account/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./google-login";
|
||||
export * from "./email-code-form";
|
||||
export * from "./email-password-form";
|
||||
export * from "./github-login-button";
|
||||
export * from "./email-signin-form";
|
@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
// icons
|
||||
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
@ -32,8 +32,7 @@ type BreadcrumbItemProps = {
|
||||
icon?: any;
|
||||
};
|
||||
|
||||
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) => {
|
||||
return (
|
||||
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) => (
|
||||
<>
|
||||
{link ? (
|
||||
<Link href={link}>
|
||||
@ -54,7 +53,6 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Breadcrumbs.BreadcrumbItem = BreadcrumbItem;
|
||||
|
@ -1,40 +1,39 @@
|
||||
// TODO: Refactor this component: into a different file, use this file to export the components
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import useTheme from "lib/hooks/useTheme";
|
||||
import useToast from "lib/hooks/useToast";
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// services
|
||||
import userService from "lib/services/user.service";
|
||||
// components
|
||||
import ShortcutsModal from "components/command-palette/shortcuts";
|
||||
import { CreateProjectModal } from "components/project";
|
||||
import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal";
|
||||
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
|
||||
import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal";
|
||||
import BulkDeleteIssuesModal from "components/common/bulk-delete-issues-modal";
|
||||
// headless ui
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// constants
|
||||
import { USER_ISSUE } from "constants/fetch-keys";
|
||||
// ui
|
||||
import { Button } from "ui";
|
||||
// icons
|
||||
import {
|
||||
FolderIcon,
|
||||
RectangleStackIcon,
|
||||
ClipboardDocumentListIcon,
|
||||
MagnifyingGlassIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import useTheme from "hooks/use-theme";
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUser from "hooks/use-user";
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// components
|
||||
import ShortcutsModal from "components/command-palette/shortcuts";
|
||||
import { CreateProjectModal } from "components/project";
|
||||
import { CreateUpdateIssueModal } from "components/issues/modal";
|
||||
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
|
||||
import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal";
|
||||
import BulkDeleteIssuesModal from "components/common/bulk-delete-issues-modal";
|
||||
// headless ui
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// common
|
||||
import { classNames, copyTextToClipboard } from "constants/common";
|
||||
// ui
|
||||
import { Button } from "components/ui";
|
||||
// icons
|
||||
// fetch-keys
|
||||
import { USER_ISSUE } from "constants/fetch-keys";
|
||||
|
||||
const CommandPalette: React.FC = () => {
|
||||
const [query, setQuery] = useState("");
|
||||
@ -98,33 +97,39 @@ const CommandPalette: React.FC = () => {
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "/") {
|
||||
if (
|
||||
!(e.target instanceof HTMLTextAreaElement) &&
|
||||
!(e.target instanceof HTMLInputElement) &&
|
||||
!(e.target as Element).classList?.contains("remirror-editor")
|
||||
) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
||||
e.preventDefault();
|
||||
setIsPaletteOpen(true);
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "i") {
|
||||
} else if (e.ctrlKey && e.key === "c") {
|
||||
console.log("Text copied");
|
||||
} else if (e.key === "c") {
|
||||
e.preventDefault();
|
||||
setIsIssueModalOpen(true);
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "p") {
|
||||
} else if (e.key === "p") {
|
||||
e.preventDefault();
|
||||
setIsProjectModalOpen(true);
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "b") {
|
||||
e.preventDefault();
|
||||
toggleCollapsed();
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "h") {
|
||||
} else if (e.key === "h") {
|
||||
e.preventDefault();
|
||||
setIsShortcutsModalOpen(true);
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "q") {
|
||||
} else if (e.key === "q") {
|
||||
e.preventDefault();
|
||||
setIsCreateCycleModalOpen(true);
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "m") {
|
||||
} else if (e.key === "m") {
|
||||
e.preventDefault();
|
||||
setIsCreateModuleModalOpen(true);
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "d") {
|
||||
} else if (e.key === "Delete") {
|
||||
e.preventDefault();
|
||||
setIsBulkDeleteIssuesModalOpen(true);
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") {
|
||||
e.preventDefault();
|
||||
|
||||
if (!router.query.issueId) return;
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
@ -142,6 +147,7 @@ const CommandPalette: React.FC = () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[toggleCollapsed, setToastAlert, router]
|
||||
);
|
||||
@ -173,10 +179,9 @@ const CommandPalette: React.FC = () => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<CreateUpdateIssuesModal
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isIssueModalOpen}
|
||||
setIsOpen={setIsIssueModalOpen}
|
||||
projectId={projectId as string}
|
||||
handleClose={() => setIsIssueModalOpen(false)}
|
||||
/>
|
||||
<BulkDeleteIssuesModal
|
||||
isOpen={isBulkDeleteIssuesModalOpen}
|
||||
@ -188,7 +193,7 @@ const CommandPalette: React.FC = () => {
|
||||
afterLeave={() => setQuery("")}
|
||||
appear
|
||||
>
|
||||
<Dialog as="div" className="relative z-10" onClose={handleCommandPaletteClose}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleCommandPaletteClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -201,7 +206,7 @@ const CommandPalette: React.FC = () => {
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -228,7 +233,7 @@ const CommandPalette: React.FC = () => {
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
autoComplete="off"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -255,10 +260,9 @@ const CommandPalette: React.FC = () => {
|
||||
url: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`,
|
||||
}}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
"flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2",
|
||||
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
|
||||
active ? "bg-gray-500 bg-opacity-5 text-gray-900" : ""
|
||||
)
|
||||
}`
|
||||
}
|
||||
>
|
||||
{({ active }) => (
|
||||
@ -307,19 +311,17 @@ const CommandPalette: React.FC = () => {
|
||||
onClick: action.onClick,
|
||||
}}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
"flex cursor-default select-none items-center rounded-md px-3 py-2",
|
||||
`flex cursor-default select-none items-center rounded-md px-3 py-2 ${
|
||||
active ? "bg-gray-500 bg-opacity-5 text-gray-900" : ""
|
||||
)
|
||||
}`
|
||||
}
|
||||
>
|
||||
{({ active }) => (
|
||||
<>
|
||||
<action.icon
|
||||
className={classNames(
|
||||
"h-6 w-6 flex-none text-gray-900 text-opacity-40",
|
||||
className={`h-6 w-6 flex-none text-gray-900 text-opacity-40 ${
|
||||
active ? "text-opacity-100" : ""
|
||||
)}
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="ml-3 flex-auto truncate">{action.name}</span>
|
||||
|
@ -4,7 +4,7 @@ import { Dialog, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
// ui
|
||||
import { Input } from "ui";
|
||||
import { Input } from "components/ui";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@ -15,7 +15,7 @@ const shortcuts = [
|
||||
{
|
||||
title: "Navigation",
|
||||
shortcuts: [
|
||||
{ keys: "ctrl,/", description: "To open navigator" },
|
||||
{ keys: "ctrl,cmd,k", description: "To open navigator" },
|
||||
{ keys: "↑", description: "Move up" },
|
||||
{ keys: "↓", description: "Move down" },
|
||||
{ keys: "←", description: "Move left" },
|
||||
@ -27,14 +27,14 @@ const shortcuts = [
|
||||
{
|
||||
title: "Common",
|
||||
shortcuts: [
|
||||
{ keys: "ctrl,p", description: "To create project" },
|
||||
{ keys: "ctrl,i", description: "To create issue" },
|
||||
{ keys: "ctrl,q", description: "To create cycle" },
|
||||
{ keys: "ctrl,m", description: "To create module" },
|
||||
{ keys: "ctrl,d", description: "To bulk delete issues" },
|
||||
{ keys: "ctrl,h", description: "To open shortcuts guide" },
|
||||
{ keys: "p", description: "To create project" },
|
||||
{ keys: "c", description: "To create issue" },
|
||||
{ keys: "q", description: "To create cycle" },
|
||||
{ keys: "m", description: "To create module" },
|
||||
{ keys: "Delete", description: "To bulk delete issues" },
|
||||
{ keys: "h", description: "To open shortcuts guide" },
|
||||
{
|
||||
keys: "ctrl,alt,c",
|
||||
keys: "ctrl,cmd,alt,c",
|
||||
description: "To copy issue url when on issue detail page.",
|
||||
},
|
||||
],
|
||||
@ -52,7 +52,7 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={setIsOpen}>
|
||||
<Dialog as="div" className="relative z-20" onClose={setIsOpen}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -65,7 +65,7 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<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={React.Fragment}
|
||||
@ -79,10 +79,10 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="bg-white p-5">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="flex flex-col gap-y-4 text-center sm:text-left w-full">
|
||||
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900 flex justify-between"
|
||||
className="flex justify-between text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
<span>Keyboard Shortcuts</span>
|
||||
<span>
|
||||
@ -103,11 +103,11 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-3 w-full">
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
{filteredShortcuts.length > 0 ? (
|
||||
filteredShortcuts.map(({ title, shortcuts }) => (
|
||||
<div key={title} className="w-full flex flex-col">
|
||||
<p className="font-medium mb-4">{title}</p>
|
||||
<div key={title} className="flex w-full flex-col">
|
||||
<p className="mb-4 font-medium">{title}</p>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{shortcuts.map(({ keys, description }, index) => (
|
||||
<div key={index} className="flex justify-between">
|
||||
@ -115,7 +115,7 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
<div className="flex items-center gap-x-1">
|
||||
{keys.split(",").map((key, index) => (
|
||||
<span key={index} className="flex items-center gap-1">
|
||||
<kbd className="bg-gray-200 text-sm px-1 rounded">
|
||||
<kbd className="rounded bg-gray-200 px-1 text-sm">
|
||||
{key}
|
||||
</kbd>
|
||||
</span>
|
||||
|
109
apps/app/components/common/board-view/board-header.tsx
Normal file
109
apps/app/components/common/board-view/board-header.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import React from "react";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DraggableProvided } from "react-beautiful-dnd";
|
||||
// icons
|
||||
import {
|
||||
ArrowsPointingInIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, NestedKeyOf } from "types";
|
||||
type Props = {
|
||||
isCollapsed: boolean;
|
||||
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
};
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
groupTitle: string;
|
||||
createdBy: string | null;
|
||||
bgColor: string;
|
||||
addIssueToState: () => void;
|
||||
provided?: DraggableProvided;
|
||||
};
|
||||
|
||||
const BoardHeader: React.FC<Props> = ({
|
||||
isCollapsed,
|
||||
setIsCollapsed,
|
||||
provided,
|
||||
groupedByIssues,
|
||||
selectedGroup,
|
||||
groupTitle,
|
||||
createdBy,
|
||||
bgColor,
|
||||
addIssueToState,
|
||||
}) => (
|
||||
<div
|
||||
className={`flex justify-between p-3 pb-0 ${
|
||||
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
|
||||
{provided && (
|
||||
<button
|
||||
type="button"
|
||||
{...provided.dragHandleProps}
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
||||
!isCollapsed ? "" : "rotate-90"
|
||||
} ${selectedGroup !== "state_detail.name" ? "hidden" : ""}`}
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
|
||||
<EllipsisHorizontalIcon className="mt-[-0.7rem] h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${
|
||||
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
||||
}`}
|
||||
style={{
|
||||
border: `2px solid ${bgColor}`,
|
||||
backgroundColor: `${bgColor}20`,
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
className={`text-[0.9rem] font-medium capitalize`}
|
||||
style={{
|
||||
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
|
||||
}}
|
||||
>
|
||||
{groupTitle === null || groupTitle === "null"
|
||||
? "None"
|
||||
: createdBy
|
||||
? createdBy
|
||||
: addSpaceIfCamelCase(groupTitle)}
|
||||
</h2>
|
||||
<span className="ml-0.5 text-sm text-gray-500">{groupedByIssues[groupTitle].length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
|
||||
onClick={() => {
|
||||
setIsCollapsed((prevData) => !prevData);
|
||||
}}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<ArrowsPointingInIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ArrowsPointingOutIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
|
||||
onClick={addIssueToState}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default BoardHeader;
|
@ -1,5 +1,3 @@
|
||||
const SingleBoard = () => {
|
||||
return <></>;
|
||||
};
|
||||
const SingleBoard = () => <></>;
|
||||
|
||||
export default SingleBoard;
|
||||
|
@ -1,72 +1,68 @@
|
||||
import React from "react";
|
||||
// next
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DraggableStateSnapshot } from "react-beautiful-dnd";
|
||||
// react-datepicker
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/";
|
||||
import { PROJECT_ISSUES_LIST, STATE_LIST } from "constants/fetch-keys";
|
||||
import { getPriorityIcon } from "constants/global";
|
||||
// services
|
||||
import issuesService from "lib/services/issues.service";
|
||||
import stateService from "lib/services/state.service";
|
||||
import projectService from "lib/services/project.service";
|
||||
// icons
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
|
||||
import User from "public/user.png";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import stateService from "services/state.service";
|
||||
import projectService from "services/project.service";
|
||||
// components
|
||||
import { AssigneesList, CustomDatePicker } from "components/ui";
|
||||
// helpers
|
||||
import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, IssueResponse, IWorkspaceMember, Properties } from "types";
|
||||
import { IIssue, IUserLite, IWorkspaceMember, Properties, UserAuth } from "types";
|
||||
// common
|
||||
import { PRIORITIES } from "constants/";
|
||||
import {
|
||||
addSpaceIfCamelCase,
|
||||
classNames,
|
||||
findHowManyDaysLeft,
|
||||
renderShortNumericDateFormat,
|
||||
} from "constants/common";
|
||||
import { PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
STATE_LIST,
|
||||
PROJECT_DETAILS,
|
||||
CYCLE_ISSUES,
|
||||
MODULE_ISSUES,
|
||||
PROJECT_ISSUES_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import { getPriorityIcon } from "constants/global";
|
||||
|
||||
type Props = {
|
||||
type?: string;
|
||||
typeId?: string;
|
||||
issue: IIssue;
|
||||
properties: Properties;
|
||||
snapshot?: DraggableStateSnapshot;
|
||||
assignees: {
|
||||
avatar: string | undefined;
|
||||
first_name: string | undefined;
|
||||
email: string | undefined;
|
||||
}[];
|
||||
assignees: Partial<IUserLite>[] | (Partial<IUserLite> | undefined)[];
|
||||
people: IWorkspaceMember[] | undefined;
|
||||
handleDeleteIssue?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, childIssueId: string) => void;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
const SingleBoardIssue: React.FC<Props> = ({
|
||||
type,
|
||||
typeId,
|
||||
issue,
|
||||
properties,
|
||||
snapshot,
|
||||
assignees,
|
||||
people,
|
||||
handleDeleteIssue,
|
||||
partialUpdateIssue,
|
||||
userAuth,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: issues } = useSWR<IssueResponse>(
|
||||
workspaceSlug && projectId
|
||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: states } = useSWR(
|
||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
@ -81,7 +77,25 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
: null
|
||||
);
|
||||
|
||||
const totalChildren = issues?.results.filter((i) => i.parent === issue.id).length;
|
||||
const partialUpdateIssue = (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
||||
.then((res) => {
|
||||
if (typeId) {
|
||||
mutate(CYCLE_ISSUES(typeId ?? ""));
|
||||
mutate(MODULE_ISSUES(typeId ?? ""));
|
||||
}
|
||||
|
||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -90,7 +104,7 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
}`}
|
||||
>
|
||||
<div className="group/card relative select-none p-2">
|
||||
{handleDeleteIssue && (
|
||||
{handleDeleteIssue && !isNotAllowed && (
|
||||
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
@ -122,15 +136,18 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
as="div"
|
||||
value={issue.priority}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ priority: data }, issue.id);
|
||||
partialUpdateIssue({ priority: data });
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button
|
||||
className={`grid cursor-pointer place-items-center rounded px-2 py-1 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
className={`grid ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} place-items-center rounded px-2 py-1 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
issue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: issue.priority === "high"
|
||||
@ -157,10 +174,9 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
<Listbox.Option
|
||||
key={priority}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"flex cursor-pointer select-none items-center gap-2 px-3 py-2 capitalize"
|
||||
)
|
||||
`flex cursor-pointer select-none items-center gap-2 px-3 py-2 capitalize ${
|
||||
active ? "bg-indigo-50" : "bg-white"
|
||||
}`
|
||||
}
|
||||
value={priority}
|
||||
>
|
||||
@ -180,20 +196,25 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
as="div"
|
||||
value={issue.state}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ state: data }, issue.id);
|
||||
partialUpdateIssue({ state: data });
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button className="flex cursor-pointer items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<Listbox.Button
|
||||
className={`flex ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`}
|
||||
>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
></span>
|
||||
/>
|
||||
{addSpaceIfCamelCase(issue.state_detail.name)}
|
||||
</Listbox.Button>
|
||||
|
||||
@ -209,10 +230,9 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
<Listbox.Option
|
||||
key={state.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"flex cursor-pointer select-none items-center gap-2 px-3 py-2"
|
||||
)
|
||||
`flex cursor-pointer select-none items-center gap-2 px-3 py-2 ${
|
||||
active ? "bg-indigo-50" : "bg-white"
|
||||
}`
|
||||
}
|
||||
value={state.id}
|
||||
>
|
||||
@ -221,24 +241,25 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
style={{
|
||||
backgroundColor: state.color,
|
||||
}}
|
||||
></span>
|
||||
/>
|
||||
{addSpaceIfCamelCase(state.name)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
{/* <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||
<h5 className="font-medium mb-1">State</h5>
|
||||
<div>{issue.state_detail.name}</div>
|
||||
</div> */}
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
{/* {properties.cycle && !typeId && (
|
||||
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
{issue.issue_cycle ? issue.issue_cycle.cycle_detail.name : "None"}
|
||||
</div>
|
||||
)} */}
|
||||
{properties.due_date && (
|
||||
<div
|
||||
className={`group flex flex-shrink-0 cursor-pointer items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
className={`group relative ${
|
||||
issue.target_date === null
|
||||
? ""
|
||||
: issue.target_date < new Date().toISOString()
|
||||
@ -246,13 +267,42 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{issue.target_date ? renderShortNumericDateFormat(issue.target_date) : "N/A"}
|
||||
<CustomDatePicker
|
||||
placeholder="N/A"
|
||||
value={issue?.target_date}
|
||||
onChange={(val: Date) => {
|
||||
partialUpdateIssue({
|
||||
target_date: val
|
||||
? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}`
|
||||
: null,
|
||||
});
|
||||
}}
|
||||
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
|
||||
/>
|
||||
{/* <DatePicker
|
||||
placeholderText="N/A"
|
||||
value={
|
||||
issue?.target_date ? `${renderShortNumericDateFormat(issue.target_date)}` : "N/A"
|
||||
}
|
||||
selected={issue?.target_date ? new Date(issue.target_date) : null}
|
||||
onChange={(val: Date) => {
|
||||
partialUpdateIssue({
|
||||
target_date: val
|
||||
? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}`
|
||||
: null,
|
||||
});
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
className={`cursor-pointer rounded-md border px-2 py-[3px] text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
issue?.target_date ? "w-[4.5rem]" : "w-[3rem] text-center"
|
||||
}`}
|
||||
isClearable
|
||||
/> */}
|
||||
</div>
|
||||
)}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
{totalChildren} {totalChildren === 1 ? "sub-issue" : "sub-issues"}
|
||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
@ -261,62 +311,24 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
value={issue.assignees}
|
||||
onChange={(data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
if (newData.includes(data)) {
|
||||
newData.splice(newData.indexOf(data), 1);
|
||||
} else {
|
||||
newData.push(data);
|
||||
}
|
||||
partialUpdateIssue({ assignees_list: newData }, issue.id);
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: newData });
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button>
|
||||
<div className="flex cursor-pointer items-center gap-1 text-xs">
|
||||
{assignees.length > 0 ? (
|
||||
assignees.map((assignee, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||
index !== 0 ? "-ml-2.5" : ""
|
||||
}`}
|
||||
className={`flex ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} items-center gap-1 text-xs`}
|
||||
>
|
||||
{assignee.avatar && assignee.avatar !== "" ? (
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
|
||||
<Image
|
||||
src={assignee.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={assignee?.first_name}
|
||||
priority={false}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
|
||||
{assignee.first_name && assignee.first_name !== ""
|
||||
? assignee.first_name.charAt(0)
|
||||
: assignee?.email?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
|
||||
<Image
|
||||
src={User}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt="No user"
|
||||
priority={false}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<AssigneesList users={assignees} length={3} />
|
||||
</div>
|
||||
</Listbox.Button>
|
||||
|
||||
@ -332,19 +344,18 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
<Listbox.Option
|
||||
key={person.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer select-none p-2"
|
||||
)
|
||||
`cursor-pointer select-none p-2 ${active ? "bg-indigo-50" : "bg-white"}`
|
||||
}
|
||||
value={person.member.id}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-x-1 ${
|
||||
assignees.includes({
|
||||
avatar: person.member.avatar,
|
||||
id: person.member.last_name,
|
||||
first_name: person.member.first_name,
|
||||
last_name: person.member.last_name,
|
||||
email: person.member.email,
|
||||
avatar: person.member.avatar,
|
||||
})
|
||||
? "font-medium"
|
||||
: "font-normal"
|
||||
@ -380,7 +391,6 @@ const SingleBoardIssue: React.FC<Props> = ({
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
|
@ -7,27 +7,24 @@ import useSWR, { mutate } from "swr";
|
||||
// react hook form
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
// services
|
||||
import issuesServices from "lib/services/issues.service";
|
||||
import projectService from "lib/services/project.service";
|
||||
// hooks
|
||||
import useToast from "lib/hooks/useToast";
|
||||
// headless ui
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button } from "ui";
|
||||
// icons
|
||||
import { FolderIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import issuesServices from "services/issues.service";
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// headless ui
|
||||
// ui
|
||||
import { Button } from "components/ui";
|
||||
// icons
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
// types
|
||||
import { IIssue, IssueResponse } from "types";
|
||||
// fetch keys
|
||||
import { PROJECT_ISSUES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
// common
|
||||
import { classNames } from "constants/common";
|
||||
import { LayerDiagonalIcon } from "ui/icons";
|
||||
|
||||
type FormInput = {
|
||||
issue_ids: string[];
|
||||
cycleId: string;
|
||||
delete_issue_ids: string[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@ -62,13 +59,28 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { register, handleSubmit, reset } = useForm<FormInput>();
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<FormInput>({
|
||||
defaultValues: {
|
||||
delete_issue_ids: [],
|
||||
},
|
||||
});
|
||||
|
||||
const filteredIssues: IIssue[] =
|
||||
query === ""
|
||||
? issues?.results ?? []
|
||||
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
||||
[];
|
||||
: issues?.results.filter(
|
||||
(issue) =>
|
||||
issue.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
`${issue.project_detail.identifier}-${issue.sequence_id}`
|
||||
.toLowerCase()
|
||||
.includes(query.toLowerCase())
|
||||
) ?? [];
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
@ -77,7 +89,7 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
};
|
||||
|
||||
const handleDelete: SubmitHandler<FormInput> = async (data) => {
|
||||
if (!data.issue_ids || data.issue_ids.length === 0) {
|
||||
if (!data.delete_issue_ids || data.delete_issue_ids.length === 0) {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
@ -86,32 +98,34 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.issue_ids)) data.issue_ids = [data.issue_ids];
|
||||
if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids];
|
||||
|
||||
if (workspaceSlug && projectId) {
|
||||
await issuesServices
|
||||
.bulkDeleteIssues(workspaceSlug as string, projectId as string, data)
|
||||
.bulkDeleteIssues(workspaceSlug as string, projectId as string, {
|
||||
issue_ids: data.delete_issue_ids,
|
||||
})
|
||||
.then((res) => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: res.message,
|
||||
});
|
||||
|
||||
mutate<IssueResponse>(
|
||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
||||
(prevData) => {
|
||||
return {
|
||||
(prevData) => ({
|
||||
...(prevData as IssueResponse),
|
||||
count: (prevData?.results ?? []).filter(
|
||||
(p) => !data.issue_ids.some((id) => p.id === id)
|
||||
(p) => !data.delete_issue_ids.some((id) => p.id === id)
|
||||
).length,
|
||||
results: (prevData?.results ?? []).filter(
|
||||
(p) => !data.issue_ids.some((id) => p.id === id)
|
||||
(p) => !data.delete_issue_ids.some((id) => p.id === id)
|
||||
),
|
||||
};
|
||||
},
|
||||
}),
|
||||
false
|
||||
);
|
||||
handleClose();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
@ -120,22 +134,9 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
||||
<Dialog as="div" className="relative z-10" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.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-gray-500 bg-opacity-25 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -145,15 +146,31 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
||||
<form>
|
||||
<Combobox>
|
||||
<Combobox
|
||||
onChange={(val: string) => {
|
||||
const selectedIssues = watch("delete_issue_ids");
|
||||
if (selectedIssues.includes(val))
|
||||
setValue(
|
||||
"delete_issue_ids",
|
||||
selectedIssues.filter((i) => i !== val)
|
||||
);
|
||||
else {
|
||||
const newToDelete = selectedIssues;
|
||||
newToDelete.push(val);
|
||||
|
||||
setValue("delete_issue_ids", newToDelete);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="relative m-1">
|
||||
<MagnifyingGlassIcon
|
||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Combobox.Input
|
||||
<input
|
||||
type="text"
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
@ -175,25 +192,19 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
{filteredIssues.map((issue) => (
|
||||
<Combobox.Option
|
||||
key={issue.id}
|
||||
as="label"
|
||||
htmlFor={`issue-${issue.id}`}
|
||||
value={{
|
||||
name: issue.name,
|
||||
url: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`,
|
||||
}}
|
||||
as="div"
|
||||
value={issue.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
"flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2",
|
||||
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
|
||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||
)
|
||||
}`
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...register("issue_ids")}
|
||||
id={`issue-${issue.id}`}
|
||||
value={issue.id}
|
||||
checked={watch("delete_issue_ids").includes(issue.id)}
|
||||
readOnly
|
||||
/>
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
@ -215,26 +226,11 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
<LayerDiagonalIcon height="56" width="56" />
|
||||
<h3 className="text-gray-500">
|
||||
No issues found. Create a new issue with{" "}
|
||||
<pre className="inline rounded bg-gray-100 px-2 py-1">
|
||||
Ctrl/Command + I
|
||||
</pre>
|
||||
.
|
||||
<pre className="inline rounded bg-gray-100 px-2 py-1">C</pre>.
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
|
||||
{query !== "" && filteredIssues.length === 0 && (
|
||||
<div className="py-14 px-6 text-center sm:px-14">
|
||||
<FolderIcon
|
||||
className="mx-auto h-6 w-6 text-gray-900 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="mt-4 text-sm text-gray-900">
|
||||
We couldn{"'"}t find any issue with that term. Please try again.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
|
||||
{filteredIssues.length > 0 && (
|
||||
@ -242,8 +238,13 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
<Button type="button" theme="secondary" size="sm" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(handleDelete)} theme="danger" size="sm">
|
||||
Delete selected issues
|
||||
<Button
|
||||
onClick={handleSubmit(handleDelete)}
|
||||
theme="danger"
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Deleting..." : "Delete selected issues"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@ -253,7 +254,6 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -6,22 +6,19 @@ import useSWR from "swr";
|
||||
// react-hook-form
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
// hooks
|
||||
import useToast from "lib/hooks/useToast";
|
||||
// services
|
||||
import projectService from "lib/services/project.service";
|
||||
// headless ui
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button } from "ui";
|
||||
// icons
|
||||
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
// headless ui
|
||||
// ui
|
||||
import { Button } from "components/ui";
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
// common
|
||||
import { classNames } from "constants/common";
|
||||
import { LayerDiagonalIcon } from "ui/icons";
|
||||
|
||||
type FormInput = {
|
||||
issues: string[];
|
||||
@ -32,7 +29,7 @@ type Props = {
|
||||
handleClose: () => void;
|
||||
type: string;
|
||||
issues: IIssue[];
|
||||
handleOnSubmit: (data: FormInput) => void;
|
||||
handleOnSubmit: any;
|
||||
};
|
||||
|
||||
const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
@ -73,18 +70,25 @@ const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<FormInput> = (data) => {
|
||||
const onSubmit: SubmitHandler<FormInput> = async (data) => {
|
||||
if (!data.issues || data.issues.length === 0) {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: "Please select atleast one issue",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
handleOnSubmit(data);
|
||||
await handleOnSubmit(data);
|
||||
handleClose();
|
||||
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: `Issue${data.issues.length > 1 ? "s" : ""} added successfully`,
|
||||
});
|
||||
};
|
||||
|
||||
const filteredIssues: IIssue[] =
|
||||
@ -149,18 +153,16 @@ const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
</h2>
|
||||
)}
|
||||
<ul className="text-sm text-gray-700">
|
||||
{filteredIssues.map((issue) => {
|
||||
return (
|
||||
{filteredIssues.map((issue) => (
|
||||
<Combobox.Option
|
||||
key={issue.id}
|
||||
as="label"
|
||||
htmlFor={`issue-${issue.id}`}
|
||||
value={issue.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
"flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2",
|
||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${
|
||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||
)
|
||||
}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
@ -179,8 +181,7 @@ const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
@ -188,10 +189,7 @@ const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
<LayerDiagonalIcon height="56" width="56" />
|
||||
<h3 className="text-gray-500">
|
||||
No issues found. Create a new issue with{" "}
|
||||
<pre className="inline rounded bg-gray-100 px-2 py-1">
|
||||
Ctrl/Command + I
|
||||
</pre>
|
||||
.
|
||||
<pre className="inline rounded bg-gray-100 px-2 py-1">C</pre>.
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import NextImage from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
@ -8,11 +8,11 @@ import { useDropzone } from "react-dropzone";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
|
||||
// services
|
||||
import fileServices from "lib/services/file.service";
|
||||
import fileServices from "services/file.service";
|
||||
// icon
|
||||
import { UserCircleIcon } from "ui/icons";
|
||||
import { UserCircleIcon } from "components/icons";
|
||||
// ui
|
||||
import { Button } from "ui";
|
||||
import { Button } from "components/ui";
|
||||
|
||||
type TImageUploadModalProps = {
|
||||
value?: string | null;
|
||||
@ -70,7 +70,7 @@ export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={handleClose}>
|
||||
<Dialog as="div" className="relative z-30" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -83,7 +83,7 @@ export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="fixed inset-0 z-30 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
@ -103,13 +103,13 @@ export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`relative block w-full rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
||||
className={`relative block h-80 w-full rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
||||
(image === null && isDragActive) || !value
|
||||
? "border-2 border-dashed border-gray-300 hover:border-gray-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{value && value !== "" ? (
|
||||
{image !== null || (value && value !== null && value !== "") ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
@ -121,7 +121,7 @@ export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
|
||||
<NextImage
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
src={image ? URL.createObjectURL(image) : value}
|
||||
src={image ? URL.createObjectURL(image) : value ? value : ""}
|
||||
alt="image"
|
||||
/>
|
||||
</>
|
||||
|
@ -1,59 +1,106 @@
|
||||
// next
|
||||
import React, { useState } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// react-datepicker
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
// services
|
||||
import issuesService from "lib/services/issues.service";
|
||||
import issuesService from "services/issues.service";
|
||||
import workspaceService from "services/workspace.service";
|
||||
import stateService from "services/state.service";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { CustomMenu } from "ui";
|
||||
import { CustomMenu, CustomSelect, AssigneesList, Avatar, CustomDatePicker } from "components/ui";
|
||||
// components
|
||||
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
|
||||
// icons
|
||||
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, IssueResponse, Properties } from "types";
|
||||
import { IIssue, IWorkspaceMember, Properties, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||
// common
|
||||
import {
|
||||
addSpaceIfCamelCase,
|
||||
findHowManyDaysLeft,
|
||||
renderShortNumericDateFormat,
|
||||
} from "constants/common";
|
||||
CYCLE_ISSUES,
|
||||
MODULE_ISSUES,
|
||||
PROJECT_ISSUES_LIST,
|
||||
STATE_LIST,
|
||||
WORKSPACE_MEMBERS,
|
||||
} from "constants/fetch-keys";
|
||||
// constants
|
||||
import { getPriorityIcon } from "constants/global";
|
||||
import { PRIORITIES } from "constants/";
|
||||
|
||||
type Props = {
|
||||
type?: string;
|
||||
typeId?: string;
|
||||
issue: IIssue;
|
||||
properties: Properties;
|
||||
editIssue: () => void;
|
||||
handleDeleteIssue: () => void;
|
||||
removeIssue: () => void;
|
||||
removeIssue?: () => void;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
const SingleListIssue: React.FC<Props> = ({
|
||||
type,
|
||||
typeId,
|
||||
issue,
|
||||
properties,
|
||||
editIssue,
|
||||
handleDeleteIssue,
|
||||
removeIssue,
|
||||
userAuth,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
let { workspaceSlug, projectId } = router.query;
|
||||
const [deleteIssue, setDeleteIssue] = useState<IIssue | undefined>();
|
||||
|
||||
const { data: issues } = useSWR<IssueResponse>(
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: states } = useSWR(
|
||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const totalChildren = issues?.results.filter((i) => i.parent === issue.id).length;
|
||||
const { data: people } = useSWR<IWorkspaceMember[]>(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
const partialUpdateIssue = (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
|
||||
.then((res) => {
|
||||
if (typeId) {
|
||||
mutate(CYCLE_ISSUES(typeId ?? ""));
|
||||
mutate(MODULE_ISSUES(typeId ?? ""));
|
||||
}
|
||||
|
||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div key={issue.id} className="flex items-center justify-between gap-2 px-4 py-3 text-sm">
|
||||
<ConfirmIssueDeletion
|
||||
handleClose={() => setDeleteIssue(undefined)}
|
||||
isOpen={!!deleteIssue}
|
||||
data={deleteIssue}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-3 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`block h-1.5 w-1.5 flex-shrink-0 rounded-full`}
|
||||
@ -74,8 +121,22 @@ const SingleListIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
|
||||
{properties.priority && (
|
||||
<div
|
||||
className={`group relative flex flex-shrink-0 cursor-pointer items-center gap-1 rounded px-2 py-1 text-xs capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.priority}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ priority: data });
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button
|
||||
className={`flex ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} items-center gap-x-2 rounded px-2 py-0.5 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
issue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: issue.priority === "high"
|
||||
@ -87,8 +148,37 @@ const SingleListIssue: React.FC<Props> = ({
|
||||
: "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{/* {getPriorityIcon(issue.priority ?? "")} */}
|
||||
{issue.priority ?? "None"}
|
||||
{getPriorityIcon(
|
||||
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
||||
"text-sm"
|
||||
)}
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 w-36 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
{PRIORITIES?.map((priority) => (
|
||||
<Listbox.Option
|
||||
key={priority}
|
||||
className={({ active }) =>
|
||||
`flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${
|
||||
active ? "bg-indigo-50" : "bg-white"
|
||||
}`
|
||||
}
|
||||
value={priority}
|
||||
>
|
||||
{getPriorityIcon(priority, "text-sm")}
|
||||
{priority ?? "None"}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
|
||||
<h5 className="mb-1 font-medium text-gray-900">Priority</h5>
|
||||
<div
|
||||
@ -107,26 +197,54 @@ const SingleListIssue: React.FC<Props> = ({
|
||||
{issue.priority ?? "None"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
{properties.state && (
|
||||
<div className="group relative flex flex-shrink-0 cursor-pointer items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<CustomSelect
|
||||
label={
|
||||
<>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue?.state_detail?.color,
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
></span>
|
||||
{addSpaceIfCamelCase(issue?.state_detail.name)}
|
||||
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
|
||||
<h5 className="mb-1 font-medium">State</h5>
|
||||
<div>{issue?.state_detail.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
{addSpaceIfCamelCase(issue.state_detail.name)}
|
||||
</>
|
||||
}
|
||||
value={issue.state}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ state: data });
|
||||
}}
|
||||
maxHeight="md"
|
||||
noChevron
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{states?.map((state) => (
|
||||
<CustomSelect.Option key={state.id} value={state.id}>
|
||||
<>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: state.color,
|
||||
}}
|
||||
/>
|
||||
{addSpaceIfCamelCase(state.name)}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
{/* {properties.cycle && !typeId && (
|
||||
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
{issue.issue_cycle ? issue.issue_cycle.cycle_detail.name : "None"}
|
||||
</div>
|
||||
)} */}
|
||||
{properties.due_date && (
|
||||
<div
|
||||
className={`group group relative flex flex-shrink-0 cursor-pointer items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
className={`group relative ${
|
||||
issue.target_date === null
|
||||
? ""
|
||||
: issue.target_date < new Date().toISOString()
|
||||
@ -134,8 +252,37 @@ const SingleListIssue: React.FC<Props> = ({
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{issue.target_date ? renderShortNumericDateFormat(issue.target_date) : "N/A"}
|
||||
<CustomDatePicker
|
||||
placeholder="N/A"
|
||||
value={issue?.target_date}
|
||||
onChange={(val: Date) => {
|
||||
partialUpdateIssue({
|
||||
target_date: val
|
||||
? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}`
|
||||
: null,
|
||||
});
|
||||
}}
|
||||
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
|
||||
/>
|
||||
{/* <DatePicker
|
||||
placeholderText="N/A"
|
||||
value={
|
||||
issue?.target_date ? `${renderShortNumericDateFormat(issue.target_date)}` : "N/A"
|
||||
}
|
||||
selected={issue?.target_date ? new Date(issue.target_date) : null}
|
||||
onChange={(val: Date) => {
|
||||
partialUpdateIssue({
|
||||
target_date: val
|
||||
? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}`
|
||||
: null,
|
||||
});
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
className={`cursor-pointer rounded-md border px-2 py-[3px] text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||
issue?.target_date ? "w-[4.5rem]" : "w-[3rem] text-center"
|
||||
}`}
|
||||
isClearable
|
||||
/> */}
|
||||
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
|
||||
<h5 className="mb-1 font-medium text-gray-900">Due date</h5>
|
||||
<div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div>
|
||||
@ -150,18 +297,93 @@ const SingleListIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{properties.sub_issue_count && projectId && (
|
||||
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
{totalChildren} {totalChildren === 1 ? "sub-issue" : "sub-issues"}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm">
|
||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
)}
|
||||
{type && (
|
||||
{properties.assignee && (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.assignees}
|
||||
onChange={(data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: newData });
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button>
|
||||
<div
|
||||
className={`flex ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
|
||||
} items-center gap-1 text-xs`}
|
||||
>
|
||||
<AssigneesList userIds={issue.assignees ?? []} />
|
||||
</div>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
{people?.map((person) => (
|
||||
<Listbox.Option
|
||||
key={person.id}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center gap-x-1 cursor-pointer select-none p-2 ${
|
||||
active ? "bg-indigo-50" : ""
|
||||
} ${
|
||||
selected || issue.assignees?.includes(person.member.id)
|
||||
? "bg-indigo-50 font-medium"
|
||||
: "font-normal"
|
||||
}`
|
||||
}
|
||||
value={person.member.id}
|
||||
>
|
||||
<Avatar user={person.member} />
|
||||
<p>
|
||||
{person.member.first_name && person.member.first_name !== ""
|
||||
? person.member.first_name
|
||||
: person.member.email}
|
||||
</p>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
|
||||
<h5 className="mb-1 font-medium">Assigned to</h5>
|
||||
<div>
|
||||
{issue.assignee_details?.length > 0
|
||||
? issue.assignee_details.map((assignee) => assignee.first_name).join(", ")
|
||||
: "No one"}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
{type && !isNotAllowed && (
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => editIssue()}>Edit</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => removeIssue()}>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>Edit</CustomMenu.MenuItem>
|
||||
{type !== "issue" && (
|
||||
<CustomMenu.MenuItem onClick={removeIssue}>
|
||||
<>Remove from {type}</>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue()}>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={() => setDeleteIssue(issue)}>
|
||||
Delete permanently
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
|
@ -5,9 +5,9 @@ import { useRouter } from "next/router";
|
||||
// layouts
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
import useUser from "hooks/use-user";
|
||||
// icons
|
||||
import { LockIcon } from "ui/icons";
|
||||
import { LockIcon } from "components/icons";
|
||||
|
||||
type TNotAuthorizedViewProps = {
|
||||
actionButton?: React.ReactNode;
|
||||
|
@ -1,60 +1,84 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useIssuesProperties from "lib/hooks/useIssuesProperties";
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useIssueView from "hooks/use-issue-view";
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { CustomMenu } from "ui";
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { ChevronDownIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
||||
import { Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, NestedKeyOf, Properties } from "types";
|
||||
import { IIssue, Properties } from "types";
|
||||
// common
|
||||
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
|
||||
// constants
|
||||
import { filterIssueOptions, groupByOptions, orderByOptions } from "constants/";
|
||||
|
||||
type Props = {
|
||||
groupByProperty: NestedKeyOf<IIssue> | null;
|
||||
setGroupByProperty: (property: NestedKeyOf<IIssue> | null) => void;
|
||||
orderBy: NestedKeyOf<IIssue> | null;
|
||||
setOrderBy: (property: NestedKeyOf<IIssue> | null) => void;
|
||||
filterIssue: "activeIssue" | "backlogIssue" | null;
|
||||
setFilterIssue: (property: "activeIssue" | "backlogIssue" | null) => void;
|
||||
resetFilterToDefault: () => void;
|
||||
setNewFilterDefaultView: () => void;
|
||||
issues?: IIssue[];
|
||||
};
|
||||
|
||||
const View: React.FC<Props> = ({
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
filterIssue,
|
||||
setFilterIssue,
|
||||
resetFilterToDefault,
|
||||
setNewFilterDefaultView,
|
||||
}) => {
|
||||
const View: React.FC<Props> = ({ issues }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
issueView,
|
||||
setIssueViewToList,
|
||||
setIssueViewToKanban,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
setOrderBy,
|
||||
setFilterIssue,
|
||||
orderBy,
|
||||
filterIssue,
|
||||
resetFilterToDefault,
|
||||
setNewFilterDefaultView,
|
||||
} = useIssueView(issues ?? []);
|
||||
|
||||
const [properties, setProperties] = useIssuesProperties(
|
||||
workspaceSlug as string,
|
||||
projectId as string
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
{issues && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
||||
issueView === "list" ? "bg-gray-200" : ""
|
||||
}`}
|
||||
onClick={() => setIssueViewToList()}
|
||||
>
|
||||
<ListBulletIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
|
||||
issueView === "kanban" ? "bg-gray-200" : ""
|
||||
}`}
|
||||
onClick={() => setIssueViewToKanban()}
|
||||
>
|
||||
<Squares2X2Icon className="h-4 w-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={classNames(
|
||||
open ? "bg-gray-100 text-gray-900" : "text-gray-500",
|
||||
"group flex items-center gap-2 rounded-md border bg-transparent p-2 text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none"
|
||||
)}
|
||||
className={`group flex items-center gap-2 rounded-md border bg-transparent p-2 text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none ${
|
||||
open ? "bg-gray-100 text-gray-900" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
<span>View</span>
|
||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||
@ -71,6 +95,7 @@ const View: React.FC<Props> = ({
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg bg-white p-3 shadow-lg">
|
||||
<div className="relative divide-y-2">
|
||||
{issues && (
|
||||
<div className="space-y-4 pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm text-gray-600">Group by</h4>
|
||||
@ -95,7 +120,8 @@ const View: React.FC<Props> = ({
|
||||
<h4 className="text-sm text-gray-600">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
orderByOptions.find((option) => option.key === orderBy)?.name ?? "Select"
|
||||
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
@ -147,6 +173,7 @@ const View: React.FC<Props> = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-gray-600">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@ -172,7 +199,7 @@ const View: React.FC<Props> = ({
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
135
apps/app/components/cycles/form.tsx
Normal file
135
apps/app/components/cycles/form.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import { FC } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// components
|
||||
import { Button, Input, TextArea, CustomSelect } from "components/ui";
|
||||
// types
|
||||
import type { ICycle } from "types";
|
||||
|
||||
const defaultValues: Partial<ICycle> = {
|
||||
name: "",
|
||||
description: "",
|
||||
status: "draft",
|
||||
start_date: new Date().toString(),
|
||||
end_date: new Date().toString(),
|
||||
};
|
||||
|
||||
export interface CycleFormProps {
|
||||
handleFormSubmit: (values: Partial<ICycle>) => void;
|
||||
handleFormCancel?: () => void;
|
||||
initialData?: Partial<ICycle>;
|
||||
}
|
||||
|
||||
export const CycleForm: FC<CycleFormProps> = (props) => {
|
||||
const { handleFormSubmit, handleFormCancel = () => {}, initialData = null } = props;
|
||||
// form handler
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
} = useForm<ICycle>({
|
||||
defaultValues: initialData || defaultValues,
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Input
|
||||
id="name"
|
||||
label="Name"
|
||||
name="name"
|
||||
type="name"
|
||||
placeholder="Enter name"
|
||||
autoComplete="off"
|
||||
error={errors.name}
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Name is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextArea
|
||||
id="description"
|
||||
name="description"
|
||||
label="Description"
|
||||
placeholder="Enter description"
|
||||
error={errors.description}
|
||||
register={register}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="text-gray-500">Status</h6>
|
||||
<Controller
|
||||
name="status"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<CustomSelect
|
||||
{...field}
|
||||
label={<span className="capitalize">{field.value ?? "Select Status"}</span>}
|
||||
input
|
||||
>
|
||||
{[
|
||||
{ label: "Draft", value: "draft" },
|
||||
{ label: "Started", value: "started" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
].map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-x-2">
|
||||
<div className="w-full">
|
||||
<Input
|
||||
id="start_date"
|
||||
label="Start Date"
|
||||
name="start_date"
|
||||
type="date"
|
||||
placeholder="Enter start date"
|
||||
error={errors.start_date}
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Start date is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Input
|
||||
id="end_date"
|
||||
label="End Date"
|
||||
name="end_date"
|
||||
type="date"
|
||||
placeholder="Enter end date"
|
||||
error={errors.end_date}
|
||||
register={register}
|
||||
validations={{
|
||||
required: "End date is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button theme="secondary" onClick={handleFormCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{initialData
|
||||
? isSubmitting
|
||||
? "Updating Cycle..."
|
||||
: "Update Cycle"
|
||||
: isSubmitting
|
||||
? "Creating Cycle..."
|
||||
: "Create Cycle"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
3
apps/app/components/cycles/index.ts
Normal file
3
apps/app/components/cycles/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./modal";
|
||||
export * from "./select";
|
||||
export * from "./form";
|
112
apps/app/components/cycles/modal.tsx
Normal file
112
apps/app/components/cycles/modal.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import { Fragment } from "react";
|
||||
import { mutate } from "swr";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import cycleService from "services/cycles.service";
|
||||
// components
|
||||
import { CycleForm } from "components/cycles";
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { ICycle } from "types";
|
||||
// fetch keys
|
||||
import { CYCLE_LIST } from "constants/fetch-keys";
|
||||
|
||||
export interface CycleModalProps {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
initialData?: ICycle;
|
||||
}
|
||||
|
||||
export const CycleModal: React.FC<CycleModalProps> = (props) => {
|
||||
const { isOpen, handleClose, initialData, projectId, workspaceSlug } = props;
|
||||
|
||||
const createCycle = (payload: Partial<ICycle>) => {
|
||||
cycleService
|
||||
.createCycle(workspaceSlug as string, projectId, payload)
|
||||
.then((res) => {
|
||||
mutate(CYCLE_LIST(projectId));
|
||||
handleClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO: Handle this ERROR.
|
||||
// Object.keys(err).map((key) => {
|
||||
// setError(key as keyof typeof defaultValues, {
|
||||
// message: err[key].join(", "),
|
||||
// });
|
||||
// });
|
||||
});
|
||||
};
|
||||
|
||||
const updateCycle = (cycleId: string, payload: Partial<ICycle>) => {
|
||||
cycleService
|
||||
.updateCycle(workspaceSlug, projectId, cycleId, payload)
|
||||
.then((res) => {
|
||||
mutate(CYCLE_LIST(projectId));
|
||||
handleClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO: Handle this ERROR.
|
||||
// Object.keys(err).map((key) => {
|
||||
// setError(key as keyof typeof defaultValues, {
|
||||
// message: err[key].join(", "),
|
||||
// });
|
||||
// });
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormSubmit = (formValues: Partial<ICycle>) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
const payload = {
|
||||
...formValues,
|
||||
start_date: formValues.start_date ? renderDateFormat(formValues.start_date) : null,
|
||||
end_date: formValues.end_date ? renderDateFormat(formValues.end_date) : null,
|
||||
};
|
||||
if (initialData) {
|
||||
updateCycle(initialData.id, payload);
|
||||
} else {
|
||||
createCycle(payload);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<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-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-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-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
{initialData ? "Update" : "Create"} Cycle
|
||||
</Dialog.Title>
|
||||
<CycleForm handleFormSubmit={handleFormSubmit} handleFormCancel={handleClose} />
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
132
apps/app/components/cycles/select.tsx
Normal file
132
apps/app/components/cycles/select.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { CyclesIcon } from "components/icons";
|
||||
// services
|
||||
import cycleServices from "services/cycles.service";
|
||||
// components
|
||||
import { CycleModal } from "components/cycles";
|
||||
// fetch-keys
|
||||
import { CYCLE_LIST } from "constants/fetch-keys";
|
||||
|
||||
export type IssueCycleSelectProps = {
|
||||
projectId: string;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
multiple?: boolean;
|
||||
};
|
||||
|
||||
export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
|
||||
projectId,
|
||||
value,
|
||||
onChange,
|
||||
multiple = false,
|
||||
}) => {
|
||||
// states
|
||||
const [isCycleModalActive, setCycleModalActive] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: cycles } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_LIST(projectId) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => cycleServices.getCycles(workspaceSlug as string, projectId)
|
||||
: null
|
||||
);
|
||||
|
||||
const options = cycles?.map((cycle) => ({ value: cycle.id, display: cycle.name }));
|
||||
|
||||
const openCycleModal = () => {
|
||||
setCycleModalActive(true);
|
||||
};
|
||||
|
||||
const closeCycleModal = () => {
|
||||
setCycleModalActive(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CycleModal
|
||||
isOpen={isCycleModalActive}
|
||||
handleClose={closeCycleModal}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
/>
|
||||
<Listbox as="div" className="relative" value={value} onChange={onChange} multiple={multiple}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Button
|
||||
className={`flex cursor-pointer items-center gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`}
|
||||
>
|
||||
<CyclesIcon className="h-3 w-3 text-gray-500" />
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
{cycles?.find((c) => c.id === value)?.name ?? "Cycles"}
|
||||
</div>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options
|
||||
className={`absolute mt-1 max-h-32 min-w-[8rem] overflow-y-auto whitespace-nowrap bg-white shadow-lg text-xs z-10 rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none`}
|
||||
>
|
||||
<div className="py-1">
|
||||
{options ? (
|
||||
options.length > 0 ? (
|
||||
options.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.value}
|
||||
className={({ selected, active }) =>
|
||||
`${
|
||||
selected ||
|
||||
(Array.isArray(value)
|
||||
? value.includes(option.value)
|
||||
: value === option.value)
|
||||
? "bg-indigo-50 font-medium"
|
||||
: ""
|
||||
} ${
|
||||
active ? "bg-indigo-50" : ""
|
||||
} relative cursor-pointer select-none p-2 text-gray-900`
|
||||
}
|
||||
value={option.value}
|
||||
>
|
||||
<span className={` flex items-center gap-2 truncate`}>
|
||||
{option.display}
|
||||
</span>
|
||||
</Listbox.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="text-center text-sm text-gray-500">No options</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-sm text-gray-500">Loading...</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="relative w-full flex select-none items-center gap-x-2 p-2 text-gray-400 hover:bg-indigo-50 hover:text-gray-900"
|
||||
onClick={openCycleModal}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 text-gray-400" aria-hidden="true" />
|
||||
<span>Create cycle</span>
|
||||
</button>
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
// react beautiful dnd
|
||||
import { Droppable } from "react-beautiful-dnd";
|
||||
import type { DroppableProps } from "react-beautiful-dnd";
|
||||
import { Droppable, DroppableProps } from "react-beautiful-dnd";
|
||||
|
||||
const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
@ -1,16 +1,15 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
// headless ui
|
||||
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||
// hooks
|
||||
import useOutsideClickDetector from "lib/hooks/useOutsideClickDetector";
|
||||
// common
|
||||
import { getRandomEmoji } from "constants/common";
|
||||
// emoji
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
// emojis
|
||||
import emojis from "./emojis.json";
|
||||
// helpers
|
||||
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
import { getRandomEmoji } from "helpers/functions.helper";
|
||||
// hooks
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
|
||||
const tabOptions = [
|
||||
{
|
||||
@ -43,7 +42,7 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
||||
}, [value, onChange]);
|
||||
|
||||
return (
|
||||
<Popover className="relative" ref={ref}>
|
||||
<Popover className="relative z-[1]" ref={ref}>
|
||||
<Popover.Button
|
||||
className="rounded-md border border-gray-300 p-2 outline-none sm:text-sm"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const AttachmentIcon: React.FC<Props> = ({ width, height, className }) => {
|
||||
return (
|
||||
export const AttachmentIcon: React.FC<Props> = ({ width, height, className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const AttachmentIcon: React.FC<Props> = ({ width, height, className }) =>
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const BlockedIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const BlockedIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -24,4 +23,3 @@ export const BlockedIcon: React.FC<Props> = ({ width = "24", height = "24", clas
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const BlockerIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const BlockerIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -24,4 +23,3 @@ export const BlockerIcon: React.FC<Props> = ({ width = "24", height = "24", clas
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const BoltIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const BoltIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -15,4 +14,3 @@ export const BoltIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
|
||||
<path d="M10.6002 21C10.4169 21 10.2752 20.9417 10.1752 20.825C10.0752 20.7083 10.0419 20.5583 10.0752 20.375L11.0002 13.95H7.3502C7.16686 13.95 7.03353 13.8667 6.9502 13.7C6.86686 13.5333 6.86686 13.375 6.9502 13.225L12.8752 3.325C12.9252 3.24167 13.0085 3.16667 13.1252 3.1C13.2419 3.03333 13.3585 3 13.4752 3C13.6585 3 13.8002 3.05833 13.9002 3.175C14.0002 3.29167 14.0335 3.44167 14.0002 3.625L13.0752 10.025H16.6752C16.8585 10.025 16.996 10.1083 17.0877 10.275C17.1794 10.4417 17.1835 10.6 17.1002 10.75L11.2002 20.675C11.1502 20.7583 11.0669 20.8333 10.9502 20.9C10.8335 20.9667 10.7169 21 10.6002 21V21Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const CalendarMonthIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const CalendarMonthIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const CalendarMonthIcon: React.FC<Props> = ({ width = "24", height = "24"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const CancelIcon: React.FC<Props> = ({ width, height, className }) => {
|
||||
return (
|
||||
export const CancelIcon: React.FC<Props> = ({ width, height, className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -15,4 +14,3 @@ export const CancelIcon: React.FC<Props> = ({ width, height, className }) => {
|
||||
<path d="M7.725 16.275C7.875 16.425 8.05 16.5 8.25 16.5C8.45 16.5 8.625 16.425 8.775 16.275L12 13.05L15.25 16.3C15.3833 16.4333 15.5542 16.4958 15.7625 16.4875C15.9708 16.4792 16.1417 16.4083 16.275 16.275C16.425 16.125 16.5 15.95 16.5 15.75C16.5 15.55 16.425 15.375 16.275 15.225L13.05 12L16.3 8.75C16.4333 8.61667 16.4958 8.44583 16.4875 8.2375C16.4792 8.02917 16.4083 7.85833 16.275 7.725C16.125 7.575 15.95 7.5 15.75 7.5C15.55 7.5 15.375 7.575 15.225 7.725L12 10.95L8.75 7.7C8.61667 7.56667 8.44583 7.50417 8.2375 7.5125C8.02917 7.52083 7.85833 7.59167 7.725 7.725C7.575 7.875 7.5 8.05 7.5 8.25C7.5 8.45 7.575 8.625 7.725 8.775L10.95 12L7.7 15.25C7.56667 15.3833 7.50417 15.5542 7.5125 15.7625C7.52083 15.9708 7.59167 16.1417 7.725 16.275ZM12 22C10.5833 22 9.26667 21.7458 8.05 21.2375C6.83333 20.7292 5.775 20.025 4.875 19.125C3.975 18.225 3.27083 17.1667 2.7625 15.95C2.25417 14.7333 2 13.4167 2 12C2 10.6 2.25417 9.29167 2.7625 8.075C3.27083 6.85833 3.975 5.8 4.875 4.9C5.775 4 6.83333 3.29167 8.05 2.775C9.26667 2.25833 10.5833 2 12 2C13.4 2 14.7083 2.25833 15.925 2.775C17.1417 3.29167 18.2 4 19.1 4.9C20 5.8 20.7083 6.85833 21.225 8.075C21.7417 9.29167 22 10.6 22 12C22 13.4167 21.7417 14.7333 21.225 15.95C20.7083 17.1667 20 18.225 19.1 19.125C18.2 20.025 17.1417 20.7292 15.925 21.2375C14.7083 21.7458 13.4 22 12 22ZM12 20.5C14.3333 20.5 16.3333 19.6667 18 18C19.6667 16.3333 20.5 14.3333 20.5 12C20.5 9.66667 19.6667 7.66667 18 6C16.3333 4.33333 14.3333 3.5 12 3.5C9.66667 3.5 7.66667 4.33333 6 6C4.33333 7.66667 3.5 9.66667 3.5 12C3.5 14.3333 4.33333 16.3333 6 18C7.66667 19.6667 9.66667 20.5 12 20.5Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const ClipboardIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const ClipboardIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const ClipboardIcon: React.FC<Props> = ({ width = "24", height = "24", cl
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const CommentIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const CommentIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -15,4 +14,3 @@ export const CommentIcon: React.FC<Props> = ({ width = "24", height = "24", clas
|
||||
<path d="M2 16.1V3.05C2 2.81667 2.10833 2.58333 2.325 2.35C2.54167 2.11667 2.76667 2 3 2H15.975C16.225 2 16.4583 2.1125 16.675 2.3375C16.8917 2.5625 17 2.8 17 3.05V11.95C17 12.1833 16.8917 12.4167 16.675 12.65C16.4583 12.8833 16.225 13 15.975 13H6L2.65 16.35C2.53333 16.4667 2.39583 16.4958 2.2375 16.4375C2.07917 16.3792 2 16.2667 2 16.1ZM3.5 3.5V11.5V3.5ZM7.025 18C6.79167 18 6.5625 17.8833 6.3375 17.65C6.1125 17.4167 6 17.1833 6 16.95V14.5H18.5V6H21C21.2333 6 21.4583 6.11667 21.675 6.35C21.8917 6.58333 22 6.825 22 7.075V21.075C22 21.2417 21.9208 21.3542 21.7625 21.4125C21.6042 21.4708 21.4667 21.4417 21.35 21.325L18.025 18H7.025ZM15.5 3.5H3.5V11.5H15.5V3.5Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -7,8 +7,7 @@ export const CompletedCycleIcon: React.FC<Props> = ({
|
||||
height = "24",
|
||||
className,
|
||||
color = "black",
|
||||
}) => {
|
||||
return (
|
||||
}) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height={height} width={width} className={className}>
|
||||
<path
|
||||
d="m21.65 36.6-6.9-6.85 2.1-2.1 4.8 4.7 9.2-9.2 2.1 2.15ZM6 44V7h6.25V4h3.25v3h17V4h3.25v3H42v37Zm3-3h30V19.5H9Zm0-24.5h30V10H9Zm0 0V10v6.5Z"
|
||||
@ -16,4 +15,3 @@ export const CompletedCycleIcon: React.FC<Props> = ({
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -7,8 +7,7 @@ export const CurrentCycleIcon: React.FC<Props> = ({
|
||||
height = "24",
|
||||
className,
|
||||
color = "black",
|
||||
}) => {
|
||||
return (
|
||||
}) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height={height} width={width} className={className}>
|
||||
<path
|
||||
d="M15.3 28.3q-.85 0-1.425-.575-.575-.575-.575-1.425 0-.85.575-1.425.575-.575 1.425-.575.85 0 1.425.575.575.575.575 1.425 0 .85-.575 1.425-.575.575-1.425.575Zm8.85 0q-.85 0-1.425-.575-.575-.575-.575-1.425 0-.85.575-1.425.575-.575 1.425-.575.85 0 1.425.575.575.575.575 1.425 0 .85-.575 1.425-.575.575-1.425.575Zm8.5 0q-.85 0-1.425-.575-.575-.575-.575-1.425 0-.85.575-1.425.575-.575 1.425-.575.85 0 1.425.575.575.575.575 1.425 0 .85-.575 1.425-.575.575-1.425.575ZM6 44V7h6.25V4h3.25v3h17V4h3.25v3H42v37Zm3-3h30V19.5H9Zm0-24.5h30V10H9Zm0 0V10v6.5Z"
|
||||
@ -16,4 +15,3 @@ export const CurrentCycleIcon: React.FC<Props> = ({
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -7,8 +7,7 @@ export const CyclesIcon: React.FC<Props> = ({
|
||||
height = "24",
|
||||
className,
|
||||
color = "black",
|
||||
}) => {
|
||||
return (
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -33,4 +32,3 @@ export const CyclesIcon: React.FC<Props> = ({
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const DiscordIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const DiscordIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -12,7 +11,7 @@ export const DiscordIcon: React.FC<Props> = ({ width = "24", height = "24", clas
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_282_229)">
|
||||
<g clipPath="url(#clip0_282_229)">
|
||||
<path d="M16.9312 3.64157C15.6346 3.04643 14.2662 2.62206 12.8604 2.37907C12.8476 2.37657 12.8343 2.37821 12.8225 2.38375C12.8106 2.38929 12.8009 2.39845 12.7946 2.4099C12.6196 2.7224 12.4246 3.1299 12.2879 3.45157C10.7724 3.22139 9.23088 3.22139 7.7154 3.45157C7.5633 3.09515 7.39165 2.7474 7.20123 2.4099C7.19467 2.39871 7.18486 2.38977 7.1731 2.38426C7.16135 2.37876 7.1482 2.37695 7.1354 2.37907C5.72944 2.62155 4.36101 3.04595 3.06457 3.64157C3.05359 3.64617 3.04429 3.65402 3.0379 3.66407C0.444567 7.53823 -0.266266 11.3166 0.0829005 15.0474C0.0837487 15.0567 0.0864772 15.0656 0.0909192 15.0738C0.0953611 15.082 0.101423 15.0892 0.108734 15.0949C1.6184 16.2134 3.30716 17.0672 5.1029 17.6199C5.11556 17.6236 5.12903 17.6233 5.14153 17.6191C5.15403 17.615 5.16497 17.6071 5.1729 17.5966C5.55895 17.072 5.90069 16.5162 6.19457 15.9349C6.19866 15.9269 6.20103 15.9182 6.2015 15.9093C6.20198 15.9003 6.20056 15.8914 6.19733 15.8831C6.1941 15.8747 6.18914 15.8671 6.18278 15.8609C6.17641 15.8546 6.16878 15.8497 6.1604 15.8466C5.62159 15.6404 5.09995 15.3918 4.6004 15.1032C4.59124 15.0979 4.58354 15.0905 4.57797 15.0815C4.5724 15.0725 4.56914 15.0622 4.56848 15.0517C4.56782 15.0411 4.56978 15.0306 4.57418 15.021C4.57859 15.0113 4.58531 15.003 4.59373 14.9966C4.69893 14.9179 4.80229 14.8367 4.90373 14.7532C4.91261 14.746 4.92331 14.7414 4.93464 14.74C4.94597 14.7385 4.95748 14.7402 4.9679 14.7449C8.24123 16.2391 11.7846 16.2391 15.0196 14.7449C15.0301 14.74 15.0418 14.7382 15.0533 14.7397C15.0648 14.7412 15.0756 14.7459 15.0846 14.7532C15.1846 14.8349 15.2896 14.9182 15.3954 14.9966C15.4037 15.0029 15.4104 15.0111 15.4148 15.0205C15.4193 15.03 15.4213 15.0404 15.4208 15.0508C15.4203 15.0612 15.4173 15.0714 15.412 15.0804C15.4067 15.0894 15.3993 15.0969 15.3904 15.1024C14.892 15.3937 14.3699 15.6424 13.8296 15.8457C13.8212 15.849 13.8135 15.8539 13.8071 15.8603C13.8008 15.8666 13.7958 15.8743 13.7926 15.8827C13.7894 15.8911 13.788 15.9001 13.7884 15.9091C13.7889 15.9181 13.7913 15.9269 13.7954 15.9349C14.0954 16.5166 14.4387 17.0699 14.8162 17.5957C14.824 17.6064 14.8349 17.6145 14.8475 17.6186C14.86 17.6228 14.8736 17.623 14.8862 17.6191C16.685 17.0681 18.3765 16.2142 19.8879 15.0941C19.8953 15.0889 19.9014 15.0822 19.906 15.0744C19.9106 15.0667 19.9135 15.058 19.9146 15.0491C20.3312 10.7349 19.2162 6.9874 16.9571 3.66573C16.9518 3.65453 16.9426 3.64564 16.9312 3.64073V3.64157ZM6.68373 12.7749C5.6979 12.7749 4.88623 11.8707 4.88623 10.7591C4.88623 9.64823 5.6829 8.74323 6.68373 8.74323C7.69207 8.74323 8.49707 9.65657 8.48123 10.7599C8.48123 11.8707 7.68457 12.7749 6.68373 12.7749ZM13.3296 12.7749C12.3437 12.7749 11.5321 11.8707 11.5321 10.7591C11.5321 9.64823 12.3279 8.74323 13.3296 8.74323C14.3379 8.74323 15.1429 9.65657 15.1271 10.7599C15.1271 11.8707 14.3387 12.7749 13.3296 12.7749Z" />
|
||||
</g>
|
||||
<defs>
|
||||
@ -22,4 +21,3 @@ export const DiscordIcon: React.FC<Props> = ({ width = "24", height = "24", clas
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const DocumentIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const DocumentIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -15,4 +14,3 @@ export const DocumentIcon: React.FC<Props> = ({ width = "24", height = "24", cla
|
||||
<path d="M7.27051 14.792H12.7288C12.9094 14.792 13.0587 14.733 13.1768 14.6149C13.2948 14.4969 13.3538 14.3475 13.3538 14.167C13.3538 13.9864 13.2948 13.8371 13.1768 13.7191C13.0587 13.601 12.9094 13.542 12.7288 13.542H7.27051C7.08995 13.542 6.94065 13.601 6.82259 13.7191C6.70454 13.8371 6.64551 13.9864 6.64551 14.167C6.64551 14.3475 6.70454 14.4969 6.82259 14.6149C6.94065 14.733 7.08995 14.792 7.27051 14.792ZM7.27051 11.2503H12.7288C12.9094 11.2503 13.0587 11.1913 13.1768 11.0732C13.2948 10.9552 13.3538 10.8059 13.3538 10.6253C13.3538 10.4448 13.2948 10.2955 13.1768 10.1774C13.0587 10.0594 12.9094 10.0003 12.7288 10.0003H7.27051C7.08995 10.0003 6.94065 10.0594 6.82259 10.1774C6.70454 10.2955 6.64551 10.4448 6.64551 10.6253C6.64551 10.8059 6.70454 10.9552 6.82259 11.0732C6.94065 11.1913 7.08995 11.2503 7.27051 11.2503ZM4.58301 18.3337C4.24967 18.3337 3.95801 18.2087 3.70801 17.9587C3.45801 17.7087 3.33301 17.417 3.33301 17.0837V2.91699C3.33301 2.58366 3.45801 2.29199 3.70801 2.04199C3.95801 1.79199 4.24967 1.66699 4.58301 1.66699H11.583C11.7497 1.66699 11.9129 1.70171 12.0726 1.77116C12.2323 1.8406 12.3677 1.93088 12.4788 2.04199L16.2913 5.85449C16.4025 5.9656 16.4927 6.10102 16.5622 6.26074C16.6316 6.42046 16.6663 6.58366 16.6663 6.75033V17.0837C16.6663 17.417 16.5413 17.7087 16.2913 17.9587C16.0413 18.2087 15.7497 18.3337 15.4163 18.3337H4.58301ZM11.4788 6.16699V2.91699H4.58301V17.0837H15.4163V6.79199H12.1038C11.9233 6.79199 11.774 6.73296 11.6559 6.61491C11.5379 6.49685 11.4788 6.34755 11.4788 6.16699ZM4.58301 2.91699V6.79199V2.91699V17.0837V2.91699Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const EditIcon: React.FC<Props> = ({ width, height, className }) => {
|
||||
return (
|
||||
export const EditIcon: React.FC<Props> = ({ width, height, className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const EditIcon: React.FC<Props> = ({ width, height, className }) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const EllipsisHorizontalIcon: React.FC<Props> = ({ width, height, className }) => {
|
||||
return (
|
||||
export const EllipsisHorizontalIcon: React.FC<Props> = ({ width, height, className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const EllipsisHorizontalIcon: React.FC<Props> = ({ width, height, classNa
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const GithubIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const GithubIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -12,10 +11,10 @@ export const GithubIcon: React.FC<Props> = ({ width = "24", height = "24", class
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_282_232)">
|
||||
<g clipPath="url(#clip0_282_232)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 0C4.475 0 0 4.475 0 10C0 14.425 2.8625 18.1625 6.8375 19.4875C7.3375 19.575 7.525 19.275 7.525 19.0125C7.525 18.775 7.5125 17.9875 7.5125 17.15C5 17.6125 4.35 16.5375 4.15 15.975C4.0375 15.6875 3.55 14.8 3.125 14.5625C2.775 14.375 2.275 13.9125 3.1125 13.9C3.9 13.8875 4.4625 14.625 4.65 14.925C5.55 16.4375 6.9875 16.0125 7.5625 15.75C7.65 15.1 7.9125 14.6625 8.2 14.4125C5.975 14.1625 3.65 13.3 3.65 9.475C3.65 8.3875 4.0375 7.4875 4.675 6.7875C4.575 6.5375 4.225 5.5125 4.775 4.1375C4.775 4.1375 5.6125 3.875 7.525 5.1625C8.325 4.9375 9.175 4.825 10.025 4.825C10.875 4.825 11.725 4.9375 12.525 5.1625C14.4375 3.8625 15.275 4.1375 15.275 4.1375C15.825 5.5125 15.475 6.5375 15.375 6.7875C16.0125 7.4875 16.4 8.375 16.4 9.475C16.4 13.3125 14.0625 14.1625 11.8375 14.4125C12.2 14.725 12.5125 15.325 12.5125 16.2625C12.5125 17.6 12.5 18.675 12.5 19.0125C12.5 19.275 12.6875 19.5875 13.1875 19.4875C15.1726 18.8173 16.8976 17.5414 18.1197 15.8395C19.3418 14.1375 19.9994 12.0952 20 10C20 4.475 15.525 0 10 0Z"
|
||||
fill="#858E96"
|
||||
/>
|
||||
@ -27,4 +26,3 @@ export const GithubIcon: React.FC<Props> = ({ width = "24", height = "24", class
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const HeartbeatIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const HeartbeatIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -21,4 +20,3 @@ export const HeartbeatIcon: React.FC<Props> = ({ width = "24", height = "24", cl
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -7,8 +7,7 @@ export const LayerDiagonalIcon: React.FC<Props> = ({
|
||||
height = "24",
|
||||
className,
|
||||
color = "black",
|
||||
}) => {
|
||||
return (
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -23,4 +22,3 @@ export const LayerDiagonalIcon: React.FC<Props> = ({
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const LockIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const LockIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -15,4 +14,3 @@ export const LockIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
|
||||
<path d="M6 22C5.58333 22 5.22917 21.8542 4.9375 21.5625C4.64583 21.2708 4.5 20.9167 4.5 20.5V9.65C4.5 9.23333 4.64583 8.87917 4.9375 8.5875C5.22917 8.29583 5.58333 8.15 6 8.15H7.75V5.75C7.75 4.43333 8.2125 3.3125 9.1375 2.3875C10.0625 1.4625 11.1833 1 12.5 1C13.8167 1 14.9375 1.4625 15.8625 2.3875C16.7875 3.3125 17.25 4.43333 17.25 5.75V8.15H19C19.4167 8.15 19.7708 8.29583 20.0625 8.5875C20.3542 8.87917 20.5 9.23333 20.5 9.65V20.5C20.5 20.9167 20.3542 21.2708 20.0625 21.5625C19.7708 21.8542 19.4167 22 19 22H6ZM6 20.5H19V9.65H6V20.5ZM12.5 17C13.0333 17 13.4875 16.8167 13.8625 16.45C14.2375 16.0833 14.425 15.6417 14.425 15.125C14.425 14.625 14.2375 14.1708 13.8625 13.7625C13.4875 13.3542 13.0333 13.15 12.5 13.15C11.9667 13.15 11.5125 13.3542 11.1375 13.7625C10.7625 14.1708 10.575 14.625 10.575 15.125C10.575 15.6417 10.7625 16.0833 11.1375 16.45C11.5125 16.8167 11.9667 17 12.5 17ZM9.25 8.15H15.75V5.75C15.75 4.85 15.4333 4.08333 14.8 3.45C14.1667 2.81667 13.4 2.5 12.5 2.5C11.6 2.5 10.8333 2.81667 10.2 3.45C9.56667 4.08333 9.25 4.85 9.25 5.75V8.15ZM6 20.5V9.65V20.5Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const MenuIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const MenuIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const MenuIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const PlusIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const PlusIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const PlusIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -6,8 +6,7 @@ export const QuestionMarkCircleIcon: React.FC<Props> = ({
|
||||
width = "24",
|
||||
height = "24",
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -19,4 +18,3 @@ export const QuestionMarkCircleIcon: React.FC<Props> = ({
|
||||
<path d="M12.1 17.825C12.3667 17.825 12.5917 17.7333 12.775 17.55C12.9583 17.3667 13.05 17.1417 13.05 16.875C13.05 16.6083 12.9583 16.3833 12.775 16.2C12.5917 16.0167 12.3667 15.925 12.1 15.925C11.8333 15.925 11.6083 16.0167 11.425 16.2C11.2417 16.3833 11.15 16.6083 11.15 16.875C11.15 17.1417 11.2417 17.3667 11.425 17.55C11.6083 17.7333 11.8333 17.825 12.1 17.825ZM12.075 7.5C12.6417 7.5 13.1 7.65417 13.45 7.9625C13.8 8.27083 13.975 8.66667 13.975 9.15C13.975 9.48333 13.875 9.8125 13.675 10.1375C13.475 10.4625 13.15 10.8167 12.7 11.2C12.2667 11.5833 11.9208 11.9875 11.6625 12.4125C11.4042 12.8375 11.275 13.225 11.275 13.575C11.275 13.7583 11.3458 13.9042 11.4875 14.0125C11.6292 14.1208 11.7917 14.175 11.975 14.175C12.175 14.175 12.3417 14.1083 12.475 13.975C12.6083 13.8417 12.6917 13.675 12.725 13.475C12.775 13.1417 12.8875 12.8458 13.0625 12.5875C13.2375 12.3292 13.5083 12.05 13.875 11.75C14.375 11.3333 14.7375 10.9167 14.9625 10.5C15.1875 10.0833 15.3 9.61667 15.3 9.1C15.3 8.21667 15.0125 7.50833 14.4375 6.975C13.8625 6.44167 13.1 6.175 12.15 6.175C11.5167 6.175 10.9333 6.3 10.4 6.55C9.86667 6.8 9.425 7.16667 9.075 7.65C8.94167 7.83333 8.8875 8.02083 8.9125 8.2125C8.9375 8.40417 9.01667 8.55 9.15 8.65C9.33333 8.78333 9.52917 8.825 9.7375 8.775C9.94583 8.725 10.1167 8.60833 10.25 8.425C10.4667 8.125 10.7292 7.89583 11.0375 7.7375C11.3458 7.57917 11.6917 7.5 12.075 7.5ZM12 22C10.6 22 9.29167 21.7458 8.075 21.2375C6.85833 20.7292 5.8 20.025 4.9 19.125C4 18.225 3.29167 17.1667 2.775 15.95C2.25833 14.7333 2 13.4167 2 12C2 10.6 2.25833 9.29167 2.775 8.075C3.29167 6.85833 4 5.8 4.9 4.9C5.8 4 6.85833 3.29167 8.075 2.775C9.29167 2.25833 10.6 2 12 2C13.3833 2 14.6833 2.25833 15.9 2.775C17.1167 3.29167 18.175 4 19.075 4.9C19.975 5.8 20.6875 6.85833 21.2125 8.075C21.7375 9.29167 22 10.6 22 12C22 13.4167 21.7375 14.7333 21.2125 15.95C20.6875 17.1667 19.975 18.225 19.075 19.125C18.175 20.025 17.1167 20.7292 15.9 21.2375C14.6833 21.7458 13.3833 22 12 22ZM12 20.5C14.35 20.5 16.3542 19.6667 18.0125 18C19.6708 16.3333 20.5 14.3333 20.5 12C20.5 9.66667 19.6708 7.66667 18.0125 6C16.3542 4.33333 14.35 3.5 12 3.5C9.61667 3.5 7.60417 4.33333 5.9625 6C4.32083 7.66667 3.5 9.66667 3.5 12C3.5 14.3333 4.32083 16.3333 5.9625 18C7.60417 19.6667 9.61667 20.5 12 20.5Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const SettingIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const SettingIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const SettingIcon: React.FC<Props> = ({ width = "24", height = "24", clas
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const SignalCellularIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const SignalCellularIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const SignalCellularIcon: React.FC<Props> = ({ width = "24", height = "24
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -7,8 +7,7 @@ export const TagIcon: React.FC<Props> = ({
|
||||
height = "24",
|
||||
className,
|
||||
color = "black",
|
||||
}) => {
|
||||
return (
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -23,4 +22,3 @@ export const TagIcon: React.FC<Props> = ({
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const TuneIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const TuneIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const TuneIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -7,8 +7,7 @@ export const UpcomingCycleIcon: React.FC<Props> = ({
|
||||
height = "24",
|
||||
className,
|
||||
color = "black",
|
||||
}) => {
|
||||
return (
|
||||
}) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height={height} width={width} className={className}>
|
||||
<path
|
||||
d="M28.3 44v-3H39V19.5H9v11H6V10q0-1.2.9-2.1Q7.8 7 9 7h3.25V4h3.25v3h17V4h3.25v3H39q1.2 0 2.1.9.9.9.9 2.1v31q0 1.2-.9 2.1-.9.9-2.1.9ZM16 47.3l-2.1-2.1 5.65-5.7H2.5v-3h17.05l-5.65-5.7 2.1-2.1 9.3 9.3ZM9 16.5h30V10H9Zm0 0V10v6.5Z"
|
||||
@ -16,4 +15,3 @@ export const UpcomingCycleIcon: React.FC<Props> = ({
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,8 +2,7 @@ import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const UserGroupIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
|
||||
return (
|
||||
export const UserGroupIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -18,4 +17,3 @@ export const UserGroupIcon: React.FC<Props> = ({ width = "24", height = "24", cl
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user