ui: filter modal wip

This commit is contained in:
nick-delirium 2024-11-19 17:23:35 +01:00
parent 36feeb5ba9
commit 9909511e94
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
10 changed files with 350 additions and 262 deletions

View file

@ -298,14 +298,13 @@ const FilterAutoComplete: React.FC<Props> = ({
<Icon name="close" size="12" />
</div>
)}
{showOrButton && (
<div onClick={onAddValue} className="color-teal">
<span className="px-1">or</span>
</div>
)}
{/*{showOrButton && (*/}
{/* <div onClick={onAddValue} className="color-teal">*/}
{/* <span className="px-1">or</span>*/}
{/* </div>*/}
{/*)}*/}
</div>
</div>
{!showOrButton && !hideOrText && <div className="ml-3">or</div>}
</div>
);
};

View file

@ -1,90 +1,102 @@
import React, { useState, useEffect } from 'react';
import { Icon } from 'UI';
import stl from './FilterAutoCompleteLocal.module.css';
interface Props {
showOrButton?: boolean;
showCloseButton?: boolean;
onRemoveValue?: () => void;
onAddValue?: () => void;
onRemoveValue?: (index: number) => void;
onAddValue?: (index: number) => void;
placeholder?: string;
onSelect: (e, item) => void;
onSelect: (e: any, item: Record<string, any>, index: number) => void;
value: any;
icon?: string;
type?: string;
isMultilple?: boolean;
isMultiple?: boolean;
allowDecimals?: boolean;
}
function FilterAutoCompleteLocal(props: Props) {
function FilterAutoCompleteLocal(props: Props & { index: number }) {
const {
showCloseButton = false,
placeholder = 'Enter',
showOrButton = false,
onRemoveValue = () => null,
onAddValue = () => null,
value = '',
icon = null,
type = "text",
isMultilple = true,
allowDecimals = true,
showCloseButton = false,
placeholder = 'Enter',
showOrButton = false,
onRemoveValue = () => null,
onAddValue = () => null,
value = '',
type = 'text',
isMultiple = true,
allowDecimals = true,
index,
} = props;
const [showModal, setShowModal] = useState(true)
const [query, setQuery] = useState(value);
const onInputChange = (e) => {
if(allowDecimals) {
if (allowDecimals) {
const value = e.target.value;
setQuery(value);
props.onSelect(null, value);
props.onSelect(null, value, index);
} else {
const value = e.target.value.replace(/[^\d]/, "");
const value = e.target.value.replace(/[^\d]/, '');
if (+value !== 0) {
setQuery(value);
props.onSelect(null, value);
props.onSelect(null, value, index);
}
}
};
useEffect(() => {
setQuery(value);
}, [value])
const onBlur = (e) => {
setTimeout(() => { setShowModal(false) }, 200)
props.onSelect(e, { value: query })
}
}, [value]);
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
props.onSelect(e, { value: query })
props.onSelect(e, { value: query }, index);
}
}
};
return (
<div className="relative flex items-center">
<div className={stl.wrapper}>
<input
name="query"
onInput={ onInputChange }
// onBlur={ onBlur }
onFocus={ () => setShowModal(true)}
value={ query }
autoFocus={ true }
type={ type }
placeholder={ placeholder }
onInput={onInputChange}
value={query}
autoFocus={true}
type={type}
placeholder={placeholder}
onKeyDown={handleKeyDown}
/>
<div
className={stl.right}
>
{ showCloseButton && <div onClick={onRemoveValue}><Icon name="close" size="12" /></div> }
{ showOrButton && <div onClick={onAddValue} className="color-teal"><span className="px-1">or</span></div> }
<div className={stl.right}>
{showCloseButton && (
<div onClick={() => onRemoveValue(index)}>
<Icon name="close" size="12" />
</div>
)}
{showOrButton && isMultiple ? (
<div onClick={() => onAddValue(index)} className="color-teal">
<span className="px-1">or</span>
</div>
) : null}
</div>
</div>
{ !showOrButton && isMultilple && <div className="ml-3">or</div> }
{!showOrButton && isMultiple ? <div className="ml-2">or</div> : null}
</div>
);
}
export default FilterAutoCompleteLocal;
function FilterLocalController(props: Props) {
return props.value.map((value, index) => (
<FilterAutoCompleteLocal
{...props}
key={index}
index={index}
showOrButton={index === props.value.length - 1}
showCloseButton={props.value.length > 1}
value={value}
/>
));
}
export default FilterLocalController;

View file

@ -107,7 +107,7 @@ export const getMatchingEntries = (
if (lowerCaseQuery.length === 0)
return {
matchingCategories: Object.keys(filters),
matchingCategories: ['ALL', ...Object.keys(filters)],
matchingFilters: filters,
};
@ -125,7 +125,7 @@ export const getMatchingEntries = (
}
});
return { matchingCategories, matchingFilters };
return { matchingCategories: ['ALL', ...matchingCategories], matchingFilters };
};
interface Props {
@ -211,7 +211,7 @@ function FilterModal(props: Props) {
return (
<div
className={stl.wrapper}
style={{ width: '480px', height: '380px', borderRadius: '.5rem' }}
style={{ width: '560px', height: '380px', borderRadius: '.5rem' }}
>
<Input
className={'mb-4'}
@ -224,14 +224,15 @@ function FilterModal(props: Props) {
{matchingCategories.map((key) => (
<div
key={key}
className={'rounded p-4 hover:bg-active-blue capitalize'}
onClick={() => setCategory(key)}
className={cn('rounded px-4 py-2 hover:bg-active-blue capitalize', key === category ? 'bg-active-blue' : '')}
>
{key.toLowerCase()}
</div>
))}
</div>
<div
className={'flex flex-col gap-2 overflow-y-auto w-full'}
className={'flex flex-col gap-1 overflow-y-auto w-full'}
style={{ maxHeight: 300, flex: 2 }}
>
{displayedFilters.length

View file

@ -10,16 +10,12 @@ const dropdownStyles = {
cursor: 'pointer',
height: '26px',
minHeight: '26px',
backgroundColor: '#f6f6f6',
'&:hover': {
backgroundColor: '#EEEEEE',
},
backgroundColor: 'white',
}
return obj;
},
valueContainer: (provided: any) => ({
...provided,
paddingRight: '0px',
width: 'fit-content',
'& input': {
marginTop: '-3px',
@ -29,9 +25,7 @@ const dropdownStyles = {
...provided,
}),
indicatorsContainer: (provided: any) => ({
...provided,
padding: '0px',
height: '26px',
display: 'none',
}),
// option: (provided: any, state: any) => ({
// ...provided,
@ -39,11 +33,13 @@ const dropdownStyles = {
// }),
menu: (provided: any, state: any) => ({
...provided,
top: 20,
marginTop: '0.5rem',
left: 0,
minWidth: 'fit-content',
overflow: 'hidden',
zIndex: 100,
border: 'none',
boxShadow: '0px 4px 10px rgba(0,0,0, 0.15)',
}),
container: (provided: any) => ({
...provided,

View file

@ -32,6 +32,10 @@ function FilterSelection(props: Props) {
} = props;
const [showModal, setShowModal] = useState(false);
const onAddFilter = (filter: any) => {
onFilterClick(filter);
setShowModal(false);
}
return (
<div className="relative flex-shrink-0">
<OutsideClickDetectingDiv
@ -54,13 +58,11 @@ function FilterSelection(props: Props) {
) : (
<div
className={cn(
'rounded-lg py-1 px-3 flex items-center cursor-pointer bg-gray-lightest text-ellipsis hover:bg-gray-light-shade',
'rounded-lg py-1 px-2 flex items-center cursor-pointer bg-white border border-gray-light text-ellipsis',
{ 'opacity-50 pointer-events-none': disabled }
)}
style={{
width: '150px',
height: '26px',
border: 'solid thin #e9e9e9',
}}
onClick={() => setShowModal(true)}
>
@ -70,14 +72,13 @@ function FilterSelection(props: Props) {
>
{filter.label}
</div>
<Icon name="chevron-down" size="14" />
</div>
)}
{showModal && (
<div className="absolute mt-2 left-0 rounded-lg shadow bg-white z-50">
<FilterModal
isLive={isRoute(ASSIST_ROUTE, window.location.pathname)}
onFilterClick={onFilterClick}
onFilterClick={onAddFilter}
excludeFilterKeys={excludeFilterKeys}
allowedFilterKeys={allowedFilterKeys}
isConditional={isConditional}

View file

@ -7,183 +7,233 @@ import FilterDuration from '../FilterDuration';
import { debounce } from 'App/utils';
import { assist as assistRoute, isRoute } from 'App/routes';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
const ASSIST_ROUTE = assistRoute();
interface Props {
filter: any;
onUpdate: (filter: any) => void;
isConditional?: boolean;
filter: any;
onUpdate: (filter: any) => void;
isConditional?: boolean;
}
function FilterValue(props: Props) {
const { filter } = props;
const [durationValues, setDurationValues] = useState({
minDuration: filter.value[0],
maxDuration: filter.value.length > 1 ? filter.value[1] : filter.value[0],
});
const showCloseButton = filter.value.length > 1;
const lastIndex = filter.value.length - 1;
const { filter } = props;
const [durationValues, setDurationValues] = useState({
minDuration: filter.value[0],
maxDuration: filter.value.length > 1 ? filter.value[1] : filter.value[0],
});
const showCloseButton = filter.value.length > 1;
const onAddValue = () => {
const newValue = filter.value.concat('');
props.onUpdate({ ...filter, value: newValue });
};
const onAddValue = () => {
const newValue = filter.value.concat('');
props.onUpdate({ ...filter, value: newValue });
};
const onRemoveValue = (valueIndex: any) => {
const newValue = filter.value.filter((_: any, index: any) => index !== valueIndex);
props.onUpdate({ ...filter, value: newValue });
};
const onChange = (e: any, item: any, valueIndex: any) => {
const newValues = filter.value.map((_: any, _index: any) => {
if (_index === valueIndex) {
return item;
}
return _;
});
props.onUpdate({ ...filter, value: newValues });
};
const debounceOnSelect = React.useCallback(debounce(onChange, 500), [onChange]);
const onDurationChange = (newValues: any) => {
setDurationValues({ ...durationValues, ...newValues });
};
const handleBlur = () => {
if (filter.type === FilterType.DURATION) {
const { maxDuration, minDuration } = filter;
if (maxDuration || minDuration) return;
if (maxDuration !== durationValues.maxDuration || minDuration !== durationValues.minDuration) {
props.onUpdate({ ...filter, value: [durationValues.minDuration, durationValues.maxDuration] });
}
}
};
const getParms = (key: any) => {
let params: any = { type: filter.key };
switch (filter.category) {
case FilterCategory.METADATA:
params = { type: FilterKey.METADATA, key: key };
}
if (isRoute(ASSIST_ROUTE, window.location.pathname)) {
params = { ...params, live: true };
}
return params;
};
const renderValueFiled = (value: any, valueIndex: any) => {
const showOrButton = valueIndex === lastIndex && filter.type !== FilterType.NUMBER;
switch (filter.type) {
case FilterType.STRING:
return (
<FilterAutoCompleteLocal
value={value}
showCloseButton={showCloseButton}
showOrButton={showOrButton}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
onSelect={(e, item) => debounceOnSelect(e, item, valueIndex)}
icon={filter.icon}
/>
);
case FilterType.DROPDOWN:
return (
<FilterValueDropdown
// search={true}
value={value}
placeholder={filter.placeholder}
// filter={filter}
options={filter.options}
onChange={({ value }) => onChange(null, { value }, valueIndex)}
/>
);
case FilterType.ISSUE:
case FilterType.MULTIPLE_DROPDOWN:
return (
<FilterValueDropdown
search={true}
// multiple={true}
value={value}
// filter={filter}
placeholder={filter.placeholder}
options={filter.options}
onChange={({ value }) => onChange(null, value, valueIndex)}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
showCloseButton={showCloseButton}
showOrButton={showOrButton}
/>
);
case FilterType.DURATION:
return (
<FilterDuration
onChange={onDurationChange}
// onEnterPress={ this.handleClose }
onBlur={handleBlur}
minDuration={durationValues.minDuration}
maxDuration={durationValues.maxDuration}
isConditional={props.isConditional}
/>
);
case FilterType.NUMBER_MULTIPLE:
return (
<FilterAutoCompleteLocal
value={value}
showCloseButton={showCloseButton}
showOrButton={showOrButton}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
onSelect={(e, item) => debounceOnSelect(e, item, valueIndex)}
icon={filter.icon}
type="number"
/>
);
case FilterType.NUMBER:
return (
<FilterAutoCompleteLocal
value={value}
showCloseButton={showCloseButton}
showOrButton={showOrButton}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
onSelect={(e, item) => debounceOnSelect(e, item, valueIndex)}
icon={filter.icon}
type="number"
allowDecimals={false}
isMultilple={false}
/>
);
case FilterType.MULTIPLE:
return (
<FilterAutoComplete
value={value}
showCloseButton={showCloseButton}
showOrButton={showOrButton}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
method={'GET'}
endpoint="/PROJECT_ID/events/search"
params={getParms(filter.key)}
headerText={''}
placeholder={filter.placeholder}
onSelect={(e, item) => onChange(e, item, valueIndex)}
icon={filter.icon}
/>
);
}
};
return (
<div className={cn("grid gap-3", { 'grid-cols-2': filter.hasSource, 'grid-cols-3' : !filter.hasSource })}>
{filter.type === FilterType.DURATION
? renderValueFiled(filter.value, 0)
: filter.value &&
filter.value.map((value: any, valueIndex: any) => <div key={valueIndex}>{renderValueFiled(value, valueIndex)}</div>)}
</div>
const onRemoveValue = (valueIndex: any) => {
const newValue = filter.value.filter(
(_: any, index: any) => index !== valueIndex
);
props.onUpdate({ ...filter, value: newValue });
};
const onChange = (e: any, item: any, valueIndex: any) => {
const newValues = filter.value.map((_: any, _index: any) => {
if (_index === valueIndex) {
return item;
}
return _;
});
props.onUpdate({ ...filter, value: newValues });
};
const debounceOnSelect = React.useCallback(debounce(onChange, 500), [
onChange,
]);
const onDurationChange = (newValues: any) => {
setDurationValues({ ...durationValues, ...newValues });
};
const handleBlur = () => {
if (filter.type === FilterType.DURATION) {
const { maxDuration, minDuration } = filter;
if (maxDuration || minDuration) return;
if (
maxDuration !== durationValues.maxDuration ||
minDuration !== durationValues.minDuration
) {
props.onUpdate({
...filter,
value: [durationValues.minDuration, durationValues.maxDuration],
});
}
}
};
const getParms = (key: any) => {
let params: any = { type: filter.key };
switch (filter.category) {
case FilterCategory.METADATA:
params = { type: FilterKey.METADATA, key: key };
}
if (isRoute(ASSIST_ROUTE, window.location.pathname)) {
params = { ...params, live: true };
}
return params;
};
const renderValueFiled = (value: any[]) => {
const showOrButton = filter.value.length > 1;
const valueIndex = 0;
const BaseFilterLocalAutoComplete = (props) => (
<FilterAutoCompleteLocal
value={value}
showCloseButton={showCloseButton}
onAddValue={onAddValue}
onRemoveValue={(index) => onRemoveValue(index)}
onSelect={(e, item, index) => debounceOnSelect(e, item, index)}
icon={filter.icon}
{...props}
/>
);
switch (filter.type) {
case FilterType.NUMBER_MULTIPLE:
return <BaseFilterLocalAutoComplete type="number" />;
case FilterType.NUMBER:
return (
<BaseFilterLocalAutoComplete
type="number"
allowDecimals={false}
isMultiple={false}
/>
);
case FilterType.STRING:
return <BaseFilterLocalAutoComplete />;
case FilterType.DROPDOWN:
return (
<FilterValueDropdown
value={value}
placeholder={filter.placeholder}
options={filter.options}
onChange={(item, index) => onChange(null, { value: item.value }, index)}
/>
);
case FilterType.ISSUE:
case FilterType.MULTIPLE_DROPDOWN:
return (
<FilterValueDropdown
search={true}
value={value}
placeholder={filter.placeholder}
options={filter.options}
onChange={(item, index) => onChange(null, { value: item.value }, index)}
onAddValue={onAddValue}
onRemoveValue={(ind) => onRemoveValue(ind)}
showCloseButton={showCloseButton}
showOrButton={showOrButton}
/>
);
case FilterType.DURATION:
return (
<FilterDuration
onChange={onDurationChange}
onBlur={handleBlur}
minDuration={durationValues.minDuration}
maxDuration={durationValues.maxDuration}
isConditional={props.isConditional}
/>
);
case FilterType.MULTIPLE:
return (
<FilterAutoComplete
value={value}
showCloseButton={showCloseButton}
showOrButton={showOrButton}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
method={'GET'}
endpoint="/PROJECT_ID/events/search"
params={getParms(filter.key)}
headerText={''}
placeholder={filter.placeholder}
onSelect={(e, item) => onChange(e, item, valueIndex)}
icon={filter.icon}
/>
);
}
};
return (
<div
className={cn('grid gap-3', {
'grid-cols-2': filter.hasSource,
'grid-cols-3': !filter.hasSource,
})}
>
{renderValueFiled(filter.value)}
</div>
);
}
export default FilterValue;
// const isEmpty = filter.value.length === 0 || !filter.value[0].length;
// return (
// <div
// className={
// 'rounded border border-gray-light px-2 relative w-fit whitespace-nowrap'
// }
// style={{ height: 26 }}
// ref={filterValueContainer}
// >
// <div onClick={() => setShowValueModal(true)} className={'flex items-center gap-2 '}>
// {!isEmpty ? (
// <>
// <div
// className={
// 'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'
// }
// >
// {filter.value[0]}
// </div>
// <div
// className={
// 'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'
// }
// >
// + {filter.value.length - 1} More
// </div>
// </>
// ) : (
// <div className={'text-disabled-text'}>Select values</div>
// )}
// </div>
// {showValueModal ? (
// <div
// className={cn(
// 'absolute left-0 mt-6 flex items-center gap-2 bg-white border shadow border-gray-light z-10',
// {
// 'grid-cols-2': filter.hasSource,
// 'grid-cols-3': !filter.hasSource,
// }
// )}
// style={{ minWidth: 200, minHeight: 100, top: '100%' }}
// >
// {filter.type === FilterType.DURATION
// ? renderValueFiled(filter.value, 0)
// : filter.value &&
// filter.value.map((value: any, valueIndex: any) => (
// <div key={valueIndex}>
// {renderValueFiled(value, valueIndex)}
// </div>
// ))}
// <div>
// <Button>Apply</Button>
// <Button>Cancel</Button>
// </div>
// </div>
// ) : null}
// </div>
// );
export default observer(FilterValue);

View file

@ -13,7 +13,7 @@
display: flex;
align-items: stretch;
padding: 0;
background-color: $gray-lightest;
background-color: white;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
margin-left: auto;

View file

@ -72,26 +72,28 @@ const dropdownStyles = {
interface Props {
placeholder?: string;
value: string;
onChange: (value: any) => void;
onChange: (value: any, ind: number) => void;
className?: string;
options: any[];
search?: boolean;
showCloseButton?: boolean;
showOrButton?: boolean;
onRemoveValue?: () => void;
onAddValue?: () => void;
isMultilple?: boolean;
onRemoveValue?: (ind: number) => void;
onAddValue?: (ind: number) => void;
isMultiple?: boolean;
index: number;
}
function FilterValueDropdown(props: Props) {
const {
placeholder = 'Select',
isMultilple = true,
isMultiple = true,
search = false,
options,
onChange,
value,
showCloseButton = true,
showOrButton = true,
index,
} = props;
return (
@ -102,27 +104,54 @@ function FilterValueDropdown(props: Props) {
options={options}
name="issue_type"
value={value ? options.find((item) => item.value === value) : null}
onChange={(value: any) => onChange(value.value)}
onChange={(value: any) => onChange(value.value, index)}
placeholder={placeholder}
styles={dropdownStyles}
/>
<div className={stl.right}>
{showCloseButton && (
<div onClick={props.onRemoveValue}>
<div onClick={() => props.onRemoveValue?.(index)}>
<Icon name="close" size="12" />
</div>
)}
{showOrButton && (
<div onClick={props.onAddValue} className="color-teal">
<div onClick={() => props.onAddValue?.(index)} className="color-teal">
<span className="px-1">or</span>
</div>
)}
</div>
</div>
{!showOrButton && isMultilple && <div className="ml-3">or</div>}
{!showOrButton && isMultiple && <div className="ml-3">or</div>}
</div>
);
}
export default FilterValueDropdown;
interface MainProps {
placeholder?: string;
value: string[];
onChange: (value: any, ind: number) => void;
className?: string;
options: any[];
search?: boolean;
showCloseButton?: boolean;
showOrButton?: boolean;
onRemoveValue?: (ind: number) => void;
onAddValue?: (ind: number) => void;
isMultiple?: boolean;
}
function FilterDropdownController(props: MainProps) {
return props.value.map((value, index) => (
<FilterValueDropdown
{...props}
key={index}
value={value}
index={index}
showOrButton={index === props.value.length - 1}
showCloseButton={props.value.length > 1}
/>
))
}
export default FilterDropdownController;

View file

@ -26,19 +26,19 @@ interface Props<Value extends ValueObject> {
}
export default function <Value extends ValueObject>({
placeholder = 'Select',
name = '',
onChange,
right = false,
plain = false,
options,
isSearchable = false,
components = {},
styles = {},
defaultValue = '',
controlStyle = {},
...rest
}: Props<Value>) {
placeholder = 'Select',
name = '',
onChange,
right = false,
plain = false,
options,
isSearchable = false,
components = {},
styles = {},
defaultValue = '',
controlStyle = {},
...rest
}: Props<Value>) {
const defaultSelected = Array.isArray(defaultValue) ?
defaultValue.map((value) => options.find((option) => option.value === value)) :
@ -79,7 +79,7 @@ export default function <Value extends ValueObject>({
}),
menuList: (provided: any, state: any) => ({
...provided,
padding: 0
padding: 0,
}),
control: (provided: any) => {
const obj = {

View file

@ -4,7 +4,7 @@ export enum FilterCategory {
RECORDING_ATTRIBUTES = 'Recording Attributes',
TECHNICAL = 'Technical',
USER = 'User Identification',
METADATA = 'Session & User Metadata',
METADATA = 'Metadata',
PERFORMANCE = 'Performance',
}