mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
Refactor SelectSingleFilter to use subcomponents SelectSingleFilterPlain and SelectSingleFilterPopup
This commit is contained in:
parent
d89aa64e57
commit
ac3db9efa3
8 changed files with 529 additions and 105 deletions
|
|
@ -152,6 +152,7 @@ const SearchFiltersComponent = props => {
|
|||
urlParam={categoryFilter.paramName}
|
||||
label={categoryLabel}
|
||||
onSelect={handleSelectOption}
|
||||
showAsPopup
|
||||
options={categoryFilter.options}
|
||||
initialValue={initialCategory}
|
||||
contentPlacementOffset={FILTER_DROPDOWN_OFFSET}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
ModalInMobile,
|
||||
Button,
|
||||
PriceFilter,
|
||||
SelectSingleFilterPlain,
|
||||
SelectSingleFilter,
|
||||
SelectMultipleFilter,
|
||||
BookingDateRangeFilter,
|
||||
} from '../../components';
|
||||
|
|
@ -221,10 +221,11 @@ class SearchFiltersMobileComponent extends Component {
|
|||
const initialCategory = categoryFilter ? this.initialValue(categoryFilter.paramName) : null;
|
||||
|
||||
const categoryFilterElement = categoryFilter ? (
|
||||
<SelectSingleFilterPlain
|
||||
<SelectSingleFilter
|
||||
urlParam={categoryFilter.paramName}
|
||||
label={categoryLabel}
|
||||
onSelect={this.handleSelectSingle}
|
||||
liveEdit
|
||||
options={categoryFilter.options}
|
||||
initialValue={initialCategory}
|
||||
intl={intl}
|
||||
|
|
|
|||
|
|
@ -1,111 +1,23 @@
|
|||
import React, { Component } from 'react';
|
||||
import { string, func, arrayOf, shape, number } from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { bool } from 'prop-types';
|
||||
import SelectSingleFilterPlain from './SelectSingleFilterPlain';
|
||||
import SelectSingleFilterPopup from './SelectSingleFilterPopup';
|
||||
|
||||
import { Menu, MenuContent, MenuItem, MenuLabel } from '../../components';
|
||||
import css from './SelectSingleFilter.css';
|
||||
|
||||
const optionLabel = (options, key) => {
|
||||
const option = options.find(o => o.key === key);
|
||||
return option ? option.label : key;
|
||||
const SelectSingleFilter = props => {
|
||||
const { showAsPopup, ...rest } = props;
|
||||
return showAsPopup ? (
|
||||
<SelectSingleFilterPopup {...rest} />
|
||||
) : (
|
||||
<SelectSingleFilterPlain {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
class SelectSingleFilter extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { isOpen: false };
|
||||
this.onToggleActive = this.onToggleActive.bind(this);
|
||||
this.selectOption = this.selectOption.bind(this);
|
||||
}
|
||||
|
||||
onToggleActive(isOpen) {
|
||||
this.setState({ isOpen: isOpen });
|
||||
}
|
||||
|
||||
selectOption(urlParam, option) {
|
||||
this.setState({ isOpen: false });
|
||||
this.props.onSelect(urlParam, option);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
urlParam,
|
||||
label,
|
||||
options,
|
||||
initialValue,
|
||||
contentPlacementOffset,
|
||||
} = this.props;
|
||||
|
||||
// resolve menu label text and class
|
||||
const menuLabel = initialValue ? optionLabel(options, initialValue) : label;
|
||||
const menuLabelClass = initialValue ? css.menuLabelSelected : css.menuLabel;
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={classes}
|
||||
useArrow={false}
|
||||
contentPlacementOffset={contentPlacementOffset}
|
||||
onToggleActive={this.onToggleActive}
|
||||
isOpen={this.state.isOpen}
|
||||
>
|
||||
<MenuLabel className={menuLabelClass}>{menuLabel}</MenuLabel>
|
||||
<MenuContent className={css.menuContent}>
|
||||
{options.map(option => {
|
||||
// check if this option is selected
|
||||
const selected = initialValue === option.key;
|
||||
// menu item border class
|
||||
const menuItemBorderClass = selected ? css.menuItemBorderSelected : css.menuItemBorder;
|
||||
|
||||
return (
|
||||
<MenuItem key={option.key}>
|
||||
<button
|
||||
className={css.menuItem}
|
||||
onClick={() => this.selectOption(urlParam, option.key)}
|
||||
>
|
||||
<span className={menuItemBorderClass} />
|
||||
{option.label}
|
||||
</button>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
<MenuItem key={'clearLink'}>
|
||||
<button className={css.clearMenuItem} onClick={() => this.selectOption(urlParam, null)}>
|
||||
<FormattedMessage id={'SelectSingleFilter.clear'} />
|
||||
</button>
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSingleFilter.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
initialValue: null,
|
||||
contentPlacementOffset: 0,
|
||||
showAsPopup: false,
|
||||
};
|
||||
|
||||
SelectSingleFilter.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
urlParam: string.isRequired,
|
||||
label: string.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
options: arrayOf(
|
||||
shape({
|
||||
key: string.isRequired,
|
||||
label: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
initialValue: string,
|
||||
contentPlacementOffset: number,
|
||||
showAsPopup: bool,
|
||||
};
|
||||
|
||||
export default SelectSingleFilter;
|
||||
|
|
|
|||
151
src/components/SelectSingleFilter/SelectSingleFilterPlain.css
Normal file
151
src/components/SelectSingleFilter/SelectSingleFilterPlain.css
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.root {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 17px;
|
||||
border-bottom: 1px solid var(--matterColorNegative);
|
||||
}
|
||||
|
||||
.filterLabel,
|
||||
.filterLabelSelected {
|
||||
@apply --marketplaceH3FontStyles;
|
||||
|
||||
/* Baseline adjustment for label text */
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 4px 0 2px 0;
|
||||
}
|
||||
|
||||
.filterLabel {
|
||||
color: var(--matterColorDark);
|
||||
}
|
||||
|
||||
.filterLabelSelected {
|
||||
color: var(--marketplaceColor);
|
||||
}
|
||||
|
||||
.labelButton {
|
||||
/* Override button styles */
|
||||
outline: none;
|
||||
text-align: left;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.optionsContainerOpen {
|
||||
height: auto;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.optionsContainerClosed {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hasBullets {
|
||||
padding-left: 26px;
|
||||
}
|
||||
|
||||
.twoColumns {
|
||||
@media (--viewportMedium) {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.optionBorder,
|
||||
.optionBorderSelected {
|
||||
position: absolute;
|
||||
height: calc(100% - 12px);
|
||||
top: 4px;
|
||||
left: -24px;
|
||||
transition: width var(--transitionStyleButton);
|
||||
}
|
||||
|
||||
/* left animated "border" like hover element */
|
||||
.optionBorder {
|
||||
width: 0;
|
||||
background-color: var(--matterColorDark);
|
||||
}
|
||||
|
||||
/* left static border for selected element */
|
||||
.optionBorderSelected {
|
||||
width: 8px;
|
||||
background-color: var(--matterColorDark);
|
||||
}
|
||||
|
||||
.optionBullet,
|
||||
.optionBulletSelected {
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: 13px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--marketplaceColor);
|
||||
transition: opacity var(--transitionStyleButton);
|
||||
}
|
||||
|
||||
/* left animated "border" like hover element */
|
||||
.optionBullet {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* left static border for selected element */
|
||||
.optionBulletSelected {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.option {
|
||||
@apply --marketplaceH4FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
font-size: 18px;
|
||||
color: var(--matterColor);
|
||||
|
||||
/* Layout */
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 4px 0 8px 20px;
|
||||
|
||||
/* Override button styles */
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--matterColorDark);
|
||||
}
|
||||
|
||||
&:hover .menuItemBorder {
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.optionSelected {
|
||||
composes: option;
|
||||
color: var(--matterColorDark);
|
||||
}
|
||||
|
||||
.clearButton {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
/* Layout */
|
||||
display: inline;
|
||||
float: right;
|
||||
margin-top: 6px;
|
||||
padding: 0;
|
||||
|
||||
/* Override button styles */
|
||||
outline: none;
|
||||
text-align: left;
|
||||
border: none;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--matterColor);
|
||||
}
|
||||
}
|
||||
121
src/components/SelectSingleFilter/SelectSingleFilterPlain.js
Normal file
121
src/components/SelectSingleFilter/SelectSingleFilterPlain.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import React, { Component } from 'react';
|
||||
import { arrayOf, bool, func, shape, string } from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import css from './SelectSingleFilterPlain.css';
|
||||
|
||||
class SelectSingleFilterPlain extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isOpen: true };
|
||||
this.selectOption = this.selectOption.bind(this);
|
||||
this.toggleIsOpen = this.toggleIsOpen.bind(this);
|
||||
}
|
||||
|
||||
selectOption(option, e) {
|
||||
const { urlParam, onSelect } = this.props;
|
||||
onSelect(urlParam, option);
|
||||
|
||||
// blur event target if event is passed
|
||||
if (e && e.currentTarget) {
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
}
|
||||
|
||||
toggleIsOpen() {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
label,
|
||||
options,
|
||||
initialValue,
|
||||
twoColumns,
|
||||
useBullets,
|
||||
} = this.props;
|
||||
|
||||
const labelClass = initialValue ? css.filterLabelSelected : css.filterLabel;
|
||||
|
||||
const hasBullets = useBullets || twoColumns;
|
||||
const optionsContainerClass = classNames({
|
||||
[css.optionsContainerOpen]: this.state.isOpen,
|
||||
[css.optionsContainerClosed]: !this.state.isOpen,
|
||||
[css.hasBullets]: hasBullets,
|
||||
[css.twoColumns]: twoColumns,
|
||||
});
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={labelClass}>
|
||||
<button className={css.labelButton} onClick={this.toggleIsOpen}>
|
||||
<span className={labelClass}>{label}</span>
|
||||
</button>
|
||||
<button className={css.clearButton} onClick={e => this.selectOption(null, e)}>
|
||||
<FormattedMessage id={'SelectSingleFilter.plainClear'} />
|
||||
</button>
|
||||
</div>
|
||||
<div className={optionsContainerClass}>
|
||||
{options.map(option => {
|
||||
// check if this option is selected
|
||||
const selected = initialValue === option.key;
|
||||
const optionClass = hasBullets && selected ? css.optionSelected : css.option;
|
||||
// menu item selected bullet or border class
|
||||
const optionBorderClass = hasBullets
|
||||
? classNames({
|
||||
[css.optionBulletSelected]: selected,
|
||||
[css.optionBullet]: !selected,
|
||||
})
|
||||
: classNames({
|
||||
[css.optionBorderSelected]: selected,
|
||||
[css.optionBorder]: !selected,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
className={optionClass}
|
||||
onClick={() => this.selectOption(option.key)}
|
||||
>
|
||||
<span className={optionBorderClass} />
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSingleFilterPlain.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
initialValue: null,
|
||||
twoColumns: false,
|
||||
useBullets: false,
|
||||
};
|
||||
|
||||
SelectSingleFilterPlain.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
urlParam: string.isRequired,
|
||||
label: string.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
|
||||
options: arrayOf(
|
||||
shape({
|
||||
key: string.isRequired,
|
||||
label: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
initialValue: string,
|
||||
twoColumns: bool,
|
||||
useBullets: bool,
|
||||
};
|
||||
|
||||
export default SelectSingleFilterPlain;
|
||||
127
src/components/SelectSingleFilter/SelectSingleFilterPopup.css
Normal file
127
src/components/SelectSingleFilter/SelectSingleFilterPopup.css
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.root {
|
||||
display: inline-block;
|
||||
|
||||
&:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menuLabel {
|
||||
@apply --marketplaceButtonStylesSecondary;
|
||||
@apply --marketplaceSearchFilterLabelFontStyles;
|
||||
|
||||
padding: 9px 16px 10px 16px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background-color: var(--matterColorLight);
|
||||
border-color: transparent;
|
||||
text-decoration: none;
|
||||
box-shadow: var(--boxShadowFilterButton);
|
||||
}
|
||||
}
|
||||
|
||||
.menuLabelSelected {
|
||||
@apply --marketplaceButtonStyles;
|
||||
@apply --marketplaceSearchFilterLabelFontStyles;
|
||||
font-weight: var(--fontWeightSemiBold);
|
||||
|
||||
padding: 9px 16px 10px 16px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--marketplaceColor);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border: 1px solid var(--marketplaceColorDark);
|
||||
}
|
||||
}
|
||||
|
||||
.menuContent {
|
||||
margin-top: 7px;
|
||||
padding-top: 13px;
|
||||
min-width: 300px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* left animated "border" like hover element */
|
||||
.menuItemBorder {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 0px;
|
||||
height: calc(100% - 4px);
|
||||
width: 0;
|
||||
background-color: var(--marketplaceColor);
|
||||
transition: width var(--transitionStyleButton);
|
||||
}
|
||||
|
||||
/* left static border for selected element */
|
||||
.menuItemBorderSelected {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 0px;
|
||||
height: calc(100% - 7px);
|
||||
width: 6px;
|
||||
background-color: var(--matterColorDark);
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
@apply --marketplaceListingAttributeFontStyles;
|
||||
color: var(--matterColor);
|
||||
|
||||
/* Layout */
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
margin: 0;
|
||||
padding: 4px 30px;
|
||||
|
||||
/* Override button styles */
|
||||
outline: none;
|
||||
text-align: left;
|
||||
border: none;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--matterColorDark);
|
||||
}
|
||||
|
||||
&:hover .menuItemBorder {
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.clearMenuItem {
|
||||
@apply --marketplaceH4FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
/* Layout */
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
margin: 0;
|
||||
padding: 32px 30px 18px 30px;
|
||||
|
||||
/* Override button styles */
|
||||
outline: none;
|
||||
text-align: left;
|
||||
border: none;
|
||||
|
||||
cursor: pointer;
|
||||
transition: width var(--transitionStyleButton);
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--matterColor);
|
||||
transition: width var(--transitionStyleButton);
|
||||
}
|
||||
}
|
||||
111
src/components/SelectSingleFilter/SelectSingleFilterPopup.js
Normal file
111
src/components/SelectSingleFilter/SelectSingleFilterPopup.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import React, { Component } from 'react';
|
||||
import { string, func, arrayOf, shape, number } from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Menu, MenuContent, MenuItem, MenuLabel } from '..';
|
||||
import css from './SelectSingleFilterPopup.css';
|
||||
|
||||
const optionLabel = (options, key) => {
|
||||
const option = options.find(o => o.key === key);
|
||||
return option ? option.label : key;
|
||||
};
|
||||
|
||||
class SelectSingleFilterPopup extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { isOpen: false };
|
||||
this.onToggleActive = this.onToggleActive.bind(this);
|
||||
this.selectOption = this.selectOption.bind(this);
|
||||
}
|
||||
|
||||
onToggleActive(isOpen) {
|
||||
this.setState({ isOpen: isOpen });
|
||||
}
|
||||
|
||||
selectOption(urlParam, option) {
|
||||
this.setState({ isOpen: false });
|
||||
this.props.onSelect(urlParam, option);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
urlParam,
|
||||
label,
|
||||
options,
|
||||
initialValue,
|
||||
contentPlacementOffset,
|
||||
} = this.props;
|
||||
|
||||
// resolve menu label text and class
|
||||
const menuLabel = initialValue ? optionLabel(options, initialValue) : label;
|
||||
const menuLabelClass = initialValue ? css.menuLabelSelected : css.menuLabel;
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={classes}
|
||||
useArrow={false}
|
||||
contentPlacementOffset={contentPlacementOffset}
|
||||
onToggleActive={this.onToggleActive}
|
||||
isOpen={this.state.isOpen}
|
||||
>
|
||||
<MenuLabel className={menuLabelClass}>{menuLabel}</MenuLabel>
|
||||
<MenuContent className={css.menuContent}>
|
||||
{options.map(option => {
|
||||
// check if this option is selected
|
||||
const selected = initialValue === option.key;
|
||||
// menu item border class
|
||||
const menuItemBorderClass = selected ? css.menuItemBorderSelected : css.menuItemBorder;
|
||||
|
||||
return (
|
||||
<MenuItem key={option.key}>
|
||||
<button
|
||||
className={css.menuItem}
|
||||
onClick={() => this.selectOption(urlParam, option.key)}
|
||||
>
|
||||
<span className={menuItemBorderClass} />
|
||||
{option.label}
|
||||
</button>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
<MenuItem key={'clearLink'}>
|
||||
<button className={css.clearMenuItem} onClick={() => this.selectOption(urlParam, null)}>
|
||||
<FormattedMessage id={'SelectSingleFilter.popupClear'} />
|
||||
</button>
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSingleFilterPopup.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
initialValue: null,
|
||||
contentPlacementOffset: 0,
|
||||
};
|
||||
|
||||
SelectSingleFilterPopup.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
urlParam: string.isRequired,
|
||||
label: string.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
options: arrayOf(
|
||||
shape({
|
||||
key: string.isRequired,
|
||||
label: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
initialValue: string,
|
||||
contentPlacementOffset: number,
|
||||
};
|
||||
|
||||
export default SelectSingleFilterPopup;
|
||||
|
|
@ -691,8 +691,8 @@
|
|||
"SelectMultipleFilter.labelSelected": "{labelText} • {count}",
|
||||
"SelectMultipleFilterPlainForm.clear": "Clear",
|
||||
"SelectMultipleFilterPlainForm.labelSelected": "{labelText} • {count}",
|
||||
"SelectSingleFilter.clear": "Clear",
|
||||
"SelectSingleFilterPlain.clear": "Clear",
|
||||
"SelectSingleFilter.popupClear": "Clear",
|
||||
"SelectSingleFilter.plainClear": "Clear",
|
||||
"SendMessageForm.sendFailed": "Failed to send. Please try again.",
|
||||
"SendMessageForm.sendMessage": "Send message",
|
||||
"SignupForm.emailInvalid": "A valid email address is required",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue