Merge branch 'develop' of github.com:makeplane/plane into fix/project_creation

This commit is contained in:
pablohashescobar 2023-07-31 18:24:12 +05:30
commit 54c2a23a7e
66 changed files with 1000 additions and 667 deletions

View File

@ -11,9 +11,9 @@
<p align="center"> <p align="center">
<a href="https://discord.com/invite/A92xrEGCge"> <a href="https://discord.com/invite/A92xrEGCge">
<img alt="Discord" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" /> <img alt="Discord online members" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" />
</a> </a>
<img alt="Discord" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" /> <img alt="Commit activity per month" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
</p> </p>
<p> <p>

View File

@ -1,2 +1,2 @@
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, WorkspaceViewerPermission
from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission

View File

@ -61,3 +61,13 @@ class WorkspaceEntityPermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug member=request.user, workspace__slug=view.workspace_slug
).exists() ).exists()
class WorkspaceViewerPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug, role__gte=10
).exists()

View File

@ -93,6 +93,7 @@ class ProjectDetailSerializer(BaseSerializer):
total_cycles = serializers.IntegerField(read_only=True) total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True) total_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True) is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
class Meta: class Meta:
model = Project model = Project

View File

@ -5,7 +5,7 @@ from datetime import datetime
# Django imports # Django imports
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import Q, Exists, OuterRef, Func, F from django.db.models import Q, Exists, OuterRef, Func, F, Min, Subquery
from django.core.validators import validate_email from django.core.validators import validate_email
from django.conf import settings from django.conf import settings
@ -120,9 +120,15 @@ class ProjectViewSet(BaseViewSet):
project_id=OuterRef("pk"), project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
) )
sort_order_query = ProjectMember.objects.filter(
member=request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
).values("sort_order")
projects = ( projects = (
self.get_queryset() self.get_queryset()
.annotate(is_favorite=Exists(subquery)) .annotate(is_favorite=Exists(subquery))
.annotate(sort_order=Subquery(sort_order_query))
.order_by("sort_order", "name") .order_by("sort_order", "name")
.annotate( .annotate(
total_members=ProjectMember.objects.filter( total_members=ProjectMember.objects.filter(
@ -592,17 +598,26 @@ class AddMemberToProjectEndpoint(BaseAPIView):
{"error": "Atleast one member is required"}, {"error": "Atleast one member is required"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
bulk_project_members = []
project_members = ProjectMember.objects.bulk_create( 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]
bulk_project_members.append(
ProjectMember( ProjectMember(
member_id=member.get("member_id"), member_id=member.get("member_id"),
role=member.get("role", 10), role=member.get("role", 10),
project_id=project_id, project_id=project_id,
workspace_id=project.workspace_id, workspace_id=project.workspace_id,
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535
) )
for member in members )
],
project_members = ProjectMember.objects.bulk_create(
bulk_project_members,
batch_size=10, batch_size=10,
ignore_conflicts=True, ignore_conflicts=True,
) )
@ -845,12 +860,14 @@ class ProjectUserViewsEndpoint(BaseAPIView):
view_props = project_member.view_props view_props = project_member.view_props
default_props = project_member.default_props default_props = project_member.default_props
preferences = project_member.preferences preferences = project_member.preferences
sort_order = project_member.sort_order
project_member.view_props = request.data.get("view_props", view_props) project_member.view_props = request.data.get("view_props", view_props)
project_member.default_props = request.data.get( project_member.default_props = request.data.get(
"default_props", default_props "default_props", default_props
) )
project_member.preferences = request.data.get("preferences", preferences) project_member.preferences = request.data.get("preferences", preferences)
project_member.sort_order = request.data.get("sort_order", sort_order)
project_member.save() project_member.save()

View File

@ -73,12 +73,14 @@ from plane.db.models import (
IssueSubscriber, IssueSubscriber,
Project, Project,
Label, Label,
State, WorkspaceMember,
CycleIssue,
) )
from plane.api.permissions import ( from plane.api.permissions import (
WorkSpaceBasePermission, WorkSpaceBasePermission,
WorkSpaceAdminPermission, WorkSpaceAdminPermission,
WorkspaceEntityPermission, WorkspaceEntityPermission,
WorkspaceViewerPermission,
) )
from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
@ -1140,6 +1142,19 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
.count() .count()
) )
upcoming_cycles = CycleIssue.objects.filter(
workspace__slug=slug,
cycle__start_date__gt=timezone.now().date(),
issue__assignees__in=[user_id,]
).values("cycle__name", "cycle__id", "cycle__project_id")
present_cycle = CycleIssue.objects.filter(
workspace__slug=slug,
cycle__start_date__lt=timezone.now().date(),
cycle__end_date__gt=timezone.now().date(),
issue__assignees__in=[user_id,]
).values("cycle__name", "cycle__id", "cycle__project_id")
return Response( return Response(
{ {
"state_distribution": state_distribution, "state_distribution": state_distribution,
@ -1149,6 +1164,8 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
"completed_issues": completed_issues_count, "completed_issues": completed_issues_count,
"pending_issues": pending_issues_count, "pending_issues": pending_issues_count,
"subscribed_issues": subscribed_issues_count, "subscribed_issues": subscribed_issues_count,
"present_cycles": present_cycle,
"upcoming_cycles": upcoming_cycles,
} }
) )
except Exception as e: except Exception as e:
@ -1194,64 +1211,64 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
class WorkspaceUserProfileEndpoint(BaseAPIView): class WorkspaceUserProfileEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug, user_id): def get(self, request, slug, user_id):
try: try:
user_data = User.objects.get(pk=user_id) user_data = User.objects.get(pk=user_id)
projects = ( requesting_workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user)
Project.objects.filter( projects = []
workspace__slug=slug, if requesting_workspace_member.role >= 10:
project_projectmember__member=request.user, projects = (
) Project.objects.filter(
.annotate( workspace__slug=slug,
created_issues=Count( project_projectmember__member=request.user,
"project_issue", filter=Q(project_issue__created_by_id=user_id) )
.annotate(
created_issues=Count(
"project_issue", filter=Q(project_issue__created_by_id=user_id)
)
)
.annotate(
assigned_issues=Count(
"project_issue",
filter=Q(project_issue__assignees__in=[user_id]),
)
)
.annotate(
completed_issues=Count(
"project_issue",
filter=Q(
project_issue__completed_at__isnull=False,
project_issue__assignees__in=[user_id],
),
)
)
.annotate(
pending_issues=Count(
"project_issue",
filter=Q(
project_issue__state__group__in=[
"backlog",
"unstarted",
"started",
],
project_issue__assignees__in=[user_id],
),
)
)
.values(
"id",
"name",
"identifier",
"emoji",
"icon_prop",
"created_issues",
"assigned_issues",
"completed_issues",
"pending_issues",
) )
) )
.annotate(
assigned_issues=Count(
"project_issue",
filter=Q(project_issue__assignees__in=[user_id]),
)
)
.annotate(
completed_issues=Count(
"project_issue",
filter=Q(
project_issue__completed_at__isnull=False,
project_issue__assignees__in=[user_id],
),
)
)
.annotate(
pending_issues=Count(
"project_issue",
filter=Q(
project_issue__state__group__in=[
"backlog",
"unstarted",
"started",
],
project_issue__assignees__in=[user_id],
),
)
)
.values(
"id",
"name",
"identifier",
"emoji",
"icon_prop",
"created_issues",
"assigned_issues",
"completed_issues",
"pending_issues",
)
)
return Response( return Response(
{ {
@ -1268,6 +1285,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
except WorkspaceMember.DoesNotExist:
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
except Exception as e: except Exception as e:
capture_exception(e) capture_exception(e)
return Response( return Response(
@ -1278,7 +1297,7 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
WorkspaceEntityPermission, WorkspaceViewerPermission,
] ]
def get(self, request, slug, user_id): def get(self, request, slug, user_id):
@ -1317,7 +1336,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
) ).distinct()
# Priority Ordering # Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority": if order_by_param == "priority" or order_by_param == "-priority":
@ -1394,9 +1413,10 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
class WorkspaceLabelsEndpoint(BaseAPIView): class WorkspaceLabelsEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
WorkspaceEntityPermission, WorkspaceViewerPermission,
] ]
def get(self, request, slug): def get(self, request, slug):

View File

@ -58,16 +58,16 @@ def update_workspace_member_props(apps, schema_editor):
Model.objects.bulk_update(updated_workspace_member, ["view_props"], batch_size=100) Model.objects.bulk_update(updated_workspace_member, ["view_props"], batch_size=100)
def update_project_sort_order(apps, schema_editor): def update_project_member_sort_order(apps, schema_editor):
Model = apps.get_model("db", "Project") Model = apps.get_model("db", "ProjectMember")
updated_projects = [] updated_project_members = []
for obj in Model.objects.all(): for obj in Model.objects.all():
obj.sort_order = random.randint(1, 65536) obj.sort_order = random.randint(1, 65536)
updated_projects.append(obj) updated_project_members.append(obj)
Model.objects.bulk_update(updated_projects, ["sort_order"], batch_size=100) Model.objects.bulk_update(updated_project_members, ["sort_order"], batch_size=100)
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -93,5 +93,5 @@ class Migration(migrations.Migration):
name='sort_order', name='sort_order',
field=models.FloatField(default=65535), field=models.FloatField(default=65535),
), ),
migrations.RunPython(update_project_sort_order), migrations.RunPython(update_project_member_sort_order),
] ]

View File

@ -91,7 +91,6 @@ class Project(BaseModel):
default_state = models.ForeignKey( default_state = models.ForeignKey(
"db.State", on_delete=models.SET_NULL, null=True, related_name="default_state" "db.State", on_delete=models.SET_NULL, null=True, related_name="default_state"
) )
sort_order = models.FloatField(default=65535)
def __str__(self): def __str__(self):
"""Return name of the project""" """Return name of the project"""

View File

