UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: стандартизация таблицы участников workspace
This commit is contained in:
parent
3dd99491a4
commit
bf75ce84eb
|
|
@ -6,16 +6,36 @@
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { EUserPermissions, EUserPermissionsLevel, LOGIN_MEDIUM_LABELS } from "@plane/constants";
|
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { renderFormattedDate } from "@plane/utils";
|
import { renderFormattedDate } from "@plane/utils";
|
||||||
import { MemberHeaderColumn } from "@/components/project/member-header-column";
|
import { MemberHeaderColumn } from "@/components/project/member-header-column";
|
||||||
import type { RowData } from "@/components/workspace/settings/member-columns";
|
import type { RowData } from "@/components/workspace/settings/member-columns";
|
||||||
import { AccountTypeColumn, NameColumn } from "@/components/workspace/settings/member-columns";
|
import {
|
||||||
|
AccountTypeColumn,
|
||||||
|
NameColumn,
|
||||||
|
WorkspaceMemberActionsColumn,
|
||||||
|
} from "@/components/workspace/settings/member-columns";
|
||||||
import { useMember } from "@/hooks/store/use-member";
|
import { useMember } from "@/hooks/store/use-member";
|
||||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||||
import type { IMemberFilters } from "@/store/member/utils";
|
import type { IMemberFilters } from "@/store/member/utils";
|
||||||
|
|
||||||
|
const NODEDC_LOGIN_MEDIUM_LABELS: Record<string, string> = {
|
||||||
|
email: "Почта",
|
||||||
|
"magic-code": "Код",
|
||||||
|
github: "GitHub",
|
||||||
|
gitlab: "GitLab",
|
||||||
|
google: "Google",
|
||||||
|
gitea: "Gitea",
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLoginMediumLabel = (loginMedium: string) => NODEDC_LOGIN_MEDIUM_LABELS[loginMedium] ?? loginMedium;
|
||||||
|
|
||||||
|
const isSuspended = (rowData: RowData) => rowData.is_active === false;
|
||||||
|
|
||||||
|
const stickyNameHeaderClassName = "nodedc-settings-table-sticky left-0 z-20 min-w-max";
|
||||||
|
const stickyNameCellClassName = "nodedc-settings-table-sticky left-0 z-10 min-w-max";
|
||||||
|
|
||||||
export const useMemberColumns = () => {
|
export const useMemberColumns = () => {
|
||||||
// states
|
// states
|
||||||
const [removeMemberModal, setRemoveMemberModal] = useState<RowData | null>(null);
|
const [removeMemberModal, setRemoveMemberModal] = useState<RowData | null>(null);
|
||||||
|
|
@ -34,8 +54,6 @@ 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);
|
||||||
|
|
@ -45,13 +63,16 @@ export const useMemberColumns = () => {
|
||||||
{
|
{
|
||||||
key: "Full name",
|
key: "Full name",
|
||||||
content: t("workspace_settings.settings.members.details.full_name"),
|
content: t("workspace_settings.settings.members.details.full_name"),
|
||||||
thClassName: "text-left",
|
thClassName: stickyNameHeaderClassName,
|
||||||
|
tdClassName: stickyNameCellClassName,
|
||||||
thRender: () => (
|
thRender: () => (
|
||||||
|
<div className="w-max min-w-[8.5rem] pr-3">
|
||||||
<MemberHeaderColumn
|
<MemberHeaderColumn
|
||||||
property="full_name"
|
property="full_name"
|
||||||
displayFilters={filters}
|
displayFilters={filters}
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
tdRender: (rowData: RowData) => (
|
tdRender: (rowData: RowData) => (
|
||||||
<NameColumn
|
<NameColumn
|
||||||
|
|
@ -68,7 +89,9 @@ 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) => (
|
tdRender: (rowData: RowData) => (
|
||||||
<div className={`w-32 ${isSuspended(rowData) ? "text-placeholder" : ""}`}>{rowData.member.display_name}</div>
|
<div className={`min-w-[7.5rem] pr-3 ${isSuspended(rowData) ? "text-placeholder" : ""}`}>
|
||||||
|
{rowData.member.display_name}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
thRender: () => (
|
thRender: () => (
|
||||||
<MemberHeaderColumn
|
<MemberHeaderColumn
|
||||||
|
|
@ -83,7 +106,9 @@ export const useMemberColumns = () => {
|
||||||
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) => (
|
tdRender: (rowData: RowData) => (
|
||||||
<div className={`w-48 truncate ${isSuspended(rowData) ? "text-placeholder" : ""}`}>{rowData.member.email}</div>
|
<div className={`min-w-[10.5rem] pr-3 ${isSuspended(rowData) ? "text-placeholder" : ""}`}>
|
||||||
|
{rowData.member.email}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
thRender: () => (
|
thRender: () => (
|
||||||
<MemberHeaderColumn
|
<MemberHeaderColumn
|
||||||
|
|
@ -109,12 +134,12 @@ export const useMemberColumns = () => {
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "Authentication",
|
key: "Authentication",
|
||||||
content: t("workspace_settings.settings.members.details.authentication"),
|
content: <div className="min-w-[5.5rem] pr-3">Вход</div>,
|
||||||
tdRender: (rowData: RowData) => {
|
tdRender: (rowData: RowData) => {
|
||||||
if (isSuspended(rowData)) return null;
|
if (isSuspended(rowData)) return null;
|
||||||
const loginMedium = rowData.member.last_login_medium;
|
const loginMedium = rowData.member.last_login_medium;
|
||||||
if (!loginMedium) return null;
|
if (!loginMedium) return null;
|
||||||
return <div>{LOGIN_MEDIUM_LABELS[loginMedium]}</div>;
|
return <div className="min-w-[5.5rem] pr-3">{getLoginMediumLabel(loginMedium)}</div>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -122,7 +147,9 @@ export const useMemberColumns = () => {
|
||||||
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) =>
|
tdRender: (rowData: RowData) =>
|
||||||
isSuspended(rowData) ? null : <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
|
isSuspended(rowData) ? null : (
|
||||||
|
<div className="min-w-[7rem] pr-3">{renderFormattedDate(rowData?.member?.joining_date)}</div>
|
||||||
|
),
|
||||||
thRender: () => (
|
thRender: () => (
|
||||||
<MemberHeaderColumn
|
<MemberHeaderColumn
|
||||||
property="joining_date"
|
property="joining_date"
|
||||||
|
|
@ -131,6 +158,19 @@ export const useMemberColumns = () => {
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "Actions",
|
||||||
|
content: <span className="sr-only">Действия</span>,
|
||||||
|
tdRender: (rowData: RowData) => (
|
||||||
|
<WorkspaceMemberActionsColumn
|
||||||
|
rowData={rowData}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
currentUser={currentUser}
|
||||||
|
setRemoveMemberModal={setRemoveMemberModal}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
return { columns, workspaceSlug, removeMemberModal, setRemoveMemberModal };
|
return { columns, workspaceSlug, removeMemberModal, setRemoveMemberModal };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,15 @@
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import { Disclosure } from "@headlessui/react";
|
|
||||||
// plane imports
|
// plane imports
|
||||||
import { ROLE, EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
|
import { EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
|
||||||
import { TrashIcon, SuspendedUserIcon } from "@plane/propel/icons";
|
import { ChevronDownIcon, TrashIcon, SuspendedUserIcon } from "@plane/propel/icons";
|
||||||
import { Pill, EPillVariant, EPillSize } from "@plane/propel/pill";
|
import { Pill, EPillVariant, EPillSize } from "@plane/propel/pill";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import type { IUser, IWorkspaceMember } from "@plane/types";
|
import type { IUser, IWorkspaceMember } from "@plane/types";
|
||||||
// plane ui
|
|
||||||
import { PopoverMenu } from "@plane/ui";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { getFileURL } from "@plane/utils";
|
import { cn, getFileURL } from "@plane/utils";
|
||||||
import { SelectionDropdown } from "@/components/common/selection-dropdown";
|
import { SelectionDropdown } from "@/components/common/selection-dropdown";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember } from "@/hooks/store/use-member";
|
import { useMember } from "@/hooks/store/use-member";
|
||||||
|
|
@ -43,18 +39,26 @@ type AccountTypeProps = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const WORKSPACE_ROLE_LABELS: Record<EUserPermissions, string> = {
|
||||||
|
[EUserPermissions.GUEST]: "Гость",
|
||||||
|
[EUserPermissions.MEMBER]: "Участник",
|
||||||
|
[EUserPermissions.ADMIN]: "Админ",
|
||||||
|
};
|
||||||
|
|
||||||
|
const WORKSPACE_ROLE_OPTIONS = [EUserPermissions.GUEST, EUserPermissions.MEMBER, EUserPermissions.ADMIN];
|
||||||
|
|
||||||
|
export const getWorkspaceRoleLabel = (role: EUserPermissions | undefined) =>
|
||||||
|
role ? (WORKSPACE_ROLE_LABELS[role] ?? "Не назначено") : "Не назначено";
|
||||||
|
|
||||||
export function NameColumn(props: NameProps) {
|
export function NameColumn(props: NameProps) {
|
||||||
const { rowData, workspaceSlug, isAdmin, currentUser, setRemoveMemberModal } = props;
|
const { rowData, workspaceSlug } = 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;
|
const isSuspended = rowData.is_active === false;
|
||||||
|
const fullName = [first_name, last_name].filter(Boolean).join(" ") || display_name || email;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure>
|
<div className="flex w-max min-w-[8.5rem] items-center gap-x-2 gap-y-2 pr-3">
|
||||||
{() => (
|
|
||||||
<div className="group relative">
|
|
||||||
<div className="flex w-72 items-center justify-between gap-x-4 gap-y-2">
|
|
||||||
<div className="flex flex-1 items-center gap-x-2 gap-y-2">
|
|
||||||
{isSuspended ? (
|
{isSuspended ? (
|
||||||
<div className="rounded-full bg-layer-1">
|
<div className="rounded-full bg-layer-1">
|
||||||
<SuspendedUserIcon className="size-6 text-placeholder" />
|
<SuspendedUserIcon className="size-6 text-placeholder" />
|
||||||
|
|
@ -76,50 +80,39 @@ export function NameColumn(props: NameProps) {
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<span className={isSuspended ? "text-placeholder" : ""}>
|
<span className={cn(isSuspended ? "text-placeholder" : "")}>{fullName}</span>
|
||||||
{first_name} {last_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{!isSuspended && (isAdmin || id === currentUser?.id) && (
|
|
||||||
<PopoverMenu
|
|
||||||
data={[""]}
|
|
||||||
keyExtractor={(item) => item}
|
|
||||||
popoverClassName="justify-end"
|
|
||||||
buttonClassName="outline-none origin-center rotate-90 size-8 aspect-square flex-shrink-0 grid place-items-center opacity-0 group-hover:opacity-100 transition-opacity"
|
|
||||||
render={() => (
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
className="flex cursor-pointer items-center gap-x-3"
|
|
||||||
onClick={() => setRemoveMemberModal(rowData)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
setRemoveMemberModal(rowData);
|
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
|
export function WorkspaceMemberActionsColumn(props: NameProps) {
|
||||||
|
const { rowData, isAdmin, currentUser, setRemoveMemberModal } = props;
|
||||||
|
const { display_name, email, id } = rowData.member;
|
||||||
|
const isSuspended = rowData.is_active === false;
|
||||||
|
const canRemoveMember = !isSuspended && (isAdmin || id === currentUser?.id);
|
||||||
|
const isCurrentUser = id === currentUser?.id;
|
||||||
|
const label = isCurrentUser ? "Покинуть workspace" : `Удалить ${display_name || email}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-12 justify-end">
|
||||||
|
{canRemoveMember && (
|
||||||
|
<button
|
||||||
|
aria-label={label}
|
||||||
|
className="nodedc-external-icon-button"
|
||||||
data-ph-element={MEMBER_TRACKER_ELEMENTS.WORKSPACE_MEMBER_TABLE_CONTEXT_MENU}
|
data-ph-element={MEMBER_TRACKER_ELEMENTS.WORKSPACE_MEMBER_TABLE_CONTEXT_MENU}
|
||||||
|
title={label}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRemoveMemberModal(rowData)}
|
||||||
>
|
>
|
||||||
<TrashIcon className="size-3.5 align-middle" /> {id === currentUser?.id ? "Leave " : "Remove "}
|
<TrashIcon className="h-3.5 w-3.5" />
|
||||||
</div>
|
</button>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AccountTypeColumn = observer(function AccountTypeColumn(props: AccountTypeProps) {
|
export const AccountTypeColumn = observer(function AccountTypeColumn(props: AccountTypeProps) {
|
||||||
const { rowData, workspaceSlug } = props;
|
const { rowData, workspaceSlug } = props;
|
||||||
// form info
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm();
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
|
||||||
|
|
@ -133,56 +126,52 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco
|
||||||
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;
|
const isSuspended = rowData.is_active === false;
|
||||||
|
const roleLabel = getWorkspaceRoleLabel(rowData.role);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isSuspended ? (
|
{isSuspended ? (
|
||||||
<div className="flex w-32">
|
<div className="flex min-w-[9rem]">
|
||||||
<Pill variant={EPillVariant.DEFAULT} size={EPillSize.SM} className="border-none">
|
<Pill variant={EPillVariant.DEFAULT} size={EPillSize.SM} className="border-none">
|
||||||
Suspended
|
Заблокирован
|
||||||
</Pill>
|
</Pill>
|
||||||
</div>
|
</div>
|
||||||
) : isRoleNonEditable ? (
|
) : isRoleNonEditable ? (
|
||||||
<div className="flex w-32">
|
<div className="nodedc-settings-chip flex min-h-10 min-w-[9rem] items-center px-4 py-2 text-caption-sm-medium">
|
||||||
<span>{ROLE[rowData.role]}</span>
|
<span className="truncate">{roleLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Controller
|
|
||||||
name="role"
|
|
||||||
control={control}
|
|
||||||
rules={{ required: "Role is required." }}
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<SelectionDropdown
|
<SelectionDropdown
|
||||||
options={Object.keys(ROLE).map((item) => ({
|
dropdownClassName="!p-2"
|
||||||
key: item,
|
dropdownContentClassName="!w-44"
|
||||||
title: ROLE[item as unknown as keyof typeof ROLE],
|
options={WORKSPACE_ROLE_OPTIONS.map((role) => ({
|
||||||
isChecked: String(value) === item,
|
key: String(role),
|
||||||
|
title: getWorkspaceRoleLabel(role),
|
||||||
|
isChecked: rowData.role === role,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug || rowData.role === role) return;
|
||||||
try {
|
try {
|
||||||
await updateMember(workspaceSlug.toString(), rowData.member.id, {
|
await updateMember(workspaceSlug.toString(), rowData.member.id, { role });
|
||||||
role: item as unknown as EUserPermissions,
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const error = err as { error?: string | string[] };
|
const error = err as { error?: string | string[] };
|
||||||
const errorString = Array.isArray(error?.error) ? error.error[0] : error?.error;
|
const errorString = Array.isArray(error?.error) ? error.error[0] : error?.error;
|
||||||
|
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: "Error!",
|
title: "Ошибка",
|
||||||
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
message: errorString ?? "Не удалось обновить роль участника. Попробуйте ещё раз.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}))}
|
}))}
|
||||||
menuButton={
|
menuButton={({ open }) => (
|
||||||
<div className="flex">
|
<div className="nodedc-settings-chip flex min-h-10 min-w-[9rem] items-center justify-between gap-2 px-4 py-2 text-caption-sm-medium">
|
||||||
<span>{ROLE[rowData.role]}</span>
|
<span className="truncate">{roleLabel}</span>
|
||||||
|
<ChevronDownIcon className={cn("h-3 w-3 transition-transform", open ? "rotate-180" : "")} />
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
menuButtonWrapperClassName={`w-32 rounded-md p-0 !justify-start !px-0 hover:bg-surface-1 ${errors.role ? "border-danger-strong" : "border-none"}`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
menuButtonWrapperClassName="flex rounded-[1.25rem] border-0 outline-none"
|
||||||
|
placement="bottom-end"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
|
||||||
if (isEmpty(columns)) return <MembersLayoutLoader />;
|
if (isEmpty(columns)) return <MembersLayoutLoader />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid overflow-hidden rounded-[1.2rem]">
|
<div className="horizontal-scrollbar scrollbar-sm w-full overflow-x-auto overflow-y-hidden rounded-[1.2rem]">
|
||||||
{removeMemberModal && (
|
{removeMemberModal && (
|
||||||
<ConfirmWorkspaceMemberRemove
|
<ConfirmWorkspaceMemberRemove
|
||||||
isOpen={removeMemberModal.member.id.length > 0}
|
isOpen={removeMemberModal.member.id.length > 0}
|
||||||
|
|
@ -109,6 +109,7 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
|
||||||
(memberDetails?.filter((member): member is IWorkspaceMember => member !== null) ?? []) as unknown as RowData[]
|
(memberDetails?.filter((member): member is IWorkspaceMember => member !== null) ?? []) as unknown as RowData[]
|
||||||
}
|
}
|
||||||
keyExtractor={(rowData) => rowData?.member.id ?? ""}
|
keyExtractor={(rowData) => rowData?.member.id ?? ""}
|
||||||
|
tableClassName="nodedc-settings-table-surface w-max table-auto border-separate border-spacing-0 overflow-visible"
|
||||||
tHeadClassName="border-b border-white/6"
|
tHeadClassName="border-b border-white/6"
|
||||||
thClassName="text-left font-medium divide-x-0 text-placeholder"
|
thClassName="text-left font-medium divide-x-0 text-placeholder"
|
||||||
tBodyClassName="divide-y-0"
|
tBodyClassName="divide-y-0"
|
||||||
|
|
|
||||||
|
|
@ -2094,6 +2094,27 @@
|
||||||
-webkit-backdrop-filter: blur(18px);
|
-webkit-backdrop-filter: blur(18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-table-surface,
|
||||||
|
.nodedc-settings-table-sticky {
|
||||||
|
background: rgb(36, 36, 38) !important;
|
||||||
|
-webkit-backdrop-filter: none !important;
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-table-sticky {
|
||||||
|
position: sticky;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-table-sticky::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -1px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.045);
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-settings-sidebar-shell {
|
.nodedc-settings-sidebar-shell {
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export function Table<T>(props: TTableData<T>) {
|
||||||
<thead className={cn("divide-y divide-subtle", tHeadClassName)}>
|
<thead className={cn("divide-y divide-subtle", tHeadClassName)}>
|
||||||
<tr className={cn("divide-x divide-subtle text-13 text-primary", tHeadTrClassName)}>
|
<tr className={cn("divide-x divide-subtle text-13 text-primary", tHeadTrClassName)}>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<th key={column.key} className={cn("px-2.5 py-2", thClassName)}>
|
<th key={column.key} className={cn("px-2.5 py-2", thClassName, column.thClassName)}>
|
||||||
{(column?.thRender && column?.thRender()) || column.content}
|
{(column?.thRender && column?.thRender()) || column.content}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
|
|
@ -42,7 +42,14 @@ export function Table<T>(props: TTableData<T>) {
|
||||||
className={cn("divide-x divide-subtle text-13 text-secondary", tBodyTrClassName)}
|
className={cn("divide-x divide-subtle text-13 text-secondary", tBodyTrClassName)}
|
||||||
>
|
>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<td key={`${column.key}-${keyExtractor(item)}`} className={cn("px-2.5 py-2", tdClassName)}>
|
<td
|
||||||
|
key={`${column.key}-${keyExtractor(item)}`}
|
||||||
|
className={cn(
|
||||||
|
"px-2.5 py-2",
|
||||||
|
tdClassName,
|
||||||
|
typeof column.tdClassName === "function" ? column.tdClassName(item) : column.tdClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
{column.tdRender(item)}
|
{column.tdRender(item)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,15 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export type TTableColumn<T> = {
|
export type TTableColumn<T> = {
|
||||||
key: string;
|
key: string;
|
||||||
content: string;
|
content: ReactNode;
|
||||||
thRender?: () => React.ReactNode;
|
thClassName?: string;
|
||||||
tdRender: (rowData: T) => React.ReactNode;
|
thRender?: () => ReactNode;
|
||||||
|
tdClassName?: string | ((rowData: T) => string);
|
||||||
|
tdRender: (rowData: T) => ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TTableData<T> = {
|
export type TTableData<T> = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue