1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

feat: add change request table filter buttons (#10679)

Adds filter buttons for filtering between "CRs created by me" and "CRs
where I've been requested as an approver".

The current implementation is fairly simplistic and the buttons are not
connected to the actual table state directly (instead being set up with
their own simple state and onChange hooks), but it covers the simple
scenario. I want to defer a more complex solution until we know we need
it and until we know exactly what we need. The implementation is based
on the lifecycle filters that we have on the project flags page.

The current logic is such that: when you land on the page, there's no
query params in the URL, but the data fetch applies `createdBy:IS<your
user>`. If you switch to "approval requested" (and back again), the URL
will reflect this.

For reference, the github workflow works like this, where each URL has a
set of default filters, e.g.:
- `/pulls`: `is:open is:pr assignee:thomasheartman archived:false`
- `/pulls/review-requested`: `is:open is:pr
review-requested:thomasheartman archived:false`

But if you change the default filters or add new ones, the URL will
update to `pulls?<query-string>` (e.g.
`/pulls?q=is%3Aopen+is%3Apr+review-requested%3Athomasheartman+archived%3Atrue`)

So this takes a similar approach, but better suited to the way we do
tables in general.

Rendered:

<img width="1816" height="791" alt="image"
src="https://github.com/user-attachments/assets/60935900-488d-4ca9-b110-39f3568a08a6"
/>

<img width="1855" height="329" alt="image"
src="https://github.com/user-attachments/assets/5e865a2e-8fdc-41ab-ba38-bbe6776d04ad"
/>
This commit is contained in:
Thomas Heartman 2025-09-26 12:42:47 +02:00 committed by GitHub
parent 7f97121c3b
commit 99c4f7111a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 133 additions and 14 deletions

View File

@ -0,0 +1,82 @@
import { Box, Chip, styled } from '@mui/material';
import { useState, type FC } from 'react';
const StyledChip = styled(Chip)(({ theme }) => ({
borderRadius: `${theme.shape.borderRadius}px`,
padding: theme.spacing(0.5),
fontSize: theme.typography.body2.fontSize,
height: 'auto',
'&[aria-current="true"]': {
backgroundColor: theme.palette.secondary.light,
fontWeight: 'bold',
borderColor: theme.palette.primary.main,
color: theme.palette.primary.main,
},
':focus-visible': {
outline: `1px solid ${theme.palette.primary.main}`,
borderColor: theme.palette.primary.main,
},
}));
export type ChangeRequestQuickFilter = 'Created' | 'Approval Requested';
interface IChangeRequestFiltersProps {
ariaControlTarget: string;
initialSelection?: ChangeRequestQuickFilter;
onSelectionChange: (selection: ChangeRequestQuickFilter) => void;
}
const Wrapper = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'flex-start',
padding: theme.spacing(1.5, 3, 0, 3),
minHeight: theme.spacing(7),
gap: theme.spacing(2),
}));
const StyledContainer = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: theme.spacing(1),
}));
export const ChangeRequestFilters: FC<IChangeRequestFiltersProps> = ({
onSelectionChange,
initialSelection,
ariaControlTarget,
}) => {
const [selected, setSelected] = useState<ChangeRequestQuickFilter>(
initialSelection || 'Created',
);
const handleSelectionChange = (value: ChangeRequestQuickFilter) => () => {
if (value === selected) {
return;
}
setSelected(value);
onSelectionChange(value);
};
return (
<Wrapper>
<StyledContainer>
<StyledChip
label={'Created'}
variant='outlined'
aria-current={selected === 'Created'}
aria-controls={ariaControlTarget}
onClick={handleSelectionChange('Created')}
title={'Show change requests created by you'}
/>
<StyledChip
label={'Approval Requested'}
variant='outlined'
aria-current={selected === 'Approval Requested'}
aria-controls={ariaControlTarget}
onClick={handleSelectionChange('Approval Requested')}
title={'Show change requests requesting your approval'}
/>
</StyledContainer>
</Wrapper>
);
};

View File

@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useId, useMemo } from 'react';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { PaginatedTable } from 'component/common/Table';
@ -11,7 +11,6 @@ import { GlobalChangeRequestTitleCell } from './GlobalChangeRequestTitleCell.js'
import { FeaturesCell } from '../ProjectChangeRequests/ChangeRequestsTabs/FeaturesCell.js';
import { useUiFlag } from 'hooks/useUiFlag.js';
import { withTableState } from 'utils/withTableState';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import {
useChangeRequestSearch,
DEFAULT_PAGE_LIMIT,
@ -28,20 +27,24 @@ import {
import useLoading from 'hooks/useLoading';
import { styles as themeStyles } from 'component/common';
import { FilterItemParam } from 'utils/serializeQueryParams';
import {
ChangeRequestFilters,
type ChangeRequestQuickFilter,
} from './ChangeRequestFilters.js';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser.js';
const columnHelper = createColumnHelper<ChangeRequestSearchItemSchema>();
const ChangeRequestsInner = () => {
const { user } = useAuthUser();
const shouldApplyDefaults = useMemo(() => {
const urlParams = new URLSearchParams(window.location.search);
return (
!urlParams.has('createdBy') &&
!urlParams.has('requestedApproverId') &&
user
);
}, [user]);
const urlParams = new URLSearchParams(window.location.search);
const shouldApplyDefaults =
user &&
!urlParams.has('createdBy') &&
!urlParams.has('requestedApproverId');
const initialFilter = urlParams.has('requestedApproverId')
? 'Approval Requested'
: 'Created';
const stateConfig = {
offset: withDefault(NumberParam, 0),
@ -56,7 +59,7 @@ const ChangeRequestsInner = () => {
? {
createdBy: {
operator: 'IS' as const,
values: user ? [user.id.toString()] : [],
values: [user.id.toString()],
},
}
: {};
@ -67,8 +70,8 @@ const ChangeRequestsInner = () => {
const effectiveTableState = useMemo(
() => ({
...initialState,
...tableState,
...initialState,
}),
[initialState, tableState],
);
@ -158,6 +161,30 @@ const ChangeRequestsInner = () => {
data,
}),
);
const tableId = useId();
const handleQuickFilterChange = (filter: ChangeRequestQuickFilter) => {
if (!user) {
// todo (globalChangeRequestList): handle this somehow? Or just ignore.
return;
}
const [targetProperty, otherProperty] =
filter === 'Created'
? ['createdBy', 'requestedApproverId']
: ['requestedApproverId', 'createdBy'];
// todo (globalChangeRequestList): extract and test the logic for wiping out createdby/requestedapproverid
setTableState((state) => ({
[targetProperty]: {
operator: 'IS',
values: [user.id.toString()],
},
[otherProperty]:
state[otherProperty]?.values.length === 1 &&
state[otherProperty].values[0] === user.id.toString()
? null
: state[otherProperty],
}));
};
const bodyLoadingRef = useLoading(loading);
@ -166,7 +193,17 @@ const ChangeRequestsInner = () => {
bodyClass='no-padding'
header={<PageHeader title='Change requests' />}
>
<div className={themeStyles.fullwidth} ref={bodyLoadingRef}>
<ChangeRequestFilters
ariaControlTarget={tableId}
initialSelection={initialFilter}
onSelectionChange={handleQuickFilterChange}
/>
<div
id={tableId}
className={themeStyles.fullwidth}
ref={bodyLoadingRef}
>
<PaginatedTable tableInstance={table} totalItems={total} />
</div>
</PageContent>