fix: re-implement filter components in /insights/routing (#18655)

* fix: re-implement team filter & people filter components

* implement DateRangeFilter

* show selected filter values

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
This commit is contained in:
Eunjae Lee
2025-01-24 17:38:48 +01:00
committed by GitHub
co-authored by Benny Joo
parent a069af715e
commit d5e633b481
25 changed files with 770 additions and 265 deletions
+21
View File
@@ -118,6 +118,16 @@
<path d="M16 2v4" ></path>
<path d="M21.29 14.7a2.43 2.43 0 0 0-2.65-.52c-.3.12-.57.3-.8.53l-.34.34-.35-.34a2.43 2.43 0 0 0-2.65-.53c-.3.12-.56.3-.79.53-.95.94-1 2.53.2 3.74L17.5 22l3.6-3.55c1.2-1.21 1.14-2.8.19-3.74Z" ></path>
</symbol>
<symbol class="lucide lucide-calendar-range" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="calendar-range">
<rect width="18" height="18" x="3" y="4" rx="2" ></rect>
<path d="M16 2v4" ></path>
<path d="M3 10h18" ></path>
<path d="M8 2v4" ></path>
<path d="M17 14h-6" ></path>
<path d="M13 18H7" ></path>
<path d="M7 14h.01" ></path>
<path d="M17 18h.01" ></path>
</symbol>
<symbol class="lucide lucide-calendar-search" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="calendar-search">
<path d="M16 2v4" ></path>
<path d="M21 11.75V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7.25" ></path>
@@ -548,6 +558,17 @@
<path d="M22 18h-5.9c-1.3 0-2.6-.7-3.3-1.8l-.5-.8" ></path>
<path d="m18 14 4 4-4 4" ></path>
</symbol>
<symbol class="lucide lucide-sliders-horizontal" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="sliders-horizontal">
<line x1="21" x2="14" y1="4" y2="4" ></line>
<line x1="10" x2="3" y1="4" y2="4" ></line>
<line x1="21" x2="12" y1="12" y2="12" ></line>
<line x1="8" x2="3" y1="12" y2="12" ></line>
<line x1="21" x2="16" y1="20" y2="20" ></line>
<line x1="12" x2="3" y1="20" y2="20" ></line>
<line x1="14" x2="14" y1="2" y2="6" ></line>
<line x1="8" x2="8" y1="10" y2="14" ></line>
<line x1="16" x2="16" y1="18" y2="22" ></line>
</symbol>
<symbol class="lucide lucide-sliders-vertical" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="sliders-vertical">
<line x1="4" x2="4" y1="21" y2="14" ></line>
<line x1="4" x2="4" y1="10" y2="3" ></line>

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

@@ -2078,6 +2078,7 @@
"looking_for_more_analytics": "Looking for more analytics?",
"looking_for_more_insights": "Looking for more Insights?",
"filters": "Filters",
"filter": "Filter",
"add_filter": "Add filter",
"add_rule": "Add rule",
"add_rule_group": "Add rule group",
@@ -2741,10 +2742,10 @@
"no_matching_members_will_fallback_to_all_assigned_members": "No matching members. It will fallback to using the team members assigned to the event type. Consider adding a fallback or correcting the logic of using_fallback_members",
"hide_calendar_event_details": "Hide calendar event details on shared calendars",
"description_hide_calendar_event_details": "When a calendar is shared, events are visible to readers but their details are hidden from those without write access.",
"last_number_of_days": "last {{count}} days",
"month_to_date": "month to date",
"year_to_date": "year to date",
"custom_range": "custom range",
"last_number_of_days": "Last {{count}} days",
"month_to_date": "Month to date",
"year_to_date": "Year to date",
"custom_range": "Custom range",
"refund_policy": "Refund Policy",
"always": "Always",
"never": "Never",
@@ -70,8 +70,8 @@ export function DataTableWrapper<TData, TValue>({
<DataTableToolbar.Root>
<div className="flex w-full flex-col gap-2">
<div className="flex w-full flex-wrap justify-between gap-2">
<div className="flex flex-wrap gap-2">{ToolbarLeft}</div>
<div className="flex flex-wrap gap-2">{ToolbarRight}</div>
<div className="flex flex-wrap items-center gap-2">{ToolbarLeft}</div>
<div className="flex flex-wrap items-center gap-2">{ToolbarRight}</div>
</div>
</div>
@@ -0,0 +1,27 @@
import { useDataTable } from "@calcom/features/data-table";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, Button, Tooltip } from "@calcom/ui";
export const ClearFiltersButton = ({ exclude }: { exclude?: string[] }) => {
const { t } = useLocale();
const { activeFilters, clearAll } = useDataTable();
if (!activeFilters?.length || (exclude && activeFilters.every((filter) => exclude.includes(filter.f)))) {
return null;
}
return (
<Tooltip content={t("clear_filters")}>
<Button
variant="icon"
color="secondary"
target="_blank"
rel="noreferrer"
className="min-w-24 h-[38px] border-0"
onClick={() => clearAll(exclude)}>
<Icon name="x" className="mr-1 h-4 w-4" />
{t("clear")}
</Button>
</Tooltip>
);
};
@@ -0,0 +1,191 @@
import type { Dayjs } from "dayjs";
import { useState } from "react";
import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { DateRangePicker } from "@calcom/ui";
import { Select } from "@calcom/ui";
import "../../../insights/filters/DateSelect.css";
import { useDataTable, useFilterValue } from "../../hooks";
import type { FilterableColumn } from "../../lib/types";
import { ZDateRangeFilterValue } from "../../lib/types";
type PresetOption = {
labelKey: string;
i18nOptions?: Record<string, string | number>;
value: string;
};
type DateRangeFilterProps = {
className?: string;
column: Extract<FilterableColumn, { type: "date_range" }>;
};
const CUSTOM_PRESET_VALUE = "c" as const;
const DEFAULT_PRESET: PresetOption = {
labelKey: "last_number_of_days",
i18nOptions: { count: 7 },
value: "w",
};
const CUSTOM_PRESET: PresetOption = { labelKey: "custom_range", value: CUSTOM_PRESET_VALUE };
const PRESET_OPTIONS: PresetOption[] = [
{ labelKey: "today", value: "tdy" },
DEFAULT_PRESET,
{ labelKey: "last_number_of_days", i18nOptions: { count: 30 }, value: "t" },
{ labelKey: "month_to_date", value: "m" },
{ labelKey: "year_to_date", value: "y" },
CUSTOM_PRESET,
];
const getDateRangeFromPreset = (val: string | null) => {
let startDate;
let endDate;
const preset = PRESET_OPTIONS.find((o) => o.value === val);
if (!preset) {
return { startDate: getDefaultStartDate(), endDate: getDefaultEndDate(), preset: CUSTOM_PRESET };
}
switch (val) {
case "tdy": // Today
startDate = dayjs().startOf("day");
endDate = dayjs().endOf("day");
break;
case "w": // Last 7 days
startDate = dayjs().subtract(1, "week").startOf("day");
endDate = dayjs().endOf("day");
break;
case "t": // Last 30 days
startDate = dayjs().subtract(30, "day").startOf("day");
endDate = dayjs().endOf("day");
break;
case "m": // Month to Date
startDate = dayjs().startOf("month");
endDate = dayjs().endOf("day");
break;
case "y": // Year to Date
startDate = dayjs().startOf("year");
endDate = dayjs().endOf("day");
break;
default:
startDate = getDefaultStartDate();
endDate = getDefaultEndDate();
break;
}
return { startDate, endDate, preset };
};
const getDefaultStartDate = () => dayjs().subtract(1, "week").startOf("day");
const getDefaultEndDate = () => dayjs().endOf("day");
export const DateRangeFilter = ({ className, column }: DateRangeFilterProps) => {
const filterValue = useFilterValue(column.id, ZDateRangeFilterValue);
const { updateFilter } = useDataTable();
const { t } = useLocale();
const currentDate = dayjs();
const [startDate, setStartDate] = useState<Dayjs>(
filterValue?.data.startDate ? dayjs(filterValue.data.startDate) : getDefaultStartDate()
);
const [endDate, setEndDate] = useState<Dayjs | undefined>(
filterValue?.data.endDate ? dayjs(filterValue.data.endDate) : getDefaultEndDate()
);
const [selectedPreset, setSelectedPreset] = useState<PresetOption>(DEFAULT_PRESET);
const updateValues = ({
preset,
startDate,
endDate,
}: {
preset: PresetOption;
startDate: Dayjs;
endDate?: Dayjs;
}) => {
setSelectedPreset(preset);
setStartDate(startDate);
setEndDate(endDate);
if (startDate && endDate) {
updateFilter(column.id, {
type: "date_range",
data: {
startDate: startDate.toDate().toISOString(),
endDate: endDate.toDate().toISOString(),
preset: preset.value,
},
});
}
};
const updateDateRangeFromPreset = (val: string | null) => {
if (val === CUSTOM_PRESET_VALUE) {
updateValues({
preset: CUSTOM_PRESET,
startDate,
endDate,
});
} else {
const r = getDateRangeFromPreset(val);
updateValues({
preset: r.preset,
startDate: r.startDate,
endDate: r.endDate,
});
}
};
const updateDateRangeFromPicker = ({
startDate,
endDate,
}: {
startDate?: Date | undefined;
endDate?: Date | undefined;
}) => {
updateValues({
preset: CUSTOM_PRESET,
startDate: startDate ? dayjs(startDate) : getDefaultStartDate(),
endDate: endDate ? dayjs(endDate) : undefined,
});
};
return (
<div className={classNames("ml inline-flex space-x-2 rtl:space-x-reverse", className)}>
<DateRangePicker
dates={{
startDate: startDate.toDate(),
endDate: endDate?.toDate(),
}}
minDate={currentDate.subtract(2, "year").toDate()}
maxDate={currentDate.toDate()}
disabled={false}
onDatesChange={updateDateRangeFromPicker}
/>
<Select
variant="default"
data-testid="insights-preset"
options={PRESET_OPTIONS.map((o) => ({
label: t(o.labelKey, o.i18nOptions),
value: o.value,
}))}
value={{ label: t(selectedPreset.labelKey, selectedPreset.i18nOptions), value: selectedPreset.value }}
className="w-40 capitalize text-black"
defaultValue={{
label: t(selectedPreset.labelKey, selectedPreset.i18nOptions),
value: selectedPreset.value,
}}
styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }) }}
menuPortalTarget={document.body}
onChange={(e) => {
if (e) {
updateDateRangeFromPreset(e.value);
}
}}
/>
</div>
);
};
@@ -1,6 +1,7 @@
"use client";
import type { FilterableColumn } from "../../lib/types";
import { DateRangeFilter } from "./DateRangeFilter";
import { MultiSelectFilterOptions } from "./MultiSelectFilterOptions";
import { NumberFilterOptions } from "./NumberFilterOptions";
import { SingleSelectFilterOptions } from "./SingleSelectFilterOptions";
@@ -19,6 +20,8 @@ export function FilterOptions({ column }: FilterOptionsProps) {
return <SingleSelectFilterOptions column={column} />;
} else if (column.type === "number") {
return <NumberFilterOptions column={column} />;
} else if (column.type === "date_range") {
return <DateRangeFilter column={column} />;
} else {
return null;
}
@@ -0,0 +1,73 @@
// eslint-disable-next-line no-restricted-imports
import startCase from "lodash/startCase";
import type { IconName } from "@calcom/ui";
import { Button, Icon, Popover, PopoverContent, PopoverTrigger } from "@calcom/ui";
import { useFilterValue } from "../../hooks";
import { type FilterableColumn, type FilterValue, ZFilterValue } from "../../lib/types";
import {
isSingleSelectFilterValue,
isMultiSelectFilterValue,
isTextFilterValue,
isNumberFilterValue,
} from "../../lib/utils";
import { FilterOptions } from "./FilterOptions";
const FILTER_ICONS: Record<FilterableColumn["type"], IconName> = {
text: "file-text",
number: "binary",
multi_select: "layers",
single_select: "disc",
date_range: "calendar-range",
};
type FilterPopoverProps = {
column: FilterableColumn;
};
const useSelectedLabels = ({
column,
filterValue,
}: {
column: FilterableColumn;
filterValue?: FilterValue;
}) => {
if (isTextFilterValue(filterValue)) {
return { text: filterValue.data.operand, moreCount: 0 };
} else if (isNumberFilterValue(filterValue)) {
return { text: filterValue.data.operand, moreCount: 0 };
} else if (isSingleSelectFilterValue(filterValue)) {
const options = (column as Extract<FilterableColumn, { type: "single_select" }>).options;
const text = options.find((opt) => opt.value === filterValue.data)?.label;
return { text, moreCount: 0 };
} else if (isMultiSelectFilterValue(filterValue)) {
const options = (column as Extract<FilterableColumn, { type: "multi_select" }>).options;
const text = options.find((opt) => opt.value === filterValue.data[0])?.label;
return { text, moreCount: filterValue.data.length - 1 };
} else {
return { text: "", moreCount: 0 };
}
};
export function FilterPopover({ column }: FilterPopoverProps) {
const icon = column.icon || FILTER_ICONS[column.type];
const filterValue = useFilterValue(column.id, ZFilterValue);
const { text, moreCount } = useSelectedLabels({ column, filterValue });
return (
<Popover>
<PopoverTrigger asChild>
<Button color="secondary" className="items-center">
<Icon name={icon} className="mr-2 h-4 w-4" />
<span>{startCase(column.title)}</span>
{text && <span className="bg-subtle ml-2 rounded-md px-2">{text}</span>}
{moreCount > 0 && <span className="bg-subtle ml-2 rounded-md px-2">+{moreCount}</span>}
<Icon name="chevron-down" className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<FilterOptions column={column} />
</PopoverContent>
</Popover>
);
}
@@ -29,10 +29,10 @@ export function MultiSelectFilterOptions({ column }: MultiSelectFilterOptionsPro
return (
<Command>
<CommandInput placeholder={t("search_options")} />
<CommandInput placeholder={t("search")} />
<CommandList>
<CommandEmpty>{t("no_options_found")}</CommandEmpty>
{Array.from(column.options.keys()).map((option) => {
{column.options.map((option) => {
if (!option) return null;
const { label: optionLabel, value: optionValue } =
typeof option === "string" ? { label: option, value: option } : option;
@@ -29,10 +29,10 @@ export function SingleSelectFilterOptions({ column }: SingleSelectFilterOptionsP
return (
<Command>
<CommandInput placeholder={t("search_options")} />
<CommandInput placeholder={t("search")} />
<CommandList>
<CommandEmpty>{t("no_options_found")}</CommandEmpty>
{Array.from(column.options.keys()).map((option) => {
{column.options.map((option) => {
if (!option) return null;
const { label: optionLabel, value: optionValue } =
typeof option === "string" ? { label: option, value: option } : option;
@@ -25,8 +25,10 @@ import {
} from "@calcom/ui";
import { useDataTable } from "../../hooks";
import type { FilterableColumn, ExternalFilter } from "../../lib/types";
import { FilterOptions } from "./FilterOptions";
import type { FilterableColumn } from "../../lib/types";
import { ClearFiltersButton } from "./ClearFiltersButton";
import { DateRangeFilter } from "./DateRangeFilter";
import { FilterPopover } from "./FilterPopover";
interface ColumnVisiblityProps<TData> {
table: Table<TData>;
@@ -122,16 +124,14 @@ const ColumnVisibilityButton = forwardRef(ColumnVisibilityButtonComponent) as <T
// Filters
interface AddFilterButtonProps<TData> {
table: Table<TData>;
externalFilters?: ExternalFilter[];
}
function AddFilterButtonComponent<TData>(
{ table, externalFilters }: AddFilterButtonProps<TData>,
{ table }: AddFilterButtonProps<TData>,
ref: React.Ref<HTMLButtonElement>
) {
const { t } = useLocale();
const { activeFilters, setActiveFilters, displayedExternalFilters, setDisplayedExternalFilters } =
useDataTable();
const { activeFilters, setActiveFilters } = useDataTable();
const filterableColumns = useFilterableColumns(table);
@@ -148,14 +148,14 @@ function AddFilterButtonComponent<TData>(
<div className="flex items-center space-x-2">
<Popover>
<PopoverTrigger asChild>
<Button ref={ref} color="secondary" className="border-dashed">
<Icon name="filter" className="mr-2 h-4 w-4" />
{t("add_filter")}
<Button ref={ref} color="secondary" data-testid="add-filter-button">
<Icon name="sliders-horizontal" className="mr-2 h-4 w-4" />
{t("filter")}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder={t("search_columns")} />
<CommandInput placeholder={t("search")} />
<CommandList>
<CommandEmpty>{t("no_columns_found")}</CommandEmpty>
{filterableColumns.map((column) => {
@@ -169,16 +169,6 @@ function AddFilterButtonComponent<TData>(
</CommandItem>
);
})}
{(externalFilters || [])
.filter((filter) => !displayedExternalFilters.includes(filter.key))
.map((filter, index) => (
<CommandItem
key={index}
onSelect={() => setDisplayedExternalFilters((prev) => [...prev, filter.key])}
className="px-4 py-2">
{t(filter.titleKey)}
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
@@ -205,9 +195,23 @@ function useFilterableColumns<TData>(table: Table<TData>) {
type,
};
if (type === "multi_select" || type === "single_select") {
const values = column.getFacetedUniqueValues();
const options = Array.from(values.keys()).map((option) => {
if (typeof option === "string") {
return {
label: option,
value: option,
};
} else {
return {
label: option.label as string,
value: option.value as string | number,
};
}
});
return {
...base,
options: column.getFacetedUniqueValues(),
options,
};
} else {
return {
@@ -229,18 +233,10 @@ const AddFilterButton = forwardRef(AddFilterButtonComponent) as <TData>(
// Add the new ActiveFilters component
interface ActiveFiltersProps<TData> {
table: Table<TData>;
externalFilters?: ExternalFilter[];
}
const filterIcons = {
text: "file-text",
number: "binary",
multi_select: "layers",
single_select: "disc",
} as const;
function ActiveFilters<TData>({ table, externalFilters }: ActiveFiltersProps<TData>) {
const { activeFilters, displayedExternalFilters } = useDataTable();
function ActiveFilters<TData>({ table }: ActiveFiltersProps<TData>) {
const { activeFilters } = useDataTable();
const filterableColumns = useFilterableColumns(table);
return (
@@ -248,30 +244,18 @@ function ActiveFilters<TData>({ table, externalFilters }: ActiveFiltersProps<TDa
{activeFilters.map((filter) => {
const column = filterableColumns.find((col) => col.id === filter.f);
if (!column) return null;
const icon = column.icon || filterIcons[column.type];
return (
<Popover key={column.id}>
<PopoverTrigger asChild>
<Button color="secondary">
<Icon name={icon} className="mr-2 h-4 w-4" />
{startCase(column.title)}
<Icon name="chevron-down" className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<FilterOptions column={column} />
</PopoverContent>
</Popover>
);
})}
{(displayedExternalFilters || []).map((key) => {
const filter = externalFilters?.find((filter) => filter.key === key);
if (!filter) return null;
return <Fragment key={key}>{filter.component()}</Fragment>;
return <FilterPopover key={column.id} column={column} />;
})}
</>
);
}
// Update the export to include ActiveFilters
export const DataTableFilters = { ColumnVisibilityButton, AddFilterButton, ActiveFilters };
export const DataTableFilters = {
ColumnVisibilityButton,
AddFilterButton,
ActiveFilters,
ClearFiltersButton,
};
export { DateRangeFilter };
@@ -1,7 +1,7 @@
export { DataTableToolbar } from "./DataTableToolbar";
export { DataTableSelectionBar } from "./DataTableSelectionBar";
export { DataTablePagination } from "./DataTablePagination";
export { DataTableFilters } from "./filters";
export * from "./filters";
export { DataTable } from "./DataTable";
export { DataTableSkeleton } from "./DataTableSkeleton";
export { DataTableWrapper } from "./DataTableWrapper";
@@ -1,12 +1,22 @@
import { useMemo } from "react";
import { useCallback, useMemo } from "react";
import type { ColumnFilter } from "../lib/types";
import { ZFilterValue } from "../lib/types";
import { isMultiSelectFilterValue } from "../lib/utils";
import { useDataTable } from "./useDataTable";
export function useColumnFilters(): ColumnFilter[] {
export function useColumnFilters({ exclude }: { exclude?: string[] } = {}): ColumnFilter[] {
const { activeFilters } = useDataTable();
// Stringify the exclude array for stable memoization
const excludeKey = useMemo(() => JSON.stringify(exclude?.sort()), [exclude]);
// Now this will only change when the actual contents change
const filterExcluded = useCallback(
(filter: ColumnFilter) => !exclude?.includes(filter.id),
[excludeKey] // Use the stringified key instead of the array
);
return useMemo(() => {
return (activeFilters || [])
.filter(
@@ -22,6 +32,7 @@ export function useColumnFilters(): ColumnFilter[] {
};
})
.filter((filter): filter is ColumnFilter => filter !== null)
.filter(filterExcluded)
.filter((filter) => {
// The empty arrays in `filtersSearchState` keep the filter UI component,
// but we do not send them to the actual query.
@@ -31,5 +42,5 @@ export function useColumnFilters(): ColumnFilter[] {
}
return true;
});
}, [activeFilters]);
}, [activeFilters, filterExcluded]);
}
+18 -21
View File
@@ -2,7 +2,7 @@
import type { SortingState, OnChangeFn } from "@tanstack/react-table";
import { useQueryState, parseAsArrayOf, parseAsJson } from "nuqs";
import { createContext, useCallback, useState, type Dispatch, type SetStateAction } from "react";
import { createContext, useCallback } from "react";
import { z } from "zod";
import { type FilterValue, ZFilterValue, ZSorting } from "./types";
@@ -17,16 +17,12 @@ type ActiveFilter = z.infer<typeof ZActiveFilter>;
export type DataTableContextType = {
activeFilters: ActiveFilter[];
setActiveFilters: (filters: ActiveFilter[]) => void;
clearAll: () => void;
clearAll: (exclude?: string[]) => void;
updateFilter: (columnId: string, value: FilterValue) => void;
removeFilter: (columnId: string) => void;
sorting: SortingState;
setSorting: OnChangeFn<SortingState>;
displayedExternalFilters: string[];
setDisplayedExternalFilters: Dispatch<SetStateAction<string[]>>;
removeDisplayedExternalFilter: (key: string) => void;
};
export const DataTableContext = createContext<DataTableContextType | null>(null);
@@ -41,24 +37,28 @@ export function DataTableProvider({ children }: { children: React.ReactNode }) {
parseAsArrayOf(parseAsJson(ZSorting.parse)).withDefault([])
);
const [displayedExternalFilters, setDisplayedExternalFilters] = useState<string[]>([]);
const removeDisplayedExternalFilter = useCallback(
(key: string) => {
setDisplayedExternalFilters((prev) => prev.filter((f) => f !== key));
const clearAll = useCallback(
(exclude?: string[]) => {
setActiveFilters((prev) => prev.filter((filter) => exclude?.includes(filter.f)));
},
[setDisplayedExternalFilters]
[setActiveFilters]
);
const clearAll = useCallback(() => {
setActiveFilters([]);
setDisplayedExternalFilters([]);
}, [setActiveFilters, setDisplayedExternalFilters]);
const updateFilter = useCallback(
(columnId: string, value: FilterValue) => {
setActiveFilters((prev) => {
return prev.map((item) => (item.f === columnId ? { ...item, v: value } : item));
let added = false;
const newFilters = prev.map((item) => {
if (item.f === columnId) {
added = true;
return { ...item, v: value };
}
return item;
});
if (!added) {
newFilters.push({ f: columnId, v: value });
}
return newFilters;
});
},
[setActiveFilters]
@@ -81,9 +81,6 @@ export function DataTableProvider({ children }: { children: React.ReactNode }) {
removeFilter,
sorting,
setSorting,
displayedExternalFilters,
setDisplayedExternalFilters,
removeDisplayedExternalFilter,
}}>
{children}
</DataTableContext.Provider>
+22 -10
View File
@@ -17,7 +17,7 @@ export type TextFilterOperator = z.infer<typeof ZTextFilterOperator>;
export const ZSingleSelectFilterValue = z.object({
type: z.literal("single_select"),
data: z.string(),
data: z.union([z.string(), z.number()]),
});
export type SingleSelectFilterValue = z.infer<typeof ZSingleSelectFilterValue>;
@@ -60,16 +60,28 @@ export const ZNumberFilterValue = z.object({
export type NumberFilterValue = z.infer<typeof ZNumberFilterValue>;
export const ZDateRangeFilterValue = z.object({
type: z.literal("date_range"),
data: z.object({
startDate: z.string().nullable(),
endDate: z.string().nullable(),
preset: z.string(),
}),
});
export type DateRangeFilterValue = z.infer<typeof ZDateRangeFilterValue>;
export const ZFilterValue = z.union([
ZSingleSelectFilterValue,
ZMultiSelectFilterValue,
ZTextFilterValue,
ZNumberFilterValue,
ZDateRangeFilterValue,
]);
export type FilterValue = z.infer<typeof ZFilterValue>;
export type ColumnFilterType = "single_select" | "multi_select" | "text" | "number";
export type ColumnFilterType = "single_select" | "multi_select" | "text" | "number" | "date_range";
export type ColumnFilterMeta = {
type?: ColumnFilterType;
@@ -83,12 +95,12 @@ export type FilterableColumn = {
| {
type: "single_select";
icon?: IconName;
options: Map<string | { label: string; value: string }, number>;
options: Array<{ label: string; value: string | number }>;
}
| {
type: "multi_select";
icon?: IconName;
options: Map<string | { label: string; value: string }, number>;
options: Array<{ label: string; value: string | number }>;
}
| {
type: "text";
@@ -98,6 +110,10 @@ export type FilterableColumn = {
type: "number";
icon?: IconName;
}
| {
type: "date_range";
icon?: IconName;
}
);
export const ZColumnFilter = z.object({
@@ -117,15 +133,11 @@ export type TypedColumnFilter<T extends ColumnFilterType> = {
? SingleSelectFilterValue
: T extends "multi_select"
? MultiSelectFilterValue
: T extends "date_range"
? DateRangeFilterValue
: never;
};
export type ExternalFilter = {
key: string;
titleKey: string;
component: () => React.ReactNode;
};
export const ZSorting = z.object({
id: z.string(),
desc: z.boolean(),
@@ -6,12 +6,14 @@ import type {
TextFilterValue,
FilterValue,
NumberFilterValue,
DateRangeFilterValue,
} from "./types";
import {
ZNumberFilterValue,
ZSingleSelectFilterValue,
ZMultiSelectFilterValue,
ZTextFilterValue,
ZDateRangeFilterValue,
} from "./types";
export const textFilter = (cellValue: unknown, filterValue: TextFilterValue) => {
@@ -113,6 +115,10 @@ export const isNumberFilterValue = (filterValue: unknown): filterValue is Number
return ZNumberFilterValue.safeParse(filterValue).success;
};
export const isDateRangeFilterValue = (filterValue: unknown): filterValue is DateRangeFilterValue => {
return ZDateRangeFilterValue.safeParse(filterValue).success;
};
export const dataTableFilter = (cellValue: unknown, filterValue: FilterValue) => {
if (isSingleSelectFilterValue(filterValue)) {
return singleSelectFilter(cellValue, filterValue);
@@ -10,22 +10,27 @@ import {
} from "@tanstack/react-table";
// eslint-disable-next-line no-restricted-imports
import startCase from "lodash/startCase";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useMemo, useId } from "react";
import { useMemo, useId, useState } from "react";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import type { ExternalFilter } from "@calcom/features/data-table";
import {
DataTableWrapper,
DataTableFilters,
DataTableProvider,
DataTableSkeleton,
useColumnFilters,
useFilterValue,
multiSelectFilter,
textFilter,
dataTableFilter,
useDataTable,
ZDateRangeFilterValue,
ZSingleSelectFilterValue,
DateRangeFilter,
type FilterableColumn,
} from "@calcom/features/data-table";
import classNames from "@calcom/lib/classNames";
import { useCopy } from "@calcom/lib/hooks/useCopy";
@@ -44,13 +49,9 @@ import {
HoverCardPortal,
} from "@calcom/ui";
import { useFilterContext } from "../context/provider";
import { ClearFilters } from "../filters/ClearFilters";
import { DateSelect } from "../filters/DateSelect";
import { RoutingFormResponsesDownload } from "../filters/Download";
import { RoutingFormFilterList } from "../filters/RoutingFormFilterList";
import { TeamAndSelfList } from "../filters/TeamAndSelfList";
import { UserListInTeam } from "../filters/UserListInTeam";
import type { OrgTeamsType } from "../filters/OrgTeamsFilter";
import { OrgTeamsFilter } from "../filters/OrgTeamsFilter";
import {
ZResponseMultipleValues,
ZResponseSingleValue,
@@ -259,57 +260,65 @@ export function RoutingFormResponsesTable() {
);
}
const createdAtColumn: Extract<FilterableColumn, { type: "date_range" }> = {
id: "createdAt",
title: "createdAt",
type: "date_range",
};
export function RoutingFormResponsesTableContent() {
const { t } = useLocale();
const { filter } = useFilterContext();
const { copyToClipboard } = useCopy();
const session = useSession();
const currentOrgId = session.data?.user.org?.id;
const [orgTeamsType, setOrgTeamsType] = useState<OrgTeamsType>(currentOrgId ? "org" : "yours");
const [selectedTeamId, setSelectedTeamId] = useState<number | undefined>();
const {
dateRange,
selectedTeamId,
isAll,
initialConfig,
selectedRoutingFormId,
selectedMemberUserId,
selectedUserId,
} = filter;
const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
const [startDate, endDate] = dateRange;
const columnFilters = useColumnFilters({ exclude: ["bookingUserId", "formId", "createdAt"] });
const columnFilters = useColumnFilters();
const isAll = orgTeamsType === "org";
const teamId = orgTeamsType === "team" ? selectedTeamId : undefined;
const userId = orgTeamsType === "yours" ? session.data?.user.id : undefined;
const teamId = selectedTeamId ?? undefined;
const userId = selectedUserId ?? undefined;
const memberUserId = selectedMemberUserId ?? undefined;
const routingFormId = selectedRoutingFormId ?? undefined;
const memberUserId = useFilterValue("bookingUserId", ZSingleSelectFilterValue)?.data as number | undefined;
const routingFormId = useFilterValue("formId", ZSingleSelectFilterValue)?.data as string | undefined;
const createdAtRange = useFilterValue("createdAt", ZDateRangeFilterValue)?.data;
const startDate = createdAtRange?.startDate ?? dayjs().subtract(1, "week").startOf("day").toISOString();
const endDate = createdAtRange?.endDate ?? dayjs().endOf("day").toISOString();
const {
data: headers,
isLoading: isHeadersLoading,
isSuccess: isHeadersSuccess,
} = trpc.viewer.insights.routingFormResponsesHeaders.useQuery(
{
userId,
teamId,
isAll: isAll ?? false,
routingFormId,
},
{
enabled: initialConfigIsReady,
}
);
} = trpc.viewer.insights.routingFormResponsesHeaders.useQuery({
userId,
teamId,
isAll,
routingFormId,
});
const { removeDisplayedExternalFilter, sorting, setSorting } = useDataTable();
const { data: forms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery({
userId,
teamId,
isAll,
});
const { data: users } = trpc.viewer.insights.userList.useQuery({
teamId: teamId ?? -1,
isAll,
});
const { sorting, setSorting } = useDataTable();
const { data, fetchNextPage, isFetching, hasNextPage, isLoading } =
trpc.viewer.insights.routingFormResponses.useInfiniteQuery(
{
teamId,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
startDate,
endDate,
userId,
memberUserId,
isAll: isAll ?? false,
isAll,
routingFormId,
columnFilters,
sorting,
@@ -321,7 +330,6 @@ export function RoutingFormResponsesTableContent() {
trpc: {
context: { skipBatch: true },
},
enabled: initialConfigIsReady,
}
);
@@ -337,6 +345,26 @@ export function RoutingFormResponsesTableContent() {
return [];
}
return [
columnHelper.accessor("formId", {
id: "formId",
header: t("routing_forms"),
enableColumnFilter: true,
enableSorting: false,
meta: {
filter: { type: "single_select" },
},
cell: () => null,
}),
columnHelper.accessor("bookingUserId", {
id: "bookingUserId",
header: t("people"),
enableColumnFilter: true,
enableSorting: false,
meta: {
filter: { type: "single_select" },
},
cell: () => null,
}),
columnHelper.accessor("bookingAttendees", {
id: "bookingAttendees",
header: t("routing_form_insights_booked_by"),
@@ -500,6 +528,12 @@ export function RoutingFormResponsesTableContent() {
defaultColumn: {
size: 150,
},
initialState: {
columnVisibility: {
formId: false,
bookingUserId: false,
},
},
state: {
sorting,
columnFilters,
@@ -510,41 +544,47 @@ export function RoutingFormResponsesTableContent() {
return new Map();
}
const fromArrayToMap = (array: { label: string; value: string | number }[]) => {
return new Map(array.map((option) => [{ label: option.label, value: option.value }, 1]));
};
const fieldHeader = headers.find((h) => h.id === columnId);
if (fieldHeader?.options) {
return new Map(fieldHeader.options.map((option) => [{ label: option.label, value: option.id }, 1]));
return fromArrayToMap(
fieldHeader.options
.filter((option): option is { id: string; label: string } => option.id !== null)
.map((option) => ({
label: option.label,
value: option.id,
}))
);
} else if (columnId === "bookingStatusOrder") {
return new Map(
Object.keys(statusOrder).map((status) => [
{
value: statusOrder[status as BookingStatus],
label: bookingStatusToText(status as BookingStatus),
},
1,
])
return fromArrayToMap(
Object.keys(statusOrder).map((status) => ({
value: statusOrder[status as BookingStatus],
label: bookingStatusToText(status as BookingStatus),
}))
);
} else if (columnId === "formId") {
return fromArrayToMap(
forms?.map((form) => ({
label: form.name,
value: form.id,
})) ?? []
);
} else if (columnId === "bookingUserId") {
return fromArrayToMap(
users?.map((user) => ({
label: user.name ?? user.email,
value: user.id,
})) ?? []
);
}
return new Map();
},
});
const externalFilters = useMemo<ExternalFilter[]>(
() => [
{
key: "memberUserId",
titleKey: "people",
component: () => (
<UserListInTeam
showOnlyWhenSelectedInContext={false}
onClear={() => removeDisplayedExternalFilter("memberUserId")}
/>
),
},
],
[removeDisplayedExternalFilter]
);
if (isHeadersLoading || ((isFetching || isLoading) && !data)) {
if ((isHeadersLoading && !headers) || ((isFetching || isLoading) && !data)) {
return <DataTableSkeleton columns={4} columnWidths={[200, 200, 250, 250]} />;
}
@@ -558,16 +598,22 @@ export function RoutingFormResponsesTableContent() {
isFetching={isFetching}
ToolbarLeft={
<>
<TeamAndSelfList omitOrg={true} className="mb-0" />
<DataTableFilters.AddFilterButton table={table} externalFilters={externalFilters} />
<RoutingFormFilterList showOnlyWhenSelectedInContext={false} />
<DataTableFilters.ActiveFilters table={table} externalFilters={externalFilters} />
<ClearFilters />
<OrgTeamsFilter
selectedType={orgTeamsType}
selectedTeamId={selectedTeamId}
onSelected={(params) => {
setOrgTeamsType(params.type);
setSelectedTeamId(params.teamId);
}}
/>
<DataTableFilters.AddFilterButton table={table} />
<DataTableFilters.ActiveFilters table={table} />
<DataTableFilters.ClearFiltersButton exclude={["createdAt"]} />
</>
}
ToolbarRight={
<>
<DateSelect />
<DateRangeFilter column={createdAtColumn} />
<RoutingFormResponsesDownload
startDate={startDate}
endDate={endDate}
@@ -582,7 +628,7 @@ export function RoutingFormResponsesTableContent() {
<DataTableFilters.ColumnVisibilityButton table={table} />
</>
}>
<RoutingKPICards />
<RoutingKPICards given={{ isAll, teamId, userId }} />
</DataTableWrapper>
</div>
);
@@ -8,26 +8,31 @@ import { useFilterContext } from "../context/provider";
import { valueFormatter } from "../lib";
import { CardInsights } from "./Card";
export const RoutingKPICards = () => {
export const RoutingKPICards = ({
given,
}: {
given?: { isAll: boolean; teamId: number | undefined; userId: number | undefined };
}) => {
const { t } = useLocale();
const { filter } = useFilterContext();
const userId = given?.userId ?? filter.selectedUserId;
const isAll = given?.isAll ?? filter.isAll;
const teamId = given?.teamId ?? filter.selectedTeamId;
const {
dateRange,
selectedEventTypeId,
selectedUserId,
selectedMemberUserId,
isAll,
initialConfig,
selectedRoutingFormId,
selectedBookingStatus,
selectedRoutingFormFilter,
} = filter;
const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
const initialConfigIsReady =
Boolean(given) || !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
const [startDate, endDate] = dateRange;
const { selectedTeamId: teamId } = filter;
const { data, isSuccess, isPending } = trpc.viewer.insights.routingFormsByStatus.useQuery(
{
startDate: startDate.toISOString(),
@@ -36,7 +41,7 @@ export const RoutingKPICards = () => {
eventTypeId: selectedEventTypeId ?? undefined,
isAll,
routingFormId: selectedRoutingFormId ?? undefined,
userId: selectedUserId ?? undefined,
userId,
memberUserId: selectedMemberUserId ?? undefined,
bookingStatus: selectedBookingStatus ?? undefined,
fieldFilter: selectedRoutingFormFilter ?? undefined,
@@ -72,7 +77,7 @@ export const RoutingKPICards = () => {
return <LoadingKPICards categories={categories} />;
}
if (!isSuccess || !startDate || !endDate || (!teamId && !selectedUserId)) return null;
if (!isSuccess || !startDate || !endDate || (!teamId && !userId)) return null;
return (
<>
@@ -1,6 +1,6 @@
import { useState } from "react";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import type { ColumnFilter, SortingState } from "@calcom/features/data-table";
import { downloadAsCsv } from "@calcom/lib/csvUtils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -17,8 +17,8 @@ type Props = {
memberUserId: number | undefined;
routingFormId: string | undefined;
isAll: boolean;
startDate: Dayjs;
endDate: Dayjs;
startDate: string;
endDate: string;
columnFilters: ColumnFilter[];
sorting: SortingState;
};
@@ -52,8 +52,8 @@ export const RoutingFormResponsesDownload = ({
}> => {
const { data, nextCursor } = await utils.viewer.insights.routingFormResponsesForDownload.fetch({
teamId,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
startDate,
endDate,
userId,
memberUserId,
isAll: isAll ?? false,
@@ -84,9 +84,9 @@ export const RoutingFormResponsesDownload = ({
}
if (allData.length > 0) {
const filename = `RoutingFormResponses-${startDate.format("YYYY-MM-DD")}-${endDate.format(
"YYYY-MM-DD"
)}.csv`;
const filename = `RoutingFormResponses-${dayjs(startDate).format("YYYY-MM-DD")}-${dayjs(
endDate
).format("YYYY-MM-DD")}.csv`;
downloadAsCsv(allData as Record<string, unknown>[], filename);
}
} catch (error) {
@@ -0,0 +1,149 @@
import { useSession } from "next-auth/react";
import { useState } from "react";
import {
FilterCheckboxField,
FilterCheckboxFieldsContainer,
} from "@calcom/features/filters/components/TeamsFilter";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import { AnimatedPopover, Avatar, Divider, Icon, FilterSearchField } from "@calcom/ui";
export type OrgTeamsType = "org" | "team" | "yours";
// This is a clone of TeamAndSelfList, but without useFilterContext().
// It's meant to be used in the RoutingFormResponsesTable.
export const OrgTeamsFilter = ({
selectedType,
selectedTeamId,
onSelected,
}: {
selectedType: OrgTeamsType;
selectedTeamId?: number;
onSelected: (params: { type: OrgTeamsType; teamId?: number }) => void;
}) => {
const { t } = useLocale();
const session = useSession();
const currentOrgId = session.data?.user.org?.id;
const currentUserId = session.data?.user.id;
const currentUserName = session.data?.user.name;
const [query, setQuery] = useState<string>("");
const { data, isSuccess } = trpc.viewer.insights.teamListForUser.useQuery(undefined, {
// Teams don't change that frequently
refetchOnWindowFocus: false,
trpc: {
context: {
skipBatch: true,
},
},
});
const getPopoverProps = () => {
if (selectedType === "org") {
return { text: t("all"), placeholder: undefined, imageUrl: data?.[0].logoUrl };
} else if (selectedType === "yours") {
return { text: t("yours"), placeholder: currentUserName, imageUrl: session.data?.user.avatarUrl };
} else if (selectedType === "team") {
const selectedTeam = data?.find((item) => {
return item.id === selectedTeamId;
});
return {
text: `${t("team")}: ${selectedTeam?.name}`,
placeholder: selectedTeam?.name,
imageUrl: selectedTeam?.logoUrl,
};
}
return { text: t("select"), imageUrl: undefined };
};
const { text, placeholder, imageUrl } = getPopoverProps();
const isOrgDataAvailable = !!data && data.length > 0 && !!data[0].isOrg;
const PrefixComponent =
selectedType !== undefined && (imageUrl || placeholder) ? (
<Avatar
alt={`${placeholder} logo`}
imageSrc={getPlaceholderAvatar(imageUrl, placeholder)}
size="xsm"
className="mr-2"
/>
) : null;
return (
<AnimatedPopover text={text} PrefixComponent={PrefixComponent} popoverTriggerClassNames="mb-0">
<FilterCheckboxFieldsContainer>
<FilterSearchField
placeholder={t("search")}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Divider />
{isOrgDataAvailable && (
<FilterCheckboxField
id="all"
icon={<Icon name="layers" className="h-4 w-4" />}
checked={selectedType === "org"}
onChange={(e) => {
onSelected({ type: "org", teamId: undefined });
}}
label={t("all")}
/>
)}
<FilterCheckboxField
id="yours"
icon={
<Avatar
alt={`${currentUserName} avatar`}
imageSrc={getPlaceholderAvatar(session.data?.user.avatarUrl, currentUserName)}
size="xsm"
/>
}
checked={selectedType === "yours"}
onChange={(e) => {
if (e.target.checked) {
onSelected({ type: "yours", teamId: undefined });
} else if (!e.target.checked) {
onSelected({ type: currentOrgId ? "org" : "yours", teamId: undefined });
}
}}
label={t("yours")}
/>
<Divider />
{data
?.filter((team) => !team.isOrg)
.filter((team) => team.name?.toLowerCase().includes(query.toLowerCase()))
.map((team) => {
return (
<FilterCheckboxField
key={team.id}
id={team.name || ""}
label={team.name || ""}
checked={selectedTeamId === team.id && selectedType === "team"}
onChange={(e) => {
if (e.target.checked) {
onSelected({ type: "team", teamId: team.id });
} else if (!e.target.checked) {
onSelected({ type: currentOrgId ? "org" : "yours", teamId: undefined });
}
}}
icon={
<Avatar
alt={team.name || ""}
imageSrc={getPlaceholderAvatar(team.logoUrl, team.name)}
size="xs"
/>
}
/>
);
})}
</FilterCheckboxFieldsContainer>
</AnimatedPopover>
);
};
@@ -16,38 +16,36 @@ function buildFilterOptions(forms: Form[]) {
}));
}
export const RoutingFormFilterList = memo(
({ showOnlyWhenSelectedInContext = true }: { showOnlyWhenSelectedInContext?: boolean }) => {
const { t } = useLocale();
const { filter, setConfigFilters } = useFilterContext();
const { selectedTeamId, selectedUserId, selectedRoutingFormId, isAll } = filter;
const { selectedFilter } = filter;
export const RoutingFormFilterList = memo(() => {
const { t } = useLocale();
const { filter, setConfigFilters } = useFilterContext();
const { selectedTeamId, selectedUserId, selectedRoutingFormId, isAll } = filter;
const { selectedFilter } = filter;
const { data: allForms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery(
{
userId: selectedUserId ?? undefined,
teamId: selectedTeamId ?? undefined,
isAll: isAll ?? false,
},
{
enabled: selectedFilter?.includes("routing_forms"),
}
);
const { data: allForms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery(
{
userId: selectedUserId ?? undefined,
teamId: selectedTeamId ?? undefined,
isAll: isAll ?? false,
},
{
enabled: selectedFilter?.includes("routing_forms"),
}
);
if (showOnlyWhenSelectedInContext && !selectedFilter?.includes("routing_forms")) return null;
if (!selectedFilter?.includes("routing_forms")) return null;
const filterOptions = buildFilterOptions(allForms || []);
const filterOptions = buildFilterOptions(allForms || []);
return (
<FilterSelect
title={t("routing_forms")}
options={filterOptions}
selectedValue={selectedRoutingFormId}
onChange={(value) => setConfigFilters({ selectedRoutingFormId: value as string })}
buttonIcon={<Icon name="filter" className="mr-2 h-4 w-4" />}
/>
);
}
);
return (
<FilterSelect
title={t("routing_forms")}
options={filterOptions}
selectedValue={selectedRoutingFormId}
onChange={(value) => setConfigFilters({ selectedRoutingFormId: value as string })}
buttonIcon={<Icon name="filter" className="mr-2 h-4 w-4" />}
/>
);
});
RoutingFormFilterList.displayName = "RoutingFormFilterList";
@@ -13,13 +13,7 @@ const mapUserToOption = (user: User) => ({
icon: <Avatar alt={`${user.name} avatar`} imageSrc={user.avatarUrl} size="sm" className="mr-2" />,
});
export const UserListInTeam = ({
showOnlyWhenSelectedInContext = true,
onClear,
}: {
showOnlyWhenSelectedInContext?: boolean;
onClear?: () => void;
}) => {
export const UserListInTeam = ({ onClear }: { onClear?: () => void }) => {
const { t } = useLocale();
const { filter, setConfigFilters } = useFilterContext();
const { selectedFilter, selectedTeamId, selectedMemberUserId, isAll } = filter;
@@ -29,7 +23,7 @@ export const UserListInTeam = ({
isAll: !!isAll,
});
if (showOnlyWhenSelectedInContext && !selectedFilter?.includes("user")) {
if (!selectedFilter?.includes("user")) {
return null;
}
@@ -912,38 +912,11 @@ export const insightsRouter = router({
return [];
}
const membershipConditional: Prisma.MembershipWhereInput = {
team: {
slug: { not: null },
},
accepted: true,
userId: user.id,
OR: [
{
role: "ADMIN",
},
{
role: "OWNER",
},
],
};
// Validate if user belongs to org as admin/owner
if (user.organizationId && user.organization.isOrgAdmin) {
const teamsFromOrg = await ctx.insightsDb.team.findMany({
const teamsAndOrg = await ctx.insightsDb.team.findMany({
where: {
parentId: user.organizationId,
},
select: {
id: true,
slug: true,
name: true,
logoUrl: true,
},
});
const orgTeam = await ctx.insightsDb.team.findUnique({
where: {
id: user.organizationId,
OR: [{ parentId: user.organizationId }, { id: user.organizationId }],
},
select: {
id: true,
@@ -952,25 +925,18 @@ export const insightsRouter = router({
logoUrl: true,
},
});
const teamsFromOrg = teamsAndOrg.filter((team) => team.id !== user.organizationId);
const orgTeam = teamsAndOrg.find((team) => team.id === user.organizationId);
if (!orgTeam) {
return [];
}
const result: IResultTeamList[] = [
{
id: orgTeam.id,
slug: orgTeam.slug,
name: orgTeam.name,
logoUrl: orgTeam.logoUrl,
...orgTeam,
isOrg: true,
},
...teamsFromOrg.map(
(team: Prisma.TeamGetPayload<{ select: { id: true; slug: true; name: true; logoUrl: true } }>) => {
return {
...team,
};
}
),
...teamsFromOrg,
];
return result;
@@ -978,7 +944,21 @@ export const insightsRouter = router({
// Look if user it's admin/owner in multiple teams
const belongsToTeams = await ctx.insightsDb.membership.findMany({
where: membershipConditional,
where: {
team: {
slug: { not: null },
},
accepted: true,
userId: user.id,
OR: [
{
role: "ADMIN",
},
{
role: "OWNER",
},
],
},
include: {
team: {
select: {
@@ -141,4 +141,6 @@ export const lucideIconList = new Set([
"zap",
"waypoints",
"chevrons-down-up",
"sliders-horizontal",
"calendar-range",
]);
@@ -23,6 +23,7 @@ export type IconName =
| "calendar-check-2"
| "calendar-days"
| "calendar-heart"
| "calendar-range"
| "calendar-search"
| "calendar-x-2"
| "calendar"
@@ -114,6 +115,7 @@ export type IconName =
| "shield-check"
| "shield"
| "shuffle"
| "sliders-horizontal"
| "sliders-vertical"
| "smartphone"
| "sparkles"
@@ -8,6 +8,7 @@ import { Tooltip } from "../tooltip";
export const AnimatedPopover = ({
text,
PrefixComponent,
count,
popoverTriggerClassNames,
children,
@@ -16,6 +17,7 @@ export const AnimatedPopover = ({
prefix,
}: {
text: string;
PrefixComponent?: React.ReactNode;
count?: number;
children: React.ReactNode;
popoverTriggerClassNames?: string;
@@ -62,6 +64,7 @@ export const AnimatedPopover = ({
<div className="max-w-36 flex items-center">
<Tooltip content={prefix ? `${prefix}${text}` : text}>
<div className="flex select-none truncate font-medium">
{PrefixComponent ? PrefixComponent : null}
{prefix && <span className="text-subtle">{prefix}&nbsp;</span>}
{text}
{count && count > 0 && (