filters form
This commit is contained in:
96
client/src/components/Filters/FiltersList.tsx
Normal file
96
client/src/components/Filters/FiltersList.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const getIconsData = (homepage: string, source: string) => [
|
||||||
|
{
|
||||||
|
iconName: 'dashboard',
|
||||||
|
href: homepage,
|
||||||
|
className: 'ml-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'info',
|
||||||
|
href: source,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderIcons = (iconsData: { iconName: string; href: string; className?: string }[]) =>
|
||||||
|
iconsData.map(({ iconName, href, className = '' }) => (
|
||||||
|
<a
|
||||||
|
key={iconName}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={classNames('d-flex align-items-center', className)}>
|
||||||
|
<svg className="icon icon--15 mr-1 icon--gray">
|
||||||
|
<use xlinkHref={`#${iconName}`} />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
));
|
||||||
|
|
||||||
|
type Filter = {
|
||||||
|
categoryId: string;
|
||||||
|
homepage: string;
|
||||||
|
source: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Category = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
categories: Record<string, Category>;
|
||||||
|
filters: Record<string, Filter>;
|
||||||
|
selectedSources: Record<string, boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FiltersList = ({ categories, filters, selectedSources }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { register } = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.entries(categories).map(([categoryId, category]) => {
|
||||||
|
const categoryFilters = Object.entries(filters)
|
||||||
|
.filter(([, filter]) => filter.categoryId === categoryId)
|
||||||
|
.map(([key, filter]) => ({ ...filter, id: key }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={category.name} className="modal-body__item">
|
||||||
|
<h6 className="font-weight-bold mb-1">{t(category.name)}</h6>
|
||||||
|
<p className="mb-3">{t(category.description)}</p>
|
||||||
|
{categoryFilters.map((filter) => {
|
||||||
|
const { homepage, source, name, id } = filter;
|
||||||
|
const isSelected = selectedSources[source];
|
||||||
|
const iconsData = getIconsData(homepage, source);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={name} className="d-flex align-items-center pb-1">
|
||||||
|
<label className="checkbox checkbox--settings">
|
||||||
|
<span className="checkbox__marker" />
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox__input"
|
||||||
|
disabled={isSelected}
|
||||||
|
{...register(id)}
|
||||||
|
/>
|
||||||
|
<span className="checkbox__label">
|
||||||
|
<span className="checkbox__label-text">
|
||||||
|
<span className="checkbox__label-title">{t(name)}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{renderIcons(iconsData)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,130 +1,45 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
import flow from 'lodash/flow';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { validatePath, validateRequiredValue } from '../../helpers/validators';
|
import { validatePath, validateRequiredValue } from '../../helpers/validators';
|
||||||
|
|
||||||
import { CheckboxField, renderInputField } from '../../helpers/form';
|
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE } from '../../helpers/constants';
|
||||||
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE, FORM_NAME } from '../../helpers/constants';
|
|
||||||
import filtersCatalog from '../../helpers/filters/filters';
|
import filtersCatalog from '../../helpers/filters/filters';
|
||||||
|
import { FiltersList } from './FiltersList';
|
||||||
|
|
||||||
const getIconsData = (homepage: any, source: any) => [
|
type FormValues = {
|
||||||
{
|
enabled: boolean;
|
||||||
iconName: 'dashboard',
|
name: string;
|
||||||
href: homepage,
|
url: string;
|
||||||
className: 'ml-1',
|
};
|
||||||
},
|
|
||||||
{
|
|
||||||
iconName: 'info',
|
|
||||||
href: source,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const renderIcons = (iconsData: any) =>
|
type Props = {
|
||||||
iconsData.map(({ iconName, href, className = '' }: any) => (
|
closeModal: (...args: unknown[]) => void;
|
||||||
<a
|
onSubmit: (...args: unknown[]) => void;
|
||||||
key={iconName}
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={classNames('d-flex align-items-center', className)}>
|
|
||||||
<svg className="icon icon--15 mr-1 icon--gray">
|
|
||||||
<use xlinkHref={`#${iconName}`} />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
));
|
|
||||||
|
|
||||||
interface renderCheckboxFieldProps {
|
|
||||||
input: {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
checked: boolean;
|
|
||||||
onChange: (...args: unknown[]) => unknown;
|
|
||||||
};
|
|
||||||
disabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderCheckboxField = (props: renderCheckboxFieldProps) => (
|
|
||||||
<CheckboxField
|
|
||||||
{...props}
|
|
||||||
meta={{ touched: false, error: null }}
|
|
||||||
input={{
|
|
||||||
...props.input,
|
|
||||||
checked: props.disabled || props.input.checked,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderFilters = ({ categories, filters }: any, selectedSources: any, t: any) =>
|
|
||||||
Object.keys(categories).map((categoryId) => {
|
|
||||||
const category = categories[categoryId];
|
|
||||||
const categoryFilters: any = [];
|
|
||||||
Object.keys(filters)
|
|
||||||
.sort()
|
|
||||||
.forEach((key) => {
|
|
||||||
const filter = filters[key];
|
|
||||||
filter.id = key;
|
|
||||||
if (filter.categoryId === categoryId) {
|
|
||||||
categoryFilters.push(filter);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={category.name} className="modal-body__item">
|
|
||||||
<h6 className="font-weight-bold mb-1">{t(category.name)}</h6>
|
|
||||||
|
|
||||||
<p className="mb-3">{t(category.description)}</p>
|
|
||||||
|
|
||||||
{categoryFilters.map((filter) => {
|
|
||||||
const { homepage, source, name } = filter;
|
|
||||||
|
|
||||||
const isSelected = Object.prototype.hasOwnProperty.call(selectedSources, source);
|
|
||||||
|
|
||||||
const iconsData = getIconsData(homepage, source);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={name} className="d-flex align-items-center pb-1">
|
|
||||||
<Field
|
|
||||||
name={filter.id}
|
|
||||||
type="checkbox"
|
|
||||||
component={renderCheckboxField}
|
|
||||||
placeholder={t(name)}
|
|
||||||
disabled={isSelected}
|
|
||||||
/>
|
|
||||||
{renderIcons(iconsData)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
interface FormProps {
|
|
||||||
t: (...args: unknown[]) => string;
|
|
||||||
closeModal: (...args: unknown[]) => unknown;
|
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
|
||||||
processingAddFilter: boolean;
|
processingAddFilter: boolean;
|
||||||
processingConfigFilter: boolean;
|
processingConfigFilter: boolean;
|
||||||
whitelist?: boolean;
|
whitelist?: boolean;
|
||||||
modalType: string;
|
modalType: string;
|
||||||
toggleFilteringModal: (...args: unknown[]) => unknown;
|
toggleFilteringModal: (...args: unknown[]) => void;
|
||||||
selectedSources?: object;
|
selectedSources?: Record<string, boolean>;
|
||||||
}
|
initialValues?: FormValues;
|
||||||
|
};
|
||||||
|
|
||||||
const Form = (props: FormProps) => {
|
export const Form = ({
|
||||||
const {
|
closeModal,
|
||||||
t,
|
processingAddFilter,
|
||||||
closeModal,
|
processingConfigFilter,
|
||||||
handleSubmit,
|
whitelist,
|
||||||
processingAddFilter,
|
modalType,
|
||||||
processingConfigFilter,
|
toggleFilteringModal,
|
||||||
whitelist,
|
selectedSources,
|
||||||
modalType,
|
onSubmit,
|
||||||
toggleFilteringModal,
|
initialValues,
|
||||||
selectedSources,
|
}: Props) => {
|
||||||
} = props;
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const methods = useForm({ defaultValues: initialValues });
|
||||||
|
const { handleSubmit, control } = methods;
|
||||||
|
|
||||||
const openModal = (modalType: any, timeout = MODAL_OPEN_TIMEOUT) => {
|
const openModal = (modalType: any, timeout = MODAL_OPEN_TIMEOUT) => {
|
||||||
toggleFilteringModal();
|
toggleFilteringModal();
|
||||||
@@ -136,72 +51,86 @@ const Form = (props: FormProps) => {
|
|||||||
const openAddFiltersModal = () => openModal(MODAL_TYPE.ADD_FILTERS);
|
const openAddFiltersModal = () => openModal(MODAL_TYPE.ADD_FILTERS);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<FormProvider {...methods}>
|
||||||
<div className="modal-body modal-body--filters">
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE && (
|
<div className="modal-body modal-body--filters">
|
||||||
<div className="d-flex justify-content-around">
|
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||||
<button
|
<div className="d-flex justify-content-around">
|
||||||
onClick={openFilteringListModal}
|
<button
|
||||||
className="btn btn-success btn-standard mr-2 btn-large">
|
onClick={openFilteringListModal}
|
||||||
{t('choose_from_list')}
|
className="btn btn-success btn-standard mr-2 btn-large">
|
||||||
</button>
|
{t('choose_from_list')}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
|
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
|
||||||
{t('add_custom_list')}
|
{t('add_custom_list')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{modalType === MODAL_TYPE.CHOOSE_FILTERING_LIST && renderFilters(filtersCatalog, selectedSources, t)}
|
|
||||||
{modalType !== MODAL_TYPE.CHOOSE_FILTERING_LIST && modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
|
||||||
<>
|
|
||||||
<div className="form__group">
|
|
||||||
<Field
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
component={renderInputField}
|
|
||||||
className="form-control"
|
|
||||||
placeholder={t('enter_name_hint')}
|
|
||||||
normalizeOnBlur={(data: any) => data.trim()}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{modalType === MODAL_TYPE.CHOOSE_FILTERING_LIST && (
|
||||||
|
<FiltersList
|
||||||
|
categories={filtersCatalog.categories}
|
||||||
|
filters={filtersCatalog.filters}
|
||||||
|
selectedSources={selectedSources}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{modalType !== MODAL_TYPE.CHOOSE_FILTERING_LIST && modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||||
|
<>
|
||||||
|
<div className="form__group">
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('enter_name_hint')}
|
||||||
|
onBlur={(e) => field.onChange(e.target.value.trim())}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form__group">
|
<div className="form__group">
|
||||||
<Field
|
<Controller
|
||||||
id="url"
|
name="url"
|
||||||
name="url"
|
control={control}
|
||||||
type="text"
|
rules={{ validate: { validateRequiredValue, validatePath } }}
|
||||||
component={renderInputField}
|
render={({ field }) => (
|
||||||
className="form-control"
|
<input
|
||||||
placeholder={t('enter_url_or_path_hint')}
|
{...field}
|
||||||
validate={[validateRequiredValue, validatePath]}
|
type="text"
|
||||||
normalizeOnBlur={(data: any) => data.trim()}
|
className="form-control"
|
||||||
/>
|
placeholder={t('enter_url_or_path_hint')}
|
||||||
</div>
|
onBlur={(e) => field.onChange(e.target.value.trim())}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form__description">
|
<div className="form__description">
|
||||||
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
|
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button type="button" className="btn btn-secondary" onClick={closeModal}>
|
<button type="button" className="btn btn-secondary" onClick={closeModal}>
|
||||||
{t('cancel_btn')}
|
{t('cancel_btn')}
|
||||||
</button>
|
|
||||||
|
|
||||||
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-success"
|
|
||||||
disabled={processingAddFilter || processingConfigFilter}>
|
|
||||||
{t('save_btn')}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||||
</form>
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success"
|
||||||
|
disabled={processingAddFilter || processingConfigFilter}>
|
||||||
|
{t('save_btn')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default flow([withTranslation(), reduxForm({ form: FORM_NAME.FILTER })])(Form);
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { withTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { MODAL_TYPE } from '../../helpers/constants';
|
import { MODAL_TYPE } from '../../helpers/constants';
|
||||||
|
|
||||||
import Form from './Form';
|
import { Form } from './Form';
|
||||||
import '../ui/Modal.css';
|
import '../ui/Modal.css';
|
||||||
|
|
||||||
import { getMap } from '../../helpers/helpers';
|
import { getMap } from '../../helpers/helpers';
|
||||||
@@ -75,25 +75,15 @@ class Modal extends Component<ModalProps> {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|
||||||
processingAddFilter,
|
processingAddFilter,
|
||||||
|
|
||||||
processingConfigFilter,
|
processingConfigFilter,
|
||||||
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|
||||||
modalType,
|
modalType,
|
||||||
|
|
||||||
currentFilterData,
|
currentFilterData,
|
||||||
|
|
||||||
whitelist,
|
whitelist,
|
||||||
|
|
||||||
toggleFilteringModal,
|
toggleFilteringModal,
|
||||||
|
|
||||||
filters,
|
filters,
|
||||||
|
|
||||||
t,
|
t,
|
||||||
|
|
||||||
filtersCatalog,
|
filtersCatalog,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -117,6 +107,8 @@ class Modal extends Component<ModalProps> {
|
|||||||
|
|
||||||
const title = t(getTitle(modalType, whitelist));
|
const title = t(getTitle(modalType, whitelist));
|
||||||
|
|
||||||
|
console.log(modalType, initialValues);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
|
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
|
||||||
|
|||||||
Reference in New Issue
Block a user