mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into fix/project_creation
This commit is contained in:
commit
54c2a23a7e
@ -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>
|
||||
|
@ -1,2 +1,2 @@
|
||||
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission
|
||||
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, WorkspaceViewerPermission
|
||||
from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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),
|
||||
]
|
||||
|
@ -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"""
|
||||
|
@ -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>
|
||||
|
@ -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" : ""
|
||||
}`}
|
||||
>
|
||||
|
@ -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">
|
||||
|
@ -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()
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}`}>
|
||||
|
91
apps/app/components/profile/overview/activity.tsx
Normal file
91
apps/app/components/profile/overview/activity.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
export * from "./activity";
|
||||
export * from "./priority-distribution";
|
||||
export * from "./state-distribution";
|
||||
export * from "./stats";
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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()));
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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">
|
||||
|
@ -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 (
|
||||
|
@ -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 = () => {
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
||||
|
45
apps/app/layouts/profile-layout.tsx
Normal file
45
apps/app/layouts/profile-layout.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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)}
|
||||
|
@ -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={
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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={
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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={
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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={
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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 && (
|
||||
|
@ -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)
|
||||
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
1
apps/app/types/projects.d.ts
vendored
1
apps/app/types/projects.d.ts
vendored
@ -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;
|
||||
|
4
apps/app/types/users.d.ts
vendored
4
apps/app/types/users.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user