Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d7cee773e | ||
|
|
9ad0c8403f | ||
|
|
54c2a23a7e | ||
|
|
20715a195d |
@@ -291,8 +291,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
|
||||
class IssueActivitySerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
|
||||
class Meta:
|
||||
model = IssueActivity
|
||||
|
||||
@@ -332,7 +332,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
||||
# if there is no default state assign any random state
|
||||
if default_state is None:
|
||||
default_state = State.objects.filter(
|
||||
~Q(name="Triage"), project_id=project_id
|
||||
~Q(name="Triage"), sproject_id=project_id
|
||||
).first()
|
||||
|
||||
# Get the maximum sequence_id
|
||||
|
||||
@@ -477,7 +477,7 @@ class IssueActivityEndpoint(BaseAPIView):
|
||||
~Q(field="comment"),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
)
|
||||
.select_related("actor", "workspace", "issue", "project")
|
||||
.select_related("actor", "workspace")
|
||||
).order_by("created_at")
|
||||
issue_comments = (
|
||||
IssueComment.objects.filter(issue_id=issue_id)
|
||||
|
||||
@@ -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, WorkspaceMember
|
||||
from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue
|
||||
from plane.api.serializers import NotificationSerializer
|
||||
|
||||
|
||||
@@ -83,13 +83,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
|
||||
# Created issues
|
||||
if type == "created":
|
||||
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)
|
||||
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):
|
||||
|
||||
@@ -140,7 +140,7 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||
def get(self, request):
|
||||
try:
|
||||
queryset = IssueActivity.objects.filter(actor=request.user).select_related(
|
||||
"actor", "workspace", "issue", "project"
|
||||
"actor", "workspace"
|
||||
)
|
||||
|
||||
return self.paginate(
|
||||
|
||||
@@ -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
|
||||
project_member = ProjectMember.objects.create(
|
||||
ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"], member=request.user, role=20
|
||||
)
|
||||
|
||||
@@ -238,11 +238,9 @@ class ProjectViewSet(BaseViewSet):
|
||||
]
|
||||
)
|
||||
|
||||
data = serializer.data
|
||||
data["sort_order"] = project_member.sort_order
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
serializer.errors,
|
||||
[serializer.errors[error][0] for error in serializer.errors],
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
@@ -602,29 +600,19 @@ class AddMemberToProjectEndpoint(BaseAPIView):
|
||||
)
|
||||
bulk_project_members = []
|
||||
|
||||
project_members = (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member_id__in=[member.get("member_id") for member in members],
|
||||
)
|
||||
.values("member_id", "sort_order")
|
||||
.order_by("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
|
||||
if str(project_member.get("member_id"))
|
||||
== str(member.get("member_id"))
|
||||
]
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ from plane.db.models import (
|
||||
Label,
|
||||
WorkspaceMember,
|
||||
CycleIssue,
|
||||
IssueReaction,
|
||||
)
|
||||
from plane.api.permissions import (
|
||||
WorkSpaceBasePermission,
|
||||
@@ -1191,7 +1190,7 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
actor=user_id,
|
||||
).select_related("actor", "workspace", "issue", "project")
|
||||
).select_related("actor", "workspace")
|
||||
|
||||
if projects:
|
||||
queryset = queryset.filter(project__in=projects)
|
||||
@@ -1322,12 +1321,6 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
)
|
||||
.select_related("project", "workspace", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
|
||||
@@ -89,8 +89,8 @@ class Migration(migrations.Migration):
|
||||
field=models.JSONField(default=plane.db.models.workspace.get_default_props),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projectmember',
|
||||
name='sort_order',
|
||||
model_name="projectmember",
|
||||
name="sort_order",
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.RunPython(update_project_member_sort_order),
|
||||
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
# 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')},
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='identifier',
|
||||
field=models.CharField(max_length=12, verbose_name='Project Identifier'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='projectidentifier',
|
||||
name='name',
|
||||
field=models.CharField(max_length=12),
|
||||
),
|
||||
]
|
||||
@@ -161,7 +161,7 @@ class ProjectMember(ProjectBaseModel):
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
smallest_sort_order = ProjectMember.objects.filter(
|
||||
workspace_id=self.project.workspace_id, member=self.member
|
||||
workspace_id=self.project.workspace_id, member_id=self.member_id
|
||||
).aggregate(smallest=models.Min("sort_order"))["smallest"]
|
||||
|
||||
# Project ordering
|
||||
|
||||
@@ -5,8 +5,6 @@ import { Command } from "cmdk";
|
||||
import { THEMES_OBJ } from "constants/themes";
|
||||
import { useTheme } from "next-themes";
|
||||
import { SettingIcon } from "components/icons";
|
||||
import userService from "services/user.service";
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
type Props = {
|
||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -14,50 +12,24 @@ type Props = {
|
||||
|
||||
export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const { user, mutateUser } = useUser();
|
||||
|
||||
const updateUserTheme = (newTheme: string) => {
|
||||
if (!user) return;
|
||||
|
||||
setTheme(newTheme);
|
||||
|
||||
mutateUser((prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
theme: {
|
||||
...prevData.theme,
|
||||
theme: newTheme,
|
||||
},
|
||||
};
|
||||
}, false);
|
||||
|
||||
userService.updateUser({
|
||||
theme: {
|
||||
...user.theme,
|
||||
theme: newTheme,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// useEffect only runs on the client, so now we can safely show the UI
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{THEMES_OBJ.filter((t) => t.value !== "custom").map((theme) => (
|
||||
{THEMES_OBJ.map((theme) => (
|
||||
<Command.Item
|
||||
key={theme.value}
|
||||
onSelect={() => {
|
||||
updateUserTheme(theme.value);
|
||||
setTheme(theme.value);
|
||||
setIsPaletteOpen(false);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
// icons
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
Squares2X2Icon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
||||
import { Icon } from "components/ui";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import RemirrorRichTextEditor from "components/rich-text-editor";
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
message?: string;
|
||||
icon: JSX.Element;
|
||||
};
|
||||
} = {
|
||||
assignee: {
|
||||
message: "removed the assignee",
|
||||
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
assignees: {
|
||||
message: "added a new assignee",
|
||||
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
blocks: {
|
||||
message: "marked this issue being blocked by",
|
||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
blocking: {
|
||||
message: "marked this issue is blocking",
|
||||
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
cycles: {
|
||||
message: "set the cycle to",
|
||||
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
modules: {
|
||||
message: "set the module to",
|
||||
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
state: {
|
||||
message: "set the state to",
|
||||
icon: <Squares2X2Icon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
priority: {
|
||||
message: "set the priority to",
|
||||
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
name: {
|
||||
message: "set the name to",
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
description: {
|
||||
message: "updated the description.",
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
estimate_point: {
|
||||
message: "set the estimate point to",
|
||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: "set the due date to",
|
||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
parent: {
|
||||
message: "set the parent to",
|
||||
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
issue: {
|
||||
message: "deleted the issue.",
|
||||
icon: <Icon iconName="delete" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
estimate: {
|
||||
message: "updated the estimate",
|
||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
link: {
|
||||
message: "updated the link",
|
||||
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
attachment: {
|
||||
message: "updated the attachment",
|
||||
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
archived_at: {
|
||||
message: "archived",
|
||||
icon: <Icon iconName="archive" className="!text-sm text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
};
|
||||
|
||||
export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
<div>
|
||||
<ul role="list" className="-mb-4">
|
||||
{activities.map((activity: any, activityIdx: number) => {
|
||||
// determines what type of action is performed
|
||||
let action = activityDetails[activity.field as keyof typeof activityDetails]?.message;
|
||||
if (activity.field === "labels") {
|
||||
action = activity.new_value !== "" ? "added a new label" : "removed the label";
|
||||
} else if (activity.field === "blocking") {
|
||||
action =
|
||||
activity.new_value !== ""
|
||||
? "marked this issue is blocking"
|
||||
: "removed the issue from blocking";
|
||||
} else if (activity.field === "blocks") {
|
||||
action =
|
||||
activity.new_value !== "" ? "marked this issue being blocked by" : "removed blocker";
|
||||
} else if (activity.field === "target_date") {
|
||||
action =
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? "set the due date to"
|
||||
: "removed the due date";
|
||||
} else if (activity.field === "parent") {
|
||||
action =
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? "set the parent to"
|
||||
: "removed the parent";
|
||||
} else if (activity.field === "priority") {
|
||||
action =
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? "set the priority to"
|
||||
: "removed the priority";
|
||||
} else if (activity.field === "description") {
|
||||
action = "updated the";
|
||||
} else if (activity.field === "attachment") {
|
||||
action = `${activity.verb} the`;
|
||||
} else if (activity.field === "link") {
|
||||
action = `${activity.verb} the`;
|
||||
} else if (activity.field === "archived_at") {
|
||||
action =
|
||||
activity.new_value && activity.new_value === "restore"
|
||||
? "restored the issue"
|
||||
: "archived the issue";
|
||||
}
|
||||
// for values that are after the action clause
|
||||
let value: any = activity.new_value ? activity.new_value : activity.old_value;
|
||||
if (
|
||||
activity.verb === "created" &&
|
||||
activity.field !== "cycles" &&
|
||||
activity.field !== "modules" &&
|
||||
activity.field !== "attachment" &&
|
||||
activity.field !== "link" &&
|
||||
activity.field !== "estimate"
|
||||
) {
|
||||
const { workspace_detail, project, issue } = activity;
|
||||
value = (
|
||||
<span className="text-custom-text-200">
|
||||
created{" "}
|
||||
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
|
||||
<a className="inline-flex items-center hover:underline">
|
||||
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
|
||||
</a>
|
||||
</Link>
|
||||
</span>
|
||||
);
|
||||
} else if (activity.field === "state") {
|
||||
value = activity.new_value ? addSpaceIfCamelCase(activity.new_value) : "None";
|
||||
} else if (activity.field === "labels") {
|
||||
let name;
|
||||
let id = "#000000";
|
||||
if (activity.new_value !== "") {
|
||||
name = activity.new_value;
|
||||
id = activity.new_identifier ? activity.new_identifier : id;
|
||||
} else {
|
||||
name = activity.old_value;
|
||||
id = activity.old_identifier ? activity.old_identifier : id;
|
||||
}
|
||||
|
||||
value = name;
|
||||
} else if (activity.field === "assignees") {
|
||||
value = activity.new_value;
|
||||
} else if (activity.field === "target_date") {
|
||||
const date =
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? activity.new_value
|
||||
: activity.old_value;
|
||||
value = renderShortDateWithYearFormat(date as string);
|
||||
} else if (activity.field === "description") {
|
||||
value = "description";
|
||||
} else if (activity.field === "attachment") {
|
||||
value = "attachment";
|
||||
} else if (activity.field === "link") {
|
||||
value = "link";
|
||||
} else if (activity.field === "estimate_point") {
|
||||
value = activity.new_value
|
||||
? activity.new_value + ` Point${parseInt(activity.new_value ?? "", 10) > 1 ? "s" : ""}`
|
||||
: "None";
|
||||
}
|
||||
|
||||
if (activity.field === "comment") {
|
||||
return (
|
||||
<div key={activity.id} className="mt-2">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{activity.field ? (
|
||||
activity.new_value === "restore" ? (
|
||||
<Icon iconName="history" className="text-sm text-custom-text-200" />
|
||||
) : (
|
||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||
)
|
||||
) : activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
|
||||
>
|
||||
{activity.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
|
||||
<ChatBubbleLeftEllipsisIcon
|
||||
className="h-3.5 w-3.5 text-custom-text-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{activity.actor_detail.first_name}
|
||||
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
Commented {timeAgo(activity.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<RemirrorRichTextEditor
|
||||
value={
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? activity.new_value
|
||||
: activity.old_value
|
||||
}
|
||||
editable={false}
|
||||
noBorder
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ("field" in activity && activity.field !== "updated_by") {
|
||||
return (
|
||||
<li key={activity.id}>
|
||||
<div className="relative pb-1">
|
||||
{activities.length > 1 && activityIdx !== activities.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex items-start space-x-2">
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||
{activity.field ? (
|
||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||
) : activity.actor_detail.avatar &&
|
||||
activity.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||
>
|
||||
{activity.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="text-xs text-custom-text-200">
|
||||
{activity.field === "archived_at" && activity.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : (
|
||||
<span className="text-gray font-medium">
|
||||
{activity.actor_detail.first_name}
|
||||
{activity.actor_detail.is_bot
|
||||
? " Bot"
|
||||
: " " + activity.actor_detail.last_name}
|
||||
</span>
|
||||
)}
|
||||
<span> {action} </span>
|
||||
{activity.field !== "archived_at" && (
|
||||
<span className="text-xs font-medium text-custom-text-100">
|
||||
{" "}
|
||||
{value}{" "}
|
||||
</span>
|
||||
)}
|
||||
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
@@ -181,85 +181,78 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<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;
|
||||
<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;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<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>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
|
||||
@@ -3,6 +3,6 @@ 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";
|
||||
|
||||
@@ -28,7 +28,6 @@ const defaultValues: ICustomTheme = {
|
||||
sidebarText: "#c5c5c5",
|
||||
darkPalette: false,
|
||||
palette: "",
|
||||
theme: "custom",
|
||||
};
|
||||
|
||||
export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
||||
@@ -57,7 +56,6 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
||||
sidebarText: formData.sidebarText,
|
||||
darkPalette: darkPalette,
|
||||
palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`,
|
||||
theme: "custom",
|
||||
};
|
||||
|
||||
await userService
|
||||
|
||||
@@ -1,161 +1,142 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, Dispatch, SetStateAction } from "react";
|
||||
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
// constants
|
||||
import { THEMES_OBJ } from "constants/themes";
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// types
|
||||
import { ICustomTheme } from "types";
|
||||
import { unsetCustomCssVariables } from "helpers/theme.helper";
|
||||
import { ICustomTheme, IUser } from "types";
|
||||
|
||||
type Props = {
|
||||
setPreLoadedData: React.Dispatch<React.SetStateAction<ICustomTheme | null>>;
|
||||
user: IUser | undefined;
|
||||
setPreLoadedData: Dispatch<SetStateAction<ICustomTheme | null>>;
|
||||
customThemeSelectorOptions: boolean;
|
||||
setCustomThemeSelectorOptions: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setCustomThemeSelectorOptions: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const ThemeSwitch: React.FC<Props> = ({
|
||||
user,
|
||||
setPreLoadedData,
|
||||
customThemeSelectorOptions,
|
||||
setCustomThemeSelectorOptions,
|
||||
}) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const { user, mutateUser } = useUser();
|
||||
|
||||
const updateUserTheme = (newTheme: string) => {
|
||||
if (!user) return;
|
||||
|
||||
setTheme(newTheme);
|
||||
|
||||
mutateUser((prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
theme: {
|
||||
...prevData.theme,
|
||||
theme: newTheme,
|
||||
},
|
||||
};
|
||||
}, false);
|
||||
|
||||
userService.updateUser({
|
||||
theme: {
|
||||
...user.theme,
|
||||
theme: newTheme,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// useEffect only runs on the client, so now we can safely show the UI
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme);
|
||||
|
||||
return (
|
||||
<CustomSelect
|
||||
value={theme}
|
||||
label={
|
||||
currentThemeObj ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: currentThemeObj.icon.border,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<CustomSelect
|
||||
value={theme}
|
||||
label={
|
||||
currentThemeObj ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-full w-1/2 rounded-l-full"
|
||||
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
|
||||
style={{
|
||||
background: currentThemeObj.icon.color1,
|
||||
borderColor: currentThemeObj.icon.border,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="h-full w-1/2 rounded-r-full border-l"
|
||||
style={{
|
||||
borderLeftColor: currentThemeObj.icon.border,
|
||||
background: currentThemeObj.icon.color2,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<div
|
||||
className="h-full w-1/2 rounded-l-full"
|
||||
style={{
|
||||
background: currentThemeObj.icon.color1,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="h-full w-1/2 rounded-r-full border-l"
|
||||
style={{
|
||||
borderLeftColor: currentThemeObj.icon.border,
|
||||
background: currentThemeObj.icon.color2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{currentThemeObj.label}
|
||||
</div>
|
||||
{currentThemeObj.label}
|
||||
</div>
|
||||
) : (
|
||||
"Select your theme"
|
||||
)
|
||||
}
|
||||
onChange={({ value, type }: { value: string; type: string }) => {
|
||||
if (value === "custom") {
|
||||
if (user?.theme.palette) {
|
||||
setPreLoadedData({
|
||||
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
|
||||
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
|
||||
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
|
||||
sidebarBackground:
|
||||
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
|
||||
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
|
||||
darkPalette: false,
|
||||
palette:
|
||||
user.theme.palette !== ",,,,"
|
||||
? user.theme.palette
|
||||
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||
theme: "custom",
|
||||
});
|
||||
) : (
|
||||
"Select your theme"
|
||||
)
|
||||
}
|
||||
onChange={({ value, type }: { value: string; type: string }) => {
|
||||
if (value === "custom") {
|
||||
if (user?.theme.palette) {
|
||||
setPreLoadedData({
|
||||
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
|
||||
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
|
||||
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
|
||||
sidebarBackground:
|
||||
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
|
||||
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
|
||||
darkPalette: false,
|
||||
palette:
|
||||
user.theme.palette !== ",,,,"
|
||||
? user.theme.palette
|
||||
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||
});
|
||||
}
|
||||
|
||||
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
|
||||
} else {
|
||||
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
|
||||
|
||||
for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) {
|
||||
document.documentElement.style.removeProperty(`--color-background-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-text-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-border-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-primary-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-sidebar-background-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-sidebar-text-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-sidebar-border-${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
|
||||
} else {
|
||||
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
|
||||
unsetCustomCssVariables();
|
||||
}
|
||||
|
||||
updateUserTheme(value);
|
||||
document.documentElement.style.setProperty("--color-scheme", type);
|
||||
}}
|
||||
input
|
||||
width="w-full"
|
||||
position="right"
|
||||
>
|
||||
{THEMES_OBJ.map(({ value, label, type, icon }) => (
|
||||
<CustomSelect.Option key={value} value={{ value, type }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: icon.border,
|
||||
}}
|
||||
>
|
||||
setTheme(value);
|
||||
document.documentElement.style.setProperty("color-scheme", type);
|
||||
}}
|
||||
input
|
||||
width="w-full"
|
||||
position="right"
|
||||
>
|
||||
{THEMES_OBJ.map(({ value, label, type, icon }) => (
|
||||
<CustomSelect.Option key={value} value={{ value, type }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-full w-1/2 rounded-l-full"
|
||||
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
|
||||
style={{
|
||||
background: icon.color1,
|
||||
borderColor: icon.border,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="h-full w-1/2 rounded-r-full border-l"
|
||||
style={{
|
||||
borderLeftColor: icon.border,
|
||||
background: icon.color2,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<div
|
||||
className="h-full w-1/2 rounded-l-full"
|
||||
style={{
|
||||
background: icon.color1,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="h-full w-1/2 rounded-r-full border-l"
|
||||
style={{
|
||||
borderLeftColor: icon.border,
|
||||
background: icon.color2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{label}
|
||||
</div>
|
||||
{label}
|
||||
</div>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,9 +21,9 @@ import {
|
||||
GanttChartView,
|
||||
} from "components/core";
|
||||
// ui
|
||||
import { EmptyState, Spinner } from "components/ui";
|
||||
import { EmptyState, SecondaryButton, Spinner } from "components/ui";
|
||||
// icons
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { PlusIcon, 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,16 +39,6 @@ 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;
|
||||
@@ -63,7 +53,6 @@ export const AllViews: React.FC<Props> = ({
|
||||
addIssueToGroup,
|
||||
disableUserActions,
|
||||
dragDisabled = false,
|
||||
emptyState,
|
||||
handleIssueAction,
|
||||
handleOnDragEnd,
|
||||
openIssuesListModal,
|
||||
@@ -167,28 +156,41 @@ 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}
|
||||
primaryButton={{
|
||||
text: "Go to Automation Settings",
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
|
||||
},
|
||||
buttonText="Go to Automation Settings"
|
||||
onClick={() => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
title={emptyState.title}
|
||||
description={emptyState.description}
|
||||
image={emptyIssue}
|
||||
primaryButton={
|
||||
emptyState.primaryButton
|
||||
? {
|
||||
icon: emptyState.primaryButton.icon,
|
||||
text: emptyState.primaryButton.text,
|
||||
onClick: emptyState.primaryButton.onClick,
|
||||
}
|
||||
: undefined
|
||||
title={
|
||||
cycleId
|
||||
? "Cycle issues will appear here"
|
||||
: moduleId
|
||||
? "Module issues will appear here"
|
||||
: "Project issues will appear here"
|
||||
}
|
||||
secondaryButton={emptyState.secondaryButton}
|
||||
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."
|
||||
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
|
||||
}
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "c",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
|
||||
@@ -221,7 +221,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
Copy issue link
|
||||
</ContextMenu.Item>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
|
||||
href={`/${workspaceSlug}/projects/${projectId}/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}/issues/${issue.id}`}>
|
||||
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}>
|
||||
<a className="flex w-full cursor-pointer flex-col items-start justify-center gap-1.5">
|
||||
{properties.key && (
|
||||
<Tooltip
|
||||
|
||||
@@ -22,7 +22,7 @@ import { FiltersList, AllViews } from "components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { CreateUpdateViewModal } from "components/views";
|
||||
// ui
|
||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||
import { PrimaryButton } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
@@ -515,35 +515,6 @@ 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 issuePath = isArchivedIssues
|
||||
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
|
||||
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
|
||||
const singleIssuePath = isArchivedIssues
|
||||
? `/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}`
|
||||
: `/${workspaceSlug}/projects/${projectId}/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={issuePath} target="_blank" rel="noreferrer noopener">
|
||||
<a href={singleIssuePath} 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={issuePath}>
|
||||
<Link href={singleIssuePath}>
|
||||
<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}/issues/${issue.id}`}>
|
||||
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
||||
<a className="truncate text-custom-text-100 cursor-pointer w-full text-[0.825rem]">
|
||||
{issue.name}
|
||||
</a>
|
||||
|
||||
@@ -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 ? (
|
||||
<ActivityMessage activity={activityItem} />
|
||||
) : (
|
||||
"created the issue."
|
||||
);
|
||||
const message = activityItem.field
|
||||
? activityDetails[activityItem.field as keyof typeof activityDetails]?.message(
|
||||
activityItem
|
||||
)
|
||||
: "created the issue.";
|
||||
|
||||
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
||||
return (
|
||||
@@ -116,7 +116,8 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
activityItem.new_value === "restore" ? (
|
||||
<Icon iconName="history" className="text-sm text-custom-text-200" />
|
||||
) : (
|
||||
<ActivityIcon activity={activityItem} />
|
||||
activityDetails[activityItem.field as keyof typeof activityDetails]
|
||||
?.icon
|
||||
)
|
||||
) : activityItem.actor_detail.avatar &&
|
||||
activityItem.actor_detail.avatar !== "" ? (
|
||||
|
||||
@@ -140,7 +140,11 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
ref={showEditorRef}
|
||||
/>
|
||||
|
||||
<CommentReaction projectId={comment.project} commentId={comment.id} />
|
||||
<CommentReaction
|
||||
workspaceSlug={comment?.workspace_detail?.slug}
|
||||
projectId={comment.project}
|
||||
commentId={comment.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import useCommentReaction from "hooks/use-comment-reaction";
|
||||
@@ -11,15 +9,13 @@ 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 { projectId, commentId } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, projectId, commentId } = props;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
|
||||
@@ -59,9 +59,8 @@ 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)
|
||||
@@ -113,7 +112,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(data.project, params));
|
||||
else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
|
||||
}
|
||||
|
||||
handleClose();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -18,7 +18,6 @@ import useToast from "hooks/use-toast";
|
||||
import useInboxView from "hooks/use-inbox-view";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
import useProjects from "hooks/use-projects";
|
||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||
// components
|
||||
import { IssueForm } from "components/issues";
|
||||
// types
|
||||
@@ -86,8 +85,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
const { user } = useUser();
|
||||
const { projects } = useProjects();
|
||||
|
||||
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
||||
@@ -98,36 +95,20 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
||||
};
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
handleClose();
|
||||
setActiveProject(null);
|
||||
}, [handleClose]);
|
||||
|
||||
useEffect(() => {
|
||||
// if modal is closed, reset active project to null
|
||||
// and return to avoid activeProject being set to some other project
|
||||
if (!isOpen) {
|
||||
setActiveProject(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// if data is present, set active project to the project of the
|
||||
// issue. This has more priority than the project in the url.
|
||||
if (data && data.project) {
|
||||
setActiveProject(data.project);
|
||||
return;
|
||||
}
|
||||
|
||||
// if data is not present, set active project to the project
|
||||
// in the url. This has the least priority.
|
||||
if (projects && projects.length > 0 && !activeProject)
|
||||
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
||||
}, [activeProject, data, projectId, projects, isOpen]);
|
||||
}, [activeProject, data, projectId, projects]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
onClose();
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,7 +116,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [onClose]);
|
||||
}, [handleClose]);
|
||||
|
||||
const addIssueToCycle = async (issueId: string, cycleId: string) => {
|
||||
if (!workspaceSlug || !activeProject) return;
|
||||
@@ -262,7 +243,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
||||
if (issueView === "gantt_chart") mutate(ganttFetchKey);
|
||||
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||
if (groupedIssues) mutateMyIssues();
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
@@ -283,7 +263,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
if (!createMore) onClose();
|
||||
if (!createMore) handleClose();
|
||||
};
|
||||
|
||||
const updateIssue = async (payload: Partial<IIssue>) => {
|
||||
@@ -302,7 +282,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
||||
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
||||
|
||||
if (!createMore) onClose();
|
||||
if (!createMore) handleClose();
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
@@ -340,7 +320,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||
<Dialog as="div" className="relative z-20" onClose={() => handleClose()}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@@ -370,7 +350,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
initialData={data ?? prePopulateData}
|
||||
createMore={createMore}
|
||||
setCreateMore={setCreateMore}
|
||||
handleClose={onClose}
|
||||
handleClose={handleClose}
|
||||
projectId={activeProject ?? ""}
|
||||
setActiveProject={setActiveProject}
|
||||
status={data ? true : false}
|
||||
|
||||
@@ -146,108 +146,105 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
groupBy === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find((option) => option.key === groupBy)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (option.key === "state" || option.key === "created_by")
|
||||
return null;
|
||||
<CustomMenu
|
||||
label={
|
||||
groupBy === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find((option) => option.key === groupBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (option.key === "state" || option.key === "created_by")
|
||||
return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupBy(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupBy(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<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) => {
|
||||
if (groupBy === "priority" && option.key === "priority")
|
||||
return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) => {
|
||||
if (groupBy === "priority" && option.key === "priority") return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<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>
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
|
||||
</div>
|
||||
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
|
||||
</div>
|
||||
{/* <div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-custom-primary"
|
||||
onClick={() => setNewFilterDefaultView()}
|
||||
>
|
||||
Set as default
|
||||
</button>
|
||||
</div> */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ 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;
|
||||
@@ -263,20 +262,6 @@ 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}
|
||||
|
||||
@@ -27,7 +27,6 @@ import { snoozeOptions } from "constants/notification";
|
||||
type NotificationCardProps = {
|
||||
notification: IUserNotification;
|
||||
markNotificationReadStatus: (notificationId: string) => Promise<void>;
|
||||
markNotificationReadStatusToggle: (notificationId: string) => Promise<void>;
|
||||
markNotificationArchivedStatus: (notificationId: string) => Promise<void>;
|
||||
setSelectedNotificationForSnooze: (notificationId: string) => void;
|
||||
markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
|
||||
@@ -37,7 +36,6 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
const {
|
||||
notification,
|
||||
markNotificationReadStatus,
|
||||
markNotificationReadStatusToggle,
|
||||
markNotificationArchivedStatus,
|
||||
setSelectedNotificationForSnooze,
|
||||
markSnoozeNotification,
|
||||
@@ -161,7 +159,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
name: notification.read_at ? "Mark as unread" : "Mark as read",
|
||||
icon: "chat_bubble",
|
||||
onClick: () => {
|
||||
markNotificationReadStatusToggle(notification.id).then(() => {
|
||||
markNotificationReadStatus(notification.id).then(() => {
|
||||
setToastAlert({
|
||||
title: notification.read_at
|
||||
? "Notification marked as unread"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
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
|
||||
@@ -39,6 +44,11 @@ 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;
|
||||
@@ -140,31 +150,59 @@ export const NotificationHeader: React.FC<NotificationHeaderProps> = (props) =>
|
||||
</button>
|
||||
) : (
|
||||
<nav className="flex space-x-5 overflow-x-auto" aria-label="Tabs">
|
||||
{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 ${
|
||||
{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 ${
|
||||
tab.value === selectedTab
|
||||
? "bg-custom-primary-100 text-white"
|
||||
: "bg-custom-background-80 text-custom-text-200"
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{getNumberCount(tab.unreadCount)}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
))}
|
||||
{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>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,6 @@ export const NotificationPopover = () => {
|
||||
notificationMutate,
|
||||
markNotificationArchivedStatus,
|
||||
markNotificationReadStatus,
|
||||
markNotificationAsRead,
|
||||
markSnoozeNotification,
|
||||
notificationCount,
|
||||
totalNotificationCount,
|
||||
@@ -129,8 +128,7 @@ export const NotificationPopover = () => {
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
markNotificationArchivedStatus={markNotificationArchivedStatus}
|
||||
markNotificationReadStatus={markNotificationAsRead}
|
||||
markNotificationReadStatusToggle={markNotificationReadStatus}
|
||||
markNotificationReadStatus={markNotificationReadStatus}
|
||||
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
|
||||
markSnoozeNotification={markSnoozeNotification}
|
||||
/>
|
||||
|
||||
@@ -56,15 +56,13 @@ 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}
|
||||
primaryButton={{
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "New Page",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "d",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
buttonText="New Page"
|
||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "d",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -260,15 +260,13 @@ 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}
|
||||
primaryButton={{
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "New Page",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "d",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
buttonText="New Page"
|
||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
||||
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 ? (
|
||||
<ActivityMessage activity={activity} showIssue />
|
||||
activityDetails[activity.field]?.message(activity as any)
|
||||
) : (
|
||||
<span>
|
||||
created this{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
|
||||
href={`/${activity.workspace_detail.slug}/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"
|
||||
|
||||
@@ -172,109 +172,106 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
groupByProperty === "project"
|
||||
? "Project"
|
||||
: 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 === "state" || option.key === "created_by")
|
||||
return null;
|
||||
<CustomMenu
|
||||
label={
|
||||
groupByProperty === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (option.key === "state" || option.key === "created_by")
|
||||
return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<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) => {
|
||||
if (groupByProperty === "priority" && option.key === "priority")
|
||||
return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) => {
|
||||
if (groupByProperty === "priority" && option.key === "priority")
|
||||
return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<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>
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
|
||||
</div>
|
||||
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
|
||||
</div>
|
||||
{/* <div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-custom-primary"
|
||||
onClick={() => setNewFilterDefaultView()}
|
||||
>
|
||||
Set as default
|
||||
</button>
|
||||
</div> */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -8,7 +8,6 @@ import useSWR from "swr";
|
||||
import { DropResult } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import userService from "services/user.service";
|
||||
// hooks
|
||||
import useProfileIssues from "hooks/use-profile-issues";
|
||||
import useUser from "hooks/use-user";
|
||||
@@ -20,7 +19,7 @@ import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue, IIssueFilterOptions } from "types";
|
||||
// fetch-keys
|
||||
import { USER_PROFILE_PROJECT_SEGREGATION, WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
export const ProfileIssuesView = () => {
|
||||
// create issue modal
|
||||
@@ -61,16 +60,6 @@ export const ProfileIssuesView = () => {
|
||||
params,
|
||||
} = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
|
||||
|
||||
const { data: profileData } = useSWR(
|
||||
workspaceSlug && userId
|
||||
? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
|
||||
: null,
|
||||
workspaceSlug && userId
|
||||
? () =>
|
||||
userService.getUserProfileProjectsSegregation(workspaceSlug.toString(), userId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug && (filters?.labels ?? []).length > 0
|
||||
? WORKSPACE_LABELS(workspaceSlug.toString())
|
||||
@@ -151,26 +140,10 @@ export const ProfileIssuesView = () => {
|
||||
]
|
||||
);
|
||||
|
||||
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 addIssueToGroup = useCallback((groupTitle: string) => {
|
||||
setCreateIssueModal(true);
|
||||
return;
|
||||
}, []);
|
||||
|
||||
const addIssueToDate = useCallback(
|
||||
(date: string) => {
|
||||
@@ -277,13 +250,6 @@ export const ProfileIssuesView = () => {
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
disableUserActions={false}
|
||||
dragDisabled={groupByProperty !== "priority"}
|
||||
emptyState={{
|
||||
title: router.pathname.includes("assigned")
|
||||
? `Issues assigned to ${profileData?.user_data.first_name} ${profileData?.user_data.last_name} will appear here`
|
||||
: router.pathname.includes("created")
|
||||
? `Issues created by ${profileData?.user_data.first_name} ${profileData?.user_data.last_name} will appear here`
|
||||
: `Issues subscribed by ${profileData?.user_data.first_name} ${profileData?.user_data.last_name} will appear here`,
|
||||
}}
|
||||
handleOnDragEnd={handleOnDragEnd}
|
||||
handleIssueAction={handleIssueAction}
|
||||
openIssuesListModal={null}
|
||||
|
||||
@@ -97,7 +97,7 @@ export const ProfileSidebar = () => {
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-custom-background-90 flex justify-center items-center w-[52px] h-[52px] rounded text-custom-text-100">
|
||||
<div className="bg-custom-background-90 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-1531045535792-b515d59c3d1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
|
||||
"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",
|
||||
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-44 w-full rounded-lg bg-custom-background-80">
|
||||
<div className="group relative h-36 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-24"
|
||||
className="text-sm !h-[8rem]"
|
||||
placeholder="Description..."
|
||||
error={errors.description}
|
||||
register={register}
|
||||
@@ -327,7 +327,6 @@ 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 ? (
|
||||
@@ -370,7 +369,6 @@ 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 ? (
|
||||
|
||||
@@ -5,8 +5,6 @@ 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";
|
||||
@@ -17,7 +15,6 @@ 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";
|
||||
@@ -33,7 +30,7 @@ export const ProjectSidebarList: FC = () => {
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
@@ -41,18 +38,15 @@ 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 orderedJoinedProjects: IProject[] | undefined = joinedProjects
|
||||
? orderArrayBy(joinedProjects, "sort_order", "ascending")
|
||||
: undefined;
|
||||
const orderedAllProjects = allProjects
|
||||
? orderArrayBy(allProjects, "sort_order", "ascending")
|
||||
: [];
|
||||
|
||||
const orderedFavProjects: IProject[] | undefined = favoriteProjects
|
||||
const orderedFavProjects = favoriteProjects
|
||||
? orderArrayBy(favoriteProjects, "sort_order", "ascending")
|
||||
: undefined;
|
||||
: [];
|
||||
|
||||
const handleDeleteProject = (project: IProject) => {
|
||||
setProjectToDelete(project);
|
||||
@@ -75,25 +69,22 @@ export const ProjectSidebarList: FC = () => {
|
||||
const { source, destination, draggableId } = result;
|
||||
|
||||
if (!destination || !workspaceSlug) return;
|
||||
|
||||
if (source.index === destination.index) return;
|
||||
|
||||
const projectsList =
|
||||
(destination.droppableId === "joined-projects"
|
||||
? orderedJoinedProjects
|
||||
: orderedFavProjects) ?? [];
|
||||
const projectList =
|
||||
destination.droppableId === "all-projects" ? orderedAllProjects : orderedFavProjects;
|
||||
|
||||
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;
|
||||
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;
|
||||
const relativeDestinationSortingOrder =
|
||||
source.index < destination.index
|
||||
? (projectsList[destination.index + 1].sort_order as number)
|
||||
: (projectsList[destination.index - 1].sort_order as number);
|
||||
? projectList[destination.index + 1].sort_order
|
||||
: projectList[destination.index - 1].sort_order;
|
||||
|
||||
updatedSortOrder = Math.round(
|
||||
(destinationSortingOrder + relativeDestinationSortingOrder) / 2
|
||||
@@ -130,148 +121,76 @@ export const ProjectSidebarList: FC = () => {
|
||||
data={projectToDelete}
|
||||
user={user}
|
||||
/>
|
||||
<div className="h-full overflow-y-auto px-4 space-y-3 pt-3 border-t border-custom-sidebar-border-300">
|
||||
<div className="h-full overflow-y-auto px-4">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="favorite-projects">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{orderedFavProjects && orderedFavProjects.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"
|
||||
>
|
||||
Favorites
|
||||
<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">
|
||||
{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}
|
||||
</>
|
||||
<div className="flex flex-col space-y-2 mt-5">
|
||||
{!sidebarCollapse && (
|
||||
<h5 className="text-sm font-medium text-custom-sidebar-text-200">
|
||||
Favorites
|
||||
</h5>
|
||||
)}
|
||||
</Disclosure>
|
||||
{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
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="joined-projects">
|
||||
<Droppable droppableId="all-projects">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{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"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
)}
|
||||
<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}
|
||||
</>
|
||||
{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>
|
||||
)}
|
||||
</Disclosure>
|
||||
{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)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</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,58 +133,43 @@ 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 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" : ""
|
||||
className={`group relative flex items-center gap-x-1 text-custom-sidebar-text-100 ${
|
||||
snapshot.isDragging ? "opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
{provided && (
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
project.sort_order === null
|
||||
? "Join the project to rearrange"
|
||||
: "Drag to rearrange"
|
||||
}
|
||||
position="top-right"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 ${
|
||||
sidebarCollapse ? "" : "group-hover:!flex"
|
||||
} ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""}`}
|
||||
{...provided?.dragHandleProps}
|
||||
>
|
||||
<EllipsisVerticalIcon className="h-4" />
|
||||
<EllipsisVerticalIcon className="-ml-5 h-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<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>
|
||||
<Tooltip
|
||||
tooltipContent={`${project.name}`}
|
||||
tooltipContent={`${project?.name}`}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapse}
|
||||
>
|
||||
<Disclosure.Button
|
||||
as="div"
|
||||
className={`flex items-center flex-grow truncate cursor-pointer select-none text-left text-sm font-medium ${
|
||||
sidebarCollapse ? "justify-center" : `justify-between`
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center flex-grow w-full truncate gap-x-2 ${
|
||||
sidebarCollapse ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{project.emoji ? (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||
{renderEmoji(project.emoji)}
|
||||
</span>
|
||||
) : project.icon_prop ? (
|
||||
<div className="h-7 w-7 flex-shrink-0 grid place-items-center">
|
||||
<div className="h-7 w-7 grid place-items-center">
|
||||
{renderEmoji(project.icon_prop)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -194,24 +179,26 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
)}
|
||||
|
||||
{!sidebarCollapse && (
|
||||
<p className={`truncate ${open ? "" : "text-custom-sidebar-text-200"}`}>
|
||||
{project.name}
|
||||
<p
|
||||
className={`overflow-hidden text-ellipsis ${
|
||||
open ? "" : "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
>
|
||||
{truncateText(project?.name, 14)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{!sidebarCollapse && (
|
||||
<ExpandMoreOutlined
|
||||
fontSize="small"
|
||||
className={`flex-shrink-0 ${
|
||||
open ? "rotate-180" : ""
|
||||
} !hidden group-hover:!block text-custom-sidebar-text-200 duration-300`}
|
||||
className={`${open ? "rotate-180" : ""} text-custom-text-200 duration-300`}
|
||||
/>
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</Tooltip>
|
||||
|
||||
{!sidebarCollapse && (
|
||||
<CustomMenu className="hidden group-hover:block flex-shrink-0" ellipsis>
|
||||
<CustomMenu ellipsis>
|
||||
{!shortContextMenu && (
|
||||
<CustomMenu.MenuItem onClick={handleDeleteProject}>
|
||||
<span className="flex items-center justify-start gap-2 ">
|
||||
@@ -266,7 +253,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Disclosure.Panel className={`space-y-2 mt-1 ${sidebarCollapse ? "" : "ml-[2.25rem]"}`}>
|
||||
<Disclosure.Panel className={`space-y-2 ${sidebarCollapse ? "" : "ml-[2.25rem]"}`}>
|
||||
{navigation(workspaceSlug as string, project?.id).map((item) => {
|
||||
if (
|
||||
(item.name === "Cycles" && !project.cycle_view) ||
|
||||
|
||||
@@ -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-100 hover:border-opacity-70 hover:bg-opacity-70"
|
||||
: "bg-custom-background-80 hover:border-opacity-70 hover:bg-opacity-70"
|
||||
} ${loading ? "cursor-wait" : ""}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
|
||||
@@ -7,14 +7,12 @@ import { PrimaryButton } from "components/ui";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
description: React.ReactNode | string;
|
||||
image: any;
|
||||
primaryButton?: {
|
||||
icon?: any;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
buttonText?: string;
|
||||
buttonIcon?: any;
|
||||
secondaryButton?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
isFullScreen?: boolean;
|
||||
};
|
||||
|
||||
@@ -22,7 +20,9 @@ export const EmptyState: React.FC<Props> = ({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
primaryButton,
|
||||
onClick,
|
||||
buttonText,
|
||||
buttonIcon,
|
||||
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={primaryButton?.text} />
|
||||
<Image src={image} className="w-52 sm:w-60" alt={buttonText} />
|
||||
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6>
|
||||
{description && <p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>}
|
||||
<p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<PrimaryButton className="flex items-center gap-1.5" onClick={primaryButton.onClick}>
|
||||
{primaryButton.icon}
|
||||
{primaryButton.text}
|
||||
{buttonText && (
|
||||
<PrimaryButton className="flex items-center gap-1.5" onClick={onClick}>
|
||||
{buttonIcon}
|
||||
{buttonText}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
{secondaryButton}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
// headless ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
// headless ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import useThemeHook from "hooks/use-theme";
|
||||
@@ -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, Icon, Loader } from "components/ui";
|
||||
import { Avatar, Loader } from "components/ui";
|
||||
// icons
|
||||
import { CheckIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
@@ -40,19 +40,6 @@ 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;
|
||||
@@ -91,7 +78,7 @@ export const WorkspaceSidebarDropdown = () => {
|
||||
.then(() => {
|
||||
mutateUser(undefined);
|
||||
router.push("/");
|
||||
setTheme("system");
|
||||
setTheme("dark");
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
@@ -103,8 +90,8 @@ export const WorkspaceSidebarDropdown = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<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 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">
|
||||
<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 ${
|
||||
@@ -131,166 +118,127 @@ export const WorkspaceSidebarDropdown = () => {
|
||||
</div>
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="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>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
)}
|
||||
{!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>
|
||||
<div className="pt-2">
|
||||
</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"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))
|
||||
) : (
|
||||
<p>No workspace found!</p>
|
||||
)}
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
onClick={() => {
|
||||
router.push("/create-workspace");
|
||||
}}
|
||||
className="flex w-full items-center gap-1 text-sm text-custom-sidebar-text-200"
|
||||
>
|
||||
<Icon iconName="logout" className="!text-base" />
|
||||
Sign out
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
Create Workspace
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 pb-5">
|
||||
<div className="w-full cursor-pointer space-y-2 px-4 mt-5">
|
||||
{workspaceLinks(workspaceSlug as string).map((link, index) => {
|
||||
const isActive =
|
||||
link.name === "Settings"
|
||||
|
||||
@@ -285,7 +285,8 @@ export const getPaginatedNotificationKey = (
|
||||
if (index === 0)
|
||||
return `/api/workspaces/${workspaceSlug}/users/notifications?${objToQueryParams({
|
||||
...params,
|
||||
cursor: "30:0:0",
|
||||
// TODO: change to '100:0:0'
|
||||
cursor: "2:0:0",
|
||||
})}`;
|
||||
|
||||
const cursor = prevData?.next_cursor;
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"];
|
||||
|
||||
export const THEMES_OBJ = [
|
||||
{
|
||||
value: "system",
|
||||
label: "System Preference",
|
||||
type: "light",
|
||||
icon: {
|
||||
border: "#DEE2E6",
|
||||
color1: "#FAFAFA",
|
||||
color2: "#3F76FF",
|
||||
},
|
||||
},
|
||||
{
|
||||
value: "light",
|
||||
label: "Light",
|
||||
|
||||
@@ -11,7 +11,7 @@ import projectService from "services/project.service";
|
||||
// fetch-keys
|
||||
import { USER_PROJECT_VIEW } from "constants/fetch-keys";
|
||||
// helper
|
||||
import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
|
||||
import { applyTheme } from "helpers/theme.helper";
|
||||
// constants
|
||||
|
||||
export const themeContext = createContext<ContextType>({} as ContextType);
|
||||
@@ -92,18 +92,15 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
|
||||
useEffect(() => {
|
||||
const theme = localStorage.getItem("theme");
|
||||
|
||||
if (theme) {
|
||||
if (theme === "custom") {
|
||||
if (user && user.theme.palette) {
|
||||
applyTheme(
|
||||
user.theme.palette !== ",,,,"
|
||||
? user.theme.palette
|
||||
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||
user.theme.darkPalette
|
||||
);
|
||||
}
|
||||
} else unsetCustomCssVariables();
|
||||
if (theme && theme === "custom") {
|
||||
if (user && user.theme.palette) {
|
||||
applyTheme(
|
||||
user.theme.palette !== ",,,,"
|
||||
? user.theme.palette
|
||||
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||
user.theme.darkPalette
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// icons
|
||||
import { Icon, Tooltip } from "components/ui";
|
||||
import { Icon } from "components/ui";
|
||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
||||
// helpers
|
||||
@@ -10,65 +8,26 @@ import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssueActivity } from "types";
|
||||
|
||||
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: {
|
||||
export const activityDetails: {
|
||||
[key: string]: {
|
||||
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode;
|
||||
message: (activity: IIssueActivity) => React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
} = {
|
||||
assignees: {
|
||||
message: (activity, showIssue) => {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
added a new assignee{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
to <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the assignee{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
from <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
@@ -82,7 +41,7 @@ const activityDetails: {
|
||||
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
attachment: {
|
||||
message: (activity, showIssue) => {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
@@ -96,27 +55,9 @@ 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" />,
|
||||
},
|
||||
@@ -185,47 +126,17 @@ const activityDetails: {
|
||||
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
description: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
updated the description
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
of <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</>
|
||||
),
|
||||
message: (activity) => "updated the description.",
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
estimate_point: {
|
||||
message: (activity, showIssue) => {
|
||||
if (!activity.new_value)
|
||||
return (
|
||||
<>
|
||||
removed the estimate point
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
from <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
message: (activity) => {
|
||||
if (!activity.new_value) return "removed the estimate point.";
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the estimate point to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
for <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
@@ -239,7 +150,7 @@ const activityDetails: {
|
||||
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
message: (activity, showIssue) => {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
@@ -254,12 +165,6 @@ const activityDetails: {
|
||||
/>
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
to <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
else
|
||||
@@ -276,19 +181,13 @@ 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, showIssue) => {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
@@ -301,14 +200,8 @@ const activityDetails: {
|
||||
>
|
||||
link
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
to <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</a>{" "}
|
||||
to the issue.
|
||||
</>
|
||||
);
|
||||
else
|
||||
@@ -323,14 +216,8 @@ const activityDetails: {
|
||||
>
|
||||
link
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
from <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</a>{" "}
|
||||
from the issue.
|
||||
</>
|
||||
);
|
||||
},
|
||||
@@ -363,102 +250,52 @@ const activityDetails: {
|
||||
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
name: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
set the name to {activity.new_value}
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
of <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</>
|
||||
),
|
||||
message: (activity) => `set the name to ${activity.new_value}.`,
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
parent: {
|
||||
message: (activity, showIssue) => {
|
||||
message: (activity) => {
|
||||
if (!activity.new_value)
|
||||
return (
|
||||
<>
|
||||
removed the parent{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
from <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the parent to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
for <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
priority: {
|
||||
message: (activity, showIssue) => (
|
||||
message: (activity) => (
|
||||
<>
|
||||
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, showIssue) => (
|
||||
message: (activity) => (
|
||||
<>
|
||||
set the state to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
for <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
),
|
||||
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: (activity, showIssue) => {
|
||||
if (!activity.new_value)
|
||||
return (
|
||||
<>
|
||||
removed the due date
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
from <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
message: (activity) => {
|
||||
if (!activity.new_value) return "removed the due date.";
|
||||
else
|
||||
return (
|
||||
<>
|
||||
@@ -466,12 +303,6 @@ const activityDetails: {
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{renderShortDateWithYearFormat(activity.new_value)}
|
||||
</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
for <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
@@ -479,19 +310,3 @@ 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)}
|
||||
</>
|
||||
);
|
||||
@@ -60,7 +60,6 @@ const calculateShades = (hexValue: string): TShades => {
|
||||
};
|
||||
|
||||
export const applyTheme = (palette: string, isDarkPalette: boolean) => {
|
||||
const dom = document?.querySelector<HTMLElement>("[data-theme='custom']");
|
||||
// palette: [bg, text, primary, sidebarBg, sidebarText]
|
||||
const values: string[] = palette.split(",");
|
||||
values.push(isDarkPalette ? "dark" : "light");
|
||||
@@ -80,40 +79,41 @@ export const applyTheme = (palette: string, isDarkPalette: boolean) => {
|
||||
const sidebarBackgroundRgbValues = `${sidebarBackgroundShades[shade].r}, ${sidebarBackgroundShades[shade].g}, ${sidebarBackgroundShades[shade].b}`;
|
||||
const sidebarTextRgbValues = `${sidebarTextShades[shade].r}, ${sidebarTextShades[shade].g}, ${sidebarTextShades[shade].b}`;
|
||||
|
||||
dom?.style.setProperty(`--color-background-${shade}`, bgRgbValues);
|
||||
dom?.style.setProperty(`--color-text-${shade}`, textRgbValues);
|
||||
dom?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues);
|
||||
dom?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues);
|
||||
dom?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues);
|
||||
document
|
||||
.querySelector<HTMLElement>("[data-theme='custom']")
|
||||
?.style.setProperty(`--color-background-${shade}`, bgRgbValues);
|
||||
document
|
||||
.querySelector<HTMLElement>("[data-theme='custom']")
|
||||
?.style.setProperty(`--color-text-${shade}`, textRgbValues);
|
||||
document
|
||||
.querySelector<HTMLElement>("[data-theme='custom']")
|
||||
?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues);
|
||||
document
|
||||
.querySelector<HTMLElement>("[data-theme='custom']")
|
||||
?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues);
|
||||
document
|
||||
.querySelector<HTMLElement>("[data-theme='custom']")
|
||||
?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues);
|
||||
|
||||
if (i >= 100 && i <= 400) {
|
||||
const borderShade = i === 100 ? 70 : i === 200 ? 80 : i === 300 ? 90 : 100;
|
||||
|
||||
dom?.style.setProperty(
|
||||
`--color-border-${shade}`,
|
||||
`${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}`
|
||||
);
|
||||
dom?.style.setProperty(
|
||||
`--color-sidebar-border-${shade}`,
|
||||
`${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}`
|
||||
);
|
||||
document
|
||||
.querySelector<HTMLElement>("[data-theme='custom']")
|
||||
?.style.setProperty(
|
||||
`--color-border-${shade}`,
|
||||
`${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}`
|
||||
);
|
||||
document
|
||||
.querySelector<HTMLElement>("[data-theme='custom']")
|
||||
?.style.setProperty(
|
||||
`--color-sidebar-border-${shade}`,
|
||||
`${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
dom?.style.setProperty("--color-scheme", values[5]);
|
||||
};
|
||||
|
||||
export const unsetCustomCssVariables = () => {
|
||||
for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) {
|
||||
const dom = document.querySelector<HTMLElement>("[data-theme='custom']");
|
||||
|
||||
dom?.style.removeProperty(`--color-background-${i}`);
|
||||
dom?.style.removeProperty(`--color-text-${i}`);
|
||||
dom?.style.removeProperty(`--color-border-${i}`);
|
||||
dom?.style.removeProperty(`--color-primary-${i}`);
|
||||
dom?.style.removeProperty(`--color-sidebar-background-${i}`);
|
||||
dom?.style.removeProperty(`--color-sidebar-text-${i}`);
|
||||
dom?.style.removeProperty(`--color-sidebar-border-${i}`);
|
||||
dom?.style.removeProperty("--color-scheme");
|
||||
}
|
||||
document
|
||||
.querySelector<HTMLElement>("[data-theme='custom']")
|
||||
?.style.setProperty("--color-scheme", values[5]);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
@@ -14,8 +12,6 @@ import { IIssue } from "types";
|
||||
import { USER_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
const useMyIssues = (workspaceSlug: string | undefined) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { filters, groupBy, orderBy } = useMyIssuesFilters(workspaceSlug);
|
||||
|
||||
const params: any = {
|
||||
@@ -31,12 +27,8 @@ const useMyIssues = (workspaceSlug: string | undefined) => {
|
||||
};
|
||||
|
||||
const { data: myIssues, mutate: mutateMyIssues } = useSWR(
|
||||
workspaceSlug && router.pathname.includes("my-issues")
|
||||
? USER_ISSUES(workspaceSlug.toString(), params)
|
||||
: null,
|
||||
workspaceSlug && router.pathname.includes("my-issues")
|
||||
? () => userService.userIssues(workspaceSlug.toString(), params)
|
||||
: null
|
||||
workspaceSlug ? USER_ISSUES(workspaceSlug.toString(), params) : null,
|
||||
workspaceSlug ? () => userService.userIssues(workspaceSlug.toString(), params) : null
|
||||
);
|
||||
|
||||
const groupedIssues:
|
||||
|
||||
@@ -70,8 +70,7 @@ const useCommentReaction = (
|
||||
|
||||
mutateCommentReactions(
|
||||
(prevData) =>
|
||||
prevData?.filter((r) => r.actor !== user?.user?.id || r.reaction !== reaction) || [],
|
||||
false
|
||||
prevData?.filter((r) => r.actor !== user?.user?.id || r.reaction !== reaction) || []
|
||||
);
|
||||
|
||||
await reactionService.deleteIssueCommentReaction(
|
||||
|
||||
@@ -58,6 +58,8 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
|
||||
: null
|
||||
);
|
||||
|
||||
console.log(memberRole);
|
||||
|
||||
const groupedIssues:
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
|
||||
@@ -15,7 +15,8 @@ import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constan
|
||||
// type
|
||||
import type { NotificationType, NotificationCount } from "types";
|
||||
|
||||
const PER_PAGE = 30;
|
||||
// TODO: change to 100
|
||||
const PER_PAGE = 2;
|
||||
|
||||
const useUserNotification = () => {
|
||||
const router = useRouter();
|
||||
@@ -185,26 +186,6 @@ const useUserNotification = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const markNotificationAsRead = async (notificationId: string) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
const isRead =
|
||||
notifications?.find((notification) => notification.id === notificationId)?.read_at !== null;
|
||||
|
||||
if (isRead) return;
|
||||
|
||||
mutateNotification(notificationId, { read_at: new Date() });
|
||||
handleReadMutation("read");
|
||||
|
||||
await userNotificationServices
|
||||
.markUserNotificationAsRead(workspaceSlug.toString(), notificationId)
|
||||
.catch(() => {
|
||||
throw new Error("Something went wrong");
|
||||
});
|
||||
|
||||
mutateNotificationCount();
|
||||
};
|
||||
|
||||
const markNotificationArchivedStatus = async (notificationId: string) => {
|
||||
if (!workspaceSlug) return;
|
||||
const isArchived =
|
||||
@@ -303,7 +284,6 @@ const useUserNotification = () => {
|
||||
hasMore,
|
||||
isRefreshing,
|
||||
setFetchNotifications,
|
||||
markNotificationAsRead,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ 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,14 +71,12 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
||||
title="No such project exists"
|
||||
description="Try creating a new project"
|
||||
image={emptyProject}
|
||||
primaryButton={{
|
||||
text: "Create Project",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "p",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
buttonText="Create Project"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "p",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -134,15 +134,13 @@ 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}
|
||||
primaryButton={{
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "New Project",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "p",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
buttonText="New Project"
|
||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "p",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
import SettingsNavbar from "layouts/settings-navbar";
|
||||
// components
|
||||
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||
import RemirrorRichTextEditor from "components/rich-text-editor";
|
||||
// icons
|
||||
import { ArrowTopRightOnSquareIcon, ChatBubbleLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
||||
import { Feeds } from "components/core";
|
||||
// ui
|
||||
import { Icon, Loader } from "components/ui";
|
||||
import { Loader } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// fetch-keys
|
||||
import { USER_ACTIVITY } from "constants/fetch-keys";
|
||||
// helper
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
|
||||
const ProfileActivity = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity());
|
||||
|
||||
if (!userActivity) {
|
||||
return (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
@@ -56,176 +34,17 @@ const ProfileActivity = () => {
|
||||
</div>
|
||||
<SettingsNavbar profilePage />
|
||||
</div>
|
||||
{userActivity && userActivity.results.length > 0 && (
|
||||
<div>
|
||||
<ul role="list" className="-mb-4">
|
||||
{userActivity.results.map((activityItem: any, activityIdx: number) => {
|
||||
if (activityItem.field === "comment") {
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-2">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" && (
|
||||
<Icon iconName="history" className="text-sm text-custom-text-200" />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar &&
|
||||
activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.first_name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||
<ChatBubbleLeftEllipsisIcon
|
||||
className="h-3.5 w-3.5 text-custom-text-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{activityItem.actor_detail.first_name}
|
||||
{activityItem.actor_detail.is_bot
|
||||
? "Bot"
|
||||
: " " + activityItem.actor_detail.last_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
Commented {timeAgo(activityItem.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<RemirrorRichTextEditor
|
||||
value={
|
||||
activityItem.new_value && activityItem.new_value !== ""
|
||||
? activityItem.new_value
|
||||
: activityItem.old_value
|
||||
}
|
||||
editable={false}
|
||||
noBorder
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const message =
|
||||
activityItem.verb === "created" &&
|
||||
activityItem.field !== "cycles" &&
|
||||
activityItem.field !== "modules" &&
|
||||
activityItem.field !== "attachment" &&
|
||||
activityItem.field !== "link" &&
|
||||
activityItem.field !== "estimate" ? (
|
||||
<span className="text-custom-text-200">
|
||||
created{" "}
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}
|
||||
>
|
||||
<a className="inline-flex items-center hover:underline">
|
||||
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
|
||||
</a>
|
||||
</Link>
|
||||
</span>
|
||||
) : activityItem.field ? (
|
||||
<ActivityMessage activity={activityItem} showIssue />
|
||||
) : (
|
||||
"created the issue."
|
||||
);
|
||||
|
||||
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
||||
return (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-1">
|
||||
{userActivity.results.length > 1 &&
|
||||
activityIdx !== userActivity.results.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex items-start space-x-2">
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" ? (
|
||||
<Icon
|
||||
iconName="history"
|
||||
className="text-sm text-custom-text-200"
|
||||
/>
|
||||
) : (
|
||||
<ActivityIcon activity={activityItem} />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar &&
|
||||
activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.first_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="text-xs text-custom-text-200 break-words">
|
||||
{activityItem.field === "archived_at" &&
|
||||
activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : activityItem.actor_detail.is_bot ? (
|
||||
<span className="text-gray font-medium">
|
||||
{activityItem.actor_detail.first_name} Bot
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
|
||||
>
|
||||
<a className="text-gray font-medium">
|
||||
{activityItem.actor_detail.first_name}{" "}
|
||||
{activityItem.actor_detail.last_name}
|
||||
</a>
|
||||
</Link>
|
||||
)}{" "}
|
||||
{message}{" "}
|
||||
<span className="whitespace-nowrap">
|
||||
{timeAgo(activityItem.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
{userActivity ? (
|
||||
userActivity.results.length > 0 ? (
|
||||
<Feeds activities={userActivity.results} />
|
||||
) : null
|
||||
) : (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
// hooks
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// layouts
|
||||
@@ -16,13 +15,11 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { ICustomTheme } from "types";
|
||||
|
||||
const ProfilePreferences = () => {
|
||||
const { user: myProfile } = useUserAuth();
|
||||
const { theme } = useTheme();
|
||||
const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false);
|
||||
const [preLoadedData, setPreLoadedData] = useState<ICustomTheme | null>(null);
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
const { user: myProfile } = useUserAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === "custom") {
|
||||
if (myProfile?.theme.palette)
|
||||
@@ -40,7 +37,6 @@ const ProfilePreferences = () => {
|
||||
myProfile.theme.palette !== ",,,,"
|
||||
? myProfile.theme.palette
|
||||
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||
theme: "custom",
|
||||
});
|
||||
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
|
||||
}
|
||||
@@ -75,6 +71,7 @@ const ProfilePreferences = () => {
|
||||
</div>
|
||||
<div className="col-span-12 sm:col-span-6">
|
||||
<ThemeSwitch
|
||||
user={myProfile}
|
||||
setPreLoadedData={setPreLoadedData}
|
||||
customThemeSelectorOptions={customThemeSelectorOptions}
|
||||
setCustomThemeSelectorOptions={setCustomThemeSelectorOptions}
|
||||
|
||||
@@ -22,10 +22,8 @@ import useUserAuth from "hooks/use-user-auth";
|
||||
// components
|
||||
import { AnalyticsProjectModal } from "components/analytics";
|
||||
// ui
|
||||
import { CustomMenu, EmptyState, SecondaryButton } from "components/ui";
|
||||
import { CustomMenu, SecondaryButton } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// images
|
||||
import emptyCycle from "public/empty-state/cycle.svg";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||
@@ -54,14 +52,14 @@ const SingleCycle: React.FC = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: cycleDetails, error } = useSWR(
|
||||
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
|
||||
const { data: cycleDetails } = useSWR(
|
||||
cycleId ? CYCLE_DETAILS(cycleId as string) : null,
|
||||
workspaceSlug && projectId && cycleId
|
||||
? () =>
|
||||
cycleServices.getCycleDetails(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
cycleId.toString()
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
cycleId as string
|
||||
)
|
||||
: null
|
||||
);
|
||||
@@ -161,48 +159,31 @@ const SingleCycle: React.FC = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{error ? (
|
||||
<EmptyState
|
||||
image={emptyCycle}
|
||||
title="Cycle does not exist"
|
||||
description="The cycle you are looking for does not exist or has been deleted."
|
||||
primaryButton={{
|
||||
text: "View other cycles",
|
||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/cycles`),
|
||||
}}
|
||||
<TransferIssuesModal
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||
<div
|
||||
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
} duration-300`}
|
||||
>
|
||||
{cycleStatus === "completed" && (
|
||||
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
|
||||
)}
|
||||
<IssuesView
|
||||
openIssuesListModal={openIssuesListModal}
|
||||
disableUserActions={cycleStatus === "completed" ?? false}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<TransferIssuesModal
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
<AnalyticsProjectModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
/>
|
||||
<div
|
||||
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
} duration-300`}
|
||||
>
|
||||
{cycleStatus === "completed" && (
|
||||
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
|
||||
)}
|
||||
<IssuesView
|
||||
openIssuesListModal={openIssuesListModal}
|
||||
disableUserActions={cycleStatus === "completed" ?? false}
|
||||
/>
|
||||
</div>
|
||||
<CycleDetailsSidebar
|
||||
cycleStatus={cycleStatus}
|
||||
cycle={cycleDetails}
|
||||
isOpen={cycleSidebar}
|
||||
isCompleted={cycleStatus === "completed" ?? false}
|
||||
user={user}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<CycleDetailsSidebar
|
||||
cycleStatus={cycleStatus}
|
||||
cycle={cycleDetails}
|
||||
isOpen={cycleSidebar}
|
||||
isCompleted={cycleStatus === "completed" ?? false}
|
||||
user={user}
|
||||
/>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</IssueViewContextProvider>
|
||||
);
|
||||
|
||||
@@ -121,15 +121,13 @@ 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}
|
||||
primaryButton={{
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "New Cycle",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "q",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
buttonText="New Cycle"
|
||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "q",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -15,10 +15,8 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// components
|
||||
import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
|
||||
// ui
|
||||
import { EmptyState, Loader } from "components/ui";
|
||||
import { Loader } from "components/ui";
|
||||
import { Breadcrumbs } from "components/breadcrumbs";
|
||||
// images
|
||||
import emptyIssue from "public/empty-state/issue.svg";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import type { NextPage } from "next";
|
||||
@@ -47,11 +45,7 @@ const IssueDetailsPage: NextPage = () => {
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const {
|
||||
data: issueDetails,
|
||||
mutate: mutateIssueDetails,
|
||||
error,
|
||||
} = useSWR(
|
||||
const { data: issueDetails, mutate: mutateIssueDetails } = useSWR<IIssue | undefined>(
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () =>
|
||||
@@ -131,17 +125,7 @@ const IssueDetailsPage: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
{error ? (
|
||||
<EmptyState
|
||||
image={emptyIssue}
|
||||
title="Issue does not exist"
|
||||
description="The issue you are looking for does not exist, has been archived, or has been deleted."
|
||||
primaryButton={{
|
||||
text: "View other issues",
|
||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/issues`),
|
||||
}}
|
||||
/>
|
||||
) : issueDetails && projectId ? (
|
||||
{issueDetails && projectId ? (
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<div className="w-2/3 h-full overflow-y-auto space-y-5 divide-y-2 divide-custom-border-300 p-5">
|
||||
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} />
|
||||
|
||||
@@ -20,10 +20,8 @@ import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "component
|
||||
import { ModuleDetailsSidebar } from "components/modules";
|
||||
import { AnalyticsProjectModal } from "components/analytics";
|
||||
// ui
|
||||
import { CustomMenu, EmptyState, SecondaryButton } from "components/ui";
|
||||
import { CustomMenu, SecondaryButton } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// images
|
||||
import emptyModule from "public/empty-state/module.svg";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
@@ -62,7 +60,7 @@ const SingleModule: React.FC = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: moduleDetails, error } = useSWR(
|
||||
const { data: moduleDetails } = useSWR(
|
||||
moduleId ? MODULE_DETAILS(moduleId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
@@ -164,37 +162,22 @@ const SingleModule: React.FC = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{error ? (
|
||||
<EmptyState
|
||||
image={emptyModule}
|
||||
title="Module does not exist"
|
||||
description="The module you are looking for does not exist or has been deleted."
|
||||
primaryButton={{
|
||||
text: "View other modules",
|
||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/modules`),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<AnalyticsProjectModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
/>
|
||||
<div
|
||||
className={`h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
} duration-300`}
|
||||
>
|
||||
<IssuesView openIssuesListModal={openIssuesListModal} />
|
||||
</div>
|
||||
<ModuleDetailsSidebar
|
||||
module={moduleDetails}
|
||||
isOpen={moduleSidebar}
|
||||
moduleIssues={moduleIssues}
|
||||
user={user}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||
|
||||
<div
|
||||
className={`h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
} duration-300`}
|
||||
>
|
||||
<IssuesView openIssuesListModal={openIssuesListModal} />
|
||||
</div>
|
||||
|
||||
<ModuleDetailsSidebar
|
||||
module={moduleDetails}
|
||||
isOpen={moduleSidebar}
|
||||
moduleIssues={moduleIssues}
|
||||
user={user}
|
||||
/>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</IssueViewContextProvider>
|
||||
);
|
||||
|
||||
@@ -146,15 +146,13 @@ const ProjectModules: NextPage = () => {
|
||||
title="Manage your project with modules"
|
||||
description="Modules are smaller, focused projects that help you group and organize issues."
|
||||
image={emptyModule}
|
||||
primaryButton={{
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "New Module",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "m",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
buttonText="New Module"
|
||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "m",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -28,16 +28,7 @@ import { CreateLabelModal } from "components/labels";
|
||||
import { CreateBlock } from "components/pages/create-block";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import {
|
||||
CustomSearchSelect,
|
||||
EmptyState,
|
||||
Loader,
|
||||
TextArea,
|
||||
ToggleSwitch,
|
||||
Tooltip,
|
||||
} from "components/ui";
|
||||
// images
|
||||
import emptyPage from "public/empty-state/page.svg";
|
||||
import { CustomSearchSelect, Loader, TextArea, ToggleSwitch, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
@@ -49,7 +40,7 @@ import {
|
||||
XMarkIcon,
|
||||
ChevronDownIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ColorPalletteIcon } from "components/icons";
|
||||
import { ColorPalletteIcon, ClipboardIcon } from "components/icons";
|
||||
// helpers
|
||||
import { render24HourFormatTime, renderShortDate } from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
@@ -91,7 +82,7 @@ const SinglePage: NextPage = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: pageDetails, error } = useSWR(
|
||||
const { data: pageDetails } = useSWR(
|
||||
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
@@ -276,6 +267,13 @@ const SinglePage: NextPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleNewBlock = useCallback(() => {
|
||||
setCreateBlockForm(true);
|
||||
scrollToRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
});
|
||||
}, [setCreateBlockForm, scrollToRef]);
|
||||
|
||||
const handleShowBlockToggle = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
@@ -313,21 +311,22 @@ const SinglePage: NextPage = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const options = labels?.map((label) => ({
|
||||
value: label.id,
|
||||
query: label.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
const options =
|
||||
labels?.map((label) => ({
|
||||
value: label.id,
|
||||
query: label.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
),
|
||||
})) ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageDetails) return;
|
||||
@@ -347,21 +346,11 @@ const SinglePage: NextPage = () => {
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Pages`} />
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project",32)} Pages`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
{error ? (
|
||||
<EmptyState
|
||||
image={emptyPage}
|
||||
title="Page does not exist"
|
||||
description="The page you are looking for does not exist or has been deleted."
|
||||
primaryButton={{
|
||||
text: "View other pages",
|
||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/pages`),
|
||||
}}
|
||||
/>
|
||||
) : pageDetails ? (
|
||||
{pageDetails ? (
|
||||
<div className="flex h-full flex-col justify-between space-y-4 overflow-hidden p-4">
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
|
||||
@@ -166,13 +166,11 @@ const EstimatesSettings: NextPage = () => {
|
||||
title="No estimates yet"
|
||||
description="Estimates help you communicate the complexity of an issue."
|
||||
image={emptyEstimate}
|
||||
primaryButton={{
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "Add Estimate",
|
||||
onClick: () => {
|
||||
setEstimateToUpdate(undefined);
|
||||
setEstimateFormOpen(true);
|
||||
},
|
||||
buttonText="Add Estimate"
|
||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
||||
onClick={() => {
|
||||
setEstimateToUpdate(undefined);
|
||||
setEstimateFormOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -78,10 +78,8 @@ const ProjectIntegrations: NextPage = () => {
|
||||
title="You haven't configured integrations"
|
||||
description="Configure GitHub and other integrations to sync your project issues."
|
||||
image={emptyIntegration}
|
||||
primaryButton={{
|
||||
text: "Configure now",
|
||||
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
|
||||
}}
|
||||
buttonText="Configure now"
|
||||
onClick={() => router.push(`/${workspaceSlug}/settings/integrations`)}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
|
||||
@@ -181,10 +181,8 @@ const LabelsSettings: NextPage = () => {
|
||||
title="No labels yet"
|
||||
description="Create labels to help organize and filter issues in you project"
|
||||
image={emptyLabel}
|
||||
primaryButton={{
|
||||
text: "Add label",
|
||||
onClick: () => newLabel(),
|
||||
}}
|
||||
buttonText="Add label"
|
||||
onClick={newLabel}
|
||||
isFullScreen={false}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -12,13 +12,11 @@ import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||
// components
|
||||
import { IssuesFilterView, IssuesView } from "components/core";
|
||||
// ui
|
||||
import { CustomMenu, EmptyState, PrimaryButton } from "components/ui";
|
||||
import { CustomMenu, PrimaryButton } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { StackedLayersIcon } from "components/icons";
|
||||
// images
|
||||
import emptyView from "public/empty-state/view.svg";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// fetch-keys
|
||||
@@ -42,7 +40,7 @@ const SingleView: React.FC = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: viewDetails, error } = useSWR(
|
||||
const { data: viewDetails } = useSWR(
|
||||
workspaceSlug && projectId && viewId ? VIEW_DETAILS(viewId as string) : null,
|
||||
workspaceSlug && projectId && viewId
|
||||
? () =>
|
||||
@@ -103,21 +101,9 @@ const SingleView: React.FC = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{error ? (
|
||||
<EmptyState
|
||||
image={emptyView}
|
||||
title="View does not exist"
|
||||
description="The view you are looking for does not exist or has been deleted."
|
||||
primaryButton={{
|
||||
text: "View other views",
|
||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/views`),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<IssuesView />
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<IssuesView />
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</IssueViewContextProvider>
|
||||
);
|
||||
|
||||
@@ -118,15 +118,13 @@ const ProjectViews: NextPage = () => {
|
||||
title="Get focused with views"
|
||||
description="Views aid in saving your issues by applying various filters and grouping options."
|
||||
image={emptyView}
|
||||
primaryButton={{
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "New View",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "v",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
buttonText="New View"
|
||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "v",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -111,15 +111,13 @@ const ProjectsPage: NextPage = () => {
|
||||
image={emptyProject}
|
||||
title="No projects yet"
|
||||
description="Get started by creating your first project"
|
||||
primaryButton={{
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "New Project",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "p",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
buttonText="New Project"
|
||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "p",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -115,48 +115,30 @@ const MembersSettings: NextPage = () => {
|
||||
handleDelete={async () => {
|
||||
if (!workspaceSlug) return;
|
||||
if (selectedRemoveMember) {
|
||||
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)
|
||||
);
|
||||
});
|
||||
await workspaceService.deleteWorkspaceMember(
|
||||
workspaceSlug as string,
|
||||
selectedRemoveMember
|
||||
);
|
||||
mutateMembers(
|
||||
(prevData) => prevData?.filter((item) => item.id !== selectedRemoveMember),
|
||||
false
|
||||
);
|
||||
}
|
||||
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);
|
||||
}}
|
||||
@@ -294,7 +276,7 @@ const MembersSettings: NextPage = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{user?.id === member.memberId ? "Leave" : "Remove member"}
|
||||
Remove member
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@ Router.events.on("routeChangeComplete", NProgress.done);
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
// <UserProvider>
|
||||
<ThemeProvider themes={THEMES} defaultTheme="system">
|
||||
<ThemeProvider themes={THEMES} defaultTheme="dark">
|
||||
<ToastContextProvider>
|
||||
<ThemeContextProvider>
|
||||
<CrispWithNoSSR />
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import React from "react";
|
||||
|
||||
// layouts
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
|
||||
const Colors: NextPage = () => (
|
||||
<UserAuthorizationLayout>
|
||||
<DefaultLayout>
|
||||
<div className="space-y-8 p-8">
|
||||
<div>
|
||||
Primary:
|
||||
<div className="flex flex-wrap">
|
||||
<div className="h-12 w-12 bg-custom-primary-0" />
|
||||
<div className="h-12 w-12 bg-custom-primary-10" />
|
||||
<div className="h-12 w-12 bg-custom-primary-20" />
|
||||
<div className="h-12 w-12 bg-custom-primary-30" />
|
||||
<div className="h-12 w-12 bg-custom-primary-40" />
|
||||
<div className="h-12 w-12 bg-custom-primary-50" />
|
||||
<div className="h-12 w-12 bg-custom-primary-60" />
|
||||
<div className="h-12 w-12 bg-custom-primary-70" />
|
||||
<div className="h-12 w-12 bg-custom-primary-80" />
|
||||
<div className="h-12 w-12 bg-custom-primary-90" />
|
||||
<div className="h-12 w-12 bg-custom-primary-100" />
|
||||
<div className="h-12 w-12 bg-custom-primary-200" />
|
||||
<div className="h-12 w-12 bg-custom-primary-300" />
|
||||
<div className="h-12 w-12 bg-custom-primary-400" />
|
||||
<div className="h-12 w-12 bg-custom-primary-500" />
|
||||
<div className="h-12 w-12 bg-custom-primary-600" />
|
||||
<div className="h-12 w-12 bg-custom-primary-700" />
|
||||
<div className="h-12 w-12 bg-custom-primary-800" />
|
||||
<div className="h-12 w-12 bg-custom-primary-900" />
|
||||
<div className="h-12 w-12 bg-custom-primary-1000" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Background:
|
||||
<div className="flex flex-wrap">
|
||||
<div className="h-12 w-12 bg-custom-background-0" />
|
||||
<div className="h-12 w-12 bg-custom-background-10" />
|
||||
<div className="h-12 w-12 bg-custom-background-20" />
|
||||
<div className="h-12 w-12 bg-custom-background-30" />
|
||||
<div className="h-12 w-12 bg-custom-background-40" />
|
||||
<div className="h-12 w-12 bg-custom-background-50" />
|
||||
<div className="h-12 w-12 bg-custom-background-60" />
|
||||
<div className="h-12 w-12 bg-custom-background-70" />
|
||||
<div className="h-12 w-12 bg-custom-background-80" />
|
||||
<div className="h-12 w-12 bg-custom-background-90" />
|
||||
<div className="h-12 w-12 bg-custom-background-100" />
|
||||
<div className="h-12 w-12 bg-custom-background-200" />
|
||||
<div className="h-12 w-12 bg-custom-background-300" />
|
||||
<div className="h-12 w-12 bg-custom-background-400" />
|
||||
<div className="h-12 w-12 bg-custom-background-500" />
|
||||
<div className="h-12 w-12 bg-custom-background-600" />
|
||||
<div className="h-12 w-12 bg-custom-background-700" />
|
||||
<div className="h-12 w-12 bg-custom-background-800" />
|
||||
<div className="h-12 w-12 bg-custom-background-900" />
|
||||
<div className="h-12 w-12 bg-custom-background-1000" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Text:
|
||||
<div className="flex flex-wrap">
|
||||
<div className="h-12 w-12 bg-custom-text-0" />
|
||||
<div className="h-12 w-12 bg-custom-text-10" />
|
||||
<div className="h-12 w-12 bg-custom-text-20" />
|
||||
<div className="h-12 w-12 bg-custom-text-30" />
|
||||
<div className="h-12 w-12 bg-custom-text-40" />
|
||||
<div className="h-12 w-12 bg-custom-text-50" />
|
||||
<div className="h-12 w-12 bg-custom-text-60" />
|
||||
<div className="h-12 w-12 bg-custom-text-70" />
|
||||
<div className="h-12 w-12 bg-custom-text-80" />
|
||||
<div className="h-12 w-12 bg-custom-text-90" />
|
||||
<div className="h-12 w-12 bg-custom-text-100" />
|
||||
<div className="h-12 w-12 bg-custom-text-200" />
|
||||
<div className="h-12 w-12 bg-custom-text-300" />
|
||||
<div className="h-12 w-12 bg-custom-text-400" />
|
||||
<div className="h-12 w-12 bg-custom-text-500" />
|
||||
<div className="h-12 w-12 bg-custom-text-600" />
|
||||
<div className="h-12 w-12 bg-custom-text-700" />
|
||||
<div className="h-12 w-12 bg-custom-text-800" />
|
||||
<div className="h-12 w-12 bg-custom-text-900" />
|
||||
<div className="h-12 w-12 bg-custom-text-1000" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Sidebar Background:
|
||||
<div className="flex flex-wrap">
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-0" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-10" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-20" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-30" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-40" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-50" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-60" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-70" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-80" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-90" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-100" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-200" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-300" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-400" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-500" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-600" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-700" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-800" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-900" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-background-1000" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Sidebar Text:
|
||||
<div className="flex flex-wrap">
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-0" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-10" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-20" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-30" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-40" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-50" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-60" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-70" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-80" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-90" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-100" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-200" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-300" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-400" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-500" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-600" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-700" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-800" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-900" />
|
||||
<div className="h-12 w-12 bg-custom-sidebar-text-1000" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
</UserAuthorizationLayout>
|
||||
);
|
||||
|
||||
export default Colors;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
import { Spinner } from "components/ui";
|
||||
// images
|
||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ICurrentUserResponse, IUser } from "types";
|
||||
// types
|
||||
type EmailPasswordFormValues = {
|
||||
email: string;
|
||||
@@ -36,12 +34,6 @@ const HomePage: NextPage = () => {
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const changeTheme = (user: IUser) => {
|
||||
setTheme(user.theme.theme ?? "system");
|
||||
};
|
||||
|
||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||
try {
|
||||
if (clientId && credential) {
|
||||
@@ -51,10 +43,7 @@ const HomePage: NextPage = () => {
|
||||
clientId,
|
||||
};
|
||||
const response = await authenticationService.socialAuth(socialAuthPayload);
|
||||
if (response && response?.user) {
|
||||
mutateUser();
|
||||
changeTheme(response.user);
|
||||
}
|
||||
if (response && response?.user) mutateUser();
|
||||
} else {
|
||||
throw Error("Cant find credentials");
|
||||
}
|
||||
@@ -77,10 +66,7 @@ const HomePage: NextPage = () => {
|
||||
clientId: process.env.NEXT_PUBLIC_GITHUB_ID,
|
||||
};
|
||||
const response = await authenticationService.socialAuth(socialAuthPayload);
|
||||
if (response && response?.user) {
|
||||
mutateUser();
|
||||
changeTheme(response.user);
|
||||
}
|
||||
if (response && response?.user) mutateUser();
|
||||
} else {
|
||||
throw Error("Cant find credentials");
|
||||
}
|
||||
@@ -99,10 +85,7 @@ const HomePage: NextPage = () => {
|
||||
.emailLogin(formData)
|
||||
.then((response) => {
|
||||
try {
|
||||
if (response) {
|
||||
mutateUser();
|
||||
changeTheme(response.user);
|
||||
}
|
||||
if (response) mutateUser();
|
||||
} catch (err: any) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@@ -126,10 +109,7 @@ const HomePage: NextPage = () => {
|
||||
|
||||
const handleEmailCodeSignIn = async (response: any) => {
|
||||
try {
|
||||
if (response) {
|
||||
mutateUser();
|
||||
changeTheme(response.user);
|
||||
}
|
||||
if (response) mutateUser();
|
||||
} catch (err: any) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@@ -140,10 +120,6 @@ const HomePage: NextPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTheme("system");
|
||||
}, [setTheme]);
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -186,10 +186,8 @@ const OnBoard: NextPage = () => {
|
||||
title="No pending invites"
|
||||
description="You can see here if someone invites you to a workspace."
|
||||
image={emptyInvitation}
|
||||
primaryButton={{
|
||||
text: "Back to Dashboard",
|
||||
onClick: () => router.push("/"),
|
||||
}}
|
||||
buttonText="Back to Dashboard"
|
||||
onClick={() => router.push("/")}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
// next imports
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
// layouts
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
// services
|
||||
@@ -20,17 +17,11 @@ const MagicSignIn: NextPage = () => {
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const { mutateUser } = useUserAuth("sign-in");
|
||||
const { user, isLoading, mutateUser } = useUserAuth("sign-in");
|
||||
|
||||
const [isSigningIn, setIsSigningIn] = useState(false);
|
||||
const [errorSigningIn, setErrorSignIn] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
setTheme("system");
|
||||
}, [setTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsSigningIn(() => false);
|
||||
setErrorSignIn(() => undefined);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import Router from "next/router";
|
||||
import Image from "next/image";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
@@ -31,7 +32,7 @@ import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
||||
const Onboarding: NextPage = () => {
|
||||
const [step, setStep] = useState<number | null>(null);
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const { user, isLoading: userLoading } = useUserAuth("onboarding");
|
||||
|
||||
@@ -116,10 +117,6 @@ const Onboarding: NextPage = () => {
|
||||
await userService.updateUserOnBoard({ userRole: user.role }, user);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTheme("system");
|
||||
}, [setTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleStepChange = async () => {
|
||||
if (!user || !invitations) return;
|
||||
|
||||
@@ -3,8 +3,6 @@ import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Image from "next/image";
|
||||
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// hooks
|
||||
@@ -33,8 +31,6 @@ const ResetPasswordPage: NextPage = () => {
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -80,10 +76,6 @@ const ResetPasswordPage: NextPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTheme("system");
|
||||
}, [setTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0")) router.push("/");
|
||||
else setIsLoading(false);
|
||||
|
||||
@@ -3,8 +3,6 @@ import React, { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
// services
|
||||
import authenticationService from "services/authentication.service";
|
||||
// hooks
|
||||
@@ -33,8 +31,6 @@ const SignUp: NextPage = () => {
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const { mutateUser } = useUserAuth("sign-in");
|
||||
|
||||
const handleSignUp = async (formData: EmailPasswordFormValues) => {
|
||||
@@ -66,10 +62,6 @@ const SignUp: NextPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTheme("system");
|
||||
}, [setTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0")) router.push("/");
|
||||
else setIsLoading(false);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
import { ICurrentUserResponse } from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
@@ -33,11 +32,7 @@ class AuthService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async socialAuth(data: any): Promise<{
|
||||
access_token: string;
|
||||
refresh_toke: string;
|
||||
user: ICurrentUserResponse;
|
||||
}> {
|
||||
async socialAuth(data: any) {
|
||||
return this.post("/api/social-auth/", data, { headers: {} })
|
||||
.then((response) => {
|
||||
this.setAccessToken(response?.data?.access_token);
|
||||
|
||||
@@ -25,25 +25,6 @@
|
||||
: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 */
|
||||
|
||||
Vendored
+15
-10
@@ -38,6 +38,19 @@ 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;
|
||||
@@ -187,26 +200,18 @@ export interface IIssueActivity {
|
||||
created_by: string;
|
||||
field: string | null;
|
||||
id: string;
|
||||
issue: string | null;
|
||||
issue: string;
|
||||
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 {
|
||||
|
||||
Vendored
+1
-1
@@ -45,7 +45,7 @@ export interface IProject {
|
||||
network: number;
|
||||
page_view: boolean;
|
||||
project_lead: IUserLite | string | null;
|
||||
sort_order: number | null;
|
||||
sort_order: number;
|
||||
slug: string;
|
||||
total_cycles: number;
|
||||
total_members: number;
|
||||
|
||||
Vendored
+24
-3
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
IIssue,
|
||||
IIssueActivity,
|
||||
IIssueLite,
|
||||
IWorkspace,
|
||||
IWorkspaceLite,
|
||||
@@ -46,7 +45,6 @@ export interface ICustomTheme {
|
||||
sidebarText: string;
|
||||
darkPalette: boolean;
|
||||
palette: string;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export interface ICurrentUserResponse extends IUser {
|
||||
@@ -101,6 +99,29 @@ 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;
|
||||
@@ -108,7 +129,7 @@ export interface IUserActivityResponse {
|
||||
next_page_results: boolean;
|
||||
prev_cursor: string;
|
||||
prev_page_results: boolean;
|
||||
results: IIssueActivity[];
|
||||
results: IUserDetailedActivity[];
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user