Compare commits

...
Author SHA1 Message Date
pablohashescobar 056656ef1b dev: update sort order calculation 2023-08-01 17:46:55 +05:30
pablohashescobar adfb35e50d dev: fix sort order filter 2023-08-01 17:41:22 +05:30
pablohashescobar 080ab56fca dev: fix project join 2023-08-01 17:36:52 +05:30
pablohashescobar 62340b0158 fix: project join sort order 2023-08-01 17:27:04 +05:30
Aaryan KhandelwalandGitHub a8816ef473 refactor: issue activity component (#1749) 2023-08-01 17:07:11 +05:30
Anmol Singh BhatiaandGitHub d315a24c1c style: primary color variable added in global (#1748) 2023-08-01 17:04:23 +05:30
NikhilandGitHub e73a4bef4e chore: issue and project details in activity (#1747)
* chore: issue and project details in activity

* dev: update capture log
2023-08-01 17:03:19 +05:30
Dakshesh JainandGitHub a66a0680df fix: showing alert on error while deleting workspace member or invited member (#1746)
style: showing 'Leave' for current user
2023-08-01 15:32:42 +05:30
Anmol Singh BhatiaandGitHub 98c7453741 style: view dropdown (#1742) 2023-08-01 14:16:43 +05:30
NikhilandGitHub 1a5faca77c chore: show created by empty for viewers and guests (#1740)
* chore: show created by empty for viewers and guests

* dev: return empty queryset
2023-08-01 14:16:21 +05:30
NikhilandGitHub 6e7fa1a39c chore: project create to return sort order (#1738)
* chore: project create retun sort order

* chore: project create return sort order
2023-08-01 14:15:40 +05:30
NikhilandGitHub 7a6e742362 dev: fix migrations (#1735)
* dev: fix migrations

* dev: migrations for issue comment reactions and preference and cover image fields
2023-08-01 14:15:09 +05:30
Anmol Singh BhatiaandGitHub 8a9ff31009 style: sidebar project disclosure list open by default (#1744) 2023-08-01 14:13:42 +05:30
Dakshesh JainandGitHub d310b8f86f style: style if user doesn't have profile pic (#1745) 2023-08-01 14:12:57 +05:30
4e297d92f3 chore: update empty states, fix: delete issue modal (#1743)
* chore: update empty states

* fix: delete issue modal

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-08-01 13:58:58 +05:30
Anmol Singh BhatiaandGitHub e48147f87e style: create project modal (#1741) 2023-08-01 13:34:54 +05:30
Aaryan KhandelwalandGitHub 85a7a7df2b chore: profile dropdown in the sidebar (#1737)
* chore: profile dropdown in the sidebar

* style: update spacing and font sizes
2023-08-01 13:31:16 +05:30
Dakshesh JainandGitHub 92b22dc99e style: showing 'Created by me' tab to all user (#1739)
* style: showing 'Created by me' tab to all user

* refactor: removed unnecessary imports
2023-08-01 13:30:51 +05:30
Aaryan KhandelwalandGitHub cb4d294608 style: sidebar projects design (#1736)
* chore: disclosure menu for sidebar projects

* fix: projects list spacing

* style: new design
2023-08-01 12:24:34 +05:30
Aaryan KhandelwalandGitHub df8504e6f7 fix: issue redirection (#1733) 2023-08-01 11:22:47 +05:30
Dakshesh JainandGitHub 7287c27b73 refactor: changed per_page to 30 (#1734) 2023-08-01 11:19:06 +05:30
55 changed files with 1082 additions and 589 deletions
+2 -1
View File
@@ -291,7 +291,8 @@ class IssueCreateSerializer(BaseSerializer):
class IssueActivitySerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = IssueActivity
+1 -1
View File
@@ -477,7 +477,7 @@ class IssueActivityEndpoint(BaseAPIView):
~Q(field="comment"),
project__project_projectmember__member=self.request.user,
)
.select_related("actor", "workspace")
.select_related("actor", "workspace", "issue", "project")
).order_by("created_at")
issue_comments = (
IssueComment.objects.filter(issue_id=issue_id)
+8 -5
View File
@@ -10,7 +10,7 @@ from plane.utils.paginator import BasePaginator
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue
from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue, WorkspaceMember
from plane.api.serializers import NotificationSerializer
@@ -83,10 +83,13 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
# Created issues
if type == "created":
issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
if WorkspaceMember.objects.filter(workspace__slug=slug, member=request.user, role__lt=15).exists():
notifications = Notification.objects.none()
else:
issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
# Pagination
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
+1 -1
View File
@@ -140,7 +140,7 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
def get(self, request):
try:
queryset = IssueActivity.objects.filter(actor=request.user).select_related(
"actor", "workspace"
"actor", "workspace", "issue", "project"
)
return self.paginate(
+43 -13
View File
@@ -123,7 +123,7 @@ class ProjectViewSet(BaseViewSet):
sort_order_query = ProjectMember.objects.filter(
member=request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
workspace__slug=self.kwargs.get("slug"),
).values("sort_order")
projects = (
self.get_queryset()
@@ -176,7 +176,7 @@ class ProjectViewSet(BaseViewSet):
serializer.save()
# Add the user as Administrator to the project
ProjectMember.objects.create(
project_member = ProjectMember.objects.create(
project_id=serializer.data["id"], member=request.user, role=20
)
@@ -238,9 +238,11 @@ class ProjectViewSet(BaseViewSet):
]
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
data = serializer.data
data["sort_order"] = project_member.sort_order
return Response(data, status=status.HTTP_201_CREATED)
return Response(
[serializer.errors[error][0] for error in serializer.errors],
serializer.errors,
status=status.HTTP_400_BAD_REQUEST,
)
except IntegrityError as e:
@@ -600,19 +602,27 @@ class AddMemberToProjectEndpoint(BaseAPIView):
)
bulk_project_members = []
project_members = ProjectMember.objects.filter(
workspace=self.workspace, member_id__in=[member.get("member_id") for member in members]
).values("member_id").annotate(sort_order_min=Min("sort_order"))
project_members = (
ProjectMember.objects.filter(
workspace=self.workspace,
member_id__in=[member.get("member_id") for member in members],
)
.values("member_id")
.annotate(sort_order_min=Min("sort_order"))
)
for member in members:
sort_order = [project_member.get("sort_order") for project_member in project_members]
sort_order = [
project_member.get("sort_order")
for project_member in project_members
]
bulk_project_members.append(
ProjectMember(
member_id=member.get("member_id"),
role=member.get("role", 10),
project_id=project_id,
workspace_id=project.workspace_id,
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
)
)
@@ -807,11 +817,26 @@ class ProjectJoinEndpoint(BaseAPIView):
member=request.user, workspace__slug=slug
)
smallest_sort_order = (
ProjectMember.objects.filter(
workspace__slug=slug, member=request.user
)
.aggregate(smallest=Min("sort_order"))
.get("smallest", 65535)
)
if smallest_sort_order is None:
smallest_sort_order = 55535
else:
smallest_sort_order = smallest_sort_order - 10000
workspace_role = workspace_member.role
workspace = workspace_member.workspace
ProjectMember.objects.bulk_create(
[
bulk_project_members = []
for project_id in project_ids:
bulk_project_members.append(
ProjectMember(
project_id=project_id,
member=request.user,
@@ -820,9 +845,14 @@ class ProjectJoinEndpoint(BaseAPIView):
else (15 if workspace_role == 10 else workspace_role),
workspace=workspace,
created_by=request.user,
sort_order=smallest_sort_order,
)
for project_id in project_ids
],
)
smallest_sort_order = smallest_sort_order - 10000
ProjectMember.objects.bulk_create(
bulk_project_members,
ignore_conflicts=True,
)
+1 -1
View File
@@ -1190,7 +1190,7 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
workspace__slug=slug,
project__project_projectmember__member=request.user,
actor=user_id,
).select_related("actor", "workspace")
).select_related("actor", "workspace", "issue", "project")
if projects:
queryset = queryset.filter(project__in=projects)
@@ -89,7 +89,7 @@ class Migration(migrations.Migration):
field=models.JSONField(default=plane.db.models.workspace.get_default_props),
),
migrations.AddField(
model_name='project',
model_name='projectmember',
name='sort_order',
field=models.FloatField(default=65535),
),
@@ -0,0 +1,71 @@
# Generated by Django 4.2.3 on 2023-08-01 06:02
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.project
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0039_auto_20230723_2203'),
]
operations = [
migrations.AddField(
model_name='projectmember',
name='preferences',
field=models.JSONField(default=plane.db.models.project.get_default_preferences),
),
migrations.AddField(
model_name='user',
name='cover_image',
field=models.URLField(blank=True, max_length=800, null=True),
),
migrations.CreateModel(
name='IssueReaction',
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)),
('reaction', models.CharField(max_length=20)),
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_reactions', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_reactions', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Issue Reaction',
'verbose_name_plural': 'Issue Reactions',
'db_table': 'issue_reactions',
'ordering': ('-created_at',),
'unique_together': {('issue', 'actor', 'reaction')},
},
),
migrations.CreateModel(
name='CommentReaction',
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)),
('reaction', models.CharField(max_length=20)),
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comment_reactions', to=settings.AUTH_USER_MODEL)),
('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comment_reactions', to='db.issuecomment')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Comment Reaction',
'verbose_name_plural': 'Comment Reactions',
'db_table': 'comment_reactions',
'ordering': ('-created_at',),
'unique_together': {('comment', 'actor', 'reaction')},
},
),
]
+1 -1
View File
@@ -161,7 +161,7 @@ class ProjectMember(ProjectBaseModel):
def save(self, *args, **kwargs):
if self._state.adding:
smallest_sort_order = ProjectMember.objects.filter(
workspace=self.workspace, member=self.member
workspace_id=self.project.workspace_id, member=self.member
).aggregate(smallest=models.Min("sort_order"))["smallest"]
# Project ordering
@@ -1,5 +1,7 @@
import { useRouter } from "next/router";
// icons
import { Icon } from "components/ui";
import { Icon, Tooltip } from "components/ui";
import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon } from "components/icons";
// helpers
@@ -8,26 +10,65 @@ import { capitalizeFirstLetter } from "helpers/string.helper";
// types
import { IIssueActivity } from "types";
export const activityDetails: {
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<Tooltip
tooltipContent={
activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"
}
>
<a
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.issue_detail
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
: "Issue"}
<Icon iconName="launch" className="!text-xs" />
</a>
</Tooltip>
);
};
const activityDetails: {
[key: string]: {
message: (activity: IIssueActivity) => React.ReactNode;
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode;
icon: React.ReactNode;
};
} = {
assignees: {
message: (activity) => {
message: (activity, showIssue) => {
if (activity.old_value === "")
return (
<>
added a new assignee{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
.
</>
);
else
return (
<>
removed the assignee{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</>
);
},
@@ -41,7 +82,7 @@ export const activityDetails: {
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />,
},
attachment: {
message: (activity) => {
message: (activity, showIssue) => {
if (activity.verb === "created")
return (
<>
@@ -55,9 +96,27 @@ export const activityDetails: {
attachment
<Icon iconName="launch" className="!text-xs" />
</a>
{showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
</>
);
else
return (
<>
removed an attachment
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</>
);
else return "removed an attachment.";
},
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
},
@@ -126,17 +185,47 @@ export const activityDetails: {
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
},
description: {
message: (activity) => "updated the description.",
message: (activity, showIssue) => (
<>
updated the description
{showIssue && (
<>
{" "}
of <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
},
estimate_point: {
message: (activity) => {
if (!activity.new_value) return "removed the estimate point.";
message: (activity, showIssue) => {
if (!activity.new_value)
return (
<>
removed the estimate point
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</>
);
else
return (
<>
set the estimate point to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</>
);
},
@@ -150,7 +239,7 @@ export const activityDetails: {
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
},
labels: {
message: (activity) => {
message: (activity, showIssue) => {
if (activity.old_value === "")
return (
<>
@@ -165,6 +254,12 @@ export const activityDetails: {
/>
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
</span>
{showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
</>
);
else
@@ -181,13 +276,19 @@ export const activityDetails: {
/>
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
</span>
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
</>
);
},
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
},
link: {
message: (activity) => {
message: (activity, showIssue) => {
if (activity.verb === "created")
return (
<>
@@ -200,8 +301,14 @@ export const activityDetails: {
>
link
<Icon iconName="launch" className="!text-xs" />
</a>{" "}
to the issue.
</a>
{showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
.
</>
);
else
@@ -216,8 +323,14 @@ export const activityDetails: {
>
link
<Icon iconName="launch" className="!text-xs" />
</a>{" "}
from the issue.
</a>
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</>
);
},
@@ -250,52 +363,102 @@ export const activityDetails: {
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
},
name: {
message: (activity) => `set the name to ${activity.new_value}.`,
message: (activity, showIssue) => (
<>
set the name to {activity.new_value}
{showIssue && (
<>
{" "}
of <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
},
parent: {
message: (activity) => {
message: (activity, showIssue) => {
if (!activity.new_value)
return (
<>
removed the parent{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</>
);
else
return (
<>
set the parent to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</>
);
},
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
},
priority: {
message: (activity) => (
message: (activity, showIssue) => (
<>
set the priority to{" "}
<span className="font-medium text-custom-text-100">
{activity.new_value ? capitalizeFirstLetter(activity.new_value) : "None"}
</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
},
state: {
message: (activity) => (
message: (activity, showIssue) => (
<>
set the state to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />,
},
target_date: {
message: (activity) => {
if (!activity.new_value) return "removed the due date.";
message: (activity, showIssue) => {
if (!activity.new_value)
return (
<>
removed the due date
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</>
);
else
return (
<>
@@ -303,6 +466,12 @@ export const activityDetails: {
<span className="font-medium text-custom-text-100">
{renderShortDateWithYearFormat(activity.new_value)}
</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</>
);
@@ -310,3 +479,19 @@ export const activityDetails: {
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
},
};
export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
<>{activityDetails[activity.field as keyof typeof activityDetails].icon}</>
);
export const ActivityMessage = ({
activity,
showIssue = false,
}: {
activity: IIssueActivity;
showIssue?: boolean;
}) => (
<>
{activityDetails[activity.field as keyof typeof activityDetails].message(activity, showIssue)}
</>
);
@@ -181,78 +181,85 @@ export const IssuesFilterView: React.FC = () => {
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<CustomMenu
label={
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
?.name ?? "Select"
}
className="w-28"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null;
if (option.key === "project") return null;
<div className="w-28">
<CustomMenu
label={
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null;
if (option.key === "project") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupByProperty(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupByProperty(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
className="w-28"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" &&
option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
</div>
</>
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
?.name ?? "Select"
}
className="w-28"
buttonClassName="w-full"
>
{FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem
key={option.key}
onClick={() =>
setFilters({
type: option.key,
})
}
>
{option.name}
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="w-28">
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem
key={option.key}
onClick={() =>
setFilters({
type: option.key,
})
}
>
{option.name}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && (
+1
View File
@@ -3,6 +3,7 @@ export * from "./modals";
export * from "./sidebar";
export * from "./theme";
export * from "./views";
export * from "./activity";
export * from "./feeds";
export * from "./reaction-selector";
export * from "./image-picker-popover";
+29 -31
View File
@@ -21,9 +21,9 @@ import {
GanttChartView,
} from "components/core";
// ui
import { EmptyState, SecondaryButton, Spinner } from "components/ui";
import { EmptyState, Spinner } from "components/ui";
// icons
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
import { TrashIcon } from "@heroicons/react/24/outline";
// images
import emptyIssue from "public/empty-state/issue.svg";
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
@@ -39,6 +39,16 @@ type Props = {
addIssueToGroup: (groupTitle: string) => void;
disableUserActions: boolean;
dragDisabled?: boolean;
emptyState: {
title: string;
description?: string;
primaryButton?: {
icon: any;
text: string;
onClick: () => void;
};
secondaryButton?: React.ReactNode;
};
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleOnDragEnd: (result: DropResult) => Promise<void>;
openIssuesListModal: (() => void) | null;
@@ -53,6 +63,7 @@ export const AllViews: React.FC<Props> = ({
addIssueToGroup,
disableUserActions,
dragDisabled = false,
emptyState,
handleIssueAction,
handleOnDragEnd,
openIssuesListModal,
@@ -156,41 +167,28 @@ export const AllViews: React.FC<Props> = ({
title="Archived Issues will be shown here"
description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
image={emptyIssueArchive}
buttonText="Go to Automation Settings"
onClick={() => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
primaryButton={{
text: "Go to Automation Settings",
onClick: () => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
},
}}
/>
) : (
<EmptyState
title={
cycleId
? "Cycle issues will appear here"
: moduleId
? "Module issues will appear here"
: "Project issues will appear here"
}
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
title={emptyState.title}
description={emptyState.description}
image={emptyIssue}
buttonText="New Issue"
buttonIcon={<PlusIcon className="h-4 w-4" />}
secondaryButton={
cycleId || moduleId ? (
<SecondaryButton
className="flex items-center gap-1.5"
onClick={openIssuesListModal ?? (() => {})}
>
<PlusIcon className="h-4 w-4" />
Add an existing issue
</SecondaryButton>
) : null
primaryButton={
emptyState.primaryButton
? {
icon: emptyState.primaryButton.icon,
text: emptyState.primaryButton.text,
onClick: emptyState.primaryButton.onClick,
}
: undefined
}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
secondaryButton={emptyState.secondaryButton}
/>
)
) : (
@@ -221,7 +221,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
Copy issue link
</ContextMenu.Item>
<a
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
target="_blank"
rel="noreferrer noopener"
>
@@ -192,7 +192,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
</CustomMenu>
</div>
)}
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}>
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a className="flex w-full cursor-pointer flex-col items-start justify-center gap-1.5">
{properties.key && (
<Tooltip
+30 -1
View File
@@ -22,7 +22,7 @@ import { FiltersList, AllViews } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { CreateUpdateViewModal } from "components/views";
// ui
import { PrimaryButton } from "components/ui";
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// helpers
@@ -515,6 +515,35 @@ export const IssuesView: React.FC<Props> = ({
selectedGroup === "labels" ||
selectedGroup === "state_detail.group"
}
emptyState={{
title: cycleId
? "Cycle issues will appear here"
: moduleId
? "Module issues will appear here"
: "Project issues will appear here",
description:
"Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done.",
primaryButton: {
icon: <PlusIcon className="h-4 w-4" />,
text: "New Issue",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
},
secondaryButton:
cycleId || moduleId ? (
<SecondaryButton
className="flex items-center gap-1.5"
onClick={openIssuesListModal ?? (() => {})}
>
<PlusIcon className="h-4 w-4" />
Add an existing issue
</SecondaryButton>
) : null,
}}
handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction}
openIssuesListModal={openIssuesListModal ? openIssuesListModal : null}
@@ -156,9 +156,9 @@ export const SingleListIssue: React.FC<Props> = ({
});
};
const singleIssuePath = isArchivedIssues
? `/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}`
: `/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`;
const issuePath = isArchivedIssues
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
const isNotAllowed =
userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
@@ -187,7 +187,7 @@ export const SingleListIssue: React.FC<Props> = ({
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link
</ContextMenu.Item>
<a href={singleIssuePath} target="_blank" rel="noreferrer noopener">
<a href={issuePath} target="_blank" rel="noreferrer noopener">
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab
</ContextMenu.Item>
@@ -202,7 +202,7 @@ export const SingleListIssue: React.FC<Props> = ({
}}
>
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
<Link href={singleIssuePath}>
<Link href={issuePath}>
<a className="group relative flex items-center gap-2">
{properties.key && (
<Tooltip
@@ -263,7 +263,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
)}
</div>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a className="truncate text-custom-text-100 cursor-pointer w-full text-[0.825rem]">
{issue.name}
</a>
+7 -8
View File
@@ -8,12 +8,12 @@ import useSWR from "swr";
// services
import issuesService from "services/issues.service";
// components
import { ActivityIcon, ActivityMessage } from "components/core";
import { CommentCard } from "components/issues/comment";
// ui
import { Icon, Loader } from "components/ui";
// helpers
import { timeAgo } from "helpers/date-time.helper";
import { activityDetails } from "helpers/activity.helper";
// types
import { ICurrentUserResponse, IIssueComment } from "types";
// fetch-keys
@@ -91,11 +91,11 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
<ul role="list" className="-mb-4">
{issueActivities.map((activityItem, index) => {
// determines what type of action is performed
const message = activityItem.field
? activityDetails[activityItem.field as keyof typeof activityDetails]?.message(
activityItem
)
: "created the issue.";
const message = activityItem.field ? (
<ActivityMessage activity={activityItem} />
) : (
"created the issue."
);
if ("field" in activityItem && activityItem.field !== "updated_by") {
return (
@@ -116,8 +116,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
activityItem.new_value === "restore" ? (
<Icon iconName="history" className="text-sm text-custom-text-200" />
) : (
activityDetails[activityItem.field as keyof typeof activityDetails]
?.icon
<ActivityIcon activity={activityItem} />
)
) : activityItem.actor_detail.avatar &&
activityItem.actor_detail.avatar !== "" ? (
@@ -140,11 +140,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
ref={showEditorRef}
/>
<CommentReaction
workspaceSlug={comment?.workspace_detail?.slug}
projectId={comment.project}
commentId={comment.id}
/>
<CommentReaction projectId={comment.project} commentId={comment.id} />
</div>
</div>
</div>
@@ -1,5 +1,7 @@
import React from "react";
import { useRouter } from "next/router";
// hooks
import useUser from "hooks/use-user";
import useCommentReaction from "hooks/use-comment-reaction";
@@ -9,13 +11,15 @@ import { ReactionSelector } from "components/core";
import { renderEmoji } from "helpers/emoji.helper";
type Props = {
workspaceSlug?: string | string[];
projectId?: string | string[];
commentId: string;
};
export const CommentReaction: React.FC<Props> = (props) => {
const { workspaceSlug, projectId, commentId } = props;
const { projectId, commentId } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUser();
@@ -59,8 +59,9 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
};
const handleDeletion = async () => {
if (!workspaceSlug || !data) return;
setIsDeleteLoading(true);
if (!workspaceSlug || !projectId || !data) return;
await issueServices
.deleteIssue(workspaceSlug as string, data.project, data.id, user)
@@ -112,7 +113,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
} else {
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, params));
}
handleClose();
@@ -21,6 +21,7 @@ import { orderArrayBy } from "helpers/array.helper";
import { IIssue, IIssueFilterOptions } from "types";
// fetch-keys
import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys";
import { PlusIcon } from "@heroicons/react/24/outline";
type Props = {
openIssuesListModal?: () => void;
@@ -262,6 +263,20 @@ export const MyIssuesView: React.FC<Props> = ({
addIssueToGroup={addIssueToGroup}
disableUserActions={disableUserActions}
dragDisabled={groupBy !== "priority"}
emptyState={{
title: "You don't have any issue assigned to you yet",
description: "Keep track of your work in a single place.",
primaryButton: {
icon: <PlusIcon className="h-4 w-4" />,
text: "New Issue",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
},
}}
handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction}
openIssuesListModal={openIssuesListModal ? openIssuesListModal : null}
@@ -1,10 +1,5 @@
import React from "react";
import { useRouter } from "next/router";
// hooks
import useWorkspaceMembers from "hooks/use-workspace-members";
// components
import { Icon, Tooltip } from "components/ui";
// helpers
@@ -44,11 +39,6 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
setSelectedTab,
} = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { isOwner, isMember } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
const notificationTabs: Array<{
label: string;
value: NotificationType;
@@ -150,59 +140,31 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
</button>
) : (
<nav className="flex space-x-5 overflow-x-auto" aria-label="Tabs">
{notificationTabs.map((tab) =>
tab.value === "created" ? (
isMember || isOwner ? (
<button
type="button"
key={tab.value}
onClick={() => setSelectedTab(tab.value)}
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium outline-none ${
{notificationTabs.map((tab) => (
<button
type="button"
key={tab.value}
onClick={() => setSelectedTab(tab.value)}
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium outline-none ${
tab.value === selectedTab
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent text-custom-text-200"
}`}
>
{tab.label}
{tab.unreadCount && tab.unreadCount > 0 ? (
<span
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
tab.value === selectedTab
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent text-custom-text-200"
? "bg-custom-primary-100 text-white"
: "bg-custom-background-80 text-custom-text-200"
}`}
>
{tab.label}
{tab.unreadCount && tab.unreadCount > 0 ? (
<span
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
tab.value === selectedTab
? "bg-custom-primary-100 text-white"
: "bg-custom-background-80 text-custom-text-200"
}`}
>
{getNumberCount(tab.unreadCount)}
</span>
) : null}
</button>
) : null
) : (
<button
type="button"
key={tab.value}
onClick={() => setSelectedTab(tab.value)}
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium ${
tab.value === selectedTab
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent text-custom-text-200"
}`}
>
{tab.label}
{tab.unreadCount && tab.unreadCount > 0 ? (
<span
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
tab.value === selectedTab
? "bg-custom-primary-100 text-white"
: "bg-custom-background-80 text-custom-text-200"
}`}
>
{getNumberCount(tab.unreadCount)}
</span>
) : null}
</button>
)
)}
{getNumberCount(tab.unreadCount)}
</span>
) : null}
</button>
))}
</nav>
)}
</div>
@@ -56,13 +56,15 @@ export const RecentPagesList: React.FC<TPagesListProps> = ({ viewType }) => {
title="Have your thoughts in place"
description="You can think of Pages as an AI-powered notepad."
image={emptyPage}
buttonText="New Page"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "d",
});
document.dispatchEvent(e);
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New Page",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "d",
});
document.dispatchEvent(e);
},
}}
/>
)
+9 -7
View File
@@ -260,13 +260,15 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
title="Have your thoughts in place"
description="You can think of Pages as an AI-powered notepad."
image={emptyPage}
buttonText="New Page"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "d",
});
document.dispatchEvent(e);
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New Page",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "d",
});
document.dispatchEvent(e);
},
}}
/>
)}
@@ -1,14 +1,14 @@
import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr";
// services
import userService from "services/user.service";
// components
import { ActivityMessage } from "components/core";
// ui
import { Icon, Loader } from "components/ui";
// helpers
import { activityDetails } from "helpers/activity.helper";
import { timeAgo } from "helpers/date-time.helper";
// fetch-keys
import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys";
@@ -55,12 +55,12 @@ export const ProfileActivity = () => {
{activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "}
</span>
{activity.field ? (
activityDetails[activity.field]?.message(activity as any)
<ActivityMessage activity={activity} showIssue />
) : (
<span>
created this{" "}
<a
href={`/${activity.workspace_detail.slug}/projects/${activity.project}/issues/${activity.issue}`}
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
@@ -140,10 +140,26 @@ export const ProfileIssuesView = () => {
]
);
const addIssueToGroup = useCallback((groupTitle: string) => {
setCreateIssueModal(true);
return;
}, []);
const addIssueToGroup = useCallback(
(groupTitle: string) => {
setCreateIssueModal(true);
let preloadedValue: string | string[] = groupTitle;
if (groupByProperty === "labels") {
if (groupTitle === "None") preloadedValue = [];
else preloadedValue = [groupTitle];
}
if (groupByProperty)
setPreloadedData({
[groupByProperty]: preloadedValue,
actionType: "createIssue",
});
else setPreloadedData({ actionType: "createIssue" });
},
[setCreateIssueModal, setPreloadedData, groupByProperty]
);
const addIssueToDate = useCallback(
(date: string) => {
@@ -250,6 +266,13 @@ export const ProfileIssuesView = () => {
addIssueToGroup={addIssueToGroup}
disableUserActions={false}
dragDisabled={groupByProperty !== "priority"}
emptyState={{
title: router.pathname.includes("assigned")
? `Issues assigned to ${user?.first_name} ${user?.last_name} will appear here`
: router.pathname.includes("created")
? `Issues created by ${user?.first_name} ${user?.last_name} will appear here`
: `Issues subscribed by ${user?.first_name} ${user?.last_name} will appear here`,
}}
handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction}
openIssuesListModal={null}
+1 -1
View File
@@ -97,7 +97,7 @@ export const ProfileSidebar = () => {
className="rounded"
/>
) : (
<div className="bg-custom-background-90 text-custom-text-100">
<div className="bg-custom-background-90 flex justify-center items-center w-[52px] h-[52px] rounded text-custom-text-100">
{userProjectsData.user_data.first_name[0]}
</div>
)}
@@ -47,7 +47,7 @@ type Props = {
const defaultValues: Partial<IProject> = {
cover_image:
"https://images.unsplash.com/photo-1575116464504-9e7652fddcb3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwyODUyNTV8MHwxfHNlYXJjaHwxOHx8cGxhbmV8ZW58MHx8fHwxNjgxNDY4NTY5&ixlib=rb-4.0.3&q=80&w=1080",
"https://images.unsplash.com/photo-1531045535792-b515d59c3d1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
description: "",
emoji_and_icon: getRandomEmoji(),
identifier: "",
@@ -213,7 +213,7 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="transform rounded-lg bg-custom-background-100 text-left shadow-xl transition-all p-3 w-full sm:w-3/5 lg:w-1/2 xl:w-2/5">
<div className="group relative h-36 w-full rounded-lg bg-custom-background-80">
<div className="group relative h-44 w-full rounded-lg bg-custom-background-80">
{watch("cover_image") !== null && (
<img
src={watch("cover_image")!}
@@ -310,7 +310,7 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
<TextArea
id="description"
name="description"
className="text-sm !h-[8rem]"
className="text-sm !h-24"
placeholder="Description..."
error={errors.description}
register={register}
@@ -327,6 +327,7 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
<CustomSelect
value={value}
onChange={onChange}
buttonClassName="border-[0.5px] !px-2 shadow-md"
label={
<div className="flex items-center gap-2 -mb-0.5 py-1">
{currentNetwork ? (
@@ -369,6 +370,7 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
value={value}
onChange={onChange}
options={options}
buttonClassName="!px-2 shadow-md"
label={
<div className="flex items-center justify-center gap-2 py-[1px]">
{value ? (
+146 -65
View File
@@ -5,6 +5,8 @@ import { mutate } from "swr";
// react-beautiful-dnd
import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd";
// headless ui
import { Disclosure } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
import useTheme from "hooks/use-theme";
@@ -15,6 +17,7 @@ import { DeleteProjectModal, SingleSidebarProject } from "components/project";
// services
import projectService from "services/project.service";
// icons
import { Icon } from "components/ui";
import { PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
@@ -30,7 +33,7 @@ export const ProjectSidebarList: FC = () => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
const { user } = useUserAuth();
@@ -38,15 +41,18 @@ export const ProjectSidebarList: FC = () => {
const { setToastAlert } = useToast();
const { projects: allProjects } = useProjects();
const joinedProjects = allProjects?.filter((p) => p.sort_order);
const favoriteProjects = allProjects?.filter((p) => p.is_favorite);
const otherProjects = allProjects?.filter((p) => p.sort_order === null);
const orderedAllProjects = allProjects
? orderArrayBy(allProjects, "sort_order", "ascending")
: [];
const orderedJoinedProjects: IProject[] | undefined = joinedProjects
? orderArrayBy(joinedProjects, "sort_order", "ascending")
: undefined;
const orderedFavProjects = favoriteProjects
const orderedFavProjects: IProject[] | undefined = favoriteProjects
? orderArrayBy(favoriteProjects, "sort_order", "ascending")
: [];
: undefined;
const handleDeleteProject = (project: IProject) => {
setProjectToDelete(project);
@@ -69,22 +75,25 @@ export const ProjectSidebarList: FC = () => {
const { source, destination, draggableId } = result;
if (!destination || !workspaceSlug) return;
if (source.index === destination.index) return;
const projectList =
destination.droppableId === "all-projects" ? orderedAllProjects : orderedFavProjects;
const projectsList =
(destination.droppableId === "joined-projects"
? orderedJoinedProjects
: orderedFavProjects) ?? [];
let updatedSortOrder = projectList[source.index].sort_order;
if (destination.index === 0) {
updatedSortOrder = projectList[0].sort_order - 1000;
} else if (destination.index === projectList.length - 1) {
updatedSortOrder = projectList[projectList.length - 1].sort_order + 1000;
} else {
const destinationSortingOrder = projectList[destination.index].sort_order;
let updatedSortOrder = projectsList[source.index].sort_order;
if (destination.index === 0) updatedSortOrder = (projectsList[0].sort_order as number) - 1000;
else if (destination.index === projectsList.length - 1)
updatedSortOrder = (projectsList[projectsList.length - 1].sort_order as number) + 1000;
else {
const destinationSortingOrder = projectsList[destination.index].sort_order as number;
const relativeDestinationSortingOrder =
source.index < destination.index
? projectList[destination.index + 1].sort_order
: projectList[destination.index - 1].sort_order;
? (projectsList[destination.index + 1].sort_order as number)
: (projectsList[destination.index - 1].sort_order as number);
updatedSortOrder = Math.round(
(destinationSortingOrder + relativeDestinationSortingOrder) / 2
@@ -121,76 +130,148 @@ export const ProjectSidebarList: FC = () => {
data={projectToDelete}
user={user}
/>
<div className="h-full overflow-y-auto px-4">
<div className="h-full overflow-y-auto px-5 space-y-3 pt-3 border-t border-custom-sidebar-border-300">
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="favorite-projects">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{orderedFavProjects && orderedFavProjects.length > 0 && (
<div className="flex flex-col space-y-2 mt-5">
{!sidebarCollapse && (
<h5 className="text-sm font-medium text-custom-sidebar-text-200">
Favorites
</h5>
)}
{orderedFavProjects.map((project, index) => (
<Draggable key={project.id} draggableId={project.id} index={index}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<SingleSidebarProject
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
provided={provided}
snapshot={snapshot}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
shortContextMenu
<Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={true}>
{({ open }) => (
<>
{!sidebarCollapse && (
<Disclosure.Button
as="button"
type="button"
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap"
>
Favorites
<Icon
iconName={open ? "arrow_drop_down" : "arrow_right"}
className="group-hover:opacity-100 opacity-0 !text-lg"
/>
</div>
</Disclosure.Button>
)}
</Draggable>
))}
{provided.placeholder}
</div>
<Disclosure.Panel as="div" className="space-y-2">
{orderedFavProjects.map((project, index) => (
<Draggable
key={project.id}
draggableId={project.id}
index={index}
isDragDisabled={project.sort_order === null}
>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<SingleSidebarProject
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
provided={provided}
snapshot={snapshot}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
shortContextMenu
/>
</div>
)}
</Draggable>
))}
</Disclosure.Panel>
{provided.placeholder}
</>
)}
</Disclosure>
)}
</div>
)}
</Droppable>
</DragDropContext>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="all-projects">
<Droppable droppableId="joined-projects">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{orderedAllProjects && orderedAllProjects.length > 0 && (
<div className="flex flex-col space-y-2 mt-5">
{!sidebarCollapse && (
<h5 className="text-sm font-medium text-custom-sidebar-text-200">Projects</h5>
)}
{orderedAllProjects.map((project, index) => (
<Draggable key={project.id} draggableId={project.id} index={index}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<SingleSidebarProject
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
provided={provided}
snapshot={snapshot}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
{orderedJoinedProjects && orderedJoinedProjects.length > 0 && (
<Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={true}>
{({ open }) => (
<>
{!sidebarCollapse && (
<Disclosure.Button
as="button"
type="button"
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap"
>
Projects
<Icon
iconName={open ? "arrow_drop_down" : "arrow_right"}
className="group-hover:opacity-100 opacity-0 !text-lg"
/>
</div>
</Disclosure.Button>
)}
</Draggable>
))}
{provided.placeholder}
</div>
<Disclosure.Panel as="div" className="space-y-2">
{orderedJoinedProjects.map((project, index) => (
<Draggable key={project.id} draggableId={project.id} index={index}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<SingleSidebarProject
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
provided={provided}
snapshot={snapshot}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
/>
</div>
)}
</Draggable>
))}
</Disclosure.Panel>
{provided.placeholder}
</>
)}
</Disclosure>
)}
</div>
)}
</Droppable>
</DragDropContext>
{otherProjects && otherProjects.length > 0 && (
<Disclosure
as="div"
className="flex flex-col space-y-2"
defaultOpen={projectId && otherProjects.find((p) => p.id === projectId) ? true : false}
>
{({ open }) => (
<>
{!sidebarCollapse && (
<Disclosure.Button
as="button"
type="button"
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap"
>
Other Projects
<Icon
iconName={open ? "arrow_drop_down" : "arrow_right"}
className="group-hover:opacity-100 opacity-0 !text-lg"
/>
</Disclosure.Button>
)}
<Disclosure.Panel as="div" className="space-y-2">
{otherProjects?.map((project, index) => (
<SingleSidebarProject
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
shortContextMenu
/>
))}
</Disclosure.Panel>
</>
)}
</Disclosure>
)}
{allProjects && allProjects.length === 0 && (
<button
type="button"
@@ -36,8 +36,8 @@ import { PROJECTS_LIST } from "constants/fetch-keys";
type Props = {
project: IProject;
sidebarCollapse: boolean;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
provided?: DraggableProvided;
snapshot?: DraggableStateSnapshot;
handleDeleteProject: () => void;
handleCopyText: () => void;
shortContextMenu?: boolean;
@@ -133,34 +133,36 @@ export const SingleSidebarProject: React.FC<Props> = ({
};
return (
<Disclosure key={project?.id} defaultOpen={projectId === project?.id}>
<Disclosure key={project.id} defaultOpen={projectId === project.id}>
{({ open }) => (
<>
<div
className={`group relative flex items-center gap-x-1 text-custom-sidebar-text-100 ${
snapshot.isDragging ? "opacity-60" : ""
className={`group relative text-custom-sidebar-text-10 px-2 py-1 w-full flex items-center hover:bg-custom-sidebar-background-80 rounded-md ${
snapshot?.isDragging ? "opacity-60" : ""
}`}
>
<button
type="button"
className={`absolute top-2 left-0 hidden rounded p-0.5 ${
sidebarCollapse ? "" : "group-hover:!flex"
}`}
{...provided.dragHandleProps}
>
<EllipsisVerticalIcon className="h-4" />
<EllipsisVerticalIcon className="-ml-5 h-4" />
</button>
{provided && (
<button
type="button"
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 ${
sidebarCollapse ? "" : "group-hover:!flex"
}`}
{...provided?.dragHandleProps}
>
<EllipsisVerticalIcon className="h-4" />
<EllipsisVerticalIcon className="-ml-5 h-4" />
</button>
)}
<Tooltip
tooltipContent={`${project?.name}`}
tooltipContent={`${project.name}`}
position="right"
className="ml-2"
disabled={!sidebarCollapse}
>
<Disclosure.Button
as="div"
className={`flex w-full cursor-pointer select-none items-center rounded-sm py-1 text-left text-sm font-medium ${
sidebarCollapse ? "justify-center" : "justify-between ml-4"
className={`flex items-center w-full cursor-pointer select-none text-left text-sm font-medium ${
sidebarCollapse ? "justify-center" : `justify-between`
}`}
>
<div className="flex items-center gap-x-2">
@@ -184,21 +186,23 @@ export const SingleSidebarProject: React.FC<Props> = ({
open ? "" : "text-custom-sidebar-text-200"
}`}
>
{truncateText(project?.name, 14)}
{truncateText(project.name, 15)}
</p>
)}
</div>
{!sidebarCollapse && (
<ExpandMoreOutlined
fontSize="small"
className={`${open ? "rotate-180" : ""} text-custom-text-200 duration-300`}
className={`${
open ? "rotate-180" : ""
} !hidden group-hover:!block text-custom-sidebar-text-200 duration-300`}
/>
)}
</Disclosure.Button>
</Tooltip>
{!sidebarCollapse && (
<CustomMenu ellipsis>
<CustomMenu className="hidden group-hover:block" ellipsis>
{!shortContextMenu && (
<CustomMenu.MenuItem onClick={handleDeleteProject}>
<span className="flex items-center justify-start gap-2 ">
@@ -22,7 +22,7 @@ export const SecondaryButton: React.FC<ButtonProps> = ({
} ${disabled ? "cursor-not-allowed border-custom-border-200 bg-custom-background-90" : ""} ${
outline
? "bg-transparent hover:bg-custom-background-80"
: "bg-custom-background-80 hover:border-opacity-70 hover:bg-opacity-70"
: "bg-custom-background-100 hover:border-opacity-70 hover:bg-opacity-70"
} ${loading ? "cursor-wait" : ""}`}
onClick={onClick}
disabled={disabled || loading}
+11 -11
View File
@@ -9,10 +9,12 @@ type Props = {
title: string;
description: React.ReactNode | string;
image: any;
buttonText?: string;
buttonIcon?: any;
primaryButton?: {
icon?: any;
text: string;
onClick: () => void;
};
secondaryButton?: React.ReactNode;
onClick?: () => void;
isFullScreen?: boolean;
};
@@ -20,9 +22,7 @@ export const EmptyState: React.FC<Props> = ({
title,
description,
image,
onClick,
buttonText,
buttonIcon,
primaryButton,
secondaryButton,
isFullScreen = true,
}) => (
@@ -32,14 +32,14 @@ export const EmptyState: React.FC<Props> = ({
}`}
>
<div className="text-center flex flex-col items-center w-full">
<Image src={image} className="w-52 sm:w-60" alt={buttonText} />
<Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text} />
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6>
<p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>
<div className="flex items-center gap-4">
{buttonText && (
<PrimaryButton className="flex items-center gap-1.5" onClick={onClick}>
{buttonIcon}
{buttonText}
{primaryButton && (
<PrimaryButton className="flex items-center gap-1.5" onClick={primaryButton.onClick}>
{primaryButton.icon}
{primaryButton.text}
</PrimaryButton>
)}
{secondaryButton}
+167 -115
View File
@@ -16,7 +16,7 @@ import useToast from "hooks/use-toast";
import userService from "services/user.service";
import authenticationService from "services/authentication.service";
// components
import { Avatar, Loader } from "components/ui";
import { Avatar, Icon, Loader } from "components/ui";
// icons
import { CheckIcon, PlusIcon } from "@heroicons/react/24/outline";
// helpers
@@ -40,6 +40,19 @@ const userLinks = (workspaceSlug: string, userId: string) => [
},
];
const profileLinks = (workspaceSlug: string, userId: string) => [
{
name: "View profile",
icon: "account_circle",
link: `/${workspaceSlug}/profile/${userId}`,
},
{
name: "Settings",
icon: "settings",
link: `/${workspaceSlug}/me/profile`,
},
];
export const WorkspaceSidebarDropdown = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -90,8 +103,8 @@ export const WorkspaceSidebarDropdown = () => {
};
return (
<Menu as="div" className="relative col-span-4 inline-block w-full px-4 pt-4 text-left">
<div className="flex items-center justify-between gap-2">
<div className="inline-flex items-center gap-2 px-4 pt-4">
<Menu as="div" className="relative col-span-4 inline-block w-full text-left">
<Menu.Button className="text-custom-sidebar-text-200 flex w-full items-center rounded-sm text-sm font-medium focus:outline-none">
<div
className={`flex w-full items-center gap-x-2 rounded-sm bg-custom-sidebar-background-80 p-1 ${
@@ -118,127 +131,166 @@ export const WorkspaceSidebarDropdown = () => {
</div>
</Menu.Button>
{!sidebarCollapse && (
<Link href={`/${workspaceSlug}/profile/${user?.id}`}>
<a>
<div className="flex flex-grow justify-end">
<Avatar user={user} height="28px" width="28px" fontSize="14px" />
</div>
</a>
</Link>
)}
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="fixed left-4 z-20 mt-1 flex flex-col w-full max-w-[17rem] origin-top-left rounded-md
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 shadow-lg outline-none"
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<div className="flex flex-col items-start justify-start gap-3 p-3">
<div className="text-sm text-custom-sidebar-text-200">{user?.email}</div>
<span className="text-sm font-semibold text-custom-sidebar-text-200">Workspace</span>
{workspaces ? (
<div className="flex h-full w-full flex-col items-start justify-start gap-3.5">
{workspaces.length > 0 ? (
workspaces.map((workspace) => (
<Menu.Item key={workspace.id}>
{({ active }) => (
<button
type="button"
onClick={() => handleWorkspaceNavigation(workspace)}
className="flex w-full items-center justify-between gap-1 rounded-md text-sm text-custom-sidebar-text-100"
>
<div className="flex items-center justify-start gap-2.5">
<span className="relative flex h-6 w-6 items-center justify-center rounded bg-gray-700 p-2 text-xs uppercase text-white">
{workspace?.logo && workspace.logo !== "" ? (
<img
src={workspace.logo}
className="absolute top-0 left-0 h-full w-full object-cover rounded"
alt="Workspace Logo"
/>
) : (
workspace?.name?.charAt(0) ?? "..."
)}
</span>
<Menu.Items
className="fixed left-4 z-20 mt-1 flex flex-col w-full max-w-[17rem] origin-top-left rounded-md
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 shadow-lg outline-none"
>
<div className="flex flex-col items-start justify-start gap-3 p-3">
<div className="text-sm text-custom-sidebar-text-200">{user?.email}</div>
<span className="text-sm font-semibold text-custom-sidebar-text-200">Workspace</span>
{workspaces ? (
<div className="flex h-full w-full flex-col items-start justify-start gap-3.5">
{workspaces.length > 0 ? (
workspaces.map((workspace) => (
<Menu.Item key={workspace.id}>
{({ active }) => (
<button
type="button"
onClick={() => handleWorkspaceNavigation(workspace)}
className="flex w-full items-center justify-between gap-1 rounded-md text-sm text-custom-sidebar-text-100"
>
<div className="flex items-center justify-start gap-2.5">
<span className="relative flex h-6 w-6 items-center justify-center rounded bg-gray-700 p-2 text-xs uppercase text-white">
{workspace?.logo && workspace.logo !== "" ? (
<img
src={workspace.logo}
className="absolute top-0 left-0 h-full w-full object-cover rounded"
alt="Workspace Logo"
/>
) : (
workspace?.name?.charAt(0) ?? "..."
)}
</span>
<h5
className={`text-sm ${
workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
}`}
>
{truncateText(workspace.name, 18)}
</h5>
</div>
<span className="p-1">
<CheckIcon
className={`h-3 w-3.5 text-custom-sidebar-text-100 ${
active || workspace.id === activeWorkspace?.id
? "opacity-100"
: "opacity-0"
}`}
/>
</span>
</button>
)}
<h5
className={`text-sm ${
workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
}`}
>
{truncateText(workspace.name, 18)}
</h5>
</div>
<span className="p-1">
<CheckIcon
className={`h-3 w-3.5 text-custom-sidebar-text-100 ${
active || workspace.id === activeWorkspace?.id
? "opacity-100"
: "opacity-0"
}`}
/>
</span>
</button>
)}
</Menu.Item>
))
) : (
<p>No workspace found!</p>
)}
<Menu.Item
as="button"
type="button"
onClick={() => {
router.push("/create-workspace");
}}
className="flex w-full items-center gap-1 text-sm text-custom-sidebar-text-200"
>
<PlusIcon className="h-3 w-3" />
Create Workspace
</Menu.Item>
</div>
) : (
<div className="w-full">
<Loader className="space-y-2">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
</div>
)}
</div>
<div className="flex w-full flex-col items-start justify-start gap-2 border-t border-custom-sidebar-border-200 px-3 py-2 text-sm">
{userLinks(workspaceSlug?.toString() ?? "", user?.id ?? "").map((link, index) => (
<Menu.Item
key={index}
as="div"
className="flex w-full items-center justify-start rounded px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
>
<Link href={link.href}>
<a className="w-full">{link.name}</a>
</Link>
</Menu.Item>
))}
</div>
<div className="w-full border-t border-t-custom-sidebar-border-100 px-3 py-2">
<Menu.Item
as="button"
type="button"
className="flex w-full items-center justify-start rounded px-2 py-1 text-sm text-red-600 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
Sign out
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
{!sidebarCollapse && (
<Menu as="div" className="relative flex-shrink-0">
<Menu.Button className="grid place-items-center outline-none">
<Avatar user={user} height="28px" width="28px" fontSize="14px" />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 origin-top-left rounded-md
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 p-2 divide-y divide-custom-sidebar-border-200 shadow-lg text-xs outline-none"
>
<div className="flex flex-col space-y-2 pb-2">
{profileLinks(workspaceSlug?.toString() ?? "", user?.id ?? "").map(
(link, index) => (
<Menu.Item key={index} as="button" type="button">
<Link href={link.link}>
<a className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
<Icon iconName={link.icon} className="!text-base" />
{link.name}
</a>
</Link>
</Menu.Item>
))
) : (
<p>No workspace found!</p>
)
)}
</div>
<div className="pt-2">
<Menu.Item
as="button"
type="button"
onClick={() => {
router.push("/create-workspace");
}}
className="flex w-full items-center gap-1 text-sm text-custom-sidebar-text-200"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
<PlusIcon className="h-3 w-3" />
Create Workspace
<Icon iconName="logout" className="!text-base" />
Log out
</Menu.Item>
</div>
) : (
<div className="w-full">
<Loader className="space-y-2">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
</div>
)}
</div>
<div className="flex w-full flex-col items-start justify-start gap-2 border-t border-custom-sidebar-border-200 px-3 py-2 text-sm">
{userLinks(workspaceSlug?.toString() ?? "", user?.id ?? "").map((link, index) => (
<Menu.Item
key={index}
as="div"
className="flex w-full items-center justify-start rounded px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
>
<Link href={link.href}>
<a className="w-full">{link.name}</a>
</Link>
</Menu.Item>
))}
</div>
<div className="w-full border-t border-t-custom-sidebar-border-100 px-3 py-2">
<Menu.Item
as="button"
type="button"
className="flex w-full items-center justify-start rounded px-2 py-1 text-sm text-red-600 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
Sign out
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</Menu.Items>
</Transition>
</Menu>
)}
</div>
);
};
@@ -47,7 +47,7 @@ export const WorkspaceSidebarMenu = () => {
const { collapsed: sidebarCollapse } = useTheme();
return (
<div className="w-full cursor-pointer space-y-2 px-4 mt-5">
<div className="w-full cursor-pointer space-y-2 px-4 mt-5 pb-5">
{workspaceLinks(workspaceSlug as string).map((link, index) => {
const isActive =
link.name === "Settings"
+1 -2
View File
@@ -285,8 +285,7 @@ export const getPaginatedNotificationKey = (
if (index === 0)
return `/api/workspaces/${workspaceSlug}/users/notifications?${objToQueryParams({
...params,
// TODO: change to '100:0:0'
cursor: "2:0:0",
cursor: "30:0:0",
})}`;
const cursor = prevData?.next_cursor;
-2
View File
@@ -58,8 +58,6 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
: null
);
console.log(memberRole);
const groupedIssues:
| {
[key: string]: IIssue[];
+1 -2
View File
@@ -15,8 +15,7 @@ import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constan
// type
import type { NotificationType, NotificationCount } from "types";
// TODO: change to 100
const PER_PAGE = 2;
const PER_PAGE = 30;
const useUserNotification = () => {
const router = useRouter();
-1
View File
@@ -18,7 +18,6 @@ export default function useUser({ redirectTo = "", redirectIfFound = false, opti
);
const user = error ? undefined : data;
// console.log("useUser", user);
useEffect(() => {
// if no redirect needed, just return (example: already on /dashboard)
@@ -71,12 +71,14 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
title="No such project exists"
description="Try creating a new project"
image={emptyProject}
buttonText="Create Project"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
primaryButton={{
text: "Create Project",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
},
}}
/>
</div>
+9 -7
View File
@@ -134,13 +134,15 @@ const Analytics = () => {
title="You can see your all projects' analytics here"
description="Let's create your first project and analyse the stats with various graphs."
image={emptyAnalytics}
buttonText="New Project"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New Project",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
},
}}
/>
)
@@ -121,13 +121,15 @@ const ProjectCycles: NextPage = () => {
title="Plan your project with cycles"
description="Cycle is a custom time period in which a team works to complete items on their backlog."
image={emptyCycle}
buttonText="New Cycle"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New Cycle",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
},
}}
/>
</div>
@@ -146,13 +146,15 @@ const ProjectModules: NextPage = () => {
title="Manage your project with modules"
description="Modules are smaller, focused projects that help you group and organize issues."
image={emptyModule}
buttonText="New Module"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "m",
});
document.dispatchEvent(e);
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New Module",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "m",
});
document.dispatchEvent(e);
},
}}
/>
)
@@ -166,11 +166,13 @@ const EstimatesSettings: NextPage = () => {
title="No estimates yet"
description="Estimates help you communicate the complexity of an issue."
image={emptyEstimate}
buttonText="Add Estimate"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
setEstimateToUpdate(undefined);
setEstimateFormOpen(true);
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "Add Estimate",
onClick: () => {
setEstimateToUpdate(undefined);
setEstimateFormOpen(true);
},
}}
/>
</div>
@@ -78,8 +78,10 @@ const ProjectIntegrations: NextPage = () => {
title="You haven't configured integrations"
description="Configure GitHub and other integrations to sync your project issues."
image={emptyIntegration}
buttonText="Configure now"
onClick={() => router.push(`/${workspaceSlug}/settings/integrations`)}
primaryButton={{
text: "Configure now",
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
}}
/>
)
) : (
@@ -181,8 +181,10 @@ const LabelsSettings: NextPage = () => {
title="No labels yet"
description="Create labels to help organize and filter issues in you project"
image={emptyLabel}
buttonText="Add label"
onClick={newLabel}
primaryButton={{
text: "Add label",
onClick: () => newLabel(),
}}
isFullScreen={false}
/>
)
@@ -118,13 +118,15 @@ const ProjectViews: NextPage = () => {
title="Get focused with views"
description="Views aid in saving your issues by applying various filters and grouping options."
image={emptyView}
buttonText="New View"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "v",
});
document.dispatchEvent(e);
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New View",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "v",
});
document.dispatchEvent(e);
},
}}
/>
)
@@ -111,13 +111,15 @@ const ProjectsPage: NextPage = () => {
image={emptyProject}
title="No projects yet"
description="Get started by creating your first project"
buttonText="New Project"
buttonIcon={<PlusIcon className="h-4 w-4" />}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New Project",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
},
}}
/>
)}
@@ -115,30 +115,48 @@ const MembersSettings: NextPage = () => {
handleDelete={async () => {
if (!workspaceSlug) return;
if (selectedRemoveMember) {
await workspaceService.deleteWorkspaceMember(
workspaceSlug as string,
selectedRemoveMember
);
mutateMembers(
(prevData) => prevData?.filter((item) => item.id !== selectedRemoveMember),
false
);
workspaceService
.deleteWorkspaceMember(workspaceSlug as string, selectedRemoveMember)
.catch((err) => {
const error = err?.error;
setToastAlert({
type: "error",
title: "Error",
message: error || "Something went wrong",
});
})
.finally(() => {
mutateMembers((prevData) =>
prevData?.filter((item) => item.id !== selectedRemoveMember)
);
});
}
if (selectedInviteRemoveMember) {
await workspaceService.deleteWorkspaceInvitations(
workspaceSlug as string,
selectedInviteRemoveMember
);
mutateInvitations(
(prevData) => prevData?.filter((item) => item.id !== selectedInviteRemoveMember),
false
);
workspaceService
.deleteWorkspaceInvitations(workspaceSlug as string, selectedInviteRemoveMember)
.then(() => {
setToastAlert({
type: "success",
title: "Success",
message: "Member removed successfully",
});
})
.catch((err) => {
const error = err?.error;
setToastAlert({
type: "error",
title: "Error",
message: error || "Something went wrong",
});
})
.finally(() => {
mutateInvitations();
});
}
setToastAlert({
type: "success",
title: "Success",
message: "Member removed successfully",
});
setSelectedRemoveMember(null);
setSelectedInviteRemoveMember(null);
}}
@@ -276,7 +294,7 @@ const MembersSettings: NextPage = () => {
}
}}
>
Remove member
{user?.id === member.memberId ? "Leave" : "Remove member"}
</CustomMenu.MenuItem>
</CustomMenu>
</div>
+4 -2
View File
@@ -186,8 +186,10 @@ const OnBoard: NextPage = () => {
title="No pending invites"
description="You can see here if someone invites you to a workspace."
image={emptyInvitation}
buttonText="Back to Dashboard"
onClick={() => router.push("/")}
primaryButton={{
text: "Back to Dashboard",
onClick: () => router.push("/"),
}}
/>
</div>
)
+19
View File
@@ -25,6 +25,25 @@
:root {
color-scheme: light !important;
--color-primary-10: 236, 241, 255;
--color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
--color-primary-40: 178, 200, 255;
--color-primary-50: 159, 187, 255;
--color-primary-60: 140, 173, 255;
--color-primary-70: 121, 159, 255;
--color-primary-80: 101, 145, 255;
--color-primary-90: 82, 132, 255;
--color-primary-100: 63, 118, 255;
--color-primary-200: 57, 106, 230;
--color-primary-300: 50, 94, 204;
--color-primary-400: 44, 83, 179;
--color-primary-500: 38, 71, 153;
--color-primary-600: 32, 59, 128;
--color-primary-700: 25, 47, 102;
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 250, 250, 250; /* secondary bg */
--color-background-80: 245, 245, 245; /* tertiary bg */
+10 -15
View File
@@ -38,19 +38,6 @@ export interface IIssueModule {
workspace: string;
}
export interface IIssueCycle {
created_at: Date;
created_by: string;
cycle: string;
cycle_detail: ICycle;
id: string;
issue: string;
project: string;
updated_at: Date;
updated_by: string;
workspace: string;
}
export interface IIssueParent {
description: any;
id: string;
@@ -200,18 +187,26 @@ export interface IIssueActivity {
created_by: string;
field: string | null;
id: string;
issue: string;
issue: string | null;
issue_comment: string | null;
issue_detail: {
description: any;
description_html: string;
id: string;
name: string;
priority: string | null;
sequence_id: string;
} | null;
new_identifier: string | null;
new_value: string | null;
old_identifier: string | null;
old_value: string | null;
project: string;
project_detail: IProjectLite;
updated_at: Date;
updated_by: string;
verb: string;
workspace: string;
workspace_detail: IWorkspaceLite;
}
export interface IIssueComment extends IIssueActivity {
+1 -1
View File
@@ -45,7 +45,7 @@ export interface IProject {
network: number;
page_view: boolean;
project_lead: IUserLite | string | null;
sort_order: number;
sort_order: number | null;
slug: string;
total_cycles: number;
total_members: number;
+2 -24
View File
@@ -1,5 +1,6 @@
import {
IIssue,
IIssueActivity,
IIssueLite,
IWorkspace,
IWorkspaceLite,
@@ -99,29 +100,6 @@ export interface IUserWorkspaceDashboard {
upcoming_issues: IIssueLite[];
}
export interface IUserDetailedActivity {
actor: string;
actor_detail: IUserLite;
attachments: any[];
comment: string;
created_at: string;
created_by: string | null;
field: string;
id: string;
issue: string;
issue_comment: string | null;
new_identifier: string | null;
new_value: string | null;
old_identifier: string | null;
old_value: string | null;
project: string;
updated_at: string;
updated_by: string | null;
verb: string;
workspace: string;
workspace_detail: IWorkspaceLite;
}
export interface IUserActivityResponse {
count: number;
extra_stats: null;
@@ -129,7 +107,7 @@ export interface IUserActivityResponse {
next_page_results: boolean;
prev_cursor: string;
prev_page_results: boolean;
results: IUserDetailedActivity[];
results: IIssueActivity[];
total_pages: number;
}