Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: orgs create dynamic events from org member list #14912

Merged
merged 9 commits into from
May 10, 2024
1 change: 1 addition & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,7 @@
"user_from_team": "{{user}} from {{team}}",
"preview": "Preview",
"link_copied": "Link copied!",
"copied": "Copied!",
"private_link_copied": "Private link copied!",
"link_shared": "Link shared!",
"title": "Title",
Expand Down
59 changes: 58 additions & 1 deletion packages/features/users/components/UserTable/UserListTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { keepPreviousData } from "@tanstack/react-query";
import type { ColumnDef } from "@tanstack/react-table";
import type { ColumnDef, Table } from "@tanstack/react-table";
import { m } from "framer-motion";
import { useSession } from "next-auth/react";
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";

import { WEBAPP_URL } from "@calcom/lib/constants";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { useCopy } from "@calcom/lib/hooks/useCopy";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
Expand Down Expand Up @@ -110,13 +112,15 @@ function reducer(state: State, action: Action): State {

export function UserListTable() {
const { data: session } = useSession();
const { copyToClipboard, isCopied } = useCopy();
const { data: org } = trpc.viewer.organizations.listCurrent.useQuery();
const { data: teams } = trpc.viewer.organizations.getTeams.useQuery();
const tableContainerRef = useRef<HTMLDivElement>(null);
const [state, dispatch] = useReducer(reducer, initialState);
const { t } = useLocale();
const orgBranding = useOrgBranding();
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const [dynamicLinkVisible, setDynamicLinkVisible] = useState(false);
const { data, isPending, fetchNextPage, isFetching } =
trpc.viewer.organizations.listMembers.useInfiniteQuery(
{
Expand Down Expand Up @@ -325,6 +329,15 @@ export function UserListTable() {
type: "render",
render: (table) => <TeamListBulkAction table={table} />,
},
{
type: "action",
icon: "handshake",
label: "Group Meeting",
needsXSelected: 2,
onClick: () => {
setDynamicLinkVisible((old) => !old);
},
},
{
type: "render",
render: (table) => (
Expand All @@ -335,6 +348,50 @@ export function UserListTable() {
),
},
]}
renderAboveSelection={(table: Table<User>) => {
const numberOfSelectedRows = table.getSelectedRowModel().rows.length;
const isVisible = numberOfSelectedRows >= 2 && dynamicLinkVisible;

const users = table
.getSelectedRowModel()
.flatRows.map((row) => row.original.username)
.filter((u) => u !== null);
sean-brydon marked this conversation as resolved.
Show resolved Hide resolved

const usersNameAsString = users.join("+");

const dynamicLinkOfSelectedUsers = `${domain}/${usersNameAsString}`;
const domainWithoutHttps = dynamicLinkOfSelectedUsers.replace(/https?:\/\//g, "");

return (
<>
{isVisible ? (
<m.div
layout
className="bg-brand-default text-inverted item-center animate-fade-in-bottom hidden w-full gap-1 rounded-lg p-2 text-sm font-medium leading-none md:flex">
<div className="w-[300px] items-center truncate p-2">
<p>{domainWithoutHttps}</p>
</div>
<div className="ml-auto flex items-center">
<Button
StartIcon="copy"
size="sm"
onClick={() => copyToClipboard(dynamicLinkOfSelectedUsers)}>
{!isCopied ? t("copy") : t("copied")}
</Button>
<Button
EndIcon="external-link"
size="sm"
href={dynamicLinkOfSelectedUsers}
target="_blank"
rel="noopener noreferrer">
Open
</Button>
</div>
</m.div>
) : null}
</>
);
}}
tableContainerRef={tableContainerRef}
tableCTA={
adminOrOwner && (
Expand Down
57 changes: 37 additions & 20 deletions packages/ui/components/data-table/DataTableSelectionBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Table } from "@tanstack/react-table";
import { AnimatePresence, motion } from "framer-motion";
import type { Table as TableType } from "@tanstack/table-core/build/lib/types";
import { AnimatePresence } from "framer-motion";
import { Fragment } from "react";

import type { IconName } from "../..";
Expand All @@ -11,42 +12,58 @@ export type ActionItem<TData> =
label: string;
onClick: () => void;
icon?: IconName;
needsXSelected?: number;
}
| {
type: "render";
render: (table: Table<TData>) => React.ReactNode;
needsXSelected?: number;
};

interface DataTableSelectionBarProps<TData> {
table: Table<TData>;
actions?: ActionItem<TData>[];
renderAboveSelection?: (table: TableType<TData>) => React.ReactNode;
}

export function DataTableSelectionBar<TData>({ table, actions }: DataTableSelectionBarProps<TData>) {
export function DataTableSelectionBar<TData>({
table,
actions,
renderAboveSelection,
}: DataTableSelectionBarProps<TData>) {
const numberOfSelectedRows = table.getSelectedRowModel().rows.length;
const isVisible = numberOfSelectedRows > 0;

// Hacky left % to center
const actionsVisible = actions?.filter((a) => {
if (!a.needsXSelected) return true;
return a.needsXSelected <= numberOfSelectedRows;
});

return (
<AnimatePresence>
{isVisible ? (
<motion.div
initial={{ opacity: 0, y: 0 }}
animate={{ opacity: 1, y: 20 }}
exit={{ opacity: 0, y: 0 }}
className="bg-brand-default text-brand item-center fixed bottom-6 left-1/4 hidden gap-4 rounded-lg p-2 md:flex lg:left-1/2">
<div className="text-brand-subtle my-auto px-2">{numberOfSelectedRows} selected</div>
{actions?.map((action, index) => (
<Fragment key={index}>
{action.type === "action" ? (
<Button aria-label={action.label} onClick={action.onClick} StartIcon={action.icon}>
{action.label}
</Button>
) : action.type === "render" ? (
action.render(table)
) : null}
</Fragment>
))}
</motion.div>
<div className="fade-in fixed bottom-6 left-1/2 hidden -translate-x-1/2 gap-1 md:flex md:flex-col">
{renderAboveSelection && renderAboveSelection(table)}
<div className="bg-brand-default text-brand hidden items-center justify-between rounded-lg p-2 md:flex">
<p className="text-brand-subtle w-full px-2 text-center leading-none">
{numberOfSelectedRows} selected
</p>
{actionsVisible?.map((action, index) => {
return (
<Fragment key={index}>
{action.type === "action" ? (
<Button aria-label={action.label} onClick={action.onClick} StartIcon={action.icon}>
{action.label}
</Button>
) : action.type === "render" ? (
action.render(table)
) : null}
</Fragment>
);
})}
</div>
</div>
) : null}
</AnimatePresence>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/components/data-table/DataTableToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function DataTableToolbar<TData>({
const { t } = useLocale();

return (
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex items-center justify-end py-4">
{searchKey && (
<Input
className="max-w-64 mb-0 mr-auto rounded-md"
Expand Down
11 changes: 9 additions & 2 deletions packages/ui/components/data-table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
Row,
SortingState,
VisibilityState,
Table as TableType,
} from "@tanstack/react-table";
import {
flexRender,
Expand Down Expand Up @@ -33,6 +34,7 @@ export interface DataTableProps<TData, TValue> {
onSearch?: (value: string) => void;
filterableItems?: FilterableItems;
selectionOptions?: ActionItem<TData>[];
renderAboveSelection?: (table: TableType<TData>) => React.ReactNode;
tableCTA?: React.ReactNode;
isPending?: boolean;
onRowMouseclick?: (row: Row<TData>) => void;
Expand All @@ -54,6 +56,7 @@ export function DataTable<TData, TValue>({
isPending,
tableOverlay,
variant,
renderAboveSelection,
/** This should only really be used if you dont have actions in a row. */
onSearch,
onRowMouseclick,
Expand Down Expand Up @@ -101,7 +104,7 @@ export function DataTable<TData, TValue>({
virtualRows.length > 0 ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) : 0;

return (
<div className="space-y-4">
<div className="relative space-y-4">
<DataTableToolbar
table={table}
filterableItems={filterableItems}
Expand Down Expand Up @@ -171,7 +174,11 @@ export function DataTable<TData, TValue>({
</Table>
</div>
{/* <DataTablePagination table={table} /> */}
<DataTableSelectionBar table={table} actions={selectionOptions} />
<DataTableSelectionBar
table={table}
actions={selectionOptions}
renderAboveSelection={renderAboveSelection}
/>
</div>
);
}