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:
parent
7f97121c3b
commit
99c4f7111a
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user