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
|
||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
|
||||
const isSuspended = (rowData: RowData) => rowData.is_active === false;
|
||||
// handlers
|
||||
const handleDisplayFilterUpdate = (filterUpdates: Partial<IMemberFilters>) => {
|
||||
updateFilters(filterUpdates);
|
||||
@ -58,6 +59,11 @@ export const useMemberColumns = () => {
|
||||
{
|
||||
key: "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: () => (
|
||||
<MemberHeaderColumn
|
||||
property="display_name"
|
||||
@ -65,12 +71,16 @@ export const useMemberColumns = () => {
|
||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||
/>
|
||||
),
|
||||
tdRender: (rowData: RowData) => <div className="w-32">{rowData.member.display_name}</div>,
|
||||
},
|
||||
|
||||
{
|
||||
key: "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: () => (
|
||||
<MemberHeaderColumn
|
||||
property="email"
|
||||
@ -78,7 +88,6 @@ export const useMemberColumns = () => {
|
||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||
/>
|
||||
),
|
||||
tdRender: (rowData: RowData) => <div className="w-48 truncate">{rowData.member.email}</div>,
|
||||
},
|
||||
|
||||
{
|
||||
@ -97,14 +106,17 @@ export const useMemberColumns = () => {
|
||||
{
|
||||
key: "Authentication",
|
||||
content: t("workspace_settings.settings.members.details.authentication"),
|
||||
tdRender: (rowData: RowData) => (
|
||||
<div className="capitalize">{rowData.member.last_login_medium?.replace("-", " ")}</div>
|
||||
),
|
||||
tdRender: (rowData: RowData) =>
|
||||
isSuspended(rowData) ? null : (
|
||||
<div className="capitalize">{rowData.member.last_login_medium?.replace("-", " ")}</div>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
key: "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: () => (
|
||||
<MemberHeaderColumn
|
||||
property="joining_date"
|
||||
@ -112,7 +124,6 @@ export const useMemberColumns = () => {
|
||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||
/>
|
||||
),
|
||||
tdRender: (rowData: RowData) => <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
|
||||
},
|
||||
];
|
||||
return { columns, workspaceSlug, removeMemberModal, setRemoveMemberModal };
|
||||
|
||||
@ -3,16 +3,20 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { createPortal } from "react-dom";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Check, Search } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// plane imports
|
||||
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 { Avatar } from "@plane/ui";
|
||||
import { cn, getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
@ -37,6 +41,8 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
||||
placement,
|
||||
referenceElement,
|
||||
} = props;
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// refs
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// states
|
||||
@ -46,6 +52,9 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const {
|
||||
workspace: { isUserSuspended },
|
||||
} = useMember();
|
||||
const { isMobile } = usePlatformOS();
|
||||
// popper-js init
|
||||
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}`,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar name={userDetails?.display_name} src={getFileURL(userDetails?.avatar_url ?? "")} />
|
||||
<span className="flex-grow truncate">
|
||||
<div className="w-4">
|
||||
{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}
|
||||
</span>
|
||||
</div>
|
||||
@ -133,15 +153,26 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
cn(
|
||||
"flex w-full select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
|
||||
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 }) => (
|
||||
<>
|
||||
<span className="flex-grow truncate">{option.content}</span>
|
||||
{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>
|
||||
|
||||
@ -30,6 +30,7 @@ const WORKSPACE_ROLE_OPTIONS: IRoleOption[] = [
|
||||
{ value: String(EUserWorkspaceRoles.ADMIN), label: "Admin" },
|
||||
{ value: String(EUserWorkspaceRoles.MEMBER), label: "Member" },
|
||||
{ value: String(EUserWorkspaceRoles.GUEST), label: "Guest" },
|
||||
{ value: "suspended", label: "Suspended" },
|
||||
];
|
||||
|
||||
// Role filter group component
|
||||
|
||||
@ -5,9 +5,11 @@ import { Trash2 } from "lucide-react";
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// plane imports
|
||||
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";
|
||||
// plane ui
|
||||
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { CustomSelect, PopoverMenu, TOAST_TYPE, cn, setToast } from "@plane/ui";
|
||||
// constants
|
||||
// helpers
|
||||
import { getFileURL } from "@plane/utils";
|
||||
@ -19,6 +21,7 @@ import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
export interface RowData {
|
||||
member: IWorkspaceMember;
|
||||
role: EUserPermissions;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
type NameProps = {
|
||||
@ -38,6 +41,7 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
||||
const { rowData, workspaceSlug, isAdmin, currentUser, setRemoveMemberModal } = props;
|
||||
// derived values
|
||||
const { avatar_url, display_name, email, first_name, id, last_name } = rowData.member;
|
||||
const isSuspended = rowData.is_active === false;
|
||||
|
||||
return (
|
||||
<Disclosure>
|
||||
@ -48,24 +52,39 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
||||
{avatar_url && avatar_url.trim() !== "" ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${id}`}>
|
||||
<span className="relative flex h-6 w-6 items-center justify-center rounded-full capitalize text-white">
|
||||
<img
|
||||
src={getFileURL(avatar_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
|
||||
alt={display_name || email}
|
||||
/>
|
||||
{isSuspended ? (
|
||||
<SuspendedUserIcon className="h-4 w-4 text-custom-text-400" />
|
||||
) : (
|
||||
<img
|
||||
src={getFileURL(avatar_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
|
||||
alt={display_name || email}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<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">
|
||||
{(email ?? display_name ?? "?")[0]}
|
||||
<span
|
||||
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>
|
||||
</Link>
|
||||
)}
|
||||
{first_name} {last_name}
|
||||
<span className={isSuspended ? "text-custom-text-400" : ""}>
|
||||
{first_name} {last_name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{(isAdmin || id === currentUser?.id) && (
|
||||
{!isSuspended && (isAdmin || id === currentUser?.id) && (
|
||||
<PopoverMenu
|
||||
data={[""]}
|
||||
keyExtractor={(item) => item}
|
||||
@ -108,10 +127,17 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
||||
const isCurrentUser = currentUser?.id === rowData.member.id;
|
||||
const isAdminRole = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const isRoleNonEditable = isCurrentUser || !isAdminRole;
|
||||
const isSuspended = rowData.is_active === false;
|
||||
|
||||
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 ">
|
||||
<span>{ROLE[rowData.role]}</span>
|
||||
</div>
|
||||
|
||||
@ -53,7 +53,13 @@ export const WorkspaceMembersList: FC<{ searchQuery: string; isAdmin: boolean }>
|
||||
const filteredMemberIds = workspaceSlug ? getFilteredWorkspaceMemberIds(workspaceSlug.toString()) : [];
|
||||
const searchedMemberIds = searchQuery ? getSearchedWorkspaceMemberIds(searchQuery) : filteredMemberIds;
|
||||
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 (
|
||||
<>
|
||||
|
||||
@ -37,8 +37,15 @@ export const getMemberSortKey = (memberDetails: IUserLite, field: string, member
|
||||
}
|
||||
case "email":
|
||||
return memberDetails.email?.toLowerCase() || "";
|
||||
case "joining_date":
|
||||
return memberDetails.joining_date ? new Date(memberDetails.joining_date) : new Date(NaN);
|
||||
case "joining_date": {
|
||||
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":
|
||||
return (memberRole ?? "").toString().toLowerCase();
|
||||
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[],
|
||||
roleFilters: string[]
|
||||
): T[] => {
|
||||
@ -67,7 +74,20 @@ export const filterWorkspaceMembersByRole = <T extends { role: string | EUserPer
|
||||
|
||||
return members.filter((member) => {
|
||||
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;
|
||||
|
||||
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 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 {
|
||||
// For strings, use localeCompare for proper alphabetical sorting
|
||||
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[],
|
||||
memberDetailsMap: Record<string, IUserLite>,
|
||||
getMemberKey: (member: T) => string,
|
||||
filters?: IMemberFilters
|
||||
): T[] => {
|
||||
// Apply role filtering first
|
||||
const filteredMembers =
|
||||
filters?.roles && filters.roles.length > 0 ? filterWorkspaceMembersByRole(members, filters.roles) : members;
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@ export interface IWorkspaceMemberStore {
|
||||
data: Partial<IWorkspaceMemberInvitation>
|
||||
) => Promise<void>;
|
||||
deleteMemberInvitation: (workspaceSlug: string, invitationId: string) => Promise<void>;
|
||||
isUserSuspended: (userId: string, workspaceSlug: string) => boolean;
|
||||
}
|
||||
|
||||
export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
@ -126,9 +127,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
(m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(),
|
||||
]);
|
||||
//filter out bots
|
||||
const memberIds = members
|
||||
.filter((m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot)
|
||||
.map((m) => m.member);
|
||||
const memberIds = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot).map((m) => m.member);
|
||||
return memberIds;
|
||||
});
|
||||
|
||||
@ -139,7 +138,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
|
||||
let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
|
||||
//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
|
||||
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 "./wiki-icon";
|
||||
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";
|
||||
|
||||
@ -14,6 +14,7 @@ export enum EPillSize {
|
||||
SM = "sm",
|
||||
MD = "md",
|
||||
LG = "lg",
|
||||
XS = "xs",
|
||||
}
|
||||
|
||||
export type TPillVariant =
|
||||
@ -23,7 +24,7 @@ export type TPillVariant =
|
||||
| EPillVariant.WARNING
|
||||
| EPillVariant.ERROR
|
||||
| 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> {
|
||||
variant?: TPillVariant;
|
||||
@ -42,6 +43,7 @@ const pillVariants = {
|
||||
};
|
||||
|
||||
const pillSizes = {
|
||||
[EPillSize.XS]: "px-1.5 py-0.5 text-xs",
|
||||
[EPillSize.SM]: "px-2 py-0.5 text-xs",
|
||||
[EPillSize.MD]: "px-2.5 py-1 text-sm",
|
||||
[EPillSize.LG]: "px-3 py-1.5 text-base",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user