@ -2,7 +2,6 @@ import * as React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
// icons // icons
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { Icon } from "components/ui"; import { Icon } from "components/ui";
type BreadcrumbsProps = { type BreadcrumbsProps = {
@ -14,7 +13,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
return ( return (
<> <>
<div className="flex items-center"> <div className="flex items-center flex-grow w-full whitespace-nowrap overflow-hidden overflow-ellipsis">
<button <button
type="button" type="button"
className="group grid h-7 w-7 flex-shrink-0 cursor-pointer place-items-center rounded border border-custom-sidebar-border-200 text-center text-sm hover:bg-custom-sidebar-background-90" className="group grid h-7 w-7 flex-shrink-0 cursor-pointer place-items-center rounded border border-custom-sidebar-border-200 text-center text-sm hover:bg-custom-sidebar-background-90"
@ -35,22 +34,36 @@ type BreadcrumbItemProps = {
title: string; title: string;
link?: string; link?: string;
icon?: any; icon?: any;
linkTruncate?: boolean;
unshrinkTitle?: boolean;
}; };
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) => ( const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({
title,
link,
icon,
linkTruncate = false,
unshrinkTitle = false,
}) => (
<> <>
{link ? ( {link ? (
<Link href={link}> <Link href={link}>
<a className="border-r-2 border-custom-sidebar-border-200 px-3 text-sm"> <a
<p className={`${icon ? "flex items-center gap-2" : ""}`}> className={`border-r-2 border-custom-sidebar-border-200 px-3 text-sm ${
linkTruncate ? "truncate" : ""
}`}
>
<p
className={`${linkTruncate ? "truncate" : ""}${icon ? "flex items-center gap-2" : ""}`}
>
{icon ?? null} {icon ?? null}
{title} {title}
</p> </p>
</a> </a>
</Link> </Link>
) : ( ) : (
<div className="max-w-64 px-3 text-sm"> <div className={`px-3 text-sm truncate ${unshrinkTitle ? "flex-shrink-0" : ""}`}>
<p className={`${icon ? "flex items-center gap-2" : ""}`}> <p className={`truncate ${icon ? "flex items-center gap-2" : ""}`}>
{icon} {icon}
<span className="break-words">{title}</span> <span className="break-words">{title}</span>
</p> </p>

View File

@ -194,15 +194,15 @@ export const SingleListIssue: React.FC<Props> = ({
</a> </a>
</ContextMenu> </ContextMenu>
<div <div
className="flex flex-wrap items-center justify-between px-4 py-2.5 gap-2 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0" className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
setContextMenu(true); setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY }); setContextMenuPosition({ x: e.pageX, y: e.pageY });
}} }}
> >
<Link href={singleIssuePath}> <div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
<div className="flex-grow cursor-pointer"> <Link href={singleIssuePath}>
<a className="group relative flex items-center gap-2"> <a className="group relative flex items-center gap-2">
{properties.key && ( {properties.key && (
<Tooltip <Tooltip
@ -215,16 +215,14 @@ export const SingleListIssue: React.FC<Props> = ({
</Tooltip> </Tooltip>
)} )}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-[0.825rem] text-custom-text-100"> <span className="truncate text-[0.825rem] text-custom-text-100">{issue.name}</span>
{truncateText(issue.name, 50)}
</span>
</Tooltip> </Tooltip>
</a> </a>
</div> </Link>
</Link> </div>
<div <div
className={`flex w-full flex-shrink flex-wrap items-center gap-2 text-xs sm:w-auto ${ className={`flex flex-shrink-0 items-center gap-2 text-xs ${
isArchivedIssues ? "opacity-60" : "" isArchivedIssues ? "opacity-60" : ""
}`} }`}
> >

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -143,13 +144,17 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
{activityItem.field === "archived_at" && {activityItem.field === "archived_at" &&
activityItem.new_value !== "restore" ? ( activityItem.new_value !== "restore" ? (
<span className="text-gray font-medium">Plane</span> <span className="text-gray font-medium">Plane</span>
) : ( ) : activityItem.actor_detail.is_bot ? (
<span className="text-gray font-medium"> <span className="text-gray font-medium">
{activityItem.actor_detail.first_name} {activityItem.actor_detail.first_name} Bot
{activityItem.actor_detail.is_bot
? " Bot"
: " " + activityItem.actor_detail.last_name}
</span> </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}{" "} {message}{" "}
<span className="whitespace-nowrap"> <span className="whitespace-nowrap">

View File

@ -41,7 +41,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
position={tooltipPosition} position={tooltipPosition}
> >
<div <div
className={`group relative max-w-[6.5rem] ${ className={`group flex-shrink-0 relative max-w-[6.5rem] ${
issue.target_date === null issue.target_date === null
? "" ? ""
: issue.target_date < new Date().toISOString() : issue.target_date < new Date().toISOString()

View File

@ -1,12 +1,9 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import useSWR, { mutate } from "swr";
// react-hook-form // react-hook-form
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
// services // services
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
import userService from "services/user.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
@ -14,16 +11,15 @@ import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/
// icons // icons
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types // types
import { ICurrentUserResponse, IWorkspace, OnboardingSteps } from "types"; import { ICurrentUserResponse, IWorkspace, TOnboardingSteps } from "types";
// fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
type Props = { type Props = {
workspace: IWorkspace | undefined; finishOnboarding: () => Promise<void>;
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
stepChange: (steps: Partial<OnboardingSteps>) => Promise<void>; workspace: IWorkspace | undefined;
}; };
type EmailRole = { type EmailRole = {
@ -35,7 +31,12 @@ type FormValues = {
emails: EmailRole[]; emails: EmailRole[];
}; };
export const InviteMembers: React.FC<Props> = ({ workspace, user, stepChange }) => { export const InviteMembers: React.FC<Props> = ({
finishOnboarding,
stepChange,
user,
workspace,
}) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { const {
@ -49,38 +50,14 @@ export const InviteMembers: React.FC<Props> = ({ workspace, user, stepChange })
name: "emails", name: "emails",
}); });
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
const nextStep = async () => { const nextStep = async () => {
if (!user || !invitations) return; const payload: Partial<TOnboardingSteps> = {
const payload: Partial<OnboardingSteps> = {
workspace_invite: true, workspace_invite: true,
workspace_join: true,
}; };
// update onboarding status from this step if no invitations are present
if (invitations.length === 0) {
payload.workspace_join = true;
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
is_onboarded: true,
};
},
false
);
await userService.updateUserOnBoard({ userRole: user.role }, user);
}
await stepChange(payload); await stepChange(payload);
await finishOnboarding();
}; };
const onSubmit = async (formData: FormValues) => { const onSubmit = async (formData: FormValues) => {

View File

@ -4,7 +4,6 @@ import useSWR, { mutate } from "swr";
// services // services
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
import userService from "services/user.service";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
// ui // ui
@ -14,17 +13,23 @@ import { CheckCircleIcon } from "@heroicons/react/24/outline";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// types // types
import { ICurrentUserResponse, IUser, IWorkspaceMemberInvitation, OnboardingSteps } from "types"; import { IWorkspaceMemberInvitation, TOnboardingSteps } from "types";
// fetch-keys // fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
type Props = { type Props = {
stepChange: (steps: Partial<OnboardingSteps>) => Promise<void>; finishOnboarding: () => Promise<void>;
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
updateLastWorkspace: () => Promise<void>;
}; };
export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => { export const JoinWorkspaces: React.FC<Props> = ({
finishOnboarding,
stepChange,
updateLastWorkspace,
}) => {
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]); const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
@ -47,25 +52,13 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
} }
}; };
// complete onboarding const handleNextStep = async () => {
const finishOnboarding = async () => {
if (!user) return; if (!user) return;
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
is_onboarded: true,
};
},
false
);
await userService.updateUserOnBoard({ userRole: user.role }, user);
await stepChange({ workspace_join: true }); await stepChange({ workspace_join: true });
if (user.onboarding_step.workspace_create && user.onboarding_step.workspace_invite)
await finishOnboarding();
}; };
const submitInvitations = async () => { const submitInvitations = async () => {
@ -77,11 +70,12 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
.joinWorkspaces({ invitations: invitationsRespond }) .joinWorkspaces({ invitations: invitationsRespond })
.then(async () => { .then(async () => {
await mutateInvitations(); await mutateInvitations();
await finishOnboarding(); await mutate(USER_WORKSPACES);
await updateLastWorkspace();
setIsJoiningWorkspaces(false); await handleNextStep();
}) })
.catch(() => setIsJoiningWorkspaces(false)); .finally(() => setIsJoiningWorkspaces(false));
}; };
return ( return (
@ -142,14 +136,15 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
type="submit" type="submit"
size="md" size="md"
onClick={submitInvitations} onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0} disabled={invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
> >
Accept & Join Accept & Join
</PrimaryButton> </PrimaryButton>
<SecondaryButton <SecondaryButton
className="border border-none bg-transparent" className="border border-none bg-transparent"
size="md" size="md"
onClick={finishOnboarding} onClick={handleNextStep}
> >
Skip for now Skip for now
</SecondaryButton> </SecondaryButton>

View File

@ -3,17 +3,25 @@ import { useState } from "react";
// ui // ui
import { SecondaryButton } from "components/ui"; import { SecondaryButton } from "components/ui";
// types // types
import { ICurrentUserResponse, OnboardingSteps } from "types"; import { ICurrentUserResponse, IWorkspace, TOnboardingSteps } from "types";
// constants // constants
import { CreateWorkspaceForm } from "components/workspace"; import { CreateWorkspaceForm } from "components/workspace";
type Props = { type Props = {
user: ICurrentUserResponse | undefined; finishOnboarding: () => Promise<void>;
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
updateLastWorkspace: () => Promise<void>; updateLastWorkspace: () => Promise<void>;
stepChange: (steps: Partial<OnboardingSteps>) => Promise<void>; user: ICurrentUserResponse | undefined;
workspaces: IWorkspace[] | undefined;
}; };
export const Workspace: React.FC<Props> = ({ user, updateLastWorkspace, stepChange }) => { export const Workspace: React.FC<Props> = ({
finishOnboarding,
stepChange,
updateLastWorkspace,
user,
workspaces,
}) => {
const [defaultValues, setDefaultValues] = useState({ const [defaultValues, setDefaultValues] = useState({
name: "", name: "",
slug: "", slug: "",
@ -23,12 +31,21 @@ export const Workspace: React.FC<Props> = ({ user, updateLastWorkspace, stepChan
const completeStep = async () => { const completeStep = async () => {
if (!user) return; if (!user) return;
await stepChange({ const payload: Partial<TOnboardingSteps> = {
workspace_create: true, workspace_create: true,
}); };
await stepChange(payload);
await updateLastWorkspace(); await updateLastWorkspace();
}; };
const secondaryButtonAction = async () => {
if (workspaces && workspaces.length > 0) {
await stepChange({ workspace_create: true, workspace_invite: true, workspace_join: true });
await finishOnboarding();
} else await stepChange({ profile_complete: false, workspace_join: false });
};
return ( return (
<div className="w-full space-y-7 sm:space-y-10"> <div className="w-full space-y-7 sm:space-y-10">
<h4 className="text-xl sm:text-2xl font-semibold">Create your workspace</h4> <h4 className="text-xl sm:text-2xl font-semibold">Create your workspace</h4>
@ -43,9 +60,11 @@ export const Workspace: React.FC<Props> = ({ user, updateLastWorkspace, stepChan
default: "Continue", default: "Continue",
}} }}
secondaryButton={ secondaryButton={
<SecondaryButton onClick={() => stepChange({ profile_complete: false })}> workspaces ? (
Back <SecondaryButton onClick={secondaryButtonAction}>
</SecondaryButton> {workspaces.length > 0 ? "Skip & continue" : "Back"}
</SecondaryButton>
) : undefined
} }
/> />
</div> </div>

View File

@ -1,15 +1,26 @@
import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
// components // components
import { ProfileIssuesViewOptions } from "components/profile"; import { ProfileIssuesViewOptions } from "components/profile";
// types
import { UserAuth } from "types";
const tabsList = [ type Props = {
memberRole: UserAuth;
};
const viewerTabs = [
{ {
route: "", route: "",
label: "Overview", label: "Overview",
selected: "/[workspaceSlug]/profile/[userId]", selected: "/[workspaceSlug]/profile/[userId]",
}, },
];
const adminTabs = [
{ {
route: "assigned", route: "assigned",
label: "Assigned", label: "Assigned",
@ -27,12 +38,17 @@ const tabsList = [
}, },
]; ];
export const ProfileNavbar = () => { export const ProfileNavbar: React.FC<Props> = ({ memberRole }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, userId } = router.query; const { workspaceSlug, userId } = router.query;
const tabsList =
memberRole.isOwner || memberRole.isMember || memberRole.isViewer
? [...viewerTabs, ...adminTabs]
: viewerTabs;
return ( return (
<div className="px-4 sm:px-5 flex items-center justify-between gap-4 border-b border-custom-border-300"> <div className="sticky -top-0.5 z-[1] md:static px-4 sm:px-5 flex items-center justify-between gap-4 bg-custom-background-100 border-b border-custom-border-300">
<div className="flex items-center overflow-x-scroll"> <div className="flex items-center overflow-x-scroll">
{tabsList.map((tab) => ( {tabsList.map((tab) => (
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}> <Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>

View File

@ -0,0 +1,91 @@
import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr";
// services
import userService from "services/user.service";
// 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";
export const ProfileActivity = () => {
const router = useRouter();
const { workspaceSlug, userId } = router.query;
const { data: userProfileActivity } = useSWR(
workspaceSlug && userId
? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString())
: null,
workspaceSlug && userId
? () => userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString())
: null
);
return (
<div className="space-y-2">
<h3 className="text-lg font-medium">Recent Activity</h3>
<div className="border border-custom-border-100 rounded p-6">
{userProfileActivity ? (
<div className="space-y-5">
{userProfileActivity.results.map((activity) => (
<div key={activity.id} className="flex gap-3">
<div className="flex-shrink-0">
{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"
/>
) : (
<div className="grid h-6 w-6 place-items-center rounded border-2 bg-gray-700 text-xs text-white">
{activity.actor_detail.first_name.charAt(0)}
</div>
)}
</div>
<div className="-mt-1 w-4/5 break-words">
<p className="text-sm text-custom-text-200">
<span className="font-medium text-custom-text-100">
{activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "}
</span>
{activity.field ? (
activityDetails[activity.field]?.message(activity as any)
) : (
<span>
created this{" "}
<a
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"
>
Issue
<Icon iconName="launch" className="!text-xs" />
</a>
</span>
)}
</p>
<p className="text-xs text-custom-text-200">{timeAgo(activity.created_at)}</p>
</div>
</div>
))}
</div>
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</div>
</div>
);
};

View File

@ -1,3 +1,4 @@
export * from "./activity";
export * from "./priority-distribution"; export * from "./priority-distribution";
export * from "./state-distribution"; export * from "./state-distribution";
export * from "./stats"; export * from "./stats";

View File

@ -10,7 +10,7 @@ type Props = {
export const ProfileWorkload: React.FC<Props> = ({ stateDistribution }) => ( export const ProfileWorkload: React.FC<Props> = ({ stateDistribution }) => (
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-lg font-medium">Workload</h3> <h3 className="text-lg font-medium">Workload</h3>
<div className="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 gap-4 justify-stretch"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4 justify-stretch">
{stateDistribution.map((group) => ( {stateDistribution.map((group) => (
<div key={group.state_group}> <div key={group.state_group}>
<a className="flex gap-2 p-4 rounded border border-custom-border-100 whitespace-nowrap"> <a className="flex gap-2 p-4 rounded border border-custom-border-100 whitespace-nowrap">
@ -21,7 +21,13 @@ export const ProfileWorkload: React.FC<Props> = ({ stateDistribution }) => (
}} }}
/> />
<div className="space-y-1 -mt-1"> <div className="space-y-1 -mt-1">
<p className="text-custom-text-400 text-sm capitalize">{group.state_group}</p> <p className="text-custom-text-400 text-sm capitalize">
{group.state_group === "unstarted"
? "Not Started"
: group.state_group === "started"
? "Working on"
: group.state_group}
</p>
<p className="text-xl font-semibold">{group.state_count}</p> <p className="text-xl font-semibold">{group.state_count}</p>
</div> </div>
</a> </a>

View File

@ -10,7 +10,7 @@ import useEstimateOption from "hooks/use-estimate-option";
// components // components
import { MyIssuesSelectFilters } from "components/issues"; import { MyIssuesSelectFilters } from "components/issues";
// ui // ui
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui"; import { CustomMenu, CustomSearchSelect, ToggleSwitch, Tooltip } from "components/ui";
// icons // icons
import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material"; import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material";
@ -21,6 +21,7 @@ import { checkIfArraysHaveSameElements } from "helpers/array.helper";
import { Properties, TIssueViewOptions } from "types"; import { Properties, TIssueViewOptions } from "types";
// constants // constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue"; import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
import useProjects from "hooks/use-projects";
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
{ {
@ -37,6 +38,8 @@ export const ProfileIssuesViewOptions: React.FC = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, userId } = router.query; const { workspaceSlug, userId } = router.query;
const { projects } = useProjects();
const { const {
issueView, issueView,
setIssueView, setIssueView,
@ -54,12 +57,28 @@ export const ProfileIssuesViewOptions: React.FC = () => {
const { isEstimateActive } = useEstimateOption(); const { isEstimateActive } = useEstimateOption();
const options = projects?.map((project) => ({
value: project.id,
query: project.name + " " + project.identifier,
content: project.name,
}));
if ( if (
!router.pathname.includes("assigned") && !router.pathname.includes("assigned") &&
!router.pathname.includes("created") && !router.pathname.includes("created") &&
!router.pathname.includes("subscribed") !router.pathname.includes("subscribed")
) )
return null; return null;
// return (
// <CustomSearchSelect
// value={projects ?? null}
// onChange={(val: string[] | null) => console.log(val)}
// label="Filters"
// options={options}
// position="right"
// multiple
// />
// );
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@ -1,4 +1,5 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr"; import useSWR from "swr";
@ -8,10 +9,14 @@ import { useTheme } from "next-themes";
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
// services // services
import userService from "services/user.service"; import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// ui // ui
import { Icon, Loader } from "components/ui"; import { Icon, Loader, Tooltip } from "components/ui";
// icons
import { EditOutlined } from "@mui/icons-material";
// helpers // helpers
import { renderLongDetailDateFormat } from "helpers/date-time.helper"; import { render12HourFormatTime, renderLongDetailDateFormat } from "helpers/date-time.helper";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
// fetch-keys // fetch-keys
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
@ -22,6 +27,8 @@ export const ProfileSidebar = () => {
const { theme } = useTheme(); const { theme } = useTheme();
const { user } = useUser();
const { data: userProjectsData } = useSWR( const { data: userProjectsData } = useSWR(
workspaceSlug && userId workspaceSlug && userId
? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
@ -33,27 +40,24 @@ export const ProfileSidebar = () => {
); );
const userDetails = [ const userDetails = [
{
label: "Username",
value: "",
},
{ {
label: "Joined on", label: "Joined on",
value: renderLongDetailDateFormat(userProjectsData?.user_data.date_joined ?? ""), value: renderLongDetailDateFormat(userProjectsData?.user_data.date_joined ?? ""),
}, },
{ {
label: "Timezone", label: "Timezone",
value: userProjectsData?.user_data.user_timezone, value: (
}, <span>
{ {render12HourFormatTime(new Date())}{" "}
label: "Status", <span className="text-custom-text-200">{userProjectsData?.user_data.user_timezone}</span>
value: "Online", </span>
),
}, },
]; ];
return ( return (
<div <div
className="flex-shrink-0 h-full w-80 overflow-y-auto" className="flex-shrink-0 md:h-full w-full md:w-80 overflow-y-auto"
style={{ style={{
boxShadow: boxShadow:
theme === "light" theme === "light"
@ -64,10 +68,23 @@ export const ProfileSidebar = () => {
{userProjectsData ? ( {userProjectsData ? (
<> <>
<div className="relative h-32"> <div className="relative h-32">
{user?.id === userId && (
<div className="absolute top-3.5 right-3.5 h-5 w-5 bg-white rounded grid place-items-center">
<Link href={`/${workspaceSlug}/me/profile`}>
<a className="grid place-items-center text-black">
<EditOutlined
sx={{
fontSize: 12,
}}
/>
</a>
</Link>
</div>
)}
<img <img
src={ src={
userProjectsData.user_data.cover_image ?? userProjectsData.user_data.cover_image ??
"https://images.unsplash.com/photo-1672243775941-10d763d9adef?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80" "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
} }
alt={userProjectsData.user_data.first_name} alt={userProjectsData.user_data.first_name}
className="h-32 w-full object-cover" className="h-32 w-full object-cover"
@ -96,8 +113,8 @@ export const ProfileSidebar = () => {
<div className="mt-6 space-y-5"> <div className="mt-6 space-y-5">
{userDetails.map((detail) => ( {userDetails.map((detail) => (
<div key={detail.label} className="flex items-center gap-4 text-sm"> <div key={detail.label} className="flex items-center gap-4 text-sm">
<div className="text-custom-text-200 w-2/5">{detail.label}</div> <div className="flex-shrink-0 text-custom-text-200 w-2/5">{detail.label}</div>
<div className="font-medium">{detail.value}</div> <div className="font-medium w-3/5 break-words">{detail.value}</div>
</div> </div>
))} ))}
</div> </div>
@ -143,17 +160,19 @@ export const ProfileSidebar = () => {
</div> </div>
</div> </div>
<div className="flex-shrink-0 flex items-center gap-2"> <div className="flex-shrink-0 flex items-center gap-2">
<div <Tooltip tooltipContent="Completion percentage" position="left">
className={`px-1 py-0.5 text-xs font-medium rounded ${ <div
completedIssuePercentage <= 35 className={`px-1 py-0.5 text-xs font-medium rounded ${
? "bg-red-500/10 text-red-500" completedIssuePercentage <= 35
: completedIssuePercentage <= 70 ? "bg-red-500/10 text-red-500"
? "bg-yellow-500/10 text-yellow-500" : completedIssuePercentage <= 70
: "bg-green-500/10 text-green-500" ? "bg-yellow-500/10 text-yellow-500"
}`} : "bg-green-500/10 text-green-500"
> }`}
{completedIssuePercentage}% >
</div> {completedIssuePercentage}%
</div>
</Tooltip>
<Icon iconName="arrow_drop_down" className="!text-lg" /> <Icon iconName="arrow_drop_down" className="!text-lg" />
</div> </div>
</Disclosure.Button> </Disclosure.Button>

View File

@ -93,7 +93,6 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
.inviteProject(workspaceSlug as string, projectId as string, payload, user) .inviteProject(workspaceSlug as string, projectId as string, payload, user)
.then(() => { .then(() => {
setIsOpen(false); setIsOpen(false);
mutate(PROJECT_MEMBERS(projectId as string));
setToastAlert({ setToastAlert({
title: "Success", title: "Success",
type: "success", type: "success",
@ -105,6 +104,7 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
}) })
.finally(() => { .finally(() => {
reset(defaultValues); reset(defaultValues);
mutate(PROJECT_MEMBERS(projectId.toString()));
}); });
}; };

View File

@ -1,7 +1,10 @@
import React, { useState, FC } from "react"; import React, { useState, FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr";
// react-beautiful-dnd
import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useTheme from "hooks/use-theme"; import useTheme from "hooks/use-theme";
@ -9,12 +12,17 @@ import useUserAuth from "hooks/use-user-auth";
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
// components // components
import { DeleteProjectModal, SingleSidebarProject } from "components/project"; import { DeleteProjectModal, SingleSidebarProject } from "components/project";
// services
import projectService from "services/project.service";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IProject } from "types"; import { IProject } from "types";
// fetch-keys
import { PROJECTS_LIST } from "constants/fetch-keys";
export const ProjectSidebarList: FC = () => { export const ProjectSidebarList: FC = () => {
const [deleteProjectModal, setDeleteProjectModal] = useState(false); const [deleteProjectModal, setDeleteProjectModal] = useState(false);
@ -32,6 +40,14 @@ export const ProjectSidebarList: FC = () => {
const { projects: allProjects } = useProjects(); const { projects: allProjects } = useProjects();
const favoriteProjects = allProjects?.filter((p) => p.is_favorite); const favoriteProjects = allProjects?.filter((p) => p.is_favorite);
const orderedAllProjects = allProjects
? orderArrayBy(allProjects, "sort_order", "ascending")
: [];
const orderedFavProjects = favoriteProjects
? orderArrayBy(favoriteProjects, "sort_order", "ascending")
: [];
const handleDeleteProject = (project: IProject) => { const handleDeleteProject = (project: IProject) => {
setProjectToDelete(project); setProjectToDelete(project);
setDeleteProjectModal(true); setDeleteProjectModal(true);
@ -49,6 +65,54 @@ export const ProjectSidebarList: FC = () => {
}); });
}; };
const onDragEnd = async (result: DropResult) => {
const { source, destination, draggableId } = result;
if (!destination || !workspaceSlug) return;
if (source.index === destination.index) return;
const projectList =
destination.droppableId === "all-projects" ? orderedAllProjects : orderedFavProjects;
let updatedSortOrder = projectList[source.index].sort_order;
if (destination.index === 0) {
updatedSortOrder = projectList[0].sort_order - 1000;
} else if (destination.index === projectList.length - 1) {
updatedSortOrder = projectList[projectList.length - 1].sort_order + 1000;
} else {
const destinationSortingOrder = projectList[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? projectList[destination.index + 1].sort_order
: projectList[destination.index - 1].sort_order;
updatedSortOrder = Math.round(
(destinationSortingOrder + relativeDestinationSortingOrder) / 2
);
}
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
(prevData) => {
if (!prevData) return prevData;
return prevData.map((p) =>
p.id === draggableId ? { ...p, sort_order: updatedSortOrder } : p
);
},
false
);
await projectService
.setProjectView(workspaceSlug as string, draggableId, { sort_order: updatedSortOrder })
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again.",
});
});
};
return ( return (
<> <>
<DeleteProjectModal <DeleteProjectModal
@ -58,39 +122,75 @@ export const ProjectSidebarList: FC = () => {
user={user} user={user}
/> />
<div className="h-full overflow-y-auto px-4"> <div className="h-full overflow-y-auto px-4">
{favoriteProjects && favoriteProjects.length > 0 && ( <DragDropContext onDragEnd={onDragEnd}>
<div className="flex flex-col space-y-2 mt-5"> <Droppable droppableId="favorite-projects">
{!sidebarCollapse && ( {(provided) => (
<h5 className="text-sm font-medium text-custom-sidebar-text-200">Favorites</h5> <div ref={provided.innerRef} {...provided.droppableProps}>
{orderedFavProjects && orderedFavProjects.length > 0 && (
<div className="flex flex-col space-y-2 mt-5">
{!sidebarCollapse && (
<h5 className="text-sm font-medium text-custom-sidebar-text-200">
Favorites
</h5>
)}
{orderedFavProjects.map((project, index) => (
<Draggable key={project.id} draggableId={project.id} index={index}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<SingleSidebarProject
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
provided={provided}
snapshot={snapshot}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
shortContextMenu
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</div>
)} )}
{favoriteProjects.map((project) => ( </Droppable>
<SingleSidebarProject </DragDropContext>
key={project.id} <DragDropContext onDragEnd={onDragEnd}>
project={project} <Droppable droppableId="all-projects">
sidebarCollapse={sidebarCollapse} {(provided) => (
handleDeleteProject={() => handleDeleteProject(project)} <div ref={provided.innerRef} {...provided.droppableProps}>
handleCopyText={() => handleCopyText(project.id)} {orderedAllProjects && orderedAllProjects.length > 0 && (
shortContextMenu <div className="flex flex-col space-y-2 mt-5">
/> {!sidebarCollapse && (
))} <h5 className="text-sm font-medium text-custom-sidebar-text-200">Projects</h5>
</div> )}
)} {orderedAllProjects.map((project, index) => (
{allProjects && allProjects.length > 0 && ( <Draggable key={project.id} draggableId={project.id} index={index}>
<div className="flex flex-col space-y-2 mt-5"> {(provided, snapshot) => (
{!sidebarCollapse && ( <div ref={provided.innerRef} {...provided.draggableProps}>
<h5 className="text-sm font-medium text-custom-sidebar-text-200">Projects</h5> <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>
)} )}
{allProjects.map((project) => ( </Droppable>
<SingleSidebarProject </DragDropContext>
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
/>
))}
</div>
)}
{allProjects && allProjects.length === 0 && ( {allProjects && allProjects.length === 0 && (
<button <button
type="button" type="button"

View File

@ -3,6 +3,8 @@ import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
// react-beautiful-dnd
import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd";
// headless ui // headless ui
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
// services // services
@ -12,7 +14,7 @@ import useToast from "hooks/use-toast";
// ui // ui
import { CustomMenu, Tooltip } from "components/ui"; import { CustomMenu, Tooltip } from "components/ui";
// icons // icons
import { LinkIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline"; import { EllipsisVerticalIcon, LinkIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
import { import {
ArchiveOutlined, ArchiveOutlined,
ArticleOutlined, ArticleOutlined,
@ -34,6 +36,8 @@ import { PROJECTS_LIST } from "constants/fetch-keys";
type Props = { type Props = {
project: IProject; project: IProject;
sidebarCollapse: boolean; sidebarCollapse: boolean;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
handleDeleteProject: () => void; handleDeleteProject: () => void;
handleCopyText: () => void; handleCopyText: () => void;
shortContextMenu?: boolean; shortContextMenu?: boolean;
@ -75,6 +79,8 @@ const navigation = (workspaceSlug: string, projectId: string) => [
export const SingleSidebarProject: React.FC<Props> = ({ export const SingleSidebarProject: React.FC<Props> = ({
project, project,
sidebarCollapse, sidebarCollapse,
provided,
snapshot,
handleDeleteProject, handleDeleteProject,
handleCopyText, handleCopyText,
shortContextMenu = false, shortContextMenu = false,
@ -130,7 +136,21 @@ export const SingleSidebarProject: React.FC<Props> = ({
<Disclosure key={project?.id} defaultOpen={projectId === project?.id}> <Disclosure key={project?.id} defaultOpen={projectId === project?.id}>
{({ open }) => ( {({ open }) => (
<> <>
<div className="flex items-center gap-x-1 text-custom-sidebar-text-100"> <div
className={`group relative flex items-center gap-x-1 text-custom-sidebar-text-100 ${
snapshot.isDragging ? "opacity-60" : ""
}`}
>
<button
type="button"
className={`absolute top-2 left-0 hidden rounded p-0.5 ${
sidebarCollapse ? "" : "group-hover:!flex"
}`}
{...provided.dragHandleProps}
>
<EllipsisVerticalIcon className="h-4" />
<EllipsisVerticalIcon className="-ml-5 h-4" />
</button>
<Tooltip <Tooltip
tooltipContent={`${project?.name}`} tooltipContent={`${project?.name}`}
position="right" position="right"
@ -140,7 +160,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
<Disclosure.Button <Disclosure.Button
as="div" as="div"
className={`flex w-full cursor-pointer select-none items-center rounded-sm py-1 text-left text-sm font-medium ${ 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" sidebarCollapse ? "justify-center" : "justify-between ml-4"
}`} }`}
> >
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">

View File

@ -22,7 +22,7 @@ export type CustomSearchSelectProps = DropdownProps & {
| { multiple?: false; value: any } // if multiple is false, value can be anything | { multiple?: false; value: any } // if multiple is false, value can be anything
| { | {
multiple?: true; multiple?: true;
value: any[]; // if multiple is true, value should be an array value: any[] | null; // if multiple is true, value should be an array
} }
); );
@ -68,7 +68,7 @@ export const CustomSearchSelect = ({
className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`} className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`}
{...props} {...props}
> >
{({ open }: any) => { {({ open }: { open: boolean }) => {
if (open && onOpen) onOpen(); if (open && onOpen) onOpen();
return ( return (

View File

@ -1,5 +1,7 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
// swr
import { mutate } from "swr";
// react-hook-form // react-hook-form
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
// headless // headless
@ -13,9 +15,10 @@ import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/
// icons // icons
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types // types
import { ICurrentUserResponse, IWorkspace, IWorkspaceMemberInvitation } from "types"; import { ICurrentUserResponse } from "types";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
import { WORKSPACE_INVITATIONS } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -94,7 +97,10 @@ const SendWorkspaceInvitationModal: React.FC<Props> = ({
}); });
console.log(err); console.log(err);
}) })
.finally(() => reset(defaultValues)); .finally(() => {
reset(defaultValues);
mutate(WORKSPACE_INVITATIONS);
});
}; };
const appendField = () => { const appendField = () => {

View File

@ -25,7 +25,7 @@ import { truncateText } from "helpers/string.helper";
import { IWorkspace } from "types"; import { IWorkspace } from "types";
// Static Data // Static Data
const userLinks = (workspaceSlug: string) => [ const userLinks = (workspaceSlug: string, userId: string) => [
{ {
name: "Workspace Settings", name: "Workspace Settings",
href: `/${workspaceSlug}/settings`, href: `/${workspaceSlug}/settings`,
@ -36,7 +36,7 @@ const userLinks = (workspaceSlug: string) => [
}, },
{ {
name: "My Profile", name: "My Profile",
href: `/${workspaceSlug}/me/profile`, href: `/${workspaceSlug}/profile/${userId}`,
}, },
]; ];
@ -119,7 +119,7 @@ export const WorkspaceSidebarDropdown = () => {
</Menu.Button> </Menu.Button>
{!sidebarCollapse && ( {!sidebarCollapse && (
<Link href={`/${workspaceSlug}/me/profile`}> <Link href={`/${workspaceSlug}/profile/${user?.id}`}>
<a> <a>
<div className="flex flex-grow justify-end"> <div className="flex flex-grow justify-end">
<Avatar user={user} height="28px" width="28px" fontSize="14px" /> <Avatar user={user} height="28px" width="28px" fontSize="14px" />
@ -215,7 +215,7 @@ export const WorkspaceSidebarDropdown = () => {
)} )}
</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"> <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 as string).map((link, index) => ( {userLinks(workspaceSlug?.toString() ?? "", user?.id ?? "").map((link, index) => (
<Menu.Item <Menu.Item
key={index} key={index}
as="div" as="div"

View File

@ -12,6 +12,7 @@ import { profileIssuesContext } from "contexts/profile-issues-context";
import { IIssue } from "types"; import { IIssue } from "types";
// fetch-keys // fetch-keys
import { USER_PROFILE_ISSUES } from "constants/fetch-keys"; import { USER_PROFILE_ISSUES } from "constants/fetch-keys";
import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
const useProfileIssues = (workspaceSlug: string | undefined, userId: string | undefined) => { const useProfileIssues = (workspaceSlug: string | undefined, userId: string | undefined) => {
const { const {
@ -33,6 +34,8 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
const router = useRouter(); const router = useRouter();
const { memberRole } = useWorkspaceMyMembership();
const params: any = { const params: any = {
assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, assignees: filters?.assignees ? filters?.assignees.join(",") : undefined,
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
@ -47,14 +50,16 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
}; };
const { data: userProfileIssues, mutate: mutateProfileIssues } = useSWR( const { data: userProfileIssues, mutate: mutateProfileIssues } = useSWR(
workspaceSlug && userId workspaceSlug && userId && (memberRole.isOwner || memberRole.isMember || memberRole.isViewer)
? USER_PROFILE_ISSUES(workspaceSlug.toString(), userId.toString(), params) ? USER_PROFILE_ISSUES(workspaceSlug.toString(), userId.toString(), params)
: null, : null,
workspaceSlug && userId workspaceSlug && userId && (memberRole.isOwner || memberRole.isMember || memberRole.isViewer)
? () => userService.getUserProfileIssues(workspaceSlug.toString(), userId.toString(), params) ? () => userService.getUserProfileIssues(workspaceSlug.toString(), userId.toString(), params)
: null : null
); );
console.log(memberRole);
const groupedIssues: const groupedIssues:
| { | {
[key: string]: IIssue[]; [key: string]: IIssue[];
@ -73,8 +78,6 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
useEffect(() => { useEffect(() => {
if (!userId || !filters) return; if (!userId || !filters) return;
console.log("Triggered");
if ( if (
router.pathname.includes("assigned") && router.pathname.includes("assigned") &&
(!filters.assignees || !filters.assignees.includes(userId)) (!filters.assignees || !filters.assignees.includes(userId))

View File

@ -11,13 +11,17 @@ import { IProject } from "types";
// fetch-keys // fetch-keys
import { PROJECTS_LIST } from "constants/fetch-keys"; import { PROJECTS_LIST } from "constants/fetch-keys";
const useProjects = (type?: "all" | boolean) => { const useProjects = (type?: "all" | boolean, fetchCondition?: boolean) => {
fetchCondition = fetchCondition ?? true;
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { data: projects, mutate: mutateProjects } = useSWR( const { data: projects, mutate: mutateProjects } = useSWR(
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string, { is_favorite: type ?? "all" }) : null, workspaceSlug && fetchCondition
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string, { is_favorite: type ?? "all" })
: null,
workspaceSlug && fetchCondition
? () => projectService.getProjects(workspaceSlug as string, { is_favorite: type ?? "all" }) ? () => projectService.getProjects(workspaceSlug as string, { is_favorite: type ?? "all" })
: null : null
); );

View File

@ -11,11 +11,11 @@ type Props = {
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => ( const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => (
<div <div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 px-5 py-4 ${ className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 px-5 py-4 ${
noHeader ? "md:hidden" : "" noHeader ? "md:hidden" : ""
}`} }`}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden"> <div className="block md:hidden">
<button <button
type="button" type="button"
@ -26,9 +26,9 @@ const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar, n
</button> </button>
</div> </div>
{breadcrumbs} {breadcrumbs}
{left} <div className="flex-shrink-0">{left}</div>
</div> </div>
{right} <div className="flex-shrink-0">{right}</div>
</div> </div>
); );

View File

@ -0,0 +1,45 @@
// hooks
import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// components
import { ProfileNavbar, ProfileSidebar } from "components/profile";
// ui
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
type Props = {
children: React.ReactNode;
className?: string;
};
export const ProfileAuthWrapper = (props: Props) => (
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="User Profile" />
</Breadcrumbs>
}
>
<ProfileLayout {...props} />
</WorkspaceAuthorizationLayout>
);
const ProfileLayout: React.FC<Props> = ({ children, className }) => {
const { memberRole } = useWorkspaceMyMembership();
return (
<div className="h-full w-full md:flex md:flex-row-reverse md:overflow-hidden">
<ProfileSidebar />
<div className="md:h-full w-full flex flex-col md:overflow-hidden">
<ProfileNavbar memberRole={memberRole} />
{memberRole.isOwner || memberRole.isMember || memberRole.isViewer ? (
<div className={`md:h-full w-full overflow-hidden ${className}`}>{children}</div>
) : (
<div className="h-full w-full grid place-items-center text-custom-text-200">
You do not have the permission to access this page.
</div>
)}
</div>
</div>
);
};

View File

@ -1,48 +1,19 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
// contexts // contexts
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context"; import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
// hooks import { ProfileAuthWrapper } from "layouts/profile-layout";
import useUser from "hooks/use-user";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// components // components
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile"; import { ProfileIssuesView } from "components/profile";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
const ProfileAssignedIssues: NextPage = () => { const ProfileAssignedIssues: NextPage = () => (
const router = useRouter(); <ProfileIssuesContextProvider>
const { workspaceSlug } = router.query; <ProfileAuthWrapper>
<ProfileIssuesView />
const { user } = useUser(); </ProfileAuthWrapper>
</ProfileIssuesContextProvider>
return ( );
<ProfileIssuesContextProvider>
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Settings" link={`/${workspaceSlug}/me/profile`} />
<BreadcrumbItem title={`${user?.first_name} ${user?.last_name}`} />
</Breadcrumbs>
}
>
<div className="h-full w-full flex overflow-hidden">
<div className="h-full w-full flex flex-col overflow-hidden">
<ProfileNavbar />
<div className="h-full w-full flex flex-col overflow-hidden">
<ProfileIssuesView />
</div>
</div>
<ProfileSidebar />
</div>
</WorkspaceAuthorizationLayout>
</ProfileIssuesContextProvider>
);
};
export default ProfileAssignedIssues; export default ProfileAssignedIssues;

View File

@ -1,48 +1,20 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
// contexts // contexts
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context"; import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
// hooks
import useUser from "hooks/use-user";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { ProfileAuthWrapper } from "layouts/profile-layout";
// components // components
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile"; import { ProfileIssuesView } from "components/profile";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
const ProfileCreatedIssues: NextPage = () => { const ProfileCreatedIssues: NextPage = () => (
const router = useRouter(); <ProfileIssuesContextProvider>
const { workspaceSlug } = router.query; <ProfileAuthWrapper>
<ProfileIssuesView />
const { user } = useUser(); </ProfileAuthWrapper>
</ProfileIssuesContextProvider>
return ( );
<ProfileIssuesContextProvider>
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Settings" link={`/${workspaceSlug}/me/profile`} />
<BreadcrumbItem title={`${user?.first_name} ${user?.last_name}`} />
</Breadcrumbs>
}
>
<div className="h-full w-full flex overflow-hidden">
<div className="h-full w-full flex flex-col overflow-hidden">
<ProfileNavbar />
<div className="h-full w-full flex flex-col overflow-hidden">
<ProfileIssuesView />
</div>
</div>
<ProfileSidebar />
</div>
</WorkspaceAuthorizationLayout>
</ProfileIssuesContextProvider>
);
};
export default ProfileCreatedIssues; export default ProfileCreatedIssues;

View File

@ -1,34 +1,26 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr"; import useSWR from "swr";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// services // services
import userService from "services/user.service"; import userService from "services/user.service";
// layouts
import { ProfileAuthWrapper } from "layouts/profile-layout";
// components // components
import { import {
ProfileNavbar, ProfileActivity,
ProfilePriorityDistribution, ProfilePriorityDistribution,
ProfileSidebar,
ProfileStateDistribution, ProfileStateDistribution,
ProfileStats, ProfileStats,
ProfileWorkload, ProfileWorkload,
} from "components/profile"; } from "components/profile";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { Icon, Loader } from "components/ui";
// helpers
import { activityDetails } from "helpers/activity.helper";
import { timeAgo } from "helpers/date-time.helper";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import { IUserStateDistribution, TStateGroups } from "types"; import { IUserStateDistribution, TStateGroups } from "types";
// constants // constants
import { USER_PROFILE_DATA, USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; import { USER_PROFILE_DATA } from "constants/fetch-keys";
import { GROUP_CHOICES } from "constants/project"; import { GROUP_CHOICES } from "constants/project";
const ProfileOverview: NextPage = () => { const ProfileOverview: NextPage = () => {
@ -42,15 +34,6 @@ const ProfileOverview: NextPage = () => {
: null : null
); );
const { data: userProfileActivity } = useSWR(
workspaceSlug && userId
? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString())
: null,
workspaceSlug && userId
? () => userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString())
: null
);
const stateDistribution: IUserStateDistribution[] = Object.keys(GROUP_CHOICES).map((key) => { const stateDistribution: IUserStateDistribution[] = Object.keys(GROUP_CHOICES).map((key) => {
const group = userProfile?.state_distribution.find((g) => g.state_group === key); const group = userProfile?.state_distribution.find((g) => g.state_group === key);
@ -59,93 +42,20 @@ const ProfileOverview: NextPage = () => {
}); });
return ( return (
<WorkspaceAuthorizationLayout <ProfileAuthWrapper>
breadcrumbs={ <div className="h-full w-full px-5 md:px-9 py-5 space-y-7 overflow-y-auto">
<Breadcrumbs> <ProfileStats userProfile={userProfile} />
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} /> <ProfileWorkload stateDistribution={stateDistribution} />
<BreadcrumbItem title={`User Name`} /> <div className="grid grid-cols-1 xl:grid-cols-2 items-stretch gap-5">
</Breadcrumbs> <ProfilePriorityDistribution userProfile={userProfile} />
} <ProfileStateDistribution
> stateDistribution={stateDistribution}
<div className="h-full w-full flex overflow-hidden"> userProfile={userProfile}
<div className="h-full w-full flex flex-col overflow-hidden"> />
<ProfileNavbar />
<div className="h-full w-full overflow-y-auto px-9 py-5 space-y-7">
<ProfileStats userProfile={userProfile} />
<ProfileWorkload stateDistribution={stateDistribution} />
<div className="grid grid-cols-1 xl:grid-cols-2 items-stretch gap-5">
<ProfilePriorityDistribution userProfile={userProfile} />
<ProfileStateDistribution
stateDistribution={stateDistribution}
userProfile={userProfile}
/>
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">Recent Activity</h3>
<div className="border border-custom-border-100 rounded p-6">
{userProfileActivity ? (
<div className="space-y-5">
{userProfileActivity.results.map((activity) => (
<div key={activity.id} className="flex gap-3">
<div className="flex-shrink-0">
{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"
/>
) : (
<div className="grid h-6 w-6 place-items-center rounded border-2 bg-gray-700 text-xs text-white">
{activity.actor_detail.first_name.charAt(0)}
</div>
)}
</div>
<div className="-mt-1">
<p className="text-sm text-custom-text-200">
<span className="font-medium text-custom-text-100">
{activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "}
</span>
{activity.field ? (
activityDetails[activity.field]?.message(activity as any)
) : (
<span>
created this{" "}
<Link
href={`/${activity.workspace_detail.slug}/projects/${activity.project}/issues/${activity.issue}`}
>
<a className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline">
Issue
<Icon iconName="launch" className="!text-xs" />
</a>
</Link>
</span>
)}
</p>
<p className="text-xs text-custom-text-200">
{timeAgo(activity.created_at)}
</p>
</div>
</div>
))}
</div>
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</div>
</div>
</div>
</div> </div>
<ProfileSidebar /> <ProfileActivity />
</div> </div>
</WorkspaceAuthorizationLayout> </ProfileAuthWrapper>
); );
}; };

View File

@ -1,48 +1,20 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
// contexts // contexts
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context"; import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
// hooks
import useUser from "hooks/use-user";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { ProfileAuthWrapper } from "layouts/profile-layout";
// components // components
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile"; import { ProfileIssuesView } from "components/profile";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
const ProfileSubscribedIssues: NextPage = () => { const ProfileSubscribedIssues: NextPage = () => (
const router = useRouter(); <ProfileIssuesContextProvider>
const { workspaceSlug } = router.query; <ProfileAuthWrapper>
<ProfileIssuesView />
const { user } = useUser(); </ProfileAuthWrapper>
</ProfileIssuesContextProvider>
return ( );
<ProfileIssuesContextProvider>
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Settings" link={`/${workspaceSlug}/me/profile`} />
<BreadcrumbItem title={`${user?.first_name} ${user?.last_name}`} />
</Breadcrumbs>
}
>
<div className="h-full w-full flex overflow-hidden">
<div className="h-full w-full flex flex-col overflow-hidden">
<ProfileNavbar />
<div className="h-full w-full flex flex-col overflow-hidden">
<ProfileIssuesView />
</div>
</div>
<ProfileSidebar />
</div>
</WorkspaceAuthorizationLayout>
</ProfileIssuesContextProvider>
);
};
export default ProfileSubscribedIssues; export default ProfileSubscribedIssues;

View File

@ -23,6 +23,8 @@ import { IIssue } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys"; import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const defaultValues = { const defaultValues = {
name: "", name: "",
@ -146,13 +148,15 @@ const ArchivedIssueDetailsPage: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
title={`${issueDetails?.project_detail.name ?? "Project"} Issues`} title={`${truncateText(issueDetails?.project_detail.name ?? "Project", 32)} Issues`}
link={`/${workspaceSlug}/projects/${projectId as string}/issues`} link={`/${workspaceSlug}/projects/${projectId as string}/issues`}
linkTruncate
/> />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
title={`Issue ${issueDetails?.project_detail.identifier ?? "Project"}-${ title={`Issue ${issueDetails?.project_detail.identifier ?? "Project"}-${
issueDetails?.sequence_id ?? "..." issueDetails?.sequence_id ?? "..."
} Details`} } Details`}
unshrinkTitle
/> />
</Breadcrumbs> </Breadcrumbs>
} }

View File

@ -4,6 +4,8 @@ import useSWR from "swr";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
// hooks
import useIssuesView from "hooks/use-issues-view";
// layouts // layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// contexts // contexts
@ -21,8 +23,6 @@ import { XMarkIcon } from "@heroicons/react/24/outline";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS } from "constants/fetch-keys"; import { PROJECT_DETAILS } from "constants/fetch-keys";
import useIssuesView from "hooks/use-issues-view";
import { useEffect } from "react";
const ProjectArchivedIssues: NextPage = () => { const ProjectArchivedIssues: NextPage = () => {
const router = useRouter(); const router = useRouter();
@ -44,7 +44,7 @@ const ProjectArchivedIssues: NextPage = () => {
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} /> <BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem <BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 12)} Archived Issues`} title={`${truncateText(projectDetails?.name ?? "Project", 32)} Archived Issues`}
/> />
</Breadcrumbs> </Breadcrumbs>
} }

View File

@ -109,8 +109,9 @@ const SingleCycle: React.FC = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${cycleDetails?.project_detail.name ?? "Project"} Cycles`} title={`${truncateText(cycleDetails?.project_detail.name ?? "Project", 32)} Cycles`}
link={`/${workspaceSlug}/projects/${projectId}/cycles`} link={`/${workspaceSlug}/projects/${projectId}/cycles`}
linkTruncate
/> />
</Breadcrumbs> </Breadcrumbs>
} }
@ -122,7 +123,7 @@ const SingleCycle: React.FC = () => {
{cycleDetails?.name && truncateText(cycleDetails.name, 40)} {cycleDetails?.name && truncateText(cycleDetails.name, 40)}
</> </>
} }
className="ml-1.5" className="ml-1.5 flex-shrink-0"
width="auto" width="auto"
> >
{cycles?.map((cycle) => ( {cycles?.map((cycle) => (
@ -137,7 +138,7 @@ const SingleCycle: React.FC = () => {
</CustomMenu> </CustomMenu>
} }
right={ right={
<div className={`flex items-center gap-2 duration-300`}> <div className={`flex flex-shrink-0 items-center gap-2 duration-300`}>
<IssuesFilterView /> <IssuesFilterView />
<SecondaryButton <SecondaryButton
onClick={() => setAnalyticsModal(true)} onClick={() => setAnalyticsModal(true)}

View File

@ -29,6 +29,8 @@ import emptyCycle from "public/empty-state/cycle.svg";
// types // types
import { SelectCycleType } from "types"; import { SelectCycleType } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// helper
import { truncateText } from "helpers/string.helper";
const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"]; const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"];
@ -91,7 +93,7 @@ const ProjectCycles: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} /> <BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${projectDetails?.name ?? "Project"} Cycles`} /> <BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Cycles`} />
</Breadcrumbs> </Breadcrumbs>
} }
right={ right={

View File

@ -31,7 +31,7 @@ const ProjectInbox: NextPage = () => {
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} /> <BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem <BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 12)} Inbox`} title={`${truncateText(projectDetails?.name ?? "Project", 32)} Inbox`}
/> />
</Breadcrumbs> </Breadcrumbs>
} }

View File

@ -22,6 +22,8 @@ import { IIssue } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys"; import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const defaultValues = { const defaultValues = {
name: "", name: "",
@ -110,13 +112,15 @@ const IssueDetailsPage: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
title={`${issueDetails?.project_detail.name ?? "Project"} Issues`} title={`${truncateText(issueDetails?.project_detail.name ?? "Project", 32)} Issues`}
link={`/${workspaceSlug}/projects/${projectId as string}/issues`} link={`/${workspaceSlug}/projects/${projectId as string}/issues`}
linkTruncate
/> />
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
title={`Issue ${issueDetails?.project_detail.identifier ?? "Project"}-${ title={`Issue ${issueDetails?.project_detail.identifier ?? "Project"}-${
issueDetails?.sequence_id ?? "..." issueDetails?.sequence_id ?? "..."
} Details`} } Details`}
unshrinkTitle
/> />
</Breadcrumbs> </Breadcrumbs>
} }

View File

@ -54,7 +54,7 @@ const ProjectIssues: NextPage = () => {
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} /> <BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem <BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 12)} Issues`} title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`}
/> />
</Breadcrumbs> </Breadcrumbs>
} }

View File

@ -112,8 +112,9 @@ const SingleModule: React.FC = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${moduleDetails?.project_detail.name ?? "Project"} Modules`} title={`${truncateText(moduleDetails?.project_detail.name ?? "Project", 32)} Modules`}
link={`/${workspaceSlug}/projects/${projectId}/modules`} link={`/${workspaceSlug}/projects/${projectId}/modules`}
linkTruncate
/> />
</Breadcrumbs> </Breadcrumbs>
} }

View File

@ -29,6 +29,8 @@ import { IModule, SelectModuleType } from "types/modules";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { MODULE_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { MODULE_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const ProjectModules: NextPage = () => { const ProjectModules: NextPage = () => {
const [selectedModule, setSelectedModule] = useState<SelectModuleType>(); const [selectedModule, setSelectedModule] = useState<SelectModuleType>();
@ -73,7 +75,7 @@ const ProjectModules: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} /> <BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Modules`} /> <BreadcrumbItem title={`${truncateText(activeProject?.name ?? "Project", 32)} Modules`} />
</Breadcrumbs> </Breadcrumbs>
} }
right={ right={

View File

@ -43,7 +43,7 @@ import {
import { ColorPalletteIcon, ClipboardIcon } from "components/icons"; import { ColorPalletteIcon, ClipboardIcon } from "components/icons";
// helpers // helpers
import { render24HourFormatTime, renderShortDate } from "helpers/date-time.helper"; import { render24HourFormatTime, renderShortDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -346,7 +346,7 @@ const SinglePage: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} /> <BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${projectDetails?.name ?? "Project"} Pages`} /> <BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project",32)} Pages`} />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -11,6 +11,7 @@ import { Tab } from "@headlessui/react";
import projectService from "services/project.service"; import projectService from "services/project.service";
// hooks // hooks
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth";
// icons // icons
import { PlusIcon } from "components/icons"; import { PlusIcon } from "components/icons";
// layouts // layouts
@ -27,7 +28,8 @@ import { TPageViewProps } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS } from "constants/fetch-keys"; import { PROJECT_DETAILS } from "constants/fetch-keys";
import useUserAuth from "hooks/use-user-auth"; // helper
import { truncateText } from "helpers/string.helper";
const AllPagesList = dynamic<TPagesListProps>( const AllPagesList = dynamic<TPagesListProps>(
() => import("components/pages").then((a) => a.AllPagesList), () => import("components/pages").then((a) => a.AllPagesList),
@ -107,7 +109,9 @@ const ProjectPages: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} /> <BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${projectDetails?.name ?? "Project"} Pages`} /> <BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Pages`}
/>
</Breadcrumbs> </Breadcrumbs>
} }
right={ right={

View File

@ -22,6 +22,8 @@ import type { NextPage } from "next";
import { IProject } from "types"; import { IProject } from "types";
// constant // constant
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const AutomationsSettings: NextPage = () => { const AutomationsSettings: NextPage = () => {
const router = useRouter(); const router = useRouter();
@ -65,10 +67,11 @@ const AutomationsSettings: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`} title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`} link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
linkTruncate
/> />
<BreadcrumbItem title="Automations Settings" /> <BreadcrumbItem title="Automations Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -23,6 +23,8 @@ import { IProject, IUserLite, IWorkspace } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECTS_LIST, PROJECT_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const defaultValues: Partial<IProject> = { const defaultValues: Partial<IProject> = {
project_lead: null, project_lead: null,
@ -103,10 +105,11 @@ const ControlSettings: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`} title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectId}/issues`} link={`/${workspaceSlug}/projects/${projectId}/issues`}
linkTruncate
/> />
<BreadcrumbItem title="Control Settings" /> <BreadcrumbItem title="Control Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -29,6 +29,8 @@ import { IEstimate, IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const EstimatesSettings: NextPage = () => { const EstimatesSettings: NextPage = () => {
const [estimateFormOpen, setEstimateFormOpen] = useState(false); const [estimateFormOpen, setEstimateFormOpen] = useState(false);
@ -115,10 +117,11 @@ const EstimatesSettings: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`} title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`} link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
linkTruncate
/> />
<BreadcrumbItem title="Estimates Settings" /> <BreadcrumbItem title="Estimates Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -25,6 +25,8 @@ import { IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const featuresList = [ const featuresList = [
{ {
@ -139,10 +141,11 @@ const FeaturesSettings: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`} title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`} link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
linkTruncate
/> />
<BreadcrumbItem title="Features Settings" /> <BreadcrumbItem title="Features Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -29,6 +29,7 @@ import {
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers // helpers
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper";
// types // types
import { IProject, IWorkspace } from "types"; import { IProject, IWorkspace } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -161,10 +162,11 @@ const GeneralSettings: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`} title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`} link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
linkTruncate
/> />
<BreadcrumbItem title="General Settings" /> <BreadcrumbItem title="General Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -23,6 +23,8 @@ import { IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const ProjectIntegrations: NextPage = () => { const ProjectIntegrations: NextPage = () => {
const router = useRouter(); const router = useRouter();
@ -48,10 +50,11 @@ const ProjectIntegrations: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`} title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectId}/issues`} link={`/${workspaceSlug}/projects/${projectId}/issues`}
linkTruncate
/> />
<BreadcrumbItem title="Integrations" /> <BreadcrumbItem title="Integrations" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -32,6 +32,8 @@ import { IIssueLabels } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const LabelsSettings: NextPage = () => { const LabelsSettings: NextPage = () => {
// create/edit label form // create/edit label form
@ -103,10 +105,11 @@ const LabelsSettings: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`} title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`} link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
linkTruncate
/> />
<BreadcrumbItem title="Labels Settings" /> <BreadcrumbItem title="Labels Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr"; import useSWR from "swr";
@ -28,6 +29,8 @@ import type { NextPage } from "next";
import { PROJECT_INVITATIONS, PROJECT_MEMBERS, WORKSPACE_DETAILS } from "constants/fetch-keys"; import { PROJECT_INVITATIONS, PROJECT_MEMBERS, WORKSPACE_DETAILS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
// helper
import { truncateText } from "helpers/string.helper";
const MembersSettings: NextPage = () => { const MembersSettings: NextPage = () => {
const [inviteModal, setInviteModal] = useState(false); const [inviteModal, setInviteModal] = useState(false);
@ -93,10 +96,11 @@ const MembersSettings: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`} title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`} link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
linkTruncate
/> />
<BreadcrumbItem title="Members Settings" /> <BreadcrumbItem title="Members Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >
@ -187,9 +191,17 @@ const MembersSettings: NextPage = () => {
)} )}
</div> </div>
<div> <div>
<h4 className="text-sm"> {member.member ? (
{member.first_name} {member.last_name} <Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
</h4> <a className="text-sm">
{member.first_name} {member.last_name}
</a>
</Link>
) : (
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
)}
<p className="mt-0.5 text-xs text-custom-text-200">{member.email}</p> <p className="mt-0.5 text-xs text-custom-text-200">{member.email}</p>
</div> </div>
</div> </div>

View File

@ -26,6 +26,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
// helpers // helpers
import { getStatesList, orderStateGroups } from "helpers/state.helper"; import { getStatesList, orderStateGroups } from "helpers/state.helper";
import { truncateText } from "helpers/string.helper";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
@ -64,10 +65,11 @@ const StatesSettings: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`} title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`} link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
linkTruncate
/> />
<BreadcrumbItem title="States Settings" /> <BreadcrumbItem title="States Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -26,6 +26,8 @@ import emptyProject from "public/empty-state/project.svg";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECT_MEMBERS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const ProjectsPage: NextPage = () => { const ProjectsPage: NextPage = () => {
// router // router
@ -44,7 +46,10 @@ const ProjectsPage: NextPage = () => {
<WorkspaceAuthorizationLayout <WorkspaceAuthorizationLayout
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Projects`} /> <BreadcrumbItem
title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)} Projects`}
unshrinkTitle={false}
/>
</Breadcrumbs> </Breadcrumbs>
} }
right={ right={

View File

@ -16,6 +16,8 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { WORKSPACE_DETAILS } from "constants/fetch-keys"; import { WORKSPACE_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const BillingSettings: NextPage = () => { const BillingSettings: NextPage = () => {
const { const {
@ -32,10 +34,11 @@ const BillingSettings: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${activeWorkspace?.name ?? "Workspace"}`} title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}
link={`/${workspaceSlug}`} link={`/${workspaceSlug}`}
linkTruncate
/> />
<BreadcrumbItem title="Billing & Plans Settings" /> <BreadcrumbItem title="Billing & Plans Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -1,26 +1,43 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace"; import { SettingsHeader } from "components/workspace";
// components // components
import IntegrationGuide from "components/integration/guide"; import IntegrationGuide from "components/integration/guide";
import { IntegrationAndImportExportBanner } from "components/ui";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import { IntegrationAndImportExportBanner } from "components/ui"; // fetch-keys
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const ImportExport: NextPage = () => { const ImportExport: NextPage = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
);
return ( return (
<WorkspaceAuthorizationLayout <WorkspaceAuthorizationLayout
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title={`${workspaceSlug ?? "Workspace"}`} link={`/${workspaceSlug}`} /> <BreadcrumbItem
<BreadcrumbItem title="Import/ Export Settings" /> title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}
link={`/${workspaceSlug}`}
linkTruncate
/>
<BreadcrumbItem title="Import/ Export Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -14,7 +14,6 @@ import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components // components
import { ImageUploadModal } from "components/core"; import { ImageUploadModal } from "components/core";
import { DeleteWorkspaceModal, SettingsHeader } from "components/workspace"; import { DeleteWorkspaceModal, SettingsHeader } from "components/workspace";
@ -24,7 +23,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { LinkIcon } from "@heroicons/react/24/outline"; import { LinkIcon } from "@heroicons/react/24/outline";
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// types // types
import type { IWorkspace } from "types"; import type { IWorkspace } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -147,7 +146,9 @@ const WorkspaceSettings: NextPage = () => {
<WorkspaceAuthorizationLayout <WorkspaceAuthorizationLayout
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Settings`} /> <BreadcrumbItem
title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)} Settings`}
/>
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -19,6 +19,8 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { WORKSPACE_DETAILS, APP_INTEGRATIONS } from "constants/fetch-keys"; import { WORKSPACE_DETAILS, APP_INTEGRATIONS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const WorkspaceIntegrations: NextPage = () => { const WorkspaceIntegrations: NextPage = () => {
const router = useRouter(); const router = useRouter();
@ -38,10 +40,11 @@ const WorkspaceIntegrations: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${activeWorkspace?.name ?? "Workspace"}`} title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}
link={`/${workspaceSlug}`} link={`/${workspaceSlug}`}
linkTruncate
/> />
<BreadcrumbItem title="Integrations" /> <BreadcrumbItem title="Integrations" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >

View File

@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import Image from "next/image"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -27,6 +27,8 @@ import type { NextPage } from "next";
import { WORKSPACE_DETAILS, WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys"; import { WORKSPACE_DETAILS, WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
// helper
import { truncateText } from "helpers/string.helper";
const MembersSettings: NextPage = () => { const MembersSettings: NextPage = () => {
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null); const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
@ -89,10 +91,11 @@ const MembersSettings: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${activeWorkspace?.name ?? "Workspace"}`} title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}
link={`/${workspaceSlug}`} link={`/${workspaceSlug}`}
linkTruncate
/> />
<BreadcrumbItem title="Members Settings" /> <BreadcrumbItem title="Members Settings" unshrinkTitle />
</Breadcrumbs> </Breadcrumbs>
} }
> >
@ -187,9 +190,17 @@ const MembersSettings: NextPage = () => {
)} )}
</div> </div>
<div> <div>
<h4 className="text-sm"> {member.member ? (
{member.first_name} {member.last_name} <Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
</h4> <a className="text-sm">
{member.first_name} {member.last_name}
</a>
</Link>
) : (
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
)}
<p className="text-xs text-custom-text-200">{member.email}</p> <p className="text-xs text-custom-text-200">{member.email}</p>
</div> </div>
</div> </div>

View File

@ -24,7 +24,7 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// types // types
import { ICurrentUserResponse, IUser, OnboardingSteps } from "types"; import { ICurrentUserResponse, IUser, TOnboardingSteps } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
@ -43,33 +43,35 @@ const Onboarding: NextPage = () => {
workspaceService.userWorkspaceInvitations() workspaceService.userWorkspaceInvitations()
); );
// update last active workspace details
const updateLastWorkspace = async () => { const updateLastWorkspace = async () => {
if (!userWorkspaces) return; if (!workspaces) return;
mutate<ICurrentUserResponse>( await mutate<ICurrentUserResponse>(
CURRENT_USER, CURRENT_USER,
(prevData) => { (prevData) => {
if (!prevData) return prevData; if (!prevData) return prevData;
return { return {
...prevData, ...prevData,
last_workspace_id: userWorkspaces[0]?.id, last_workspace_id: workspaces[0]?.id,
workspace: { workspace: {
...prevData.workspace, ...prevData.workspace,
fallback_workspace_id: userWorkspaces[0]?.id, fallback_workspace_id: workspaces[0]?.id,
fallback_workspace_slug: userWorkspaces[0]?.slug, fallback_workspace_slug: workspaces[0]?.slug,
last_workspace_id: userWorkspaces[0]?.id, last_workspace_id: workspaces[0]?.id,
last_workspace_slug: userWorkspaces[0]?.slug, last_workspace_slug: workspaces[0]?.slug,
}, },
}; };
}, },
false false
); );
await userService.updateUser({ last_workspace_id: userWorkspaces?.[0]?.id }); await userService.updateUser({ last_workspace_id: workspaces?.[0]?.id });
}; };
const stepChange = async (steps: Partial<OnboardingSteps>) => { // handle step change
const stepChange = async (steps: Partial<TOnboardingSteps>) => {
if (!user) return; if (!user) return;
const payload: Partial<IUser> = { const payload: Partial<IUser> = {
@ -95,16 +97,44 @@ const Onboarding: NextPage = () => {
await userService.updateUser(payload); await userService.updateUser(payload);
}; };
// complete onboarding
const finishOnboarding = async () => {
if (!user) return;
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
is_onboarded: true,
};
},
false
);
await userService.updateUserOnBoard({ userRole: user.role }, user);
};
useEffect(() => { useEffect(() => {
const handleStepChange = async () => { const handleStepChange = async () => {
if (!user || !userWorkspaces || !invitations) return; if (!user || !invitations) return;
const onboardingStep = user.onboarding_step; const onboardingStep = user.onboarding_step;
if (!onboardingStep.profile_complete && step !== 1) setStep(1); if (!onboardingStep.profile_complete && step !== 1) setStep(1);
if (onboardingStep.profile_complete && !onboardingStep.workspace_create && step !== 2) if (onboardingStep.profile_complete) {
setStep(2); if (!onboardingStep.workspace_join && invitations.length > 0 && step !== 2 && step !== 4)
setStep(4);
else if (
!onboardingStep.workspace_create &&
(step !== 4 || onboardingStep.workspace_join) &&
step !== 2
)
setStep(2);
}
if ( if (
onboardingStep.profile_complete && onboardingStep.profile_complete &&
@ -113,21 +143,10 @@ const Onboarding: NextPage = () => {
step !== 3 step !== 3
) )
setStep(3); setStep(3);
if (
onboardingStep.profile_complete &&
onboardingStep.workspace_create &&
onboardingStep.workspace_invite &&
!onboardingStep.workspace_join &&
step !== 4
) {
if (invitations.length > 0) setStep(4);
else await Router.push("/");
}
}; };
handleStepChange(); handleStepChange();
}, [user, invitations, userWorkspaces, step]); }, [user, invitations, step]);
if (userLoading || step === null) if (userLoading || step === null)
return ( return (
@ -167,14 +186,27 @@ const Onboarding: NextPage = () => {
<UserDetails user={user} /> <UserDetails user={user} />
) : step === 2 ? ( ) : step === 2 ? (
<Workspace <Workspace
user={user} finishOnboarding={finishOnboarding}
updateLastWorkspace={updateLastWorkspace}
stepChange={stepChange} stepChange={stepChange}
updateLastWorkspace={updateLastWorkspace}
user={user}
workspaces={workspaces}
/> />
) : step === 3 ? ( ) : step === 3 ? (
<InviteMembers workspace={userWorkspaces?.[0]} user={user} stepChange={stepChange} /> <InviteMembers
finishOnboarding={finishOnboarding}
stepChange={stepChange}
user={user}
workspace={userWorkspaces?.[0]}
/>
) : ( ) : (
step === 4 && <JoinWorkspaces stepChange={stepChange} /> step === 4 && (
<JoinWorkspaces
finishOnboarding={finishOnboarding}
stepChange={stepChange}
updateLastWorkspace={updateLastWorkspace}
/>
)
)} )}
</div> </div>
{step !== 4 && ( {step !== 4 && (

View File

@ -254,6 +254,7 @@ class ProjectServices extends APIService {
view_props?: ProjectViewTheme; view_props?: ProjectViewTheme;
default_props?: ProjectViewTheme; default_props?: ProjectViewTheme;
preferences?: ProjectPreferences; preferences?: ProjectPreferences;
sort_order?: number;
} }
): Promise<any> { ): Promise<any> {
await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`, data) await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`, data)

View File

@ -23,6 +23,107 @@
} }
:root { :root {
color-scheme: light !important;
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 250, 250, 250; /* secondary bg */
--color-background-80: 245, 245, 245; /* tertiary bg */
--color-text-100: 23, 23, 23; /* primary text */
--color-text-200: 58, 58, 58; /* secondary text */
--color-text-300: 82, 82, 82; /* tertiary text */
--color-text-400: 163, 163, 163; /* placeholder text */
--color-border-100: 245, 245, 245; /* subtle border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
--color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
--color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
--color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
--color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
--color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
--color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
}
[data-theme="light"],
[data-theme="light-contrast"] {
color-scheme: light !important;
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 250, 250, 250; /* secondary bg */
--color-background-80: 245, 245, 245; /* tertiary bg */
}
[data-theme="light"] {
--color-text-100: 23, 23, 23; /* primary text */
--color-text-200: 58, 58, 58; /* secondary text */
--color-text-300: 82, 82, 82; /* tertiary text */
--color-text-400: 163, 163, 163; /* placeholder text */
--color-border-100: 245, 245, 245; /* subtle border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */
}
[data-theme="light-contrast"] {
--color-text-100: 11, 11, 11; /* primary text */
--color-text-200: 38, 38, 38; /* secondary text */
--color-text-300: 58, 58, 58; /* tertiary text */
--color-text-400: 115, 115, 115; /* placeholder text */
--color-border-100: 34, 34, 34; /* subtle border= 1 */
--color-border-200: 38, 38, 38; /* subtle border- 2 */
--color-border-300: 46, 46, 46; /* strong border- 1 */
--color-border-400: 58, 58, 58; /* strong border- 2 */
}
[data-theme="dark"],
[data-theme="dark-contrast"] {
color-scheme: dark !important;
--color-background-100: 7, 7, 7; /* primary bg */
--color-background-90: 11, 11, 11; /* secondary bg */
--color-background-80: 23, 23, 23; /* tertiary bg */
}
[data-theme="dark"] {
--color-text-100: 229, 229, 229; /* primary text */
--color-text-200: 163, 163, 163; /* secondary text */
--color-text-300: 115, 115, 115; /* tertiary text */
--color-text-400: 82, 82, 82; /* placeholder text */
--color-border-100: 34, 34, 34; /* subtle border= 1 */
--color-border-200: 38, 38, 38; /* subtle border- 2 */
--color-border-300: 46, 46, 46; /* strong border- 1 */
--color-border-400: 58, 58, 58; /* strong border- 2 */
}
[data-theme="dark-contrast"] {
--color-text-100: 250, 250, 250; /* primary text */
--color-text-200: 241, 241, 241; /* secondary text */
--color-text-300: 212, 212, 212; /* tertiary text */
--color-text-400: 115, 115, 115; /* placeholder text */
--color-border-100: 245, 245, 245; /* subtle border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */
}
[data-theme="light"],
[data-theme="dark"],
[data-theme="light-contrast"],
[data-theme="dark-contrast"] {
--color-primary-10: 236, 241, 255; --color-primary-10: 236, 241, 255;
--color-primary-20: 217, 228, 255; --color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255; --color-primary-30: 197, 214, 255;
@ -42,122 +143,19 @@
--color-primary-800: 19, 35, 76; --color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51; --color-primary-900: 13, 24, 51;
/* default theme- light */ --color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
--color-background-100: 255, 255, 255; /* primary bg */ --color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
--color-background-90: 250, 250, 250; /* secondary bg */ --color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
--color-background-80: 245, 245, 245; /* tertiary bg */
--color-text-100: 23, 23, 23; /* primary text */ --color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
--color-text-200: 82, 82, 82; /* secondary text */ --color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
--color-text-300: 115, 115, 115; /* tertiary text */ --color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
--color-text-400: 163, 163, 163; /* placeholder text */ --color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
--color-border-100: 245, 245, 245; /* subtle border= 1 */ --color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */ --color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */ --color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */ --color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
--color-sidebar-background-100: 255, 255, 255; /* primary sidebar bg */
--color-sidebar-background-90: 250, 250, 250; /* secondary sidebar bg */
--color-sidebar-background-80: 245, 245, 245; /* tertiary sidebar bg */
--color-sidebar-text-100: 23, 23, 23; /* primary sidebar text */
--color-sidebar-text-200: 82, 82, 82; /* secondary sidebar text */
--color-sidebar-text-300: 115, 115, 115; /* tertiary sidebar text */
--color-sidebar-text-400: 163, 163, 163; /* sidebar placeholder text */
--color-sidebar-border-100: 245, 245, 245; /* subtle sidebar border= 1 */
--color-sidebar-border-200: 229, 229, 229; /* subtle sidebar border- 2 */
--color-sidebar-border-300: 212, 212, 212; /* strong sidebar border- 1 */
--color-sidebar-border-400: 185, 185, 185; /* strong sidebar border- 2 */
}
[data-theme="dark"] {
color-scheme: dark !important;
--color-background-100: 7, 7, 7; /* primary bg */
--color-background-90: 11, 11, 11; /* secondary bg */
--color-background-80: 23, 23, 23; /* tertiary bg */
--color-text-100: 241, 241, 241; /* primary text */
--color-text-200: 115, 115, 115; /* secondary text */
--color-text-300: 163, 163, 163; /* tertiary text */
--color-text-400: 82, 82, 82; /* placeholder text */
--color-border-100: 34, 34, 34; /* subtle border= 1 */
--color-border-200: 38, 38, 38; /* subtle border- 2 */
--color-border-300: 46, 46, 46; /* strong border- 1 */
--color-border-400: 58, 58, 58; /* strong border- 2 */
--color-sidebar-background-100: 7, 7, 7; /* primary sidebar bg */
--color-sidebar-background-90: 11, 11, 11; /* secondary sidebar bg */
--color-sidebar-background-80: 23, 23, 23; /* tertiary sidebar bg */
--color-sidebar-text-100: 241, 241, 241; /* primary sidebar text */
--color-sidebar-text-200: 115, 115, 115; /* secondary sidebar text */
--color-sidebar-text-300: 163, 163, 163; /* tertiary sidebar text */
--color-sidebar-text-400: 82, 82, 82; /* sidebar placeholder text */
--color-sidebar-border-100: 34, 34, 34; /* subtle sidebar border= 1 */
--color-sidebar-border-200: 38, 38, 38; /* subtle sidebar border- 2 */
--color-sidebar-border-300: 46, 46, 46; /* strong sidebar border- 1 */
--color-sidebar-border-400: 58, 58, 58; /* strong sidebar border- 2 */
}
[data-theme="light-contrast"] {
color-scheme: light !important;
--color-text-100: 11, 11, 11; /* primary text */
--color-text-200: 38, 38, 38; /* secondary text */
--color-text-300: 58, 58, 58; /* tertiary text */
--color-text-400: 115, 115, 115; /* placeholder text */
--color-border-100: 34, 34, 34; /* subtle border= 1 */
--color-border-200: 38, 38, 38; /* subtle border- 2 */
--color-border-300: 46, 46, 46; /* strong border- 1 */
--color-border-400: 58, 58, 58; /* strong border- 2 */
--color-sidebar-text-100: 11, 11, 11; /* primary sidebar text */
--color-sidebar-text-200: 38, 38, 38; /* secondary sidebar text */
--color-sidebar-text-300: 58, 58, 58; /* tertiary sidebar text */
--color-sidebar-text-400: 115, 115, 115; /* sidebar placeholder text */
--color-sidebar-border-100: 34, 34, 34; /* subtle sidebar border= 1 */
--color-sidebar-border-200: 38, 38, 38; /* subtle sidebar border- 2 */
--color-sidebar-border-300: 46, 46, 46; /* strong sidebar border- 1 */
--color-sidebar-border-400: 58, 58, 58; /* strong sidebar border- 2 */
}
[data-theme="dark-contrast"] {
color-scheme: dark !important;
--color-background-100: 7, 7, 7; /* primary bg */
--color-background-90: 11, 11, 11; /* secondary bg */
--color-background-80: 23, 23, 23; /* tertiary bg */
--color-text-100: 250, 250, 250; /* primary text */
--color-text-200: 241, 241, 241; /* secondary text */
--color-text-300: 212, 212, 212; /* tertiary text */
--color-text-400: 115, 115, 115; /* placeholder text */
--color-border-100: 245, 245, 245; /* subtle border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */
--color-sidebar-background-100: 7, 7, 7; /* primary sidebar bg */
--color-sidebar-background-90: 11, 11, 11; /* secondary sidebar bg */
--color-sidebar-background-80: 23, 23, 23; /* tertiary sidebar bg */
--color-sidebar-text-100: 250, 250, 250; /* primary sidebar text */
--color-sidebar-text-200: 241, 241, 241; /* secondary sidebar text */
--color-sidebar-text-300: 212, 212, 212; /* tertiary sidebar text */
--color-sidebar-text-400: 115, 115, 115; /* sidebar placeholder text */
--color-sidebar-border-100: 245, 245, 245; /* subtle sidebar border= 1 */
--color-sidebar-border-200: 229, 229, 229; /* subtle sidebar border- 2 */
--color-sidebar-border-300: 212, 212, 212; /* strong sidebar border- 1 */
--color-sidebar-border-400: 185, 185, 185; /* strong sidebar border- 2 */
} }
} }

View File

@ -45,6 +45,7 @@ export interface IProject {
network: number; network: number;
page_view: boolean; page_view: boolean;
project_lead: IUserLite | string | null; project_lead: IUserLite | string | null;
sort_order: number;
slug: string; slug: string;
total_cycles: number; total_cycles: number;
total_members: number; total_members: number;

View File

@ -27,7 +27,7 @@ export interface IUser {
properties: Properties; properties: Properties;
groupBy: NestedKeyOf<IIssue> | null; groupBy: NestedKeyOf<IIssue> | null;
} | null; } | null;
onboarding_step: OnboardingSteps; onboarding_step: TOnboardingSteps;
role: string; role: string;
token: string; token: string;
theme: ICustomTheme; theme: ICustomTheme;
@ -140,7 +140,7 @@ export type UserAuth = {
isGuest: boolean; isGuest: boolean;
}; };
export type OnboardingSteps = { export type TOnboardingSteps = {
profile_complete: boolean; profile_complete: boolean;
workspace_create: boolean; workspace_create: boolean;
workspace_invite: boolean; workspace_invite: boolean;