mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-08 18:26:57 +02:00
Add Members while Transferring Roadmap (#4534)
* Add members while Transferring Roadmap * Implement Responsive in Roadmaps page
This commit is contained in:
@@ -86,13 +86,13 @@ export function RoadmapListPage() {
|
|||||||
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} />
|
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex flex-col justify-between gap-2 sm:flex-row sm:items-center sm:gap-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex grow items-center gap-2">
|
||||||
{tabTypes.map((tab) => {
|
{tabTypes.map((tab) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
className={`relative flex items-center justify-center rounded-md border p-1 px-3 text-sm ${
|
className={`relative flex w-full items-center justify-center whitespace-nowrap rounded-md border p-1 px-3 text-sm sm:w-auto ${
|
||||||
activeTab === tab.value ? ' border-gray-400 bg-gray-200 ' : ''
|
activeTab === tab.value ? ' border-gray-400 bg-gray-200 ' : ''
|
||||||
} w-full sm:w-auto`}
|
} w-full sm:w-auto`}
|
||||||
onClick={() => setActiveTab(tab.value)}
|
onClick={() => setActiveTab(tab.value)}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { type ReactNode, useCallback, useState } from 'react';
|
import { type ReactNode, useCallback, useState, useMemo } from 'react';
|
||||||
import { Globe2, Loader2, Lock } from 'lucide-react';
|
import { Globe2, Loader2, Lock } from 'lucide-react';
|
||||||
import { type ListFriendsResponse, ShareFriendList } from './ShareFriendList';
|
import { type ListFriendsResponse, ShareFriendList } from './ShareFriendList';
|
||||||
import { TransferToTeamList } from './TransferToTeamList';
|
import { TransferToTeamList } from './TransferToTeamList';
|
||||||
@@ -49,7 +49,10 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
const [isSettingsUpdated, setIsSettingsUpdated] = useState(false);
|
const [isSettingsUpdated, setIsSettingsUpdated] = useState(false);
|
||||||
const [friends, setFriends] = useState<ListFriendsResponse>([]);
|
const [friends, setFriends] = useState<ListFriendsResponse>([]);
|
||||||
const [teams, setTeams] = useState<UserTeamItem[]>([]);
|
const [teams, setTeams] = useState<UserTeamItem[]>([]);
|
||||||
const [members, setMembers] = useState<TeamMemberList[]>([]);
|
|
||||||
|
// Using global team members loading state to avoid glitchy UI when switching between teams
|
||||||
|
const [isTeamMembersLoading, setIsTeamMembersLoading] = useState(false);
|
||||||
|
const membersCache = useMemo(() => new Map<string, TeamMemberList[]>(), []);
|
||||||
|
|
||||||
const [visibility, setVisibility] = useState(defaultVisibility);
|
const [visibility, setVisibility] = useState(defaultVisibility);
|
||||||
const [sharedTeamMemberIds, setSharedTeamMemberIds] = useState<string[]>(
|
const [sharedTeamMemberIds, setSharedTeamMemberIds] = useState<string[]>(
|
||||||
@@ -118,7 +121,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTransferToTeam = useCallback(
|
const handleTransferToTeam = useCallback(
|
||||||
async (teamId: string) => {
|
async (teamId: string, sharedTeamMemberIds: string[]) => {
|
||||||
if (!roadmapId) {
|
if (!roadmapId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -128,6 +131,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
`${import.meta.env.PUBLIC_API_URL}/v1-transfer-roadmap/${roadmapId}`,
|
`${import.meta.env.PUBLIC_API_URL}/v1-transfer-roadmap/${roadmapId}`,
|
||||||
{
|
{
|
||||||
teamId,
|
teamId,
|
||||||
|
sharedTeamMemberIds,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -187,6 +191,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
defaultSharedFriendIds.length > 0 ? defaultSharedFriendIds : []
|
defaultSharedFriendIds.length > 0 ? defaultSharedFriendIds : []
|
||||||
);
|
);
|
||||||
} else if (visibility === 'team' && teamId) {
|
} else if (visibility === 'team' && teamId) {
|
||||||
|
setIsTeamMembersLoading(true);
|
||||||
setSharedTeamMemberIds(
|
setSharedTeamMemberIds(
|
||||||
defaultSharedMemberIds?.length > 0 ? defaultSharedMemberIds : []
|
defaultSharedMemberIds?.length > 0 ? defaultSharedMemberIds : []
|
||||||
);
|
);
|
||||||
@@ -225,14 +230,6 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
setSharedFriendIds={setSharedFriendIds}
|
setSharedFriendIds={setSharedFriendIds}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canTransferRoadmap && (
|
|
||||||
<TransferToTeamList
|
|
||||||
teams={teams}
|
|
||||||
setTeams={setTeams}
|
|
||||||
selectedTeamId={selectedTeamId}
|
|
||||||
setSelectedTeamId={setSelectedTeamId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* For Team Roadmap */}
|
{/* For Team Roadmap */}
|
||||||
{visibility === 'team' && teamId && (
|
{visibility === 'team' && teamId && (
|
||||||
@@ -240,10 +237,44 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
teamId={teamId}
|
teamId={teamId}
|
||||||
sharedTeamMemberIds={sharedTeamMemberIds}
|
sharedTeamMemberIds={sharedTeamMemberIds}
|
||||||
setSharedTeamMemberIds={setSharedTeamMemberIds}
|
setSharedTeamMemberIds={setSharedTeamMemberIds}
|
||||||
members={members}
|
membersCache={membersCache}
|
||||||
setMembers={setMembers}
|
isTeamMembersLoading={isTeamMembersLoading}
|
||||||
|
setIsTeamMembersLoading={setIsTeamMembersLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{canTransferRoadmap && (
|
||||||
|
<>
|
||||||
|
<TransferToTeamList
|
||||||
|
teams={teams}
|
||||||
|
setTeams={setTeams}
|
||||||
|
selectedTeamId={selectedTeamId}
|
||||||
|
setSelectedTeamId={setSelectedTeamId}
|
||||||
|
isTeamMembersLoading={isTeamMembersLoading}
|
||||||
|
setIsTeamMembersLoading={setIsTeamMembersLoading}
|
||||||
|
onTeamChange={() => {
|
||||||
|
setIsTeamMembersLoading(true);
|
||||||
|
setSharedTeamMemberIds([]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{selectedTeamId && (
|
||||||
|
<>
|
||||||
|
<hr className="-mx-4 my-4" />
|
||||||
|
<div className="mb-4">
|
||||||
|
<ShareTeamMemberList
|
||||||
|
title="Select who can access this roadmap. You can change this later."
|
||||||
|
teamId={selectedTeamId!}
|
||||||
|
sharedTeamMemberIds={sharedTeamMemberIds}
|
||||||
|
setSharedTeamMemberIds={setSharedTeamMemberIds}
|
||||||
|
membersCache={membersCache}
|
||||||
|
isTeamMembersLoading={isTeamMembersLoading}
|
||||||
|
setIsTeamMembersLoading={setIsTeamMembersLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 flex items-center justify-between gap-1.5">
|
<div className="mt-2 flex items-center justify-between gap-1.5">
|
||||||
@@ -255,17 +286,23 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{canTransferRoadmap ? (
|
{canTransferRoadmap && (
|
||||||
<UpdateAction
|
<UpdateAction
|
||||||
disabled={isUpdateDisabled || isLoading}
|
disabled={
|
||||||
|
isUpdateDisabled || isLoading || sharedTeamMemberIds.length === 0
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleTransferToTeam(selectedTeamId!).then(() => null);
|
handleTransferToTeam(selectedTeamId!, sharedTeamMemberIds).then(
|
||||||
|
() => null
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLoading && <Loader2 className="h-4 w-4 animate-spin" />}
|
{isLoading && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||||
Transfer
|
Transfer
|
||||||
</UpdateAction>
|
</UpdateAction>
|
||||||
) : (
|
)}
|
||||||
|
|
||||||
|
{!canTransferRoadmap && (
|
||||||
<UpdateAction
|
<UpdateAction
|
||||||
disabled={isUpdateDisabled || isLoading}
|
disabled={isUpdateDisabled || isLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@@ -33,27 +33,31 @@ export interface TeamMemberList extends TeamMemberDocument {
|
|||||||
|
|
||||||
type ShareTeamMemberListProps = {
|
type ShareTeamMemberListProps = {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
setMembers: (members: TeamMemberList[]) => void;
|
title?: string;
|
||||||
members: TeamMemberList[];
|
|
||||||
sharedTeamMemberIds: string[];
|
sharedTeamMemberIds: string[];
|
||||||
setSharedTeamMemberIds: (sharedTeamMemberIds: string[]) => void;
|
setSharedTeamMemberIds: (sharedTeamMemberIds: string[]) => void;
|
||||||
|
|
||||||
|
membersCache: Map<string, TeamMemberList[]>;
|
||||||
|
isTeamMembersLoading: boolean;
|
||||||
|
setIsTeamMembersLoading: (isLoading: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ShareTeamMemberList(props: ShareTeamMemberListProps) {
|
export function ShareTeamMemberList(props: ShareTeamMemberListProps) {
|
||||||
const {
|
const {
|
||||||
setMembers,
|
teamId,
|
||||||
members,
|
title = 'Select Members',
|
||||||
sharedTeamMemberIds,
|
sharedTeamMemberIds,
|
||||||
setSharedTeamMemberIds,
|
setSharedTeamMemberIds,
|
||||||
teamId,
|
|
||||||
|
membersCache,
|
||||||
|
isTeamMembersLoading: isLoading,
|
||||||
|
setIsTeamMembersLoading: setIsLoading,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
async function loadTeamMembers() {
|
async function loadTeamMembers() {
|
||||||
if (members?.length > 0) {
|
if (membersCache.has(teamId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,21 +71,21 @@ export function ShareTeamMemberList(props: ShareTeamMemberListProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMembers(response);
|
membersCache.set(teamId, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTeamMembers().finally(() => {
|
loadTeamMembers().finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [teamId]);
|
||||||
|
|
||||||
const loadingMembers = isLoading && (
|
const loadingMembers = isLoading && (
|
||||||
<ul className="mt-2 grid grid-cols-3 gap-2.5">
|
<ul className="mt-2 grid grid-cols-3 gap-2.5">
|
||||||
{[...Array(3)].map((_, idx) => (
|
{[...Array(3)].map((_, idx) => (
|
||||||
<li
|
<li
|
||||||
key={idx}
|
key={idx}
|
||||||
className="flex min-h-[62px] animate-pulse items-center gap-2 rounded-md border p-2"
|
className="flex min-h-[66px] animate-pulse items-center gap-2 rounded-md border p-2"
|
||||||
>
|
>
|
||||||
<div className="h-8 w-8 shrink-0 rounded-full bg-gray-200" />
|
<div className="h-8 w-8 shrink-0 rounded-full bg-gray-200" />
|
||||||
<div className="inline-grid w-full">
|
<div className="inline-grid w-full">
|
||||||
@@ -93,11 +97,13 @@ export function ShareTeamMemberList(props: ShareTeamMemberListProps) {
|
|||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const members = membersCache.get(teamId) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(members.length > 0 || isLoading) && (
|
{(members.length > 0 || isLoading) && (
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<p className="text-sm">Select Members</p>
|
<p className="text-sm">{title}</p>
|
||||||
|
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
|
@@ -11,10 +11,22 @@ type TransferToTeamListProps = {
|
|||||||
|
|
||||||
selectedTeamId: string | null;
|
selectedTeamId: string | null;
|
||||||
setSelectedTeamId: (teamId: string | null) => void;
|
setSelectedTeamId: (teamId: string | null) => void;
|
||||||
|
|
||||||
|
isTeamMembersLoading: boolean;
|
||||||
|
setIsTeamMembersLoading: (isLoading: boolean) => void;
|
||||||
|
onTeamChange: (teamId: string | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TransferToTeamList(props: TransferToTeamListProps) {
|
export function TransferToTeamList(props: TransferToTeamListProps) {
|
||||||
const { teams, setTeams, selectedTeamId, setSelectedTeamId } = props;
|
const {
|
||||||
|
teams,
|
||||||
|
setTeams,
|
||||||
|
selectedTeamId,
|
||||||
|
setSelectedTeamId,
|
||||||
|
isTeamMembersLoading,
|
||||||
|
setIsTeamMembersLoading,
|
||||||
|
onTeamChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -73,11 +85,17 @@ export function TransferToTeamList(props: TransferToTeamListProps) {
|
|||||||
<li key={team._id}>
|
<li key={team._id}>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex w-full items-center gap-2.5 rounded-lg border p-2.5',
|
'relative flex w-full items-center gap-2.5 rounded-lg border p-2.5 disabled:cursor-not-allowed disabled:opacity-70',
|
||||||
isSelected && 'border-gray-500 bg-gray-100 text-black'
|
isSelected && 'border-gray-500 bg-gray-100 text-black'
|
||||||
)}
|
)}
|
||||||
|
disabled={isTeamMembersLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedTeamId(team._id);
|
if (isSelected) {
|
||||||
|
setSelectedTeamId(null);
|
||||||
|
} else {
|
||||||
|
setSelectedTeamId(team._id);
|
||||||
|
}
|
||||||
|
onTeamChange(team._id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
Reference in New Issue
Block a user