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">
<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>
<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>

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

View File

@ -61,3 +61,13 @@ class WorkspaceEntityPermission(BasePermission):
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug
).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_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
class Meta:
model = Project

View File

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

View File

@ -73,12 +73,14 @@ from plane.db.models import (
IssueSubscriber,
Project,
Label,
State,
WorkspaceMember,
CycleIssue,
)
from plane.api.permissions import (
WorkSpaceBasePermission,
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
WorkspaceViewerPermission,
)
from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.utils.issue_filters import issue_filters
@ -1140,6 +1142,19 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
.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(
{
"state_distribution": state_distribution,
@ -1149,6 +1164,8 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
"completed_issues": completed_issues_count,
"pending_issues": pending_issues_count,
"subscribed_issues": subscribed_issues_count,
"present_cycles": present_cycle,
"upcoming_cycles": upcoming_cycles,
}
)
except Exception as e:
@ -1194,64 +1211,64 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
class WorkspaceUserProfileEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug, user_id):
try:
user_data = User.objects.get(pk=user_id)
projects = (
Project.objects.filter(
workspace__slug=slug,
project_projectmember__member=request.user,
)
.annotate(
created_issues=Count(
"project_issue", filter=Q(project_issue__created_by_id=user_id)
requesting_workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user)
projects = []
if requesting_workspace_member.role >= 10:
projects = (
Project.objects.filter(
workspace__slug=slug,
project_projectmember__member=request.user,
)
.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(
{
@ -1268,6 +1285,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
},
status=status.HTTP_200_OK,
)
except WorkspaceMember.DoesNotExist:
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
except Exception as e:
capture_exception(e)
return Response(
@ -1278,7 +1297,7 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
WorkspaceViewerPermission,
]
def get(self, request, slug, user_id):
@ -1317,7 +1336,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
).distinct()
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
@ -1394,9 +1413,10 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceLabelsEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
WorkspaceViewerPermission,
]
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)
def update_project_sort_order(apps, schema_editor):
Model = apps.get_model("db", "Project")
def update_project_member_sort_order(apps, schema_editor):
Model = apps.get_model("db", "ProjectMember")
updated_projects = []
updated_project_members = []
for obj in Model.objects.all():
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):
@ -93,5 +93,5 @@ class Migration(migrations.Migration):
name='sort_order',
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(
"db.State", on_delete=models.SET_NULL, null=True, related_name="default_state"
)
sort_order = models.FloatField(default=65535)
def __str__(self):
"""Return name of the project"""

View File

