mirror of
https://github.com/gosticks/plane.git
synced 2025-10-16 12:45:33 +00:00
[WEB-4882]feat: suspended users (#7844)
This commit is contained in:
parent
726529044e
commit
c45151d5e6
@ -27,6 +27,7 @@ export const useMemberColumns = () => {
|
|||||||
// derived values
|
// derived values
|
||||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||||
|
|
||||||
|
const isSuspended = (rowData: RowData) => rowData.is_active === false;
|
||||||
// handlers
|
// handlers
|
||||||
const handleDisplayFilterUpdate = (filterUpdates: Partial<IMemberFilters>) => {
|
const handleDisplayFilterUpdate = (filterUpdates: Partial<IMemberFilters>) => {
|
||||||
updateFilters(filterUpdates);
|
updateFilters(filterUpdates);
|
||||||
@ -58,6 +59,11 @@ export const useMemberColumns = () => {
|
|||||||
{
|
{
|
||||||
key: "Display name",
|
key: "Display name",
|
||||||
content: t("workspace_settings.settings.members.details.display_name"),
|
content: t("workspace_settings.settings.members.details.display_name"),
|
||||||
|
tdRender: (rowData: RowData) => (
|
||||||
|
<div className={`w-32 ${isSuspended(rowData) ? "text-custom-text-400" : ""}`}>
|
||||||
|
{rowData.member.display_name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
thRender: () => (
|
thRender: () => (
|
||||||
<MemberHeaderColumn
|
<MemberHeaderColumn
|
||||||
property="display_name"
|
property="display_name"
|
||||||
@ -65,12 +71,16 @@ export const useMemberColumns = () => {
|
|||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
tdRender: (rowData: RowData) => <div className="w-32">{rowData.member.display_name}</div>,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "Email address",
|
key: "Email address",
|
||||||
content: t("workspace_settings.settings.members.details.email_address"),
|
content: t("workspace_settings.settings.members.details.email_address"),
|
||||||
|
tdRender: (rowData: RowData) => (
|
||||||
|
<div className={`w-48 truncate ${isSuspended(rowData) ? "text-custom-text-400" : ""}`}>
|
||||||
|
{rowData.member.email}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
thRender: () => (
|
thRender: () => (
|
||||||
<MemberHeaderColumn
|
<MemberHeaderColumn
|
||||||
property="email"
|
property="email"
|
||||||
@ -78,7 +88,6 @@ export const useMemberColumns = () => {
|
|||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
tdRender: (rowData: RowData) => <div className="w-48 truncate">{rowData.member.email}</div>,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -97,14 +106,17 @@ export const useMemberColumns = () => {
|
|||||||
{
|
{
|
||||||
key: "Authentication",
|
key: "Authentication",
|
||||||
content: t("workspace_settings.settings.members.details.authentication"),
|
content: t("workspace_settings.settings.members.details.authentication"),
|
||||||
tdRender: (rowData: RowData) => (
|
tdRender: (rowData: RowData) =>
|
||||||
<div className="capitalize">{rowData.member.last_login_medium?.replace("-", " ")}</div>
|
isSuspended(rowData) ? null : (
|
||||||
),
|
<div className="capitalize">{rowData.member.last_login_medium?.replace("-", " ")}</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "Joining date",
|
key: "Joining date",
|
||||||
content: t("workspace_settings.settings.members.details.joining_date"),
|
content: t("workspace_settings.settings.members.details.joining_date"),
|
||||||
|
tdRender: (rowData: RowData) =>
|
||||||
|
isSuspended(rowData) ? null : <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
|
||||||
thRender: () => (
|
thRender: () => (
|
||||||
<MemberHeaderColumn
|
<MemberHeaderColumn
|
||||||
property="joining_date"
|
property="joining_date"
|
||||||
@ -112,7 +124,6 @@ export const useMemberColumns = () => {
|
|||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
tdRender: (rowData: RowData) => <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return { columns, workspaceSlug, removeMemberModal, setRemoveMemberModal };
|
return { columns, workspaceSlug, removeMemberModal, setRemoveMemberModal };
|
||||||
|
|||||||
@ -3,16 +3,20 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Placement } from "@popperjs/core";
|
import { Placement } from "@popperjs/core";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { Check, Search } from "lucide-react";
|
import { Check, Search } from "lucide-react";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { SuspendedUserIcon } from "@plane/propel/icons";
|
||||||
|
import { EPillSize, EPillVariant, Pill } from "@plane/propel/pill";
|
||||||
import { IUserLite } from "@plane/types";
|
import { IUserLite } from "@plane/types";
|
||||||
import { Avatar } from "@plane/ui";
|
import { Avatar } from "@plane/ui";
|
||||||
import { cn, getFileURL } from "@plane/utils";
|
import { cn, getFileURL } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useMember } from "@/hooks/store/use-member";
|
||||||
import { useUser } from "@/hooks/store/user";
|
import { useUser } from "@/hooks/store/user";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
|
||||||
@ -37,6 +41,8 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
|||||||
placement,
|
placement,
|
||||||
referenceElement,
|
referenceElement,
|
||||||
} = props;
|
} = props;
|
||||||
|
// router
|
||||||
|
const { workspaceSlug } = useParams();
|
||||||
// refs
|
// refs
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
// states
|
// states
|
||||||
@ -46,6 +52,9 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
const {
|
||||||
|
workspace: { isUserSuspended },
|
||||||
|
} = useMember();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
// popper-js init
|
// popper-js init
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
@ -84,8 +93,19 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
|||||||
query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`,
|
query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`,
|
||||||
content: (
|
content: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar name={userDetails?.display_name} src={getFileURL(userDetails?.avatar_url ?? "")} />
|
<div className="w-4">
|
||||||
<span className="flex-grow truncate">
|
{isUserSuspended(userId, workspaceSlug?.toString()) ? (
|
||||||
|
<SuspendedUserIcon className="h-3.5 w-3.5 text-custom-text-400" />
|
||||||
|
) : (
|
||||||
|
<Avatar name={userDetails?.display_name} src={getFileURL(userDetails?.avatar_url ?? "")} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex-grow truncate",
|
||||||
|
isUserSuspended(userId, workspaceSlug?.toString()) ? "text-custom-text-400" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
{currentUser?.id === userId ? t("you") : userDetails?.display_name}
|
{currentUser?.id === userId ? t("you") : userDetails?.display_name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -133,15 +153,26 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
|||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${
|
cn(
|
||||||
active ? "bg-custom-background-80" : ""
|
"flex w-full select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
active && "bg-custom-background-80",
|
||||||
|
selected ? "text-custom-text-100" : "text-custom-text-200",
|
||||||
|
isUserSuspended(option.value, workspaceSlug?.toString())
|
||||||
|
? "cursor-not-allowed"
|
||||||
|
: "cursor-pointer"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
disabled={isUserSuspended(option.value, workspaceSlug?.toString())}
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
<span className="flex-grow truncate">{option.content}</span>
|
<span className="flex-grow truncate">{option.content}</span>
|
||||||
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||||
|
{isUserSuspended(option.value, workspaceSlug?.toString()) && (
|
||||||
|
<Pill variant={EPillVariant.DEFAULT} size={EPillSize.XS} className="border-none">
|
||||||
|
Suspended
|
||||||
|
</Pill>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
|
|||||||
@ -30,6 +30,7 @@ const WORKSPACE_ROLE_OPTIONS: IRoleOption[] = [
|
|||||||
{ value: String(EUserWorkspaceRoles.ADMIN), label: "Admin" },
|
{ value: String(EUserWorkspaceRoles.ADMIN), label: "Admin" },
|
||||||
{ value: String(EUserWorkspaceRoles.MEMBER), label: "Member" },
|
{ value: String(EUserWorkspaceRoles.MEMBER), label: "Member" },
|
||||||
{ value: String(EUserWorkspaceRoles.GUEST), label: "Guest" },
|
{ value: String(EUserWorkspaceRoles.GUEST), label: "Guest" },
|
||||||
|
{ value: "suspended", label: "Suspended" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Role filter group component
|
// Role filter group component
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import { Trash2 } from "lucide-react";
|
|||||||
import { Disclosure } from "@headlessui/react";
|
import { Disclosure } from "@headlessui/react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { ROLE, EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
|
import { ROLE, EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
|
||||||
|
import { SuspendedUserIcon } from "@plane/propel/icons";
|
||||||
|
import { Pill, EPillVariant, EPillSize } from "@plane/propel/pill";
|
||||||
import { IUser, IWorkspaceMember } from "@plane/types";
|
import { IUser, IWorkspaceMember } from "@plane/types";
|
||||||
// plane ui
|
// plane ui
|
||||||
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
import { CustomSelect, PopoverMenu, TOAST_TYPE, cn, setToast } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
// helpers
|
// helpers
|
||||||
import { getFileURL } from "@plane/utils";
|
import { getFileURL } from "@plane/utils";
|
||||||
@ -19,6 +21,7 @@ import { useUser, useUserPermissions } from "@/hooks/store/user";
|
|||||||
export interface RowData {
|
export interface RowData {
|
||||||
member: IWorkspaceMember;
|
member: IWorkspaceMember;
|
||||||
role: EUserPermissions;
|
role: EUserPermissions;
|
||||||
|
is_active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NameProps = {
|
type NameProps = {
|
||||||
@ -38,6 +41,7 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
|||||||
const { rowData, workspaceSlug, isAdmin, currentUser, setRemoveMemberModal } = props;
|
const { rowData, workspaceSlug, isAdmin, currentUser, setRemoveMemberModal } = props;
|
||||||
// derived values
|
// derived values
|
||||||
const { avatar_url, display_name, email, first_name, id, last_name } = rowData.member;
|
const { avatar_url, display_name, email, first_name, id, last_name } = rowData.member;
|
||||||
|
const isSuspended = rowData.is_active === false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure>
|
<Disclosure>
|
||||||
@ -48,24 +52,39 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
|||||||
{avatar_url && avatar_url.trim() !== "" ? (
|
{avatar_url && avatar_url.trim() !== "" ? (
|
||||||
<Link href={`/${workspaceSlug}/profile/${id}`}>
|
<Link href={`/${workspaceSlug}/profile/${id}`}>
|
||||||
<span className="relative flex h-6 w-6 items-center justify-center rounded-full capitalize text-white">
|
<span className="relative flex h-6 w-6 items-center justify-center rounded-full capitalize text-white">
|
||||||
<img
|
{isSuspended ? (
|
||||||
src={getFileURL(avatar_url)}
|
<SuspendedUserIcon className="h-4 w-4 text-custom-text-400" />
|
||||||
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
|
) : (
|
||||||
alt={display_name || email}
|
<img
|
||||||
/>
|
src={getFileURL(avatar_url)}
|
||||||
|
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
|
||||||
|
alt={display_name || email}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<Link href={`/${workspaceSlug}/profile/${id}`}>
|
<Link href={`/${workspaceSlug}/profile/${id}`}>
|
||||||
<span className="relative flex h-4 w-4 text-xs items-center justify-center rounded-full bg-gray-700 capitalize text-white">
|
<span
|
||||||
{(email ?? display_name ?? "?")[0]}
|
className={cn(
|
||||||
|
"relative flex h-4 w-4 text-xs items-center justify-center rounded-full capitalize text-white",
|
||||||
|
isSuspended ? "bg-custom-background-80" : "bg-gray-700"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isSuspended ? (
|
||||||
|
<SuspendedUserIcon className="h-4 w-4 text-custom-text-400" />
|
||||||
|
) : (
|
||||||
|
(email ?? display_name ?? "?")[0]
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{first_name} {last_name}
|
<span className={isSuspended ? "text-custom-text-400" : ""}>
|
||||||
|
{first_name} {last_name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(isAdmin || id === currentUser?.id) && (
|
{!isSuspended && (isAdmin || id === currentUser?.id) && (
|
||||||
<PopoverMenu
|
<PopoverMenu
|
||||||
data={[""]}
|
data={[""]}
|
||||||
keyExtractor={(item) => item}
|
keyExtractor={(item) => item}
|
||||||
@ -108,10 +127,17 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
|||||||
const isCurrentUser = currentUser?.id === rowData.member.id;
|
const isCurrentUser = currentUser?.id === rowData.member.id;
|
||||||
const isAdminRole = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
const isAdminRole = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||||
const isRoleNonEditable = isCurrentUser || !isAdminRole;
|
const isRoleNonEditable = isCurrentUser || !isAdminRole;
|
||||||
|
const isSuspended = rowData.is_active === false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isRoleNonEditable ? (
|
{isSuspended ? (
|
||||||
|
<div className="w-32 flex ">
|
||||||
|
<Pill variant={EPillVariant.DEFAULT} size={EPillSize.SM} className="border-none">
|
||||||
|
Suspended
|
||||||
|
</Pill>
|
||||||
|
</div>
|
||||||
|
) : isRoleNonEditable ? (
|
||||||
<div className="w-32 flex ">
|
<div className="w-32 flex ">
|
||||||
<span>{ROLE[rowData.role]}</span>
|
<span>{ROLE[rowData.role]}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -53,7 +53,13 @@ export const WorkspaceMembersList: FC<{ searchQuery: string; isAdmin: boolean }>
|
|||||||
const filteredMemberIds = workspaceSlug ? getFilteredWorkspaceMemberIds(workspaceSlug.toString()) : [];
|
const filteredMemberIds = workspaceSlug ? getFilteredWorkspaceMemberIds(workspaceSlug.toString()) : [];
|
||||||
const searchedMemberIds = searchQuery ? getSearchedWorkspaceMemberIds(searchQuery) : filteredMemberIds;
|
const searchedMemberIds = searchQuery ? getSearchedWorkspaceMemberIds(searchQuery) : filteredMemberIds;
|
||||||
const searchedInvitationsIds = getSearchedWorkspaceInvitationIds(searchQuery);
|
const searchedInvitationsIds = getSearchedWorkspaceInvitationIds(searchQuery);
|
||||||
const memberDetails = searchedMemberIds?.map((memberId) => getWorkspaceMemberDetails(memberId));
|
const memberDetails = searchedMemberIds
|
||||||
|
?.map((memberId) => getWorkspaceMemberDetails(memberId))
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a?.is_active && !b?.is_active) return -1;
|
||||||
|
if (!a?.is_active && b?.is_active) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -37,8 +37,15 @@ export const getMemberSortKey = (memberDetails: IUserLite, field: string, member
|
|||||||
}
|
}
|
||||||
case "email":
|
case "email":
|
||||||
return memberDetails.email?.toLowerCase() || "";
|
return memberDetails.email?.toLowerCase() || "";
|
||||||
case "joining_date":
|
case "joining_date": {
|
||||||
return memberDetails.joining_date ? new Date(memberDetails.joining_date) : new Date(NaN);
|
if (!memberDetails.joining_date) {
|
||||||
|
// Return a very old date for missing dates to sort them last
|
||||||
|
return new Date(0);
|
||||||
|
}
|
||||||
|
const date = new Date(memberDetails.joining_date);
|
||||||
|
// Return a very old date for invalid dates to sort them last
|
||||||
|
return isNaN(date.getTime()) ? new Date(0) : date;
|
||||||
|
}
|
||||||
case "role":
|
case "role":
|
||||||
return (memberRole ?? "").toString().toLowerCase();
|
return (memberRole ?? "").toString().toLowerCase();
|
||||||
default:
|
default:
|
||||||
@ -59,7 +66,7 @@ export const filterProjectMembersByRole = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterWorkspaceMembersByRole = <T extends { role: string | EUserPermissions }>(
|
export const filterWorkspaceMembersByRole = <T extends { role: string | EUserPermissions; is_active?: boolean }>(
|
||||||
members: T[],
|
members: T[],
|
||||||
roleFilters: string[]
|
roleFilters: string[]
|
||||||
): T[] => {
|
): T[] => {
|
||||||
@ -67,7 +74,20 @@ export const filterWorkspaceMembersByRole = <T extends { role: string | EUserPer
|
|||||||
|
|
||||||
return members.filter((member) => {
|
return members.filter((member) => {
|
||||||
const memberRole = String(member.role ?? "");
|
const memberRole = String(member.role ?? "");
|
||||||
return roleFilters.includes(memberRole);
|
const isSuspended = member.is_active === false;
|
||||||
|
|
||||||
|
// Check if suspended is in the role filters
|
||||||
|
const hasSuspendedFilter = roleFilters.includes("suspended");
|
||||||
|
// Get non-suspended role filters
|
||||||
|
const activeRoleFilters = roleFilters.filter((role) => role !== "suspended");
|
||||||
|
|
||||||
|
// For suspended users, include them only if suspended filter is selected
|
||||||
|
if (isSuspended) {
|
||||||
|
return hasSuspendedFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For active users, include them only if their role matches any active role filter
|
||||||
|
return activeRoleFilters.includes(memberRole);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -100,10 +120,15 @@ export const sortMembers = <T>(
|
|||||||
let comparison = 0;
|
let comparison = 0;
|
||||||
|
|
||||||
if (field === "joining_date") {
|
if (field === "joining_date") {
|
||||||
// For dates, we need to handle Date objects
|
// For dates, we need to handle Date objects and ensure they're valid
|
||||||
const aDate = aValue instanceof Date ? aValue : new Date(aValue);
|
const aDate = aValue instanceof Date ? aValue : new Date(aValue);
|
||||||
const bDate = bValue instanceof Date ? bValue : new Date(bValue);
|
const bDate = bValue instanceof Date ? bValue : new Date(bValue);
|
||||||
comparison = aDate.getTime() - bDate.getTime();
|
|
||||||
|
// Handle invalid dates by treating them as very old dates
|
||||||
|
const aTime = isNaN(aDate.getTime()) ? 0 : aDate.getTime();
|
||||||
|
const bTime = isNaN(bDate.getTime()) ? 0 : bDate.getTime();
|
||||||
|
|
||||||
|
comparison = aTime - bTime;
|
||||||
} else {
|
} else {
|
||||||
// For strings, use localeCompare for proper alphabetical sorting
|
// For strings, use localeCompare for proper alphabetical sorting
|
||||||
const aStr = String(aValue);
|
const aStr = String(aValue);
|
||||||
@ -139,13 +164,12 @@ export const sortProjectMembers = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sortWorkspaceMembers = <T extends { role: string | EUserPermissions }>(
|
export const sortWorkspaceMembers = <T extends { role: string | EUserPermissions; is_active?: boolean }>(
|
||||||
members: T[],
|
members: T[],
|
||||||
memberDetailsMap: Record<string, IUserLite>,
|
memberDetailsMap: Record<string, IUserLite>,
|
||||||
getMemberKey: (member: T) => string,
|
getMemberKey: (member: T) => string,
|
||||||
filters?: IMemberFilters
|
filters?: IMemberFilters
|
||||||
): T[] => {
|
): T[] => {
|
||||||
// Apply role filtering first
|
|
||||||
const filteredMembers =
|
const filteredMembers =
|
||||||
filters?.roles && filters.roles.length > 0 ? filterWorkspaceMembersByRole(members, filters.roles) : members;
|
filters?.roles && filters.roles.length > 0 ? filterWorkspaceMembersByRole(members, filters.roles) : members;
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export interface IWorkspaceMemberStore {
|
|||||||
data: Partial<IWorkspaceMemberInvitation>
|
data: Partial<IWorkspaceMemberInvitation>
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
deleteMemberInvitation: (workspaceSlug: string, invitationId: string) => Promise<void>;
|
deleteMemberInvitation: (workspaceSlug: string, invitationId: string) => Promise<void>;
|
||||||
|
isUserSuspended: (userId: string, workspaceSlug: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||||
@ -126,9 +127,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
|||||||
(m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(),
|
(m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(),
|
||||||
]);
|
]);
|
||||||
//filter out bots
|
//filter out bots
|
||||||
const memberIds = members
|
const memberIds = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot).map((m) => m.member);
|
||||||
.filter((m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot)
|
|
||||||
.map((m) => m.member);
|
|
||||||
return memberIds;
|
return memberIds;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,7 +138,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
|||||||
getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
|
getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
|
||||||
let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
|
let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
|
||||||
//filter out bots and inactive members
|
//filter out bots and inactive members
|
||||||
members = members.filter((m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot);
|
members = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot);
|
||||||
|
|
||||||
// Use filters store to get filtered member ids
|
// Use filters store to get filtered member ids
|
||||||
const memberIds = this.filtersStore.getFilteredMemberIds(
|
const memberIds = this.filtersStore.getFilteredMemberIds(
|
||||||
@ -350,4 +349,10 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
isUserSuspended = computedFn((userId: string, workspaceSlug: string) => {
|
||||||
|
if (!workspaceSlug) return false;
|
||||||
|
const workspaceMember = this.workspaceMemberMap?.[workspaceSlug]?.[userId];
|
||||||
|
return workspaceMember?.is_active === false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,3 +56,4 @@ export * from "./ai-icon";
|
|||||||
export * from "./plane-icon";
|
export * from "./plane-icon";
|
||||||
export * from "./wiki-icon";
|
export * from "./wiki-icon";
|
||||||
export * from "./brand";
|
export * from "./brand";
|
||||||
|
export * from "./suspended-user";
|
||||||
|
|||||||
32
packages/propel/src/icons/suspended-user.tsx
Normal file
32
packages/propel/src/icons/suspended-user.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ISvgIcons } from "./type";
|
||||||
|
|
||||||
|
export const SuspendedUserIcon: React.FC<ISvgIcons> = ({ className, ...rest }) => (
|
||||||
|
<svg viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} {...rest}>
|
||||||
|
<g clipPath="url(#clip0_806_120890)">
|
||||||
|
<path
|
||||||
|
d="M3 13C3 12.304 3.18158 11.6201 3.52681 11.0158C3.87204 10.4115 4.36897 9.90774 4.9685 9.55428C5.56802 9.20082 6.24939 9.00989 6.94529 9.00037C7.64119 8.99086 8.32753 9.16307 8.9365 9.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7 9C8.38071 9 9.5 7.88071 9.5 6.5C9.5 5.11929 8.38071 4 7 4C5.61929 4 4.5 5.11929 4.5 6.5C4.5 7.88071 5.61929 9 7 9Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path d="M10.5 11L13 13.5" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
<path d="M13 11L10.5 13.5" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_806_120890">
|
||||||
|
<path
|
||||||
|
d="M2 4.5C2 3.39543 2.89543 2.5 4 2.5H12C13.1046 2.5 14 3.39543 14 4.5V12.5C14 13.6046 13.1046 14.5 12 14.5H4C2.89543 14.5 2 13.6046 2 12.5V4.5Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@ -1,2 +1,2 @@
|
|||||||
export { Pill } from "./pill";
|
export { Pill, EPillVariant, EPillSize } from "./pill";
|
||||||
export type { PillProps } from "./pill";
|
export type { PillProps } from "./pill";
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export enum EPillSize {
|
|||||||
SM = "sm",
|
SM = "sm",
|
||||||
MD = "md",
|
MD = "md",
|
||||||
LG = "lg",
|
LG = "lg",
|
||||||
|
XS = "xs",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TPillVariant =
|
export type TPillVariant =
|
||||||
@ -23,7 +24,7 @@ export type TPillVariant =
|
|||||||
| EPillVariant.WARNING
|
| EPillVariant.WARNING
|
||||||
| EPillVariant.ERROR
|
| EPillVariant.ERROR
|
||||||
| EPillVariant.INFO;
|
| EPillVariant.INFO;
|
||||||
export type TPillSize = EPillSize.SM | EPillSize.MD | EPillSize.LG;
|
export type TPillSize = EPillSize.SM | EPillSize.MD | EPillSize.LG | EPillSize.XS;
|
||||||
|
|
||||||
export interface PillProps extends React.HTMLAttributes<HTMLSpanElement> {
|
export interface PillProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||||
variant?: TPillVariant;
|
variant?: TPillVariant;
|
||||||
@ -42,6 +43,7 @@ const pillVariants = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pillSizes = {
|
const pillSizes = {
|
||||||
|
[EPillSize.XS]: "px-1.5 py-0.5 text-xs",
|
||||||
[EPillSize.SM]: "px-2 py-0.5 text-xs",
|
[EPillSize.SM]: "px-2 py-0.5 text-xs",
|
||||||
[EPillSize.MD]: "px-2.5 py-1 text-sm",
|
[EPillSize.MD]: "px-2.5 py-1 text-sm",
|
||||||
[EPillSize.LG]: "px-3 py-1.5 text-base",
|
[EPillSize.LG]: "px-3 py-1.5 text-base",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user