Skip to content

Commit 2a89846

Browse files
authored
Expected Funds: Add "Created by" filter (#11766)
* Add "Created by" filter to Expected Funds * Add all filter values as props to filter components * Don't show last filter straight away (unexpected and breaks e2e)
1 parent 8513d02 commit 2a89846

File tree

12 files changed

+231
-79
lines changed

12 files changed

+231
-79
lines changed

components/dashboard/filters/FilterDropdown.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export function SetFilter({ tmpValue, setTmpValue, filterKey, filters, setFilter
7777
valueRenderer={filterConfig.valueRenderer}
7878
labelMsg={filterConfig.labelMsg}
7979
meta={meta}
80+
values={values}
8081
/>
8182
<div className="border-t p-2">
8283
<Button
@@ -203,7 +204,7 @@ const FilterButton = ({ filterKey, setFilter, filters, tmpValue, open, highlight
203204
};
204205

205206
function FilterDropdown<FV, FM>({
206-
filterKey: currentFilterKey,
207+
filterKey: initialFilterKey,
207208
remainingFilters,
208209
filters,
209210
setFilter,
@@ -222,28 +223,34 @@ function FilterDropdown<FV, FM>({
222223
locked?: boolean;
223224
}) {
224225
const [open, setOpen] = React.useState(false);
225-
const [filterKey, setFilterKey] = React.useState(currentFilterKey);
226-
const [tmpValue, setTmpValue] = React.useState(values[currentFilterKey]);
226+
const [activeFilterKey, setActiveFilterKey] = React.useState(initialFilterKey);
227+
const currentFilterValue = values[initialFilterKey];
228+
const [tmpValue, setTmpValue] = React.useState(currentFilterValue);
227229

228230
React.useEffect(() => {
229-
setFilterKey(currentFilterKey);
230-
setTmpValue(values[currentFilterKey]);
231-
}, [remainingFilters, currentFilterKey, values]);
231+
// Reset if the currently selected filter type is no longer available
232+
// (e.g., it was just applied and moved to displayed filters)
233+
if (activeFilterKey && remainingFilters && !remainingFilters.includes(activeFilterKey)) {
234+
setActiveFilterKey(initialFilterKey);
235+
}
236+
// Always sync tmpValue with the current filter value
237+
setTmpValue(currentFilterValue);
238+
}, [activeFilterKey, initialFilterKey, currentFilterValue, remainingFilters]);
232239

233240
return (
234241
<Popover
235242
open={open}
236243
onOpenChange={open => {
237244
if (!locked) {
238-
setFilterKey(currentFilterKey);
239-
setTmpValue(values[currentFilterKey]);
245+
setActiveFilterKey(initialFilterKey);
246+
setTmpValue(values[initialFilterKey]);
240247
setOpen(open);
241248
}
242249
}}
243250
>
244251
<PopoverAnchor>
245252
<FilterButton
246-
filterKey={filterKey}
253+
filterKey={activeFilterKey}
247254
filters={filters}
248255
tmpValue={tmpValue}
249256
setFilter={setFilter}
@@ -254,19 +261,19 @@ function FilterDropdown<FV, FM>({
254261
/>
255262
</PopoverAnchor>
256263
<PopoverContent className="w-[260px] p-0" align="start">
257-
{filterKey ? (
264+
{activeFilterKey ? (
258265
<SetFilter
259266
tmpValue={tmpValue}
260267
setTmpValue={setTmpValue}
261268
filters={filters}
262-
filterKey={filterKey}
269+
filterKey={activeFilterKey}
263270
setFilter={setFilter}
264271
setOpen={setOpen}
265272
meta={meta}
266273
values={values}
267274
/>
268275
) : (
269-
<ChooseFilterType remainingFilters={remainingFilters} filters={filters} setFilterKey={setFilterKey} />
276+
<ChooseFilterType remainingFilters={remainingFilters} filters={filters} setFilterKey={setActiveFilterKey} />
270277
)}
271278
</PopoverContent>
272279
</Popover>

components/dashboard/filters/Filterbar.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import { Separator } from '../../ui/Separator';
1212
import FilterDropdown from './FilterDropdown';
1313

1414
function useGetFilterbarOptions(filters, values, defaultSchemaValues, meta) {
15-
const filterKeys = Object.keys(filters);
15+
const filterKeys = React.useMemo(() => Object.keys(filters), [filters]);
1616
const [displayedFilters, setDisplayedFilters] = React.useState(
1717
filterKeys.filter(key => filterShouldDisplay(key, { values, filters, defaultSchemaValues, meta })),
1818
);
19-
const remainingFilters = filterKeys.filter(key =>
20-
filterShouldBeInAddFilterOptions(key, { values, filters, defaultSchemaValues, meta }),
19+
20+
const remainingFilters = React.useMemo(
21+
() =>
22+
filterKeys.filter(key => filterShouldBeInAddFilterOptions(key, { values, filters, defaultSchemaValues, meta })),
23+
[filterKeys, values, filters, defaultSchemaValues, meta],
2124
);
2225

2326
// When the values change, this effect makes sure to update the displayed filter keys array and maintain the order of the filters
@@ -53,6 +56,7 @@ const renderFilter = ({ filters, values, key, activeViewId, views, lockViewFilte
5356
highlighted={highlighted}
5457
intl={intl}
5558
meta={meta}
59+
values={values}
5660
/>
5761
);
5862
} else {
@@ -155,10 +159,6 @@ export function Filterbar<FV extends Record<string, any>, FM>({
155159
<FilterDropdown
156160
filters={filters}
157161
values={values}
158-
// If last option display it directly
159-
{...(remainingFilters.length === 1 && {
160-
filterKey: remainingFilters[0],
161-
})}
162162
remainingFilters={remainingFilters}
163163
setFilter={setFilter}
164164
meta={meta}

components/dashboard/filters/HostedAccountFilter.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,21 @@ const hostedAccountFilterSearchQuery = gql`
2727
${accountHoverCardFields}
2828
`;
2929

30-
export const AccountRenderer = ({
31-
account,
32-
inOptionsList,
33-
}: {
30+
export const AccountRenderer = (props: {
3431
account: Partial<AccountHoverCardFieldsFragment> & {
3532
slug: Account['slug'];
3633
};
3734
inOptionsList?: boolean; // For positioning the HoverCard to the right to prevent blocking options list
3835
}) => {
3936
const { data } = useQuery<AccountFilterQuery>(accountFilterQuery, {
40-
variables: { slug: account.slug },
37+
variables: { slug: props.account.slug },
4138
fetchPolicy: 'cache-first',
4239

4340
// skip query if there is already a field from the hover card data (such as description),
4441
// to prevent fetching all accounts when used in the combo select filter that already queries for these fields
45-
skip: !!account.description && !!account.type,
42+
skip: !!props.account.description && !!props.account.type,
4643
});
44+
const account = data?.account ?? props.account;
4745

4846
const trigger = (
4947
<div className="flex h-full w-full max-w-48 items-center justify-between gap-2 overflow-hidden">
@@ -61,7 +59,7 @@ export const AccountRenderer = ({
6159
return (
6260
<AccountHoverCard
6361
account={data.account}
64-
{...(inOptionsList && { hoverCardContentProps: { side: 'right', sideOffset: 24 } })}
62+
{...(props.inOptionsList && { hoverCardContentProps: { side: 'right', sideOffset: 24 } })}
6563
trigger={trigger}
6664
/>
6765
);
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React from 'react';
2+
import { useLazyQuery } from '@apollo/client';
3+
import { defineMessage } from 'react-intl';
4+
import { z } from 'zod';
5+
6+
import type { FilterComponentProps, FilterConfig } from '@/lib/filters/filter-types';
7+
import { isMulti } from '@/lib/filters/schemas';
8+
import { gql } from '@/lib/graphql/helpers';
9+
import type { AccountHoverCardFieldsFragment, HostContext } from '@/lib/graphql/types/v2/graphql';
10+
import type { ExpectedFundsFilter } from '@/lib/graphql/types/v2/schema';
11+
12+
import { accountHoverCardFields } from '../../AccountHoverCard';
13+
import type { FilterValues as OrderFilterValues } from '../sections/contributions/filters';
14+
15+
import ComboSelectFilter from './ComboSelectFilter';
16+
import { AccountRenderer } from './HostedAccountFilter';
17+
18+
const createdByFilterSearchQuery = gql`
19+
query CreatedByFilterSearch(
20+
$account: AccountReferenceInput!
21+
$searchTerm: String
22+
$expectedFundsFilter: ExpectedFundsFilter
23+
$hostContext: HostContext
24+
$status: [OrderStatus]
25+
) {
26+
orders(
27+
account: $account
28+
filter: INCOMING
29+
expectedFundsFilter: $expectedFundsFilter
30+
hostContext: $hostContext
31+
status: $status
32+
) {
33+
createdByUsers(searchTerm: $searchTerm, limit: 20) {
34+
nodes {
35+
id
36+
...AccountHoverCardFields
37+
}
38+
}
39+
}
40+
}
41+
${accountHoverCardFields}
42+
`;
43+
44+
export type OrderCreatedByFilterMeta = {
45+
accountSlug: string;
46+
expectedFundsFilter?: ExpectedFundsFilter;
47+
};
48+
49+
const schema = isMulti(z.string()).optional();
50+
51+
const resultNodeToOption = (account: Partial<AccountHoverCardFieldsFragment>) => ({
52+
label: <AccountRenderer account={account as AccountHoverCardFieldsFragment & { slug: string }} inOptionsList />,
53+
keywords: [account.name],
54+
value: account.slug,
55+
});
56+
57+
type RequiredFilterValueTypes = {
58+
hostContext?: HostContext;
59+
expectedFundsFilter?: ExpectedFundsFilter;
60+
status?: OrderFilterValues['status'];
61+
};
62+
63+
function OrderCreatedByFilter({
64+
meta,
65+
values,
66+
...props
67+
}: FilterComponentProps<z.infer<typeof schema>, OrderCreatedByFilterMeta, RequiredFilterValueTypes>) {
68+
const [options, setOptions] = React.useState<{ label: React.ReactNode; keywords: string[]; value: string }[]>([]);
69+
const [search, { loading, data }] = useLazyQuery(createdByFilterSearchQuery, {
70+
fetchPolicy: 'cache-first',
71+
notifyOnNetworkStatusChange: true,
72+
});
73+
74+
const searchFunc = React.useCallback(
75+
(searchTerm: string) => {
76+
search({
77+
variables: {
78+
account: { slug: meta.accountSlug },
79+
searchTerm: searchTerm || undefined,
80+
hostContext: values.hostContext,
81+
expectedFundsFilter: values.expectedFundsFilter,
82+
status: values.status,
83+
},
84+
});
85+
},
86+
[meta.accountSlug, search, values.hostContext, values.expectedFundsFilter, values.status],
87+
);
88+
89+
// Load initial options on mount
90+
React.useEffect(() => {
91+
search({
92+
variables: {
93+
account: { slug: meta.accountSlug },
94+
searchTerm: undefined,
95+
hostContext: values.hostContext,
96+
expectedFundsFilter: values.expectedFundsFilter,
97+
status: values.status,
98+
},
99+
});
100+
}, [meta.accountSlug, search, values.hostContext, values.expectedFundsFilter, values.status]);
101+
102+
React.useEffect(() => {
103+
if (!loading && data?.orders?.createdByUsers?.nodes) {
104+
setOptions(data.orders.createdByUsers.nodes.map(resultNodeToOption));
105+
}
106+
}, [loading, data]);
107+
108+
return <ComboSelectFilter options={options} loading={loading} searchFunc={searchFunc} isMulti {...props} />;
109+
}
110+
111+
export const orderCreatedByFilter: FilterConfig<z.infer<typeof schema>> = {
112+
schema: schema,
113+
filter: {
114+
labelMsg: defineMessage({ defaultMessage: 'Created by', id: 'Agreement.createdBy' }),
115+
Component: OrderCreatedByFilter,
116+
valueRenderer: ({ value, ...props }) => <AccountRenderer account={{ slug: value }} {...props} />,
117+
},
118+
toVariables: (values, key) => ({ [key]: values.map(slug => ({ slug })) }),
119+
};

components/dashboard/sections/HostDashboardAgreements.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ type FilterMeta = {
7676
};
7777

7878
const filters: FilterComponentConfigs<z.infer<typeof schema>, FilterMeta> = {
79-
account: hostedAccountFilter.filter,
79+
account: { static: true, ...hostedAccountFilter.filter },
8080
};
8181

8282
const HostDashboardAgreements = ({ accountSlug: hostSlug }: DashboardSectionProps) => {

0 commit comments

Comments
 (0)