mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-24 09:55:57 +02:00
Updates to team functionality
This commit is contained in:
@@ -86,10 +86,7 @@ export function RoleDropdown(props: RoleDropdownProps) {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`capitalize ${
|
className={`capitalize`}>
|
||||||
selectedRole === 'admin' ? 'text-blue-600' : ''
|
|
||||||
} ${selectedRole === 'manager' ? 'text-cyan-600' : ''}`}
|
|
||||||
>
|
|
||||||
{selectedRole || 'Select Role'}
|
{selectedRole || 'Select Role'}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { useRef, useState } from 'preact/hooks';
|
import { useRef, useState } from 'preact/hooks';
|
||||||
import type { TeamMemberDocument } from './TeamMembersPage';
|
import type { TeamMemberDocument } from './TeamMembersPage';
|
||||||
import { httpDelete, httpPatch } from '../../lib/http';
|
|
||||||
import MoreIcon from '../../icons/more-vertical.svg';
|
import MoreIcon from '../../icons/more-vertical.svg';
|
||||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||||
import { useToast } from '../../hooks/use-toast';
|
import { useToast } from '../../hooks/use-toast';
|
||||||
|
import { MailIcon } from '../ReactIcons/MailIcon';
|
||||||
|
|
||||||
export function MemberActionDropdown({
|
export function MemberActionDropdown({
|
||||||
member,
|
member,
|
||||||
@@ -11,14 +11,19 @@ export function MemberActionDropdown({
|
|||||||
onDeleteMember,
|
onDeleteMember,
|
||||||
onResendInvite,
|
onResendInvite,
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
|
onSendProgressReminder,
|
||||||
|
allowProgressReminder = false,
|
||||||
|
allowUpdateRole = true,
|
||||||
}: {
|
}: {
|
||||||
onDeleteMember: () => void;
|
onDeleteMember: () => void;
|
||||||
onUpdateMember: () => void;
|
onUpdateMember: () => void;
|
||||||
onResendInvite: () => void;
|
onResendInvite: () => void;
|
||||||
|
onSendProgressReminder: () => void;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
allowProgressReminder: boolean;
|
||||||
|
allowUpdateRole: boolean;
|
||||||
member: TeamMemberDocument;
|
member: TeamMemberDocument;
|
||||||
}) {
|
}) {
|
||||||
const toast = useToast();
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -35,6 +40,8 @@ export function MemberActionDropdown({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(allowUpdateRole
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Update Role',
|
name: 'Update Role',
|
||||||
handleClick: () => {
|
handleClick: () => {
|
||||||
@@ -42,6 +49,19 @@ export function MemberActionDropdown({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(allowProgressReminder
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'Send Progress Reminder',
|
||||||
|
handleClick: () => {
|
||||||
|
onSendProgressReminder();
|
||||||
|
setIsOpen(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
...(['invited'].includes(member.status)
|
...(['invited'].includes(member.status)
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -67,7 +87,7 @@ export function MemberActionDropdown({
|
|||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className="align-right absolute right-0 top-full z-50 mt-1 w-32 rounded-md bg-slate-800 px-2 py-2 text-white shadow-md"
|
className="align-right absolute right-0 top-full z-50 mt-1 w-[200px] rounded-md bg-slate-800 px-2 py-2 text-white shadow-md"
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
{actions.map((action, index) => {
|
{actions.map((action, index) => {
|
||||||
|
@@ -2,6 +2,8 @@ import { MailIcon } from '../ReactIcons/MailIcon';
|
|||||||
import { MemberActionDropdown } from './MemberActionDropdown';
|
import { MemberActionDropdown } from './MemberActionDropdown';
|
||||||
import { MemberRoleBadge } from './RoleBadge';
|
import { MemberRoleBadge } from './RoleBadge';
|
||||||
import type { TeamMemberItem } from './TeamMembersPage';
|
import type { TeamMemberItem } from './TeamMembersPage';
|
||||||
|
import { $canManageCurrentTeam } from '../../stores/team';
|
||||||
|
import { useStore } from '@nanostores/preact';
|
||||||
|
|
||||||
type TeamMemberProps = {
|
type TeamMemberProps = {
|
||||||
member: TeamMemberItem;
|
member: TeamMemberItem;
|
||||||
@@ -9,9 +11,9 @@ type TeamMemberProps = {
|
|||||||
index: number;
|
index: number;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
canManageCurrentTeam: boolean;
|
canManageCurrentTeam: boolean;
|
||||||
handleDeleteMember: () => void;
|
onDeleteMember: () => void;
|
||||||
onUpdateMember: () => void;
|
onUpdateMember: () => void;
|
||||||
handleSendReminder: () => void;
|
onSendProgressReminder: () => void;
|
||||||
onResendInvite: () => void;
|
onResendInvite: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,16 +25,17 @@ export function TeamMemberItem(props: TeamMemberProps) {
|
|||||||
onUpdateMember,
|
onUpdateMember,
|
||||||
canManageCurrentTeam,
|
canManageCurrentTeam,
|
||||||
userId,
|
userId,
|
||||||
handleDeleteMember,
|
onDeleteMember,
|
||||||
handleSendReminder,
|
onSendProgressReminder,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const showNoProgress =
|
const canManageTeam = useStore($canManageCurrentTeam);
|
||||||
member.progress.length === 0 && member.status === 'joined';
|
const showNoProgressBadge = !member.hasProgress && member.status === 'joined';
|
||||||
const showReminder =
|
const allowProgressReminder =
|
||||||
member.progress.length === 0 &&
|
canManageTeam &&
|
||||||
|
!member.hasProgress &&
|
||||||
member.status === 'joined' &&
|
member.status === 'joined' &&
|
||||||
!(member.userId === userId);
|
member.userId !== userId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -53,15 +56,12 @@ export function TeamMemberItem(props: TeamMemberProps) {
|
|||||||
<div>
|
<div>
|
||||||
<div className="mb-1 flex items-center gap-2 sm:hidden">
|
<div className="mb-1 flex items-center gap-2 sm:hidden">
|
||||||
<MemberRoleBadge role={member.role} />
|
<MemberRoleBadge role={member.role} />
|
||||||
{showReminder && (
|
|
||||||
<SendProgressReminder handleSendReminder={handleSendReminder} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<h3 className="inline-grid grid-cols-[auto_auto_auto] items-center font-medium">
|
<h3 className="inline-grid grid-cols-[auto_auto_auto] items-center font-medium">
|
||||||
<span className="truncate">{member.name}</span>
|
<span className="truncate">{member.name}</span>
|
||||||
{showNoProgress && (
|
{showNoProgressBadge && (
|
||||||
<span className="ml-2 rounded-full bg-gray-600 px-2 py-0.5 text-xs font-normal text-white sm:inline">
|
<span className="ml-2 rounded-full bg-red-400 px-2 py-0.5 text-xs font-normal text-white">
|
||||||
No Progress
|
No Progress
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -91,18 +91,16 @@ export function TeamMemberItem(props: TeamMemberProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center text-sm">
|
<div className="flex shrink-0 items-center text-sm">
|
||||||
{showReminder && (
|
|
||||||
<span className="hidden sm:block">
|
|
||||||
<SendProgressReminder handleSendReminder={handleSendReminder} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span class={'hidden sm:block'}>
|
<span class={'hidden sm:block'}>
|
||||||
<MemberRoleBadge role={member.role} />
|
<MemberRoleBadge role={member.role} />
|
||||||
</span>
|
</span>
|
||||||
{canManageCurrentTeam && (
|
{canManageCurrentTeam && (
|
||||||
<MemberActionDropdown
|
<MemberActionDropdown
|
||||||
|
allowUpdateRole={member.status !== 'rejected'}
|
||||||
|
allowProgressReminder={allowProgressReminder}
|
||||||
onResendInvite={onResendInvite}
|
onResendInvite={onResendInvite}
|
||||||
onDeleteMember={handleDeleteMember}
|
onSendProgressReminder={onSendProgressReminder}
|
||||||
|
onDeleteMember={onDeleteMember}
|
||||||
isDisabled={member.userId === userId}
|
isDisabled={member.userId === userId}
|
||||||
onUpdateMember={onUpdateMember}
|
onUpdateMember={onUpdateMember}
|
||||||
member={member}
|
member={member}
|
||||||
@@ -123,10 +121,10 @@ function SendProgressReminder(props: SendProgressReminderProps) {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleSendReminder}
|
onClick={handleSendReminder}
|
||||||
className="mr-2 flex items-center gap-1.5 whitespace-nowrap rounded-full bg-orange-100 px-2 py-0.5 text-xs text-orange-700"
|
className="ml-2 flex items-center gap-1.5 whitespace-nowrap rounded-full bg-orange-100 px-2 py-0.5 text-xs text-orange-700"
|
||||||
>
|
>
|
||||||
<MailIcon className="h-3 w-3" />
|
<MailIcon className="h-3 w-3" />
|
||||||
<span>Reminder</span>
|
<span>Remind</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
import { httpDelete, httpGet, httpPatch } from '../../lib/http';
|
import { httpDelete, httpGet, httpPatch } from '../../lib/http';
|
||||||
import { MemberActionDropdown } from './MemberActionDropdown';
|
|
||||||
import { useAuth } from '../../hooks/use-auth';
|
import { useAuth } from '../../hooks/use-auth';
|
||||||
import { pageProgressMessage } from '../../stores/page';
|
import { pageProgressMessage } from '../../stores/page';
|
||||||
import type { TeamDocument } from '../CreateTeam/CreateTeamForm';
|
import type { TeamDocument } from '../CreateTeam/CreateTeamForm';
|
||||||
@@ -13,7 +12,6 @@ import { UpdateMemberPopup } from './UpdateMemberPopup';
|
|||||||
import { useStore } from '@nanostores/preact';
|
import { useStore } from '@nanostores/preact';
|
||||||
import { $canManageCurrentTeam } from '../../stores/team';
|
import { $canManageCurrentTeam } from '../../stores/team';
|
||||||
import { useToast } from '../../hooks/use-toast';
|
import { useToast } from '../../hooks/use-toast';
|
||||||
import { MemberRoleBadge } from './RoleBadge';
|
|
||||||
import { TeamMemberItem } from './TeamMemberItem';
|
import { TeamMemberItem } from './TeamMemberItem';
|
||||||
|
|
||||||
export interface TeamMemberDocument {
|
export interface TeamMemberDocument {
|
||||||
@@ -43,7 +41,7 @@ export interface UserResourceProgressDocument {
|
|||||||
export interface TeamMemberItem extends TeamMemberDocument {
|
export interface TeamMemberItem extends TeamMemberDocument {
|
||||||
name: string;
|
name: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
progress: UserResourceProgressDocument[];
|
hasProgress: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TeamMembersPage() {
|
export function TeamMembersPage() {
|
||||||
@@ -187,7 +185,7 @@ export function TeamMembersPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-b-sm rounded-t-md border">
|
<div className="rounded-md border">
|
||||||
<div className="flex items-center justify-between gap-2 border-b p-3">
|
<div className="flex items-center justify-between gap-2 border-b p-3">
|
||||||
<p className="hidden text-sm sm:block">
|
<p className="hidden text-sm sm:block">
|
||||||
{teamMembers.length} people in the team.
|
{teamMembers.length} people in the team.
|
||||||
@@ -211,7 +209,7 @@ export function TeamMembersPage() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
canManageCurrentTeam={canManageCurrentTeam}
|
canManageCurrentTeam={canManageCurrentTeam}
|
||||||
handleDeleteMember={() => {
|
onDeleteMember={() => {
|
||||||
deleteMember(teamId, member._id!).finally(() => {
|
deleteMember(teamId, member._id!).finally(() => {
|
||||||
pageProgressMessage.set('');
|
pageProgressMessage.set('');
|
||||||
});
|
});
|
||||||
@@ -219,7 +217,7 @@ export function TeamMembersPage() {
|
|||||||
onUpdateMember={() => {
|
onUpdateMember={() => {
|
||||||
setMemberToUpdate(member);
|
setMemberToUpdate(member);
|
||||||
}}
|
}}
|
||||||
handleSendReminder={() => {
|
onSendProgressReminder={() => {
|
||||||
handleSendReminder(teamId, member._id!).finally(() => {
|
handleSendReminder(teamId, member._id!).finally(() => {
|
||||||
pageProgressMessage.set('');
|
pageProgressMessage.set('');
|
||||||
});
|
});
|
||||||
@@ -231,8 +229,8 @@ export function TeamMembersPage() {
|
|||||||
|
|
||||||
{invitedMembers.length > 0 && (
|
{invitedMembers.length > 0 && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h3 className="text-xl font-medium">Invited Members</h3>
|
<h3 className="text-xs uppercase text-gray-400">Invited Members</h3>
|
||||||
<div className="mt-2 rounded-b-sm rounded-t-md border">
|
<div className="mt-2 rounded-md border">
|
||||||
{invitedMembers.map((member, index) => {
|
{invitedMembers.map((member, index) => {
|
||||||
return (
|
return (
|
||||||
<TeamMemberItem
|
<TeamMemberItem
|
||||||
@@ -247,7 +245,7 @@ export function TeamMembersPage() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
canManageCurrentTeam={canManageCurrentTeam}
|
canManageCurrentTeam={canManageCurrentTeam}
|
||||||
handleDeleteMember={() => {
|
onDeleteMember={() => {
|
||||||
deleteMember(teamId, member._id!).finally(() => {
|
deleteMember(teamId, member._id!).finally(() => {
|
||||||
pageProgressMessage.set('');
|
pageProgressMessage.set('');
|
||||||
});
|
});
|
||||||
@@ -255,7 +253,7 @@ export function TeamMembersPage() {
|
|||||||
onUpdateMember={() => {
|
onUpdateMember={() => {
|
||||||
setMemberToUpdate(member);
|
setMemberToUpdate(member);
|
||||||
}}
|
}}
|
||||||
handleSendReminder={() => {
|
onSendProgressReminder={() => {
|
||||||
handleSendReminder(teamId, member._id!).finally(() => {
|
handleSendReminder(teamId, member._id!).finally(() => {
|
||||||
pageProgressMessage.set('');
|
pageProgressMessage.set('');
|
||||||
});
|
});
|
||||||
@@ -269,7 +267,7 @@ export function TeamMembersPage() {
|
|||||||
|
|
||||||
{rejectedMembers.length > 0 && (
|
{rejectedMembers.length > 0 && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h3 className="text-xl font-medium">Rejected Members</h3>
|
<h3 className="text-xs uppercase text-gray-400">Rejected Invites</h3>
|
||||||
<div className="mt-2 rounded-b-sm rounded-t-md border">
|
<div className="mt-2 rounded-b-sm rounded-t-md border">
|
||||||
{rejectedMembers.map((member, index) => {
|
{rejectedMembers.map((member, index) => {
|
||||||
return (
|
return (
|
||||||
@@ -285,7 +283,7 @@ export function TeamMembersPage() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
canManageCurrentTeam={canManageCurrentTeam}
|
canManageCurrentTeam={canManageCurrentTeam}
|
||||||
handleDeleteMember={() => {
|
onDeleteMember={() => {
|
||||||
deleteMember(teamId, member._id!).finally(() => {
|
deleteMember(teamId, member._id!).finally(() => {
|
||||||
pageProgressMessage.set('');
|
pageProgressMessage.set('');
|
||||||
});
|
});
|
||||||
@@ -293,7 +291,7 @@ export function TeamMembersPage() {
|
|||||||
onUpdateMember={() => {
|
onUpdateMember={() => {
|
||||||
setMemberToUpdate(member);
|
setMemberToUpdate(member);
|
||||||
}}
|
}}
|
||||||
handleSendReminder={() => {
|
onSendProgressReminder={() => {
|
||||||
handleSendReminder(teamId, member._id!).finally(() => {
|
handleSendReminder(teamId, member._id!).finally(() => {
|
||||||
pageProgressMessage.set('');
|
pageProgressMessage.set('');
|
||||||
});
|
});
|
||||||
|
@@ -141,10 +141,16 @@ export function UpdateTeamForm() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex w-full flex-col">
|
<div className="mt-4 flex w-full flex-col">
|
||||||
<label for="website" className="text-sm leading-none text-slate-500">
|
<label
|
||||||
|
for="website"
|
||||||
|
className={`text-sm leading-none text-slate-500 ${
|
||||||
|
teamType === 'company' ? 'after:content-["*"]' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
Website
|
Website
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
required={teamType === 'company'}
|
||||||
type="text"
|
type="text"
|
||||||
name="website"
|
name="website"
|
||||||
id="website"
|
id="website"
|
||||||
|
@@ -51,7 +51,7 @@ export function Toaster(props: Props) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
$toastMessage.set(undefined);
|
$toastMessage.set(undefined);
|
||||||
}}
|
}}
|
||||||
className={`fixed bottom-5 left-1/2 z-50 min-w-[300px] max-w-[300px] animate-fade-slide-up sm:min-w-[auto]`}
|
className={`fixed bottom-5 left-1/2 z-50 min-w-[375px] max-w-[375px] animate-fade-slide-up sm:min-w-[auto]`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex -translate-x-1/2 transform cursor-pointer items-center gap-2 rounded-md border border-gray-200 bg-white py-3 pl-4 pr-5 text-black shadow-md hover:bg-gray-50`}
|
className={`flex -translate-x-1/2 transform cursor-pointer items-center gap-2 rounded-md border border-gray-200 bg-white py-3 pl-4 pr-5 text-black shadow-md hover:bg-gray-50`}
|
||||||
|
Reference in New Issue
Block a user