feat: attributes filter / refactor of user data table (#17014)

* WIP restored from .git cache

* fix exports

* sortable row model

* feat column visibility component

* wip filters with nuqs

* pull in unique values from table into filters

* correctly assign filters via v/f

* inital selection bar refactor

* data-table selection bar + optmistic update of delete

* dynamic link

* migrate member list table to new data-table

* total list shows filtered value > db valuie

* add filters for attributes

* type errors

* make content bigger on lg

* add mb-6 to teams user datatable to match spacing spec

* correctly render multi-badge

* fix: masss asignment optimistic UI

* fix type errors

* remove log

* fix toolbar type error

* chore: Remove debug artifact

* type errors

* Update apps/web/public/static/locales/en/common.json

* use max-w-fit

* chore: Remove unused translation now we don't specify 'mass' in assign

* perf: fix: use the onBlur event to prevent focus loss whilst the list is rerendering

* Move the data-table exports together in the main barrel, then import

* fix exports that were lost in a merge

* fix exports that were lost in a merge

* fix groupteammapping/availbilityslider

* fix overflow problems

* add scrollbar-thin class

* fix type error

* user serverside values for faceted filters

* pass filters to serverside

* filter serverside

* fix team server side filter

* add loaded x of y

* attributes icon change

* correct implementation for text/input attr optimistic

* type check fixes

* fix platform checks

* fix types again

* fix types again

* fix types again

* add use client

* add use client

* fix-types

* fix: Add missing translation in EN

* fix e2e tests via testid

* fix e2e tests via testid

* fix: Member invite popup not popping up

* Update copyInviteLink to new-member-button testid

* Hopefully fix test ids this time

* fix: Use the right buttons on the right pages

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
sean-brydon
2024-10-15 17:45:11 +00:00
committed by GitHub
co-authored by Alex van Andel Peer Richelsen Udit Takkar
parent 0b914bef79
commit 7e44e686e8
43 changed files with 1462 additions and 1049 deletions
@@ -2,10 +2,10 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, ConfirmationDialogContent, Dialog, DialogTrigger, showToast } from "@calcom/ui";
import type { User } from "../UserListTable";
import type { UserTableUser } from "../types";
interface Props {
users: User[];
users: UserTableUser[];
onRemove: () => void;
}
@@ -14,9 +14,22 @@ export function DeleteBulkUsers({ users, onRemove }: Props) {
const selectedRows = users; // Get selected rows from table
const utils = trpc.useUtils();
const deleteMutation = trpc.viewer.organizations.bulkDeleteUsers.useMutation({
onSuccess: () => {
utils.viewer.organizations.listMembers.invalidate();
onSuccess: (_, { userIds }) => {
showToast("Deleted Users", "success");
utils.viewer.organizations.listMembers.setInfiniteData(
{ limit: 10, searchTerm: "", expand: ["attributes"] },
// @ts-expect-error - infinite data types are not correct
(oldData) => {
if (!oldData) return oldData;
return {
...oldData,
pages: oldData.pages.map((page) => ({
...page,
rows: page.rows.filter((user) => !userIds.includes(user.id)),
})),
};
}
);
},
onError: (error) => {
showToast(error.message, "error");
@@ -0,0 +1,55 @@
import type { Table } from "@tanstack/react-table";
import { useQueryState, parseAsBoolean } from "nuqs";
import { useCopy } from "@calcom/lib/hooks/useCopy";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
export function DynamicLink<T extends { username: string | null }>({
table,
domain,
}: {
table: Table<T>;
domain: string;
}) {
const { t } = useLocale();
const [dynamicLinkVisible, _] = useQueryState("dynamicLink", parseAsBoolean);
const { copyToClipboard, isCopied } = useCopy();
const numberOfSelectedRows = table.getSelectedRowModel().rows.length;
const isVisible = numberOfSelectedRows >= 2 && dynamicLinkVisible;
const users = table
.getSelectedRowModel()
.flatRows.map((row) => row.original.username)
.filter((u): u is string => u !== null);
const usersNameAsString = users.join("+");
const dynamicLinkOfSelectedUsers = `${domain}/${usersNameAsString}`;
const domainWithoutHttps = dynamicLinkOfSelectedUsers.replace(/https?:\/\//g, "");
return (
<>
{isVisible ? (
<div className="w-full gap-1 rounded-lg text-sm font-medium leading-none md:flex">
<div className="max-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>
</div>
) : null}
</>
);
}
@@ -22,10 +22,10 @@ import {
Icon,
} from "@calcom/ui";
import type { User } from "../UserListTable";
import type { UserTableUser } from "../types";
interface Props {
table: Table<User>;
table: Table<UserTableUser>;
orgTeams: RouterOutputs["viewer"]["organizations"]["getTeams"] | undefined;
}
@@ -1,9 +1,11 @@
import type { Table } from "@tanstack/react-table";
import type { ColumnFiltersState } from "@tanstack/react-table";
import { parseAsString, useQueryState, parseAsArrayOf } from "nuqs";
import { useState } from "react";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import { trpc } from "@calcom/trpc";
import {
Alert,
@@ -22,10 +24,11 @@ import {
showToast,
} from "@calcom/ui";
import type { User } from "../UserListTable";
import type { UserTableUser } from "../types";
interface Props {
table: Table<User>;
table: Table<UserTableUser>;
filters: ColumnFiltersState;
}
function useSelectedAttributes() {
@@ -78,7 +81,7 @@ function SelectedAttributeToAssign() {
return (
<CommandList>
<div className="flex flex items-center items-center gap-2 border-b px-3 py-2">
<div className="flex items-center gap-2 border-b px-3 py-2">
<span className="block">{foundAttribute.name}</span>
{translateableType && <span className="text-muted block text-xs">({t(translateableType)})</span>}
</div>
@@ -119,9 +122,11 @@ function SelectedAttributeToAssign() {
<>
<CommandItem>
<Input
value={selectedAttributeOption[0] || ""}
defaultValue={selectedAttributeOption[0] || ""}
type={foundAttribute.type === "TEXT" ? "text" : "number"}
onChange={(e) => {
onBlur={(e) => {
// trigger onBlur so it's set as Apply is pressed (but not onChange) which triggers
// a re-render which also loses focus.
setSelectedAttributeOption([e.target.value]);
}}
/>
@@ -133,13 +138,78 @@ function SelectedAttributeToAssign() {
);
}
export function MassAssignAttributesBulkAction({ table }: Props) {
export function MassAssignAttributesBulkAction({ table, filters }: Props) {
const { selectedAttribute, setSelectedAttribute, foundAttributeInCache } = useSelectedAttributes();
const [selectedAttributeOptions, setSelectedAttributeOptions] = useSelectedAttributeOption();
const [showMultiSelectWarning, setShowMultiSelectWarning] = useState(false);
const { t } = useLocale();
const utils = trpc.useContext();
const bulkAssignAttributes = trpc.viewer.attributes.bulkAssignAttributes.useMutation({
onSuccess: (success) => {
// Optimistically update the infinite query data
const selectedRows = table.getSelectedRowModel().flatRows;
utils.viewer.organizations.listMembers.setInfiniteData(
{
limit: 10,
searchTerm: "",
expand: ["attributes"],
filters: filters.map((filter) => ({
id: filter.id,
value: filter.value as string[],
})),
},
// @ts-expect-error i really dont know how to type this
(oldData) => {
const newPages = oldData?.pages.map((page) => ({
...page,
rows: page.rows.map((row) => {
if (selectedRows.some((selectedRow) => selectedRow.original.id === row.id)) {
// Update the attributes for the selected users
const attributeOptionValues = foundAttributeInCache?.options.filter((option) =>
selectedAttributeOptions.includes(option.id)
);
const newAttributes =
row.attributes?.filter((attr) => attr.attributeId !== selectedAttribute) || [];
if (attributeOptionValues && attributeOptionValues.length > 0) {
const newAttributeValues = attributeOptionValues?.map((value) => ({
id: value.id,
attributeId: value.attributeId,
value: value.value,
slug: value.slug,
}));
newAttributes.push(...newAttributeValues);
} else {
// Text or number input we don't have an option to fall back on
newAttributes.push({
id: "-1",
attributeId: foundAttributeInCache?.id ?? "-1",
value: selectedAttributeOptions[0],
slug: slugify(selectedAttributeOptions[0]),
});
}
return {
...row,
attributes: newAttributes,
};
}
return row;
}),
}));
return {
...oldData,
pages: newPages,
};
}
);
setSelectedAttribute(null);
setSelectedAttributeOptions([]);
showToast(success.message, "success");
},
onError: (error) => {
@@ -202,7 +272,7 @@ export function MassAssignAttributesBulkAction({ table }: Props) {
<>
<Popover>
<PopoverTrigger asChild>
<Button StartIcon="users">{t("mass_assign_attributes")}</Button>
<Button StartIcon="map-pin">{t("add_attributes")}</Button>
</PopoverTrigger>
{/* We dont really use shadows much - but its needed here */}
<PopoverContent className="p-0 shadow-md" align="start" sideOffset={12}>
@@ -262,9 +332,6 @@ export function MassAssignAttributesBulkAction({ table }: Props) {
attributes: attributesToAssign,
userIds: table.getSelectedRowModel().rows.map((row) => row.original.id),
});
setSelectedAttribute(null);
setSelectedAttributeOptions([]);
}
}}>
{t("apply")}
@@ -20,10 +20,10 @@ import {
showToast,
} from "@calcom/ui";
import type { User } from "../UserListTable";
import type { UserTableUser } from "../types";
interface Props {
table: Table<User>;
table: Table<UserTableUser>;
}
export function TeamListBulkAction({ table }: Props) {