zuul/web/src/containers/FilterToolbar.jsx

334 lines
9.2 KiB
JavaScript

// Copyright 2020 BMW Group
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import {
Button,
ButtonVariant,
Dropdown,
DropdownItem,
DropdownPosition,
DropdownToggle,
InputGroup,
TextInput,
Toolbar,
ToolbarContent,
ToolbarFilter,
ToolbarGroup,
ToolbarItem,
ToolbarToggleGroup,
} from '@patternfly/react-core'
import { FilterIcon, SearchIcon } from '@patternfly/react-icons'
import { FilterSelect } from './filters/Select'
import { FilterTernarySelect } from './filters/TernarySelect'
import { FilterCheckbox } from './filters/Checkbox'
function FilterToolbar(props) {
const [isCategoryDropdownOpen, setIsCategoryDropdownOpen] = useState(false)
const [currentCategory, setCurrentCategory] = useState(
props.filterCategories[0].title
)
const [inputValue, setInputValue] = useState('')
function handleCategoryToggle(isOpen) {
setIsCategoryDropdownOpen(isOpen)
}
function handleCategorySelect(event) {
setCurrentCategory(event.target.innerText)
setIsCategoryDropdownOpen(!isCategoryDropdownOpen)
}
function handleInputChange(newValue) {
setInputValue(newValue)
}
function handleInputSend(event, category) {
const { onFilterChange, filters } = props
// In case the event comes from a key press, only accept "Enter"
if (event.key && event.key !== 'Enter') {
return
}
// Ignore empty values
if (!inputValue) {
return
}
const prevFilters = filters[category.key]
const newFilters = {
...filters,
[category.key]: prevFilters.includes(inputValue)
? prevFilters
: [...prevFilters, inputValue],
}
// Clear the input field
setInputValue('')
// Notify the parent component about the filter change
onFilterChange(newFilters)
}
function handleDelete(type = '', id = '', category) {
const { filterCategories, filters, onFilterChange } = props
// Usually the type contains the category for which a chip should be deleted
// If the type is set, we got a delete() call for a single chip. The type
// reflects the name of the Chip group which does not necessarily go in hand
// with our category keys. Thus, we use the category to identify the correct
// filter to be updated/removed.
let newFilters = {}
if (type) {
if (category.type === 'ternary') {
newFilters = {
...filters,
[category.key]: [],
}
} else {
newFilters = {
...filters,
[category.key]: filters[category.key].filter((s) => s !== id),
}
}
} else {
// Delete the values for each filter category
newFilters = filterCategories.reduce((filterDict, category) => {
filterDict[category.key] = []
return filterDict
}, {})
}
// Notify the parent component about the filter change
onFilterChange(newFilters)
}
function renderCategoryDropdown() {
const { filterCategories } = props
return (
<ToolbarItem>
<Dropdown
onSelect={handleCategorySelect}
position={DropdownPosition.left}
toggle={
<DropdownToggle
onToggle={handleCategoryToggle}
style={{ width: '100%' }}
>
<FilterIcon /> {currentCategory}
</DropdownToggle>
}
isOpen={isCategoryDropdownOpen}
dropdownItems={filterCategories.filter(
(category) => (category.type === 'search' ||
category.type === 'select' ||
category.type === 'ternary' ||
category.type === 'checkbox')
).map((category) => (
<DropdownItem key={category.key}>{category.title}</DropdownItem>
))}
style={{ width: '100%' }}
/>
</ToolbarItem>
)
}
function renderFilterInput(category, filters) {
const { onFilterChange } = props
if (category.type === 'search') {
return (
<InputGroup>
<TextInput
name={`${category.key}-input`}
id={`${category.key}-input`}
type="search"
aria-label={`${category.key} filter`}
onChange={handleInputChange}
value={inputValue}
placeholder={category.placeholder}
onKeyDown={(event) => handleInputSend(event, category)}
/>
<Button
variant={ButtonVariant.control}
aria-label="search button for search input"
onClick={(event) => handleInputSend(event, category)}
>
<SearchIcon />
</Button>
</InputGroup>
)
} else if (category.type === 'select') {
return (
<InputGroup>
<FilterSelect
onFilterChange={onFilterChange}
filters={filters}
category={category}
/>
</InputGroup>
)
} else if (category.type === 'ternary') {
return (
<InputGroup>
<FilterTernarySelect
onFilterChange={onFilterChange}
filters={filters}
category={category}
/>
</InputGroup>
)
} else if (category.type === 'checkbox') {
return (
<InputGroup>
<br />
<FilterCheckbox
onFilterChange={onFilterChange}
filters={filters}
category={category}
/>
</InputGroup>
)
}
}
function renderFilterDropdown() {
const { filterCategories, filters } = props
return (
<>
{filterCategories.map((category) => (
<ToolbarFilter
key={category.key}
chips={getChipsFromFilters(filters, category)}
deleteChip={(type, id) => handleDelete(type, id, category)}
categoryName={category.title}
showToolbarItem={currentCategory === category.title}
>
{renderFilterInput(category, filters)}
</ToolbarFilter>
))}
</>
)
}
return (
<>
<Toolbar
id="toolbar-with-chip-groups"
clearAllFilters={handleDelete}
collapseListedFiltersBreakpoint="md"
>
<ToolbarContent>
<ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint="md">
<ToolbarGroup variant="filter-group">
{renderCategoryDropdown()}
{renderFilterDropdown()}
</ToolbarGroup>
</ToolbarToggleGroup>
</ToolbarContent>
</Toolbar>
</>
)
}
FilterToolbar.propTypes = {
onFilterChange: PropTypes.func.isRequired,
filters: PropTypes.object.isRequired,
filterCategories: PropTypes.array.isRequired,
}
function getChipsFromFilters(filters, category) {
if (category.type === 'ternary') {
switch ([...filters[category.key]].pop()) {
case 1:
case '1':
return ['True',]
case 0:
case '0':
return ['False',]
default:
return []
}
} else {
return filters[category.key]
}
}
function getFiltersFromUrl(location, filterCategories) {
const urlParams = new URLSearchParams(location.search)
const _filters = filterCategories.reduce((filterDict, item) => {
// Initialize each filter category with an empty list
filterDict[item.key] = []
// And update the list with each matching element from the URL query
urlParams.getAll(item.key).forEach((param) => {
if (item.type === 'checkbox') {
switch (param) {
case '1':
filterDict[item.key].push(1)
break
case '0':
filterDict[item.key].push(0)
break
default:
break
}
} else {
filterDict[item.key].push(param)
}
})
return filterDict
}, {})
const pagination_options = {
skip: urlParams.getAll('skip') ? urlParams.getAll('skip') : [0,],
limit: urlParams.getAll('limit') ? urlParams.getAll('limit') : [50,],
}
const filters = { ..._filters, ...pagination_options }
return filters
}
function writeFiltersToUrl(filters, location, history) {
// Build new URL parameters from the filters in state
const searchParams = new URLSearchParams('')
Object.keys(filters).map((key) => {
filters[key].forEach((value) => {
searchParams.append(key, value)
})
return searchParams
})
history.push({
pathname: location.pathname,
search: searchParams.toString(),
})
}
function buildQueryString(filters) {
let queryString = '&complete=true'
if (filters) {
Object.keys(filters).map((key) => {
filters[key].forEach((value) => {
queryString += '&' + key + '=' + value
})
return queryString
})
}
return queryString
}
export { buildQueryString, FilterToolbar, getFiltersFromUrl, writeFiltersToUrl }