mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-11 19:53:59 +02:00
Allow transferring roadmap between teams
This commit is contained in:
@@ -93,7 +93,7 @@ export function AccountDropdown() {
|
|||||||
).length;
|
).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-50 animate-fade-in">
|
<div className="relative z-[90] animate-fade-in">
|
||||||
{isOnboardingModalOpen && onboardingConfig && (
|
{isOnboardingModalOpen && onboardingConfig && (
|
||||||
<OnboardingModal
|
<OnboardingModal
|
||||||
onboardingConfig={onboardingConfig}
|
onboardingConfig={onboardingConfig}
|
||||||
|
@@ -28,7 +28,7 @@ export function PageProgress(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Tailwind based spinner for full page */}
|
{/* Tailwind based spinner for full page */}
|
||||||
<div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-white bg-opacity-75">
|
<div className="fixed left-0 top-0 z-[100] flex h-full w-full items-center justify-center bg-white bg-opacity-75">
|
||||||
<div className="flex items-center justify-center rounded-md border bg-white px-4 py-2 ">
|
<div className="flex items-center justify-center rounded-md border bg-white px-4 py-2 ">
|
||||||
<Spinner
|
<Spinner
|
||||||
className="h-4 w-4 sm:h-4 sm:w-4"
|
className="h-4 w-4 sm:h-4 sm:w-4"
|
||||||
|
@@ -53,6 +53,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isTransferringToTeam, setIsTransferringToTeam] = useState(false);
|
||||||
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[]>([]);
|
||||||
@@ -71,13 +72,12 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
);
|
);
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
|
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
|
||||||
|
|
||||||
const canTransferRoadmap = visibility === 'team' && !teamId;
|
|
||||||
let isUpdateDisabled = false;
|
let isUpdateDisabled = false;
|
||||||
// Disable update button if there are no friends to share with
|
// Disable update button if there are no friends to share with
|
||||||
if (visibility === 'friends' && sharedFriendIds.length === 0) {
|
if (visibility === 'friends' && sharedFriendIds.length === 0) {
|
||||||
isUpdateDisabled = true;
|
isUpdateDisabled = true;
|
||||||
// Disable update button if there are no team to transfer
|
// Disable update button if there are no team to transfer
|
||||||
} else if (canTransferRoadmap && !selectedTeamId) {
|
} else if (isTransferringToTeam && !selectedTeamId) {
|
||||||
isUpdateDisabled = true;
|
isUpdateDisabled = true;
|
||||||
// Disable update button if there are no members to share with
|
// Disable update button if there are no members to share with
|
||||||
} else if (
|
} else if (
|
||||||
@@ -198,6 +198,8 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ShareOptionTabs
|
<ShareOptionTabs
|
||||||
|
isTransferringToTeam={isTransferringToTeam}
|
||||||
|
setIsTransferringToTeam={setIsTransferringToTeam}
|
||||||
visibility={visibility}
|
visibility={visibility}
|
||||||
setVisibility={setVisibility}
|
setVisibility={setVisibility}
|
||||||
teamId={teamId}
|
teamId={teamId}
|
||||||
@@ -226,48 +228,52 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-4 flex grow flex-col">
|
<div className="mt-4 flex grow flex-col">
|
||||||
{visibility === 'public' && (
|
{!isTransferringToTeam && (
|
||||||
<div className="flex h-full flex-grow flex-col items-center justify-center rounded-md border bg-gray-50 text-center">
|
<>
|
||||||
<Globe2 className="mb-3 h-10 w-10 text-gray-300" />
|
{visibility === 'public' && (
|
||||||
<p className="font-medium text-gray-500">
|
<div className="flex h-full flex-grow flex-col items-center justify-center rounded-md border bg-gray-50 text-center">
|
||||||
Anyone with the link can access.
|
<Globe2 className="mb-3 h-10 w-10 text-gray-300" />
|
||||||
</p>
|
<p className="font-medium text-gray-500">
|
||||||
</div>
|
Anyone with the link can access.
|
||||||
)}
|
</p>
|
||||||
{visibility === 'me' && (
|
</div>
|
||||||
<div className="flex h-full flex-grow flex-col items-center justify-center rounded-md border bg-gray-50 text-center">
|
)}
|
||||||
<Lock className="mb-3 h-10 w-10 text-gray-300" />
|
{visibility === 'me' && (
|
||||||
<p className="font-medium text-gray-500">
|
<div className="flex h-full flex-grow flex-col items-center justify-center rounded-md border bg-gray-50 text-center">
|
||||||
Only you will be able to access.
|
<Lock className="mb-3 h-10 w-10 text-gray-300" />
|
||||||
</p>
|
<p className="font-medium text-gray-500">
|
||||||
</div>
|
Only you will be able to access.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* For Personal Roadmap */}
|
||||||
|
{visibility === 'friends' && (
|
||||||
|
<ShareFriendList
|
||||||
|
friends={friends}
|
||||||
|
setFriends={setFriends}
|
||||||
|
sharedFriendIds={sharedFriendIds}
|
||||||
|
setSharedFriendIds={setSharedFriendIds}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* For Team Roadmap */}
|
||||||
|
{visibility === 'team' && teamId && (
|
||||||
|
<ShareTeamMemberList
|
||||||
|
teamId={teamId}
|
||||||
|
sharedTeamMemberIds={sharedTeamMemberIds}
|
||||||
|
setSharedTeamMemberIds={setSharedTeamMemberIds}
|
||||||
|
membersCache={membersCache}
|
||||||
|
isTeamMembersLoading={isTeamMembersLoading}
|
||||||
|
setIsTeamMembersLoading={setIsTeamMembersLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* For Personal Roadmap */}
|
{isTransferringToTeam && (
|
||||||
{visibility === 'friends' && (
|
|
||||||
<ShareFriendList
|
|
||||||
friends={friends}
|
|
||||||
setFriends={setFriends}
|
|
||||||
sharedFriendIds={sharedFriendIds}
|
|
||||||
setSharedFriendIds={setSharedFriendIds}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* For Team Roadmap */}
|
|
||||||
{visibility === 'team' && teamId && (
|
|
||||||
<ShareTeamMemberList
|
|
||||||
teamId={teamId}
|
|
||||||
sharedTeamMemberIds={sharedTeamMemberIds}
|
|
||||||
setSharedTeamMemberIds={setSharedTeamMemberIds}
|
|
||||||
membersCache={membersCache}
|
|
||||||
isTeamMembersLoading={isTeamMembersLoading}
|
|
||||||
setIsTeamMembersLoading={setIsTeamMembersLoading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canTransferRoadmap && (
|
|
||||||
<>
|
<>
|
||||||
<TransferToTeamList
|
<TransferToTeamList
|
||||||
|
currentTeamId={teamId}
|
||||||
teams={teams}
|
teams={teams}
|
||||||
setTeams={setTeams}
|
setTeams={setTeams}
|
||||||
selectedTeamId={selectedTeamId}
|
selectedTeamId={selectedTeamId}
|
||||||
@@ -319,7 +325,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{canTransferRoadmap && (
|
{isTransferringToTeam && (
|
||||||
<UpdateAction
|
<UpdateAction
|
||||||
disabled={
|
disabled={
|
||||||
isUpdateDisabled || isLoading || sharedTeamMemberIds.length === 0
|
isUpdateDisabled || isLoading || sharedTeamMemberIds.length === 0
|
||||||
@@ -335,7 +341,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
|
|||||||
</UpdateAction>
|
</UpdateAction>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!canTransferRoadmap && (
|
{!isTransferringToTeam && (
|
||||||
<UpdateAction
|
<UpdateAction
|
||||||
disabled={isUpdateDisabled || isLoading}
|
disabled={isUpdateDisabled || isLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@@ -8,6 +8,8 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { AllowedRoadmapVisibility } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
|
import type { AllowedRoadmapVisibility } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
|
||||||
import { cn } from '../../lib/classname';
|
import { cn } from '../../lib/classname';
|
||||||
|
import { $teamList } from '../../stores/team.ts';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
|
||||||
export const allowedVisibilityLabels: {
|
export const allowedVisibilityLabels: {
|
||||||
id: AllowedRoadmapVisibility;
|
id: AllowedRoadmapVisibility;
|
||||||
@@ -44,15 +46,29 @@ export const allowedVisibilityLabels: {
|
|||||||
type ShareOptionTabsProps = {
|
type ShareOptionTabsProps = {
|
||||||
visibility: AllowedRoadmapVisibility;
|
visibility: AllowedRoadmapVisibility;
|
||||||
setVisibility: (visibility: AllowedRoadmapVisibility) => void;
|
setVisibility: (visibility: AllowedRoadmapVisibility) => void;
|
||||||
|
|
||||||
|
isTransferringToTeam: boolean;
|
||||||
|
setIsTransferringToTeam: (isTransferringToTeam: boolean) => void;
|
||||||
|
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
|
|
||||||
onChange: (visibility: AllowedRoadmapVisibility) => void;
|
onChange: (visibility: AllowedRoadmapVisibility) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ShareOptionTabs(props: ShareOptionTabsProps) {
|
export function ShareOptionTabs(props: ShareOptionTabsProps) {
|
||||||
const { visibility, setVisibility, teamId, onChange } = props;
|
const {
|
||||||
|
isTransferringToTeam,
|
||||||
|
setIsTransferringToTeam,
|
||||||
|
visibility,
|
||||||
|
setVisibility,
|
||||||
|
teamId,
|
||||||
|
onChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const handleClick = (visibility: AllowedRoadmapVisibility) => {
|
const teamList = useStore($teamList);
|
||||||
|
|
||||||
|
const handleTabClick = (visibility: AllowedRoadmapVisibility) => {
|
||||||
|
setIsTransferringToTeam(false);
|
||||||
setVisibility(visibility);
|
setVisibility(visibility);
|
||||||
onChange(visibility);
|
onChange(visibility);
|
||||||
};
|
};
|
||||||
@@ -63,11 +79,9 @@ export function ShareOptionTabs(props: ShareOptionTabsProps) {
|
|||||||
{allowedVisibilityLabels.map((v) => {
|
{allowedVisibilityLabels.map((v) => {
|
||||||
if (v.id === 'friends' && teamId) {
|
if (v.id === 'friends' && teamId) {
|
||||||
return null;
|
return null;
|
||||||
} else if (v.id === 'team' && !teamId) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isActive = v.id === visibility;
|
const isActive = !isTransferringToTeam && v.id === visibility;
|
||||||
return (
|
return (
|
||||||
<li key={v.id}>
|
<li key={v.id}>
|
||||||
<OptionTab
|
<OptionTab
|
||||||
@@ -75,21 +89,21 @@ export function ShareOptionTabs(props: ShareOptionTabsProps) {
|
|||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
icon={v.icon}
|
icon={v.icon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleClick(v.id);
|
handleTabClick(v.id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
{!teamId && (
|
{(!teamId || teamList.length > 1) && (
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<OptionTab
|
<OptionTab
|
||||||
label="Transfer to team"
|
label="Transfer to team"
|
||||||
icon={ArrowLeftRight}
|
icon={ArrowLeftRight}
|
||||||
isActive={visibility === 'team'}
|
isActive={isTransferringToTeam}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleClick('team');
|
setIsTransferringToTeam(true);
|
||||||
}}
|
}}
|
||||||
className='border-red-300 text-red-600 hover:border-red-200 hover:bg-red-50 data-[active="true"]:border-red-600 data-[active="true"]:bg-red-600 data-[active="true"]:text-white'
|
className='border-red-300 text-red-600 hover:border-red-200 hover:bg-red-50 data-[active="true"]:border-red-600 data-[active="true"]:bg-red-600 data-[active="true"]:text-white'
|
||||||
/>
|
/>
|
||||||
@@ -115,7 +129,7 @@ function OptionTab(props: OptionTabProps) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center gap-2 rounded-md border px-3 py-2 text-sm text-black hover:border-gray-300 hover:bg-gray-100',
|
'flex items-center justify-center gap-2 rounded-md border px-3 py-2 text-sm text-black hover:border-gray-300 hover:bg-gray-100',
|
||||||
'data-[active="true"]:border-gray-500 data-[active="true"]:bg-gray-200 data-[active="true"]:text-black',
|
'data-[active="true"]:border-gray-500 data-[active="true"]:bg-gray-200 data-[active="true"]:text-black',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
disabled={isActive}
|
disabled={isActive}
|
||||||
|
@@ -82,25 +82,24 @@ export function ShareSuccess(props: ShareSuccessProps) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-2 border-t pt-2">
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
You can also embed this roadmap on your website.
|
|
||||||
</p>
|
|
||||||
<div className="mt-2">
|
|
||||||
<input
|
|
||||||
onClick={(e) => {
|
|
||||||
e.currentTarget.select();
|
|
||||||
copyText(embedHtml);
|
|
||||||
}}
|
|
||||||
readOnly={true}
|
|
||||||
className="w-full resize-none rounded-md border bg-gray-50 p-2 text-sm"
|
|
||||||
value={embedHtml}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{visibility === 'public' && (
|
{visibility === 'public' && (
|
||||||
<>
|
<>
|
||||||
|
<div className="mt-2 border-t pt-2">
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
You can also embed this roadmap on your website.
|
||||||
|
</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<input
|
||||||
|
onClick={(e) => {
|
||||||
|
e.currentTarget.select();
|
||||||
|
copyText(embedHtml);
|
||||||
|
}}
|
||||||
|
readOnly={true}
|
||||||
|
className="w-full resize-none rounded-md border bg-gray-50 p-2 text-sm"
|
||||||
|
value={embedHtml}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="-mx-4 mt-4 flex items-center gap-1.5">
|
<div className="-mx-4 mt-4 flex items-center gap-1.5">
|
||||||
<span className="h-px grow bg-gray-300" />
|
<span className="h-px grow bg-gray-300" />
|
||||||
<span className="px-2 text-xs uppercase text-gray-400">Or</span>
|
<span className="px-2 text-xs uppercase text-gray-400">Or</span>
|
||||||
|
@@ -9,6 +9,7 @@ type TransferToTeamListProps = {
|
|||||||
teams: UserTeamItem[];
|
teams: UserTeamItem[];
|
||||||
setTeams: (teams: UserTeamItem[]) => void;
|
setTeams: (teams: UserTeamItem[]) => void;
|
||||||
|
|
||||||
|
currentTeamId?: string;
|
||||||
selectedTeamId: string | null;
|
selectedTeamId: string | null;
|
||||||
setSelectedTeamId: (teamId: string | null) => void;
|
setSelectedTeamId: (teamId: string | null) => void;
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ export function TransferToTeamList(props: TransferToTeamListProps) {
|
|||||||
selectedTeamId,
|
selectedTeamId,
|
||||||
setSelectedTeamId,
|
setSelectedTeamId,
|
||||||
isTeamMembersLoading,
|
isTeamMembersLoading,
|
||||||
|
currentTeamId,
|
||||||
setIsTeamMembersLoading,
|
setIsTeamMembersLoading,
|
||||||
onTeamChange,
|
onTeamChange,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -38,7 +40,7 @@ export function TransferToTeamList(props: TransferToTeamListProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { response, error } = await httpGet<UserTeamItem[]>(
|
const { response, error } = await httpGet<UserTeamItem[]>(
|
||||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-teams`
|
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-teams`,
|
||||||
);
|
);
|
||||||
if (error || !response) {
|
if (error || !response) {
|
||||||
toast.error(error?.message || 'Something went wrong');
|
toast.error(error?.message || 'Something went wrong');
|
||||||
@@ -46,7 +48,7 @@ export function TransferToTeamList(props: TransferToTeamListProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTeams(
|
setTeams(
|
||||||
response.filter((team) => ['admin', 'manager'].includes(team.role))
|
response.filter((team) => ['admin', 'manager'].includes(team.role)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +82,16 @@ export function TransferToTeamList(props: TransferToTeamListProps) {
|
|||||||
<ul className="mt-2 grid grid-cols-3 gap-1.5">
|
<ul className="mt-2 grid grid-cols-3 gap-1.5">
|
||||||
{teams.map((team) => {
|
{teams.map((team) => {
|
||||||
const isSelected = team._id === selectedTeamId;
|
const isSelected = team._id === selectedTeamId;
|
||||||
|
if (team._id === currentTeamId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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 disabled:cursor-not-allowed disabled:opacity-70',
|
'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}
|
disabled={isTeamMembersLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
Reference in New Issue
Block a user