|
| 1 | +import React from 'react' |
| 2 | +import ClearFilters from 'components/ClearFilters' |
| 3 | +import TableSearchFilter from 'components/tables/TableSearchFilter' |
| 4 | +import TableTagFilter from 'components/tables/TableTagFilter' |
| 5 | +import TableValueFilter from 'components/tables/TableValueFilter' |
| 6 | +import TableOwnerFilter from 'components/tables/TableOwnerFilter' |
| 7 | +import TableGroupsFilter from 'components/tables/TableGroupsFilter' |
| 8 | +import TableFilterOptions from 'components/tables/TableFilterOptions' |
| 9 | +import TableSortFilter, { SortValue } from 'components/tables/TableSortFilter' |
| 10 | +import { getViewMode, setViewMode, ViewMode } from 'common/useViewMode' |
| 11 | +import { isEqual } from 'lodash' |
| 12 | +import Utils from 'common/utils/utils' |
| 13 | +import { TagStrategy } from 'common/types/responses' |
| 14 | +import Format from 'common/utils/format' |
| 15 | + |
| 16 | +export type FiltersValue = { |
| 17 | + search: string | null |
| 18 | + page: number |
| 19 | + tag_strategy: TagStrategy |
| 20 | + tags: (number | string)[] |
| 21 | + is_archived: boolean |
| 22 | + value_search: string | null |
| 23 | + is_enabled: boolean | null |
| 24 | + owners: number[] |
| 25 | + group_owners: number[] |
| 26 | + sort: SortValue |
| 27 | +} |
| 28 | + |
| 29 | +type Props = { |
| 30 | + value: FiltersValue |
| 31 | + onChange: (next: FiltersValue) => void |
| 32 | + projectId: string | number |
| 33 | + orgId?: number |
| 34 | + isLoading?: boolean |
| 35 | +} |
| 36 | + |
| 37 | +const DEFAULTS: FiltersValue = { |
| 38 | + group_owners: [], |
| 39 | + is_archived: false, |
| 40 | + is_enabled: null, |
| 41 | + owners: [], |
| 42 | + page: 1, |
| 43 | + search: '', |
| 44 | + sort: { label: 'Name', sortBy: 'name', sortOrder: 'asc' }, |
| 45 | + tag_strategy: 'INTERSECTION', |
| 46 | + tags: [], |
| 47 | + value_search: '', |
| 48 | +} |
| 49 | + |
| 50 | +// Converts filters to url params, excluding ones that are already default |
| 51 | +export function getURLParamsFromFilters(f: FiltersValue) { |
| 52 | + const existing = Utils.fromParam() as Record<string, string | undefined> |
| 53 | + |
| 54 | + return { |
| 55 | + ...existing, |
| 56 | + group_owners: f.group_owners?.length ? f.group_owners.join(',') : undefined, |
| 57 | + is_archived: f.is_archived ? 'true' : undefined, |
| 58 | + is_enabled: |
| 59 | + f.is_enabled === null ? undefined : f.is_enabled ? 'true' : 'false', |
| 60 | + owners: f.owners?.length ? f.owners.join(',') : undefined, |
| 61 | + page: f.page !== DEFAULTS.page ? String(f.page) : undefined, |
| 62 | + search: f.search || undefined, |
| 63 | + sortBy: f.sort.sortBy, |
| 64 | + sortOrder: f.sort.sortOrder, |
| 65 | + tag_strategy: |
| 66 | + f.tag_strategy !== DEFAULTS.tag_strategy |
| 67 | + ? String(f.tag_strategy) |
| 68 | + : undefined, |
| 69 | + tags: f.tags?.length ? f.tags.join(',') : undefined, |
| 70 | + value_search: f.value_search || undefined, |
| 71 | + } |
| 72 | +} |
| 73 | +// Gets expected filters from URL parameters |
| 74 | +export const parseFiltersFromUrlParams = ( |
| 75 | + params: Record<string, string | undefined>, |
| 76 | +) => { |
| 77 | + return { |
| 78 | + group_owners: |
| 79 | + typeof params.group_owners === 'string' |
| 80 | + ? params.group_owners.split(',').map((v) => parseInt(v)) |
| 81 | + : [], |
| 82 | + is_archived: params.is_archived === 'true', |
| 83 | + is_enabled: |
| 84 | + params.is_enabled === 'true' |
| 85 | + ? true |
| 86 | + : params.is_enabled === 'false' |
| 87 | + ? false |
| 88 | + : null, |
| 89 | + owners: |
| 90 | + typeof params.owners === 'string' |
| 91 | + ? params.owners.split(',').map((v) => parseInt(v)) |
| 92 | + : [], |
| 93 | + page: params.page ? parseInt(params.page) - 1 : 1, |
| 94 | + search: params.search || '', |
| 95 | + sort: { |
| 96 | + label: Format.camelCase(params.sortBy || 'Name'), |
| 97 | + sortBy: params.sortBy || 'name', |
| 98 | + sortOrder: params.sortOrder || 'asc', |
| 99 | + }, |
| 100 | + tag_strategy: params.tag_strategy || 'INTERSECTION', |
| 101 | + tags: |
| 102 | + typeof params.tags === 'string' |
| 103 | + ? params.tags.split(',').map((v) => (v ? parseInt(v) : v)) |
| 104 | + : [], |
| 105 | + value_search: |
| 106 | + typeof params.value_search === 'string' ? params.value_search : '', |
| 107 | + } as FiltersValue |
| 108 | +} |
| 109 | + |
| 110 | +//Converts filter to api expected properties |
| 111 | +export const getServerFilter = (f: FiltersValue) => ({ |
| 112 | + ...f, |
| 113 | + group_owners: f.group_owners?.length ? f.group_owners : undefined, |
| 114 | + owners: f.owners.length ? f.owners : undefined, |
| 115 | + search: (f.search || '').trim(), |
| 116 | + sort: f.sort, |
| 117 | + tags: f.tags.length ? f.tags.join(',') : undefined, |
| 118 | +}) |
| 119 | + |
| 120 | +//Detect if the filter is default |
| 121 | +const isDefault = (v: FiltersValue) => isEqual(v, DEFAULTS) |
| 122 | + |
| 123 | +const FeatureFilters: React.FC<Props> = ({ |
| 124 | + isLoading, |
| 125 | + onChange, |
| 126 | + orgId, |
| 127 | + projectId, |
| 128 | + value, |
| 129 | +}) => { |
| 130 | + const set = (partial: Partial<FiltersValue>) => |
| 131 | + onChange({ ...value, ...partial }) |
| 132 | + const clearAll = () => onChange({ ...DEFAULTS }) |
| 133 | + return ( |
| 134 | + <div className='table-header d-flex align-items-center'> |
| 135 | + <div className='table-column flex-row flex-fill'> |
| 136 | + <TableSearchFilter |
| 137 | + onChange={(e) => set({ search: Utils.safeParseEventValue(e) })} |
| 138 | + value={value.search} |
| 139 | + /> |
| 140 | + <div className='d-flex align-items-center py-2 py-lg-0 px-1 px-lg-0 flex-fill justify-content-lg-end'> |
| 141 | + {!isDefault(value) && <ClearFilters onClick={clearAll} />} |
| 142 | + <TableTagFilter |
| 143 | + isLoading={!!isLoading} |
| 144 | + projectId={projectId} |
| 145 | + className='me-4' |
| 146 | + tagStrategy={value.tag_strategy} |
| 147 | + onChangeStrategy={(tag_strategy) => set({ tag_strategy })} |
| 148 | + value={value.tags} |
| 149 | + onToggleArchived={(next) => set({ is_archived: next })} |
| 150 | + showArchived={value.is_archived} |
| 151 | + onChange={(tags) => { |
| 152 | + if (tags.includes('') && tags.length > 1) { |
| 153 | + if (!(value.tags || []).includes('')) set({ tags: [''] }) |
| 154 | + else set({ tags: tags.filter((v) => !!v) }) |
| 155 | + } else set({ tags }) |
| 156 | + }} |
| 157 | + /> |
| 158 | + <TableValueFilter |
| 159 | + className={'me-4'} |
| 160 | + value={{ |
| 161 | + enabled: value.is_enabled, |
| 162 | + valueSearch: value.value_search, |
| 163 | + }} |
| 164 | + onChange={({ enabled, valueSearch }) => |
| 165 | + set({ is_enabled: enabled, value_search: valueSearch }) |
| 166 | + } |
| 167 | + /> |
| 168 | + <TableOwnerFilter |
| 169 | + className={'me-4'} |
| 170 | + value={value.owners} |
| 171 | + onChange={(owners) => set({ owners })} |
| 172 | + /> |
| 173 | + <TableGroupsFilter |
| 174 | + className={'me-4'} |
| 175 | + orgId={orgId} |
| 176 | + value={value.group_owners} |
| 177 | + onChange={(group_owners) => set({ group_owners })} |
| 178 | + /> |
| 179 | + <TableFilterOptions |
| 180 | + title={'View'} |
| 181 | + className={'me-4'} |
| 182 | + value={getViewMode()} |
| 183 | + onChange={setViewMode as any} |
| 184 | + options={[ |
| 185 | + { label: 'Default', value: 'default' as ViewMode }, |
| 186 | + { label: 'Compact', value: 'compact' as ViewMode }, |
| 187 | + ]} |
| 188 | + /> |
| 189 | + <TableSortFilter |
| 190 | + isLoading={!!isLoading} |
| 191 | + value={value.sort} |
| 192 | + options={[ |
| 193 | + { label: 'Name', value: 'name' }, |
| 194 | + { label: 'Created Date', value: 'created_date' }, |
| 195 | + ]} |
| 196 | + onChange={(sort) => set({ sort })} |
| 197 | + /> |
| 198 | + </div> |
| 199 | + </div> |
| 200 | + </div> |
| 201 | + ) |
| 202 | +} |
| 203 | + |
| 204 | +export default FeatureFilters |
0 commit comments