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:
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
+19
-3
@@ -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,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";
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user