@ -2,7 +2,6 @@ import * as React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// icons
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { Icon } from "components/ui";
type BreadcrumbsProps = {
@ -14,7 +13,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
return (
<>
<div className="flex items-center">
<div className="flex items-center flex-grow w-full whitespace-nowrap overflow-hidden overflow-ellipsis">
<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"
@ -35,22 +34,36 @@ type BreadcrumbItemProps = {
title: string;
link?: string;
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 href={link}>
<a className="border-r-2 border-custom-sidebar-border-200 px-3 text-sm">
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
<a
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}
{title}
</p>
</a>
</Link>
) : (
<div className="max-w-64 px-3 text-sm">
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
<div className={`px-3 text-sm truncate ${unshrinkTitle ? "flex-shrink-0" : ""}`}>
<p className={`truncate ${icon ? "flex items-center gap-2" : ""}`}>
{icon}
<span className="break-words">{title}</span>
</p>

View File

@ -194,15 +194,15 @@ export const SingleListIssue: React.FC<Props> = ({
</a>
</ContextMenu>
<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) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
}}
>
<Link href={singleIssuePath}>
<div className="flex-grow cursor-pointer">
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
<Link href={singleIssuePath}>
<a className="group relative flex items-center gap-2">
{properties.key && (
<Tooltip
@ -215,16 +215,14 @@ export const SingleListIssue: React.FC<Props> = ({
</Tooltip>
)}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="text-[0.825rem] text-custom-text-100">
{truncateText(issue.name, 50)}
</span>
<span className="truncate text-[0.825rem] text-custom-text-100">{issue.name}</span>
</Tooltip>
</a>
</div>
</Link>
</Link>
</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" : ""
}`}
>

View File

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

View File

@ -41,7 +41,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
position={tooltipPosition}
>
<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 < new Date().toISOString()

View File

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

View File

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

View File

@ -3,17 +3,25 @@ import { useState } from "react";
// ui
import { SecondaryButton } from "components/ui";
// types
import { ICurrentUserResponse, OnboardingSteps } from "types";
import { ICurrentUserResponse, IWorkspace, TOnboardingSteps } from "types";
// constants
import { CreateWorkspaceForm } from "components/workspace";
type Props = {
user: ICurrentUserResponse | undefined;
finishOnboarding: () => Promise<void>;
stepChange: (steps: Partial<TOnboardingSteps>) => 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({
name: "",
slug: "",
@ -23,12 +31,21 @@ export const Workspace: React.FC<Props> = ({ user, updateLastWorkspace, stepChan
const completeStep = async () => {
if (!user) return;
await stepChange({
const payload: Partial<TOnboardingSteps> = {
workspace_create: true,
});
};
await stepChange(payload);
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 (
<div className="w-full space-y-7 sm:space-y-10">
<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",
}}
secondaryButton={
<SecondaryButton onClick={() => stepChange({ profile_complete: false })}>
Back
</SecondaryButton>
workspaces ? (
<SecondaryButton onClick={secondaryButtonAction}>
{workspaces.length > 0 ? "Skip & continue" : "Back"}
</SecondaryButton>
) : undefined
}
/>
</div>

View File

@ -1,15 +1,26 @@
import React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// components
import { ProfileIssuesViewOptions } from "components/profile";
// types
import { UserAuth } from "types";
const tabsList = [
type Props = {
memberRole: UserAuth;
};
const viewerTabs = [
{
route: "",
label: "Overview",
selected: "/[workspaceSlug]/profile/[userId]",
},
];
const adminTabs = [
{
route: "assigned",
label: "Assigned",
@ -27,12 +38,17 @@ const tabsList = [
},
];
export const ProfileNavbar = () => {
export const ProfileNavbar: React.FC<Props> = ({ memberRole }) => {
const router = useRouter();
const { workspaceSlug, userId } = router.query;
const tabsList =
memberRole.isOwner || memberRole.isMember || memberRole.isViewer
? [...viewerTabs, ...adminTabs]
: viewerTabs;
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">
{tabsList.map((tab) => (
<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 "./state-distribution";
export * from "./stats";

View File

@ -10,7 +10,7 @@ type Props = {
export const ProfileWorkload: React.FC<Props> = ({ stateDistribution }) => (
<div className="space-y-2">
<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) => (
<div key={group.state_group}>
<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">
<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>
</div>
</a>

View File

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

View File

@ -1,4 +1,5 @@
import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr";
@ -8,10 +9,14 @@ import { useTheme } from "next-themes";
import { Disclosure, Transition } from "@headlessui/react";
// services
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// ui
import { Icon, Loader } from "components/ui";
import { Icon, Loader, Tooltip } from "components/ui";
// icons
import { EditOutlined } from "@mui/icons-material";
// helpers
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
import { render12HourFormatTime, renderLongDetailDateFormat } from "helpers/date-time.helper";
import { renderEmoji } from "helpers/emoji.helper";
// fetch-keys
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
@ -22,6 +27,8 @@ export const ProfileSidebar = () => {
const { theme } = useTheme();
const { user } = useUser();
const { data: userProjectsData } = useSWR(
workspaceSlug && userId
? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
@ -33,27 +40,24 @@ export const ProfileSidebar = () => {
);
const userDetails = [
{
label: "Username",
value: "",
},
{
label: "Joined on",
value: renderLongDetailDateFormat(userProjectsData?.user_data.date_joined ?? ""),
},
{
label: "Timezone",
value: userProjectsData?.user_data.user_timezone,
},
{
label: "Status",
value: "Online",
value: (
<span>
{render12HourFormatTime(new Date())}{" "}
<span className="text-custom-text-200">{userProjectsData?.user_data.user_timezone}</span>
</span>
),
},
];
return (
<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={{
boxShadow:
theme === "light"
@ -64,10 +68,23 @@ export const ProfileSidebar = () => {
{userProjectsData ? (
<>
<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
src={
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}
className="h-32 w-full object-cover"
@ -96,8 +113,8 @@ export const ProfileSidebar = () => {
<div className="mt-6 space-y-5">
{userDetails.map((detail) => (
<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="font-medium">{detail.value}</div>
<div className="flex-shrink-0 text-custom-text-200 w-2/5">{detail.label}</div>
<div className="font-medium w-3/5 break-words">{detail.value}</div>
</div>
))}
</div>
@ -143,17 +160,19 @@ export const ProfileSidebar = () => {
</div>
</div>
<div className="flex-shrink-0 flex items-center gap-2">
<div
className={`px-1 py-0.5 text-xs font-medium rounded ${
completedIssuePercentage <= 35
? "bg-red-500/10 text-red-500"
: completedIssuePercentage <= 70
? "bg-yellow-500/10 text-yellow-500"
: "bg-green-500/10 text-green-500"
}`}
>
{completedIssuePercentage}%
</div>
<Tooltip tooltipContent="Completion percentage" position="left">
<div
className={`px-1 py-0.5 text-xs font-medium rounded ${
completedIssuePercentage <= 35
? "bg-red-500/10 text-red-500"
: completedIssuePercentage <= 70
? "bg-yellow-500/10 text-yellow-500"
: "bg-green-500/10 text-green-500"
}`}
>
{completedIssuePercentage}%
</div>
</Tooltip>
<Icon iconName="arrow_drop_down" className="!text-lg" />
</div>
</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)
.then(() => {
setIsOpen(false);
mutate(PROJECT_MEMBERS(projectId as string));
setToastAlert({
title: "Success",
type: "success",
@ -105,6 +104,7 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
})
.finally(() => {
reset(defaultValues);
mutate(PROJECT_MEMBERS(projectId.toString()));
});
};

View File

@ -1,7 +1,10 @@
import React, { useState, FC } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-beautiful-dnd
import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd";
// hooks
import useToast from "hooks/use-toast";
import useTheme from "hooks/use-theme";
@ -9,12 +12,17 @@ import useUserAuth from "hooks/use-user-auth";
import useProjects from "hooks/use-projects";
// components
import { DeleteProjectModal, SingleSidebarProject } from "components/project";
// services
import projectService from "services/project.service";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper";
// types
import { IProject } from "types";
// fetch-keys
import { PROJECTS_LIST } from "constants/fetch-keys";
export const ProjectSidebarList: FC = () => {
const [deleteProjectModal, setDeleteProjectModal] = useState(false);
@ -32,6 +40,14 @@ export const ProjectSidebarList: FC = () => {
const { projects: allProjects } = useProjects();
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) => {
setProjectToDelete(project);
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 (
<>
<DeleteProjectModal
@ -58,39 +122,75 @@ export const ProjectSidebarList: FC = () => {
user={user}
/>
<div className="h-full overflow-y-auto px-4">
{favoriteProjects && favoriteProjects.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>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="favorite-projects">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{orderedFavProjects && orderedFavProjects.length > 0 && (
<div className="flex flex-col space-y-2 mt-5">
{!sidebarCollapse && (
<h5 className="text-sm font-medium text-custom-sidebar-text-200">
Favorites
</h5>
)}
{orderedFavProjects.map((project, index) => (
<Draggable key={project.id} draggableId={project.id} index={index}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<SingleSidebarProject
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
provided={provided}
snapshot={snapshot}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
shortContextMenu
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</div>
)}
{favoriteProjects.map((project) => (
<SingleSidebarProject
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
shortContextMenu
/>
))}
</div>
)}
{allProjects && allProjects.length > 0 && (
<div className="flex flex-col space-y-2 mt-5">
{!sidebarCollapse && (
<h5 className="text-sm font-medium text-custom-sidebar-text-200">Projects</h5>
</Droppable>
</DragDropContext>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="all-projects">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{orderedAllProjects && orderedAllProjects.length > 0 && (
<div className="flex flex-col space-y-2 mt-5">
{!sidebarCollapse && (
<h5 className="text-sm font-medium text-custom-sidebar-text-200">Projects</h5>
)}
{orderedAllProjects.map((project, index) => (
<Draggable key={project.id} draggableId={project.id} index={index}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<SingleSidebarProject
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
provided={provided}
snapshot={snapshot}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</div>
)}
{allProjects.map((project) => (
<SingleSidebarProject
key={project.id}
project={project}
sidebarCollapse={sidebarCollapse}
handleDeleteProject={() => handleDeleteProject(project)}
handleCopyText={() => handleCopyText(project.id)}
/>
))}
</div>
)}
</Droppable>
</DragDropContext>
{allProjects && allProjects.length === 0 && (
<button
type="button"

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import { profileIssuesContext } from "contexts/profile-issues-context";
import { IIssue } from "types";
// 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 {
@ -33,6 +34,8 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
const router = useRouter();
const { memberRole } = useWorkspaceMyMembership();
const params: any = {
assignees: filters?.assignees ? filters?.assignees.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(
workspaceSlug && userId
workspaceSlug && userId && (memberRole.isOwner || memberRole.isMember || memberRole.isViewer)
? USER_PROFILE_ISSUES(workspaceSlug.toString(), userId.toString(), params)
: null,
workspaceSlug && userId
workspaceSlug && userId && (memberRole.isOwner || memberRole.isMember || memberRole.isViewer)
? () => userService.getUserProfileIssues(workspaceSlug.toString(), userId.toString(), params)
: null
);
console.log(memberRole);
const groupedIssues:
| {
[key: string]: IIssue[];
@ -73,8 +78,6 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
useEffect(() => {
if (!userId || !filters) return;
console.log("Triggered");
if (
router.pathname.includes("assigned") &&
(!filters.assignees || !filters.assignees.includes(userId))

View File

@ -11,13 +11,17 @@ import { IProject } from "types";
// 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 { workspaceSlug } = router.query;
const { data: projects, mutate: mutateProjects } = useSWR(
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string, { is_favorite: type ?? "all" }) : null,
workspaceSlug
workspaceSlug && fetchCondition
? PROJECTS_LIST(workspaceSlug as string, { is_favorite: type ?? "all" })
: null,
workspaceSlug && fetchCondition
? () => projectService.getProjects(workspaceSlug as string, { is_favorite: type ?? "all" })
: null
);

View File

@ -11,11 +11,11 @@ type Props = {
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => (
<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" : ""
}`}
>
<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">
<button
type="button"
@ -26,9 +26,9 @@ const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar, n
</button>
</div>
{breadcrumbs}
{left}
<div className="flex-shrink-0">{left}</div>
</div>
{right}
<div className="flex-shrink-0">{right}</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 { useRouter } from "next/router";
// contexts
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
// hooks
import useUser from "hooks/use-user";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { ProfileAuthWrapper } from "layouts/profile-layout";
// components
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { ProfileIssuesView } from "components/profile";
// types
import type { NextPage } from "next";
const ProfileAssignedIssues: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUser();
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>
);
};
const ProfileAssignedIssues: NextPage = () => (
<ProfileIssuesContextProvider>
<ProfileAuthWrapper>
<ProfileIssuesView />
</ProfileAuthWrapper>
</ProfileIssuesContextProvider>
);
export default ProfileAssignedIssues;

View File

@ -1,48 +1,20 @@
import React from "react";
import { useRouter } from "next/router";
// contexts
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
// hooks
import useUser from "hooks/use-user";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { ProfileAuthWrapper } from "layouts/profile-layout";
// components
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { ProfileIssuesView } from "components/profile";
// types
import type { NextPage } from "next";
const ProfileCreatedIssues: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUser();
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>
);
};
const ProfileCreatedIssues: NextPage = () => (
<ProfileIssuesContextProvider>
<ProfileAuthWrapper>
<ProfileIssuesView />
</ProfileAuthWrapper>
</ProfileIssuesContextProvider>
);
export default ProfileCreatedIssues;

View File

@ -1,34 +1,26 @@
import React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// services
import userService from "services/user.service";
// layouts
import { ProfileAuthWrapper } from "layouts/profile-layout";
// components
import {
ProfileNavbar,
ProfileActivity,
ProfilePriorityDistribution,
ProfileSidebar,
ProfileStateDistribution,
ProfileStats,
ProfileWorkload,
} 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
import type { NextPage } from "next";
import { IUserStateDistribution, TStateGroups } from "types";
// 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";
const ProfileOverview: NextPage = () => {
@ -42,15 +34,6 @@ const ProfileOverview: NextPage = () => {
: 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 group = userProfile?.state_distribution.find((g) => g.state_group === key);
@ -59,93 +42,20 @@ const ProfileOverview: NextPage = () => {
});
return (
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`User 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 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>
<ProfileAuthWrapper>
<div className="h-full w-full px-5 md:px-9 py-5 space-y-7 overflow-y-auto">
<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>
<ProfileSidebar />
<ProfileActivity />
</div>
</WorkspaceAuthorizationLayout>
</ProfileAuthWrapper>
);
};

View File

@ -1,48 +1,20 @@
import React from "react";
import { useRouter } from "next/router";
// contexts
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
// hooks
import useUser from "hooks/use-user";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { ProfileAuthWrapper } from "layouts/profile-layout";
// components
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { ProfileIssuesView } from "components/profile";
// types
import type { NextPage } from "next";
const ProfileSubscribedIssues: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUser();
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>
);
};
const ProfileSubscribedIssues: NextPage = () => (
<ProfileIssuesContextProvider>
<ProfileAuthWrapper>
<ProfileIssuesView />
</ProfileAuthWrapper>
</ProfileIssuesContextProvider>
);
export default ProfileSubscribedIssues;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@ import {
import { ColorPalletteIcon, ClipboardIcon } from "components/icons";
// helpers
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";
// types
import type { NextPage } from "next";
@ -346,7 +346,7 @@ const SinglePage: NextPage = () => {
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${projectDetails?.name ?? "Project"} Pages`} />
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project",32)} Pages`} />
</Breadcrumbs>
}
>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
@ -27,6 +27,8 @@ import type { NextPage } from "next";
import { WORKSPACE_DETAILS, WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
// helper
import { truncateText } from "helpers/string.helper";
const MembersSettings: NextPage = () => {
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
@ -89,10 +91,11 @@ const MembersSettings: NextPage = () => {
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeWorkspace?.name ?? "Workspace"}`}
title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}
link={`/${workspaceSlug}`}
linkTruncate
/>
<BreadcrumbItem title="Members Settings" />
<BreadcrumbItem title="Members Settings" unshrinkTitle />
</Breadcrumbs>
}
>
@ -187,9 +190,17 @@ const MembersSettings: NextPage = () => {
)}
</div>
<div>
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
{member.member ? (
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
<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>
</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 WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// types
import { ICurrentUserResponse, IUser, OnboardingSteps } from "types";
import { ICurrentUserResponse, IUser, TOnboardingSteps } from "types";
import type { NextPage } from "next";
// fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
@ -43,33 +43,35 @@ const Onboarding: NextPage = () => {
workspaceService.userWorkspaceInvitations()
);
// update last active workspace details
const updateLastWorkspace = async () => {
if (!userWorkspaces) return;
if (!workspaces) return;
mutate<ICurrentUserResponse>(
await mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
last_workspace_id: userWorkspaces[0]?.id,
last_workspace_id: workspaces[0]?.id,
workspace: {
...prevData.workspace,
fallback_workspace_id: userWorkspaces[0]?.id,
fallback_workspace_slug: userWorkspaces[0]?.slug,
last_workspace_id: userWorkspaces[0]?.id,
last_workspace_slug: userWorkspaces[0]?.slug,
fallback_workspace_id: workspaces[0]?.id,
fallback_workspace_slug: workspaces[0]?.slug,
last_workspace_id: workspaces[0]?.id,
last_workspace_slug: workspaces[0]?.slug,
},
};
},
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;
const payload: Partial<IUser> = {
@ -95,16 +97,44 @@ const Onboarding: NextPage = () => {
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(() => {
const handleStepChange = async () => {
if (!user || !userWorkspaces || !invitations) return;
if (!user || !invitations) return;
const onboardingStep = user.onboarding_step;
if (!onboardingStep.profile_complete && step !== 1) setStep(1);
if (onboardingStep.profile_complete && !onboardingStep.workspace_create && step !== 2)
setStep(2);
if (onboardingStep.profile_complete) {
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 (
onboardingStep.profile_complete &&
@ -113,21 +143,10 @@ const Onboarding: NextPage = () => {
step !== 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();
}, [user, invitations, userWorkspaces, step]);
}, [user, invitations, step]);
if (userLoading || step === null)
return (
@ -167,14 +186,27 @@ const Onboarding: NextPage = () => {
<UserDetails user={user} />
) : step === 2 ? (
<Workspace
user={user}
updateLastWorkspace={updateLastWorkspace}
finishOnboarding={finishOnboarding}
stepChange={stepChange}
updateLastWorkspace={updateLastWorkspace}
user={user}
workspaces={workspaces}
/>
) : 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>
{step !== 4 && (

View File

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

View File

@ -23,6 +23,107 @@
}
: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-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
@ -42,122 +143,19 @@
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
/* default theme- light */
--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-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-text-100: 23, 23, 23; /* primary text */
--color-text-200: 82, 82, 82; /* secondary text */
--color-text-300: 115, 115, 115; /* tertiary text */
--color-text-400: 163, 163, 163; /* placeholder text */
--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-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: 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 */
--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 */
}
}

View File

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

View File

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