refinedev / refine

A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.
https://refine.dev
MIT License
25.95k stars 1.96k forks source link

[FEAT] Keep filters when refresh the page with useTable #6001

Closed dfang closed 3 weeks ago

dfang commented 1 month ago

Is your feature request related to a problem? Please describe.

this is a list of customers filtered with idle status,

https://example.admin.refine.dev/customers?pageSize=10&current=9&sorters[0][field]=isActive&sorters[0][order]=asc&filters[0][field]=fullName&filters[0][operator]=contains

when I refresh the page, the list is not filtered with idle status.

Describe alternatives you've considered

No response

Additional context

when you filter then click pagination number, it's ok. but refresh and click pagination number, it's not filtered

Describe the thing to improve

keep the filter after refreshing the page (restore filters from url)

alicanerdurmaz commented 1 month ago

Hello @dfang, thanks for the issue!

we need to add defaultFilteredValue={getDefaultFilter("isActive", filters, "eq")} to <Table.Column /> to populate column's default value.

more information is here: https://refine.dev/docs/ui-integrations/ant-design/hooks/use-table/#initial-filter-and-sorter

Do you want to work on this?

dzcpy commented 1 month ago

Also experinced this problem. Hopefully it can be added soon! By the way here's how I fixed it by a workaround:

  const initialFilters: {[key: string]: string}[] = []

  const currentUrl = new URL(location.href)
  currentUrl.searchParams.forEach((value, key) => {
    const matches = key.match(/filters\[(\d+)\]\[(.*?)\]/);
    if (matches) {
      const index = Number(matches[1]);
      const field = matches[2];

      // Ensure the filter object exists
      if (!initialFilters[index]) {
        initialFilters[index] = {};
      }

      // Assign the value to the appropriate field
      initialFilters[index][field] = value;
    }
  })
        <Table.Column
          dataIndex="inspector"
          key="inspector][id"
          title="Inspector"
          className="cursor-pointer"
          render={(value) => value?.name}
          defaultFilteredValue={initialFilters.filter(filter => filter.field === 'inspector][id').map(filter => filter.value)}
          filterDropdown={(props) => (
            <FilterDropdown {...props}>
              <Select style={{ minWidth: 200 }} placeholder="Select inspector" {...inspectorsSelectProps} />
            </FilterDropdown>
          )}
        />
alicanerdurmaz commented 1 month ago

Hello @dzcpy,

Is this not working?

import { getDefaultFilter } from "@refinedev/core";

const { filters } = useTable();

// ...
defaultFilteredValue={getDefaultFilter("inspector][id", filters, "eq")}
// ...
dzcpy commented 1 month ago

Hello @dzcpy,

Is this not working?

import { getDefaultFilter } from "@refinedev/core";

const { filters } = useTable();

// ...
defaultFilteredValue={getDefaultFilter("inspector][id", filters, "eq")}
// ...

It doesn't work properly. It's just a hack. But this feature should really be integrated into Refine just like other fields (sorter and pagination from URL, if syncWithLocation is set to true)

Also when the field is a number or a boolean value, I need to convert the value to the respective type, otherwise filter select / checkbox will not find the default selected value.

I've changed it to something like this:

import { isNil } from 'lodash'

export const tableFiltersFromUrl = (url?: string) => {
  if (!url) {
    url = window.location.href
  }
  return Array.from(new URL(url).searchParams.entries()).reduce((acc, [key, value]) => {
    if (key.startsWith('filters[')) {
      const matches = key.match(/filters\[(?<index>\d+)\]\[(?<item>.*?)\]/)
      if (matches?.groups?.index && matches?.groups?.item) {
        const index = Number(matches.groups.index)
        const field = matches.groups.item

        // Ensure the filter object exists
        if (!acc[index]) {
          acc[index] = {}
        }

        // Assign the value to the appropriate field
        acc[index][field] = value
      }
    }
    return acc
  }, [] as { [key: string]: string }[])
    .filter(filter => filter.field && filter.operator && !isNil(filter.value)) as { field: string, operator: string, value: string }[]
}

export const tableFiltersGetDefaultValue = (filters: ReturnType<typeof tableFiltersFromUrl>, field: string, type: 'string' | 'number' | 'boolean' = 'string') =>
  filters.filter(filter => filter.field === field).map(filter => type === 'boolean' ? /^true$/i.test(filter.value) : type === 'number' ? Number(filter.value) : filter.value)
const initialFilters = useMemo(() => tableFiltersFromUrl(), [])
<Table.Column
  dataIndex="name"
  key="name"
  title="Name"
  defaultFilteredValue={tableFiltersGetDefaultValue(initialFilters, 'name')}
  filterDropdown={(props) => (
    <FilterDropdown {...props}>
      <Input style={{ minWidth: 200 }} placeholder="Please input name" />
    </FilterDropdown>
  )}
  className="cursor-pointer"
/>
<Table.Column
  dataIndex="inspector"
  key="inspector][id"
  title="Inspector"
  className="cursor-pointer"
  render={(value) => value?.name}
  defaultFilteredValue={tableFiltersGetDefaultValue(initialFilters, 'inspector][id', 'number')}
  filterDropdown={(props) => (
    <FilterDropdown {...props}>
      <Select style={{ minWidth: 200 }} placeholder="Please select inspector" {...inspectorsSelectProps} />
    </FilterDropdown>
  )}
/>
<Table.Column
  dataIndex="finished"
  key="finished"
  title="Finished?"
  render={(value) => (value ? locationFinishedOptions.true : locationFinishedOptions.false)}
  defaultFilteredValue={tableFiltersGetDefaultValue(initialFilters, 'finished', 'boolean')}
  filterDropdown={(props) => (
    <FilterDropdown {...props}>
      <Select
        style={{ minWidth: 200 }}
        placeholder="Choose if it's finished"
        filterOption={false}
        options={Object.entries(locationFinishedOptions).map(([value, label]) => ({ value, label }))}
      />
    </FilterDropdown>
  )}
  className="cursor-pointer"
/>
alicanerdurmaz commented 4 weeks ago

@dzcpy Thanks for the detailed explanation, you are right there is room for improvement. We'll think about it.