fix: relocate action buttons on org member list (#20336)

* fix: relocate filter segment buttons on org member list

* implement toolbarContainerRef

* adjust location

* reorganize the hierarchy of DataTableProvider

* rename toolbar container to cta container

* move clear button to toolbar right
This commit is contained in:
Eunjae Lee
2025-03-26 14:18:39 +00:00
committed by GitHub
parent e1c001baff
commit d927662dd7
13 changed files with 107 additions and 69 deletions
@@ -1,6 +1,7 @@
import SettingsLayoutAppDir from "app/(use-page-wrapper)/settings/(settings-layout)/layout";
import { _generateMetadata, getTranslate } from "app/_utils";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import LegacyPage from "@calcom/features/ee/organizations/pages/members";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
@@ -14,7 +15,10 @@ const Page = async () => {
const t = await getTranslate();
const children = (
<SettingsHeader title={t("organization_members")} description={t("organization_description")}>
<SettingsHeader
title={t("organization_members")}
description={t("organization_description")}
ctaClassName={CTA_CONTAINER_CLASS_NAME}>
<LegacyPage />
</SettingsHeader>
);
@@ -1,6 +1,6 @@
"use client";
import { DataTableProvider } from "@calcom/features/data-table";
import { DataTableProvider } from "@calcom/features/data-table/DataTableProvider";
import {
RoutingFormResponsesTable,
FailedBookingsByField,
+5 -1
View File
@@ -1,5 +1,6 @@
"use client";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import MembersView from "@calcom/features/ee/organizations/pages/members";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -11,7 +12,10 @@ const MembersPage: React.FC = () => {
withoutMain={false}
title={t("organization_members")}
description={t("organization_description")}
subtitle={t("organization_description")}>
heading={t("organization_members")}
subtitle={t("organization_description")}
headerClassName="hidden md:block"
actions={<div className={`mb-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
<MembersView />
</Shell>
);
@@ -3035,6 +3035,7 @@
"tip_username_plus": "Tip: You can a '+' between usernames: cal.com/anna+brian to make a dynamic group meeting",
"user_has_no_team_yet": "You don't have a team yet",
"no_team_members": "You don't have team members yet",
"display": "Display",
"could_not_find_slug_to_publish_org": "Could not find slug to publish the organization",
"picklist": "Picklist",
"name_or_email": "Name or Email",
@@ -2,10 +2,10 @@
import type { SortingState, OnChangeFn, VisibilityState } from "@tanstack/react-table";
import { useQueryState, parseAsArrayOf, parseAsJson, parseAsInteger } from "nuqs";
import { createContext, useCallback } from "react";
import { createContext, useCallback, useRef, useEffect } from "react";
import { z } from "zod";
import { type FilterValue, ZFilterValue, ZSorting, ZColumnVisibility } from "./types";
import { type FilterValue, ZFilterValue, ZSorting, ZColumnVisibility } from "./lib/types";
const ZActiveFilter = z.object({
f: z.string(),
@@ -15,6 +15,8 @@ const ZActiveFilter = z.object({
type ActiveFilter = z.infer<typeof ZActiveFilter>;
export type DataTableContextType = {
ctaContainerRef?: React.RefObject<HTMLDivElement>;
activeFilters: ActiveFilter[];
clearAll: (exclude?: string[]) => void;
addFilter: (columnId: string) => void;
@@ -45,10 +47,15 @@ const DEFAULT_PAGE_SIZE = 10;
interface DataTableProviderProps {
children: React.ReactNode;
ctaContainerClassName?: string;
defaultPageSize?: number;
}
export function DataTableProvider({ children, defaultPageSize = DEFAULT_PAGE_SIZE }: DataTableProviderProps) {
export function DataTableProvider({
children,
defaultPageSize = DEFAULT_PAGE_SIZE,
ctaContainerClassName,
}: DataTableProviderProps) {
const [activeFilters, setActiveFilters] = useQueryState(
"activeFilters",
parseAsArrayOf(parseAsJson(ZActiveFilter.parse)).withDefault(DEFAULT_ACTIVE_FILTERS)
@@ -119,9 +126,18 @@ export function DataTableProvider({ children, defaultPageSize = DEFAULT_PAGE_SIZ
[setPageSize, setPageIndex]
);
const ctaContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (ctaContainerClassName) {
const element = document.getElementsByClassName(ctaContainerClassName)[0] as HTMLDivElement;
ctaContainerRef.current = element;
}
}, [ctaContainerClassName]);
return (
<DataTableContext.Provider
value={{
ctaContainerRef,
activeFilters,
addFilter,
clearAll,
@@ -28,7 +28,7 @@ function ColumnVisibilityButtonComponent<TData>(
{
children,
color = "secondary",
EndIcon = "sliders-vertical",
StartIcon = "sliders-vertical",
table,
...rest
}: ColumnVisiblityProps<TData> & ButtonProps,
@@ -41,8 +41,8 @@ function ColumnVisibilityButtonComponent<TData>(
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button ref={ref} color={color} EndIcon={EndIcon} {...rest}>
{children ? children : t("View")}
<Button ref={ref} color={color} StartIcon={StartIcon} {...rest}>
{children ? children : t("display")}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
@@ -1,6 +1,6 @@
import { useContext } from "react";
import { DataTableContext } from "../lib/context";
import { DataTableContext } from "../DataTableProvider";
export function useDataTable() {
const context = useContext(DataTableContext);
+1 -1
View File
@@ -1,6 +1,6 @@
export * from "./components";
export * from "./lib/types";
export * from "./lib/utils";
export * from "./lib/context";
export * from "./DataTableProvider";
export * from "./lib/resizing";
export * from "./hooks";
@@ -1,3 +1,5 @@
"use client";
import type { Header, Table, ColumnSizingState } from "@tanstack/react-table";
// eslint-disable-next-line no-restricted-imports
import debounce from "lodash/debounce";
+2 -2
View File
@@ -1,5 +1,3 @@
"use client";
import type {
SingleSelectFilterValue,
MultiSelectFilterValue,
@@ -166,3 +164,5 @@ export const convertMapToFacetedValues = (map: Map<FacetedValue, number> | undef
return { label: option.label as string, value: option.value as string | number, section: option.section };
});
};
export const CTA_CONTAINER_CLASS_NAME = "data_table_cta_container";
@@ -1,5 +1,6 @@
"use client";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import Shell from "@calcom/features/shell/Shell";
import { UserListTable } from "@calcom/features/users/components/UserTable/UserListTable";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -66,7 +67,8 @@ const PlatformMembersView = () => {
title={t("platform_members")}
subtitle={t("platform_members_description")}
withoutMain={false}
isPlatformUser={true}>
isPlatformUser={true}
actions={<div className={CTA_CONTAINER_CLASS_NAME} />}>
<div>{!isPending && canLoggedInUserSeeMembers && <UserListTable />}</div>
</Shell>
);
@@ -1,13 +1,14 @@
import React, { Suspense } from "react";
import { Icon } from "@calcom/ui/components/icon";
import classNames from "@calcom/ui/classNames";
import { Icon } from "@calcom/ui/components/icon";
interface HeaderProps {
children: React.ReactNode;
title?: string;
description?: string;
CTA?: React.ReactNode;
ctaClassName?: string;
borderInShellHeader?: boolean;
backButton?: boolean;
}
@@ -17,6 +18,7 @@ export default function Header({
title,
description,
CTA,
ctaClassName,
borderInShellHeader,
backButton,
}: HeaderProps) {
@@ -28,27 +30,29 @@ export default function Header({
borderInShellHeader && "rounded-t-lg border px-4 py-6 sm:px-6",
borderInShellHeader === undefined && "mb-8 border-b pb-8"
)}>
<div className="flex w-full items-center">
{backButton && (
<a href="javascript:history.back()">
<Icon name="arrow-left" className="mr-7" />
</a>
)}
<div>
{title ? (
<h1 className="font-cal text-emphasis mb-1 text-xl font-semibold leading-5 tracking-wide">
{title}
</h1>
) : (
<div className="bg-emphasis mb-1 h-5 w-24 animate-pulse rounded-lg" />
)}
{description ? (
<p className="text-default text-sm ltr:mr-4 rtl:ml-4">{description}</p>
) : (
<div className="bg-emphasis h-5 w-32 animate-pulse rounded-lg" />
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center">
<div className="flex w-full items-center">
{backButton && (
<a href="javascript:history.back()">
<Icon name="arrow-left" className="mr-7" />
</a>
)}
<div>
{title ? (
<h1 className="font-cal text-emphasis mb-1 text-xl font-semibold leading-5 tracking-wide">
{title}
</h1>
) : (
<div className="bg-emphasis mb-1 h-5 w-24 animate-pulse rounded-lg" />
)}
{description ? (
<p className="text-default text-sm ltr:mr-4 rtl:ml-4">{description}</p>
) : (
<div className="bg-emphasis h-5 w-32 animate-pulse rounded-lg" />
)}
</div>
</div>
<div className="ms-auto flex-shrink-0">{CTA}</div>
<div className={classNames("flex-shrink-0 sm:ms-auto", ctaClassName)}>{CTA}</div>
</div>
</header>
<Suspense fallback={<Icon name="loader" className="mx-auto my-5 animate-spin" />}>{children}</Suspense>
@@ -5,10 +5,11 @@ import { getCoreRowModel, getSortedRowModel, useReactTable, type ColumnDef } fro
import { useSession } from "next-auth/react";
import { useQueryState, parseAsBoolean } from "nuqs";
import { useMemo, useReducer, useState } from "react";
import { createPortal } from "react-dom";
import {
DataTableWrapper,
DataTableProvider,
DataTableWrapper,
DataTableToolbar,
DataTableSelectionBar,
DataTableFilters,
@@ -16,6 +17,7 @@ import {
ColumnFilterType,
convertFacetedValuesToMap,
useDataTable,
CTA_CONTAINER_CLASS_NAME,
} from "@calcom/features/data-table";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -101,7 +103,7 @@ function reducer(state: UserTableState, action: UserTableAction): UserTableState
export function UserListTable() {
return (
<DataTableProvider defaultPageSize={25}>
<DataTableProvider defaultPageSize={25} ctaContainerClassName={CTA_CONTAINER_CLASS_NAME}>
<UserListTableContent />
</DataTableProvider>
);
@@ -138,7 +140,7 @@ function UserListTableContent() {
const columnFilters = useColumnFilters();
const { limit, offset } = useDataTable();
const { limit, offset, ctaContainerRef } = useDataTable();
const { data, isPending } = trpc.viewer.organizations.listMembers.useQuery(
{
@@ -527,40 +529,9 @@ function UserListTableContent() {
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
<DataTableFilters.ActiveFilters table={table} />
<DataTableFilters.AddFilterButton table={table} variant="sm" showWhenFilterApplied />
<DataTableFilters.ClearFiltersButton />
</>
}
ToolbarRight={
<>
<DataTableToolbar.CTA
type="button"
color="secondary"
StartIcon="file-down"
loading={isDownloading}
onClick={() => handleDownload()}
data-testid="export-members-button">
{t("download")}
</DataTableToolbar.CTA>
<DataTableFilters.ColumnVisibilityButton table={table} />
{adminOrOwner && (
<DataTableToolbar.CTA
type="button"
color="primary"
StartIcon="plus"
onClick={() =>
dispatch({
type: "INVITE_MEMBER",
payload: {
showModal: true,
},
})
}
data-testid="new-organization-member-button">
{t("add")}
</DataTableToolbar.CTA>
)}
</>
}>
ToolbarRight={<DataTableFilters.ClearFiltersButton />}>
{numberOfSelectedRows >= 2 && dynamicLinkVisible && (
<DataTableSelectionBar.Root className="!bottom-16 md:!bottom-20">
<DynamicLink table={table} domain={domain} />
@@ -603,6 +574,40 @@ function UserListTableContent() {
{state.impersonateMember.showModal && <ImpersonationMemberModal dispatch={dispatch} state={state} />}
{state.changeMemberRole.showModal && <ChangeUserRoleModal dispatch={dispatch} state={state} />}
{state.editSheet.showModal && <EditUserSheet dispatch={dispatch} state={state} />}
{ctaContainerRef?.current &&
createPortal(
<div className="flex items-center gap-2">
<DataTableToolbar.CTA
type="button"
color="secondary"
StartIcon="file-down"
loading={isDownloading}
onClick={() => handleDownload()}
data-testid="export-members-button">
{t("download")}
</DataTableToolbar.CTA>
<DataTableFilters.ColumnVisibilityButton table={table} />
{adminOrOwner && (
<DataTableToolbar.CTA
type="button"
color="primary"
StartIcon="plus"
onClick={() =>
dispatch({
type: "INVITE_MEMBER",
payload: {
showModal: true,
},
})
}
data-testid="new-organization-member-button">
{t("add")}
</DataTableToolbar.CTA>
)}
</div>,
ctaContainerRef.current
)}
</>
);
}