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 { Field, reduxForm } from 'redux-form';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
import classNames from 'classnames';
|
||||
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { validatePath, validateRequiredValue } from '../../helpers/validators';
|
||||
|
||||
import { CheckboxField, renderInputField } from '../../helpers/form';
|
||||
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE, FORM_NAME } from '../../helpers/constants';
|
||||
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE } from '../../helpers/constants';
|
||||
import filtersCatalog from '../../helpers/filters/filters';
|
||||
import { FiltersList } from './FiltersList';
|
||||
|
||||
const getIconsData = (homepage: any, source: any) => [
|
||||
{
|
||||
iconName: 'dashboard',
|
||||
href: homepage,
|
||||
className: 'ml-1',
|
||||
},
|
||||
{
|
||||
iconName: 'info',
|
||||
href: source,
|
||||
},
|
||||
];
|
||||
type FormValues = {
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const renderIcons = (iconsData: any) =>
|
||||
iconsData.map(({ iconName, href, className = '' }: any) => (
|
||||
<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>
|
||||
));
|
||||
|
||||
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;
|
||||
type Props = {
|
||||
closeModal: (...args: unknown[]) => void;
|
||||
onSubmit: (...args: unknown[]) => void;
|
||||
processingAddFilter: boolean;
|
||||
processingConfigFilter: boolean;
|
||||
whitelist?: boolean;
|
||||
modalType: string;
|
||||
toggleFilteringModal: (...args: unknown[]) => unknown;
|
||||
selectedSources?: object;
|
||||
}
|
||||
toggleFilteringModal: (...args: unknown[]) => void;
|
||||
selectedSources?: Record<string, boolean>;
|
||||
initialValues?: FormValues;
|
||||
};
|
||||
|
||||
const Form = (props: FormProps) => {
|
||||
const {
|
||||
t,
|
||||
closeModal,
|
||||
handleSubmit,
|
||||
processingAddFilter,
|
||||
processingConfigFilter,
|
||||
whitelist,
|
||||
modalType,
|
||||
toggleFilteringModal,
|
||||
selectedSources,
|
||||
} = props;
|
||||
export const Form = ({
|
||||
closeModal,
|
||||
processingAddFilter,
|
||||
processingConfigFilter,
|
||||
whitelist,
|
||||
modalType,
|
||||
toggleFilteringModal,
|
||||
selectedSources,
|
||||
onSubmit,
|
||||
initialValues,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const methods = useForm({ defaultValues: initialValues });
|
||||
const { handleSubmit, control } = methods;
|
||||
|
||||
const openModal = (modalType: any, timeout = MODAL_OPEN_TIMEOUT) => {
|
||||
toggleFilteringModal();
|
||||
@@ -136,72 +51,86 @@ const Form = (props: FormProps) => {
|
||||
const openAddFiltersModal = () => openModal(MODAL_TYPE.ADD_FILTERS);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body modal-body--filters">
|
||||
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||
<div className="d-flex justify-content-around">
|
||||
<button
|
||||
onClick={openFilteringListModal}
|
||||
className="btn btn-success btn-standard mr-2 btn-large">
|
||||
{t('choose_from_list')}
|
||||
</button>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="modal-body modal-body--filters">
|
||||
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||
<div className="d-flex justify-content-around">
|
||||
<button
|
||||
onClick={openFilteringListModal}
|
||||
className="btn btn-success btn-standard mr-2 btn-large">
|
||||
{t('choose_from_list')}
|
||||
</button>
|
||||
|
||||
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
|
||||
{t('add_custom_list')}
|
||||
</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()}
|
||||
/>
|
||||
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
|
||||
{t('add_custom_list')}
|
||||
</button>
|
||||
</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">
|
||||
<Field
|
||||
id="url"
|
||||
name="url"
|
||||
type="text"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('enter_url_or_path_hint')}
|
||||
validate={[validateRequiredValue, validatePath]}
|
||||
normalizeOnBlur={(data: any) => data.trim()}
|
||||
/>
|
||||
</div>
|
||||
<div className="form__group">
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
rules={{ validate: { validateRequiredValue, validatePath } }}
|
||||
render={({ field }) => (
|
||||
<input
|
||||
{...field}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('enter_url_or_path_hint')}
|
||||
onBlur={(e) => field.onChange(e.target.value.trim())}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__description">
|
||||
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="form__description">
|
||||
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={closeModal}>
|
||||
{t('cancel_btn')}
|
||||
</button>
|
||||
|
||||
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success"
|
||||
disabled={processingAddFilter || processingConfigFilter}>
|
||||
{t('save_btn')}
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={closeModal}>
|
||||
{t('cancel_btn')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||
<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 Form from './Form';
|
||||
import { Form } from './Form';
|
||||
import '../ui/Modal.css';
|
||||
|
||||
import { getMap } from '../../helpers/helpers';
|
||||
@@ -75,25 +75,15 @@ class Modal extends Component<ModalProps> {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
|
||||
processingAddFilter,
|
||||
|
||||
processingConfigFilter,
|
||||
|
||||
handleSubmit,
|
||||
|
||||
modalType,
|
||||
|
||||
currentFilterData,
|
||||
|
||||
whitelist,
|
||||
|
||||
toggleFilteringModal,
|
||||
|
||||
filters,
|
||||
|
||||
t,
|
||||
|
||||
filtersCatalog,
|
||||
} = this.props;
|
||||
|
||||
@@ -117,6 +107,8 @@ class Modal extends Component<ModalProps> {
|
||||
|
||||
const title = t(getTitle(modalType, whitelist));
|
||||
|
||||
console.log(modalType, initialValues);
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
|
||||
|
||||
Reference in New Issue
Block a user