From 76c2f5fa0996bf6f9686aa6dffb6cd797cbb64b8 Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Mon, 17 Feb 2025 14:51:36 +0000 Subject: [PATCH] feat: Clear filters on user and features page (#5055) --- frontend/web/components/ClearFilters.tsx | 21 ++ frontend/web/components/pages/FeaturesPage.js | 37 ++- frontend/web/components/pages/UserPage.tsx | 263 ++++++++++-------- .../components/tables/TableSearchFilter.tsx | 5 + 4 files changed, 201 insertions(+), 125 deletions(-) create mode 100644 frontend/web/components/ClearFilters.tsx diff --git a/frontend/web/components/ClearFilters.tsx b/frontend/web/components/ClearFilters.tsx new file mode 100644 index 000000000000..89a7bd611510 --- /dev/null +++ b/frontend/web/components/ClearFilters.tsx @@ -0,0 +1,21 @@ +import React, { FC } from 'react' +import { IonIcon } from '@ionic/react' +import { closeCircle, informationCircleOutline } from 'ionicons/icons' + +type ClearFiltersType = { + onClick: () => void +} + +const ClearFilters: FC = ({ onClick }) => { + return ( +
+ + Clear all +
+ ) +} + +export default ClearFilters diff --git a/frontend/web/components/pages/FeaturesPage.js b/frontend/web/components/pages/FeaturesPage.js index 8bf72731c63b..da6985a7c844 100644 --- a/frontend/web/components/pages/FeaturesPage.js +++ b/frontend/web/components/pages/FeaturesPage.js @@ -22,7 +22,9 @@ import TableOwnerFilter from 'components/tables/TableOwnerFilter' import TableGroupsFilter from 'components/tables/TableGroupsFilter' import TableValueFilter from 'components/tables/TableValueFilter' import classNames from 'classnames' +import ClearFilters from 'components/ClearFilters' import Button from 'components/base/forms/Button' +import { isEqual } from 'lodash'; const FeaturesPage = class extends Component { static displayName = 'FeaturesPage' @@ -30,11 +32,8 @@ const FeaturesPage = class extends Component { static contextTypes = { router: propTypes.object.isRequired, } - - constructor(props, context) { - super(props, context) - const params = Utils.fromParam() - this.state = { + getFiltersFromParams = (params) => { + return { group_owners: typeof params.group_owners === 'string' ? params.group_owners.split(',').map((v) => parseInt(v)) @@ -66,6 +65,10 @@ const FeaturesPage = class extends Component { value_search: typeof params.value_search === 'string' ? params.value_search : '', } + } + constructor(props, context) { + super(props, context) + this.state = this.getFiltersFromParams(Utils.fromParam()) ES6Component(this) AppActions.getFeatures( @@ -218,7 +221,26 @@ const FeaturesPage = class extends Component { const { environmentId, projectId } = this.props.match.params const readOnly = Utils.getFlagsmithHasFeature('read_only_mode') const environment = ProjectStore.getEnvironment(environmentId) - + const params = Utils.fromParam() + const hasFilters = !isEqual( + this.getFiltersFromParams({ ...params, page: '1' }), + this.getFiltersFromParams({ page: '1' }), + ) + const clearFilters = () => { + this.props.router.history.replace(`${document.location.pathname}`) + const newState = this.getFiltersFromParams({}) + this.setState(newState, () => { + AppActions.getFeatures( + this.props.match.params.projectId, + this.props.match.params.environmentId, + true, + this.state.search, + this.state.sort, + 1, + this.getFilter(), + ) + }) + } return (
+ {hasFilters && ( + + )}
diff --git a/frontend/web/components/pages/UserPage.tsx b/frontend/web/components/pages/UserPage.tsx index dbf160ecdfa0..6effb8925bf6 100644 --- a/frontend/web/components/pages/UserPage.tsx +++ b/frontend/web/components/pages/UserPage.tsx @@ -10,6 +10,7 @@ import { FeatureState, IdentityFeatureState, ProjectFlag, + TagStrategy, } from 'common/types/responses' import API from 'project/api' import AccountStore from 'common/stores/account-store' @@ -42,7 +43,7 @@ import TableFilterOptions from 'components/tables/TableFilterOptions' import TableGroupsFilter from 'components/tables/TableGroupsFilter' import TableOwnerFilter from 'components/tables/TableOwnerFilter' import TableSearchFilter from 'components/tables/TableSearchFilter' -import TableSortFilter from 'components/tables/TableSortFilter' +import TableSortFilter, { SortValue } from 'components/tables/TableSortFilter' import TableTagFilter from 'components/tables/TableTagFilter' import TableValueFilter from 'components/tables/TableValueFilter' import TagValues from 'components/tags/TagValues' @@ -52,8 +53,8 @@ import _data from 'common/data/base/_data' import classNames from 'classnames' import moment from 'moment' import { removeIdentity } from './UsersPage' -import IdentityOverridesIcon from 'components/IdentityOverridesIcon' -import SegmentOverridesIcon from 'components/SegmentOverridesIcon' +import { isEqual } from 'lodash' +import ClearFilters from 'components/ClearFilters' import SegmentsIcon from 'components/svg/SegmentsIcon' import UsersIcon from 'components/svg/UsersIcon' @@ -79,81 +80,88 @@ type UserPageType = { } } } +type FeatureFilter = { + group_owners: number[] + is_archived: boolean + is_enabled: boolean | null + owners: number[] + tag_strategy: TagStrategy + tags: (number | string)[] + value_search: string | null + search: string | null + sort: SortValue +} +const getFiltersFromParams = (params: Record) => + ({ + group_owners: + typeof params.group_owners === 'string' + ? params.group_owners.split(',').map((v: string) => parseInt(v)) + : [], + is_archived: params.is_archived === 'true', + is_enabled: + params.is_enabled === 'true' + ? true + : params.is_enabled === 'false' + ? false + : null, + owners: + typeof params.owners === 'string' + ? params.owners.split(',').map((v: string) => parseInt(v)) + : [], + search: params.search || null, + sort: { + label: Format.camelCase(params.sortBy || 'Name'), + sortBy: params.sortBy || 'name', + sortOrder: params.sortOrder || 'asc', + }, + tag_strategy: params.tag_strategy || 'INTERSECTION', + tags: + typeof params.tags === 'string' + ? params.tags.split(',').map((v: string) => parseInt(v)) + : [], + value_search: params.value_search || '', + } as FeatureFilter) + const UserPage: FC = (props) => { const params = Utils.fromParam() + const defaultState = getFiltersFromParams(params) const { router } = props const { environmentId, id, identity, projectId } = props.match.params - // Separate state hooks - const [groupOwners, setGroupOwners] = useState( - typeof params.group_owners === 'string' - ? params.group_owners.split(',').map((v: string) => parseInt(v)) - : [], - ) - const [isEnabled, setIsEnabled] = useState( - params.is_enabled === 'true' - ? true - : params.is_enabled === 'false' - ? false - : null, - ) - const [owners, setOwners] = useState( - typeof params.owners === 'string' - ? params.owners.split(',').map((v: string) => parseInt(v)) - : [], - ) - const [preselect, setPreselect] = useState(Utils.fromParam().flag) - const [search, setSearch] = useState(params.search || null) - const [showArchived, setShowArchived] = useState( - params.is_archived === 'true', - ) - const [sort, setSort] = useState({ - label: Format.camelCase(params.sortBy || 'Name'), - sortBy: params.sortBy || 'name', - sortOrder: params.sortOrder || 'asc', - }) - const [tagStrategy, setTagStrategy] = useState( - params.tag_strategy || 'INTERSECTION', - ) - const [tags, setTags] = useState( - typeof params.tags === 'string' - ? params.tags.split(',').map((v: string) => parseInt(v)) - : [], - ) - const [valueSearch, setValueSearch] = useState(params.value_search || '') + const [filter, setFilter] = useState(defaultState) const [actualFlags, setActualFlags] = useState>() + const [preselect, setPreselect] = useState(Utils.fromParam().flag) const getFilter = useCallback( - () => ({ - group_owners: groupOwners.length ? groupOwners : undefined, - is_archived: showArchived, - is_enabled: isEnabled === null ? undefined : isEnabled, - owners: owners.length ? owners : undefined, - tag_strategy: tagStrategy, - tags: tags.length ? tags.join(',') : undefined, - value_search: valueSearch ? valueSearch : undefined, + (filter) => ({ + ...filter, + group_owners: filter.group_owners.length + ? filter.group_owners + : undefined, + owners: filter.owners.length ? filter.owners : undefined, + search: (filter.search || '').trim(), + tags: filter.tags.length ? filter.tags.join(',') : undefined, }), - [ - groupOwners, - showArchived, - isEnabled, - owners, - tagStrategy, - tags, - valueSearch, - ], + [], + ) + + const hasFilters = !isEqual( + getFilter({ ...filter, search: filter.search || null }), + getFilter(getFiltersFromParams({})), ) + useEffect(() => { + const { search, sort, ...rest } = getFilter(filter) AppActions.searchFeatures( projectId, environmentId, true, search, sort, - getFilter(), + rest, ) - }, [search, sort, getFilter, environmentId, projectId]) + }, [filter, getFilter, environmentId, projectId]) useEffect(() => { AppActions.getIdentity(environmentId, id) @@ -269,22 +277,6 @@ const UserPage: FC = (props) => { ) } - const filter = () => { - const currentParams = Utils.fromParam() - if (!currentParams.flag) { - props.router.history.replace( - `${document.location.pathname}?${Utils.toParam(getFilter())}`, - ) - } - AppActions.searchFeatures( - projectId, - environmentId, - true, - search, - sort, - getFilter(), - ) - } const onTraitSaved = () => { AppActions.getIdentitySegments(projectId, id) } @@ -332,6 +324,11 @@ const UserPage: FC = (props) => { const isEdge = Utils.getIsEdge() const showAliases = isEdge && Utils.getFlagsmithHasFeature('identity_aliases') + const clearFilters = () => { + router.history.replace(`${document.location.pathname}`) + setFilter(getFiltersFromParams({})) + } + return (
= (props) => { { toggleFlag }: any, ) => isLoading && - !tags.length && - !showArchived && - typeof search !== 'string' && + !filter.tags.length && + !filter.is_archived && + typeof filter.search !== 'string' && (!identityFlags || !actualFlags || !projectFlags) ? (
@@ -384,10 +381,12 @@ const UserPage: FC = (props) => { Aliases allow you to add searchable names to an identity - + {!!identity && ( + + )} )}
@@ -476,57 +475,74 @@ const UserPage: FC = (props) => { { FeatureListStore.isLoading = true - setSearch(Utils.safeParseEventValue(e)) + setFilter({ + ...filter, + search: Utils.safeParseEventValue(e), + }) }} - value={search} + value={filter.search} /> { - setTagStrategy(strategy) + value={filter.tags} + tagStrategy={filter.tag_strategy} + onChangeStrategy={(tag_strategy) => { + setFilter({ + ...filter, + tag_strategy, + }) }} isLoading={FeatureListStore.isLoading} onToggleArchived={(value) => { - if (value !== showArchived) { + if (value !== filter.is_archived) { FeatureListStore.isLoading = true - setShowArchived(!showArchived) + setFilter({ + ...filter, + is_archived: !filter.is_archived, + }) } }} - showArchived={showArchived} + showArchived={filter.is_archived} onChange={(newTags) => { FeatureListStore.isLoading = true - setTags( - newTags.includes('') && + setFilter({ + ...filter, + tags: + newTags.includes('') && newTags.length > 1 - ? [''] - : newTags, - ) + ? [''] + : newTags, + }) }} /> { - setIsEnabled(enabled) - setValueSearch(valueSearch) + setFilter({ + ...filter, + is_enabled: enabled, + value_search: valueSearch, + }) }} /> { + value={filter.owners} + onChange={(owners) => { FeatureListStore.isLoading = true - setOwners(newOwners) + setFilter({ + ...filter, + owners, + }) }} /> = (props) => { orgId={ AccountStore.getOrganisation()?.id } - value={groupOwners} - onChange={(newGroupOwners) => { + value={filter.group_owners} + onChange={(group_owners) => { FeatureListStore.isLoading = true - setGroupOwners(newGroupOwners) + setFilter({ + ...filter, + group_owners, + }) }} /> = (props) => { ]} /> = (props) => { value: 'created_date', }, ]} - onChange={(newSort) => { + onChange={(sort) => { FeatureListStore.isLoading = true - setSort(newSort) + setFilter({ + ...filter, + sort, + }) }} /> + {hasFilters && ( + + )}
@@ -924,16 +949,16 @@ const UserPage: FC = (props) => { }} renderSearchWithNoResults paging={FeatureListStore.paging} - search={search} + search={filter.search} nextPage={() => AppActions.getFeatures( projectId, environmentId, true, - search, - sort, + filter.search, + filter.sort, FeatureListStore.paging.next, - getFilter(), + getFilter(filter), ) } prevPage={() => @@ -941,10 +966,10 @@ const UserPage: FC = (props) => { projectId, environmentId, true, - search, - sort, + filter.search, + filter.sort, FeatureListStore.paging.previous, - getFilter(), + getFilter(filter), ) } goToPage={(pageNumber: number) => @@ -952,10 +977,10 @@ const UserPage: FC = (props) => { projectId, environmentId, true, - search, - sort, + filter.search, + filter.sort, pageNumber, - getFilter(), + getFilter(filter), ) } /> diff --git a/frontend/web/components/tables/TableSearchFilter.tsx b/frontend/web/components/tables/TableSearchFilter.tsx index a2af013a2826..4174c3200a82 100644 --- a/frontend/web/components/tables/TableSearchFilter.tsx +++ b/frontend/web/components/tables/TableSearchFilter.tsx @@ -21,9 +21,14 @@ const TableSearchFilter: FC = ({ exact, onChange, value }) => { }, []), 100, ) + useEffect(() => { searchItems(localValue) }, [localValue]) + + useEffect(() => { + setLocalValue(value || '') + }, [value]) return ( <>