diff --git a/src/components/IntegerField/index.jsx b/src/components/IntegerField/index.jsx
index 0af814f..e327ccb 100644
--- a/src/components/IntegerField/index.jsx
+++ b/src/components/IntegerField/index.jsx
@@ -1,6 +1,8 @@
-import React from "react";
+import React, { useMemo } from "react";
import PT from "prop-types";
import cn from "classnames";
+import IconExclamationMark from "components/Icons/ExclamationMarkCircled";
+import Popover from "components/Popover";
import styles from "./styles.module.scss";
/**
@@ -9,58 +11,119 @@ import styles from "./styles.module.scss";
* @param {Object} props component properties
* @param {string} [props.className] class name to be added to root element
* @param {boolean} [props.isDisabled] if the field is disabled
+ * @param {boolean} [props.readOnly] if the field is readOnly
+ * @param {boolean} [props.displayButtons] whether to display +/- buttons
* @param {string} props.name field's name
* @param {number} props.value field's value
* @param {number} [props.maxValue] maximum allowed value
* @param {number} [props.minValue] minimum allowed value
- * @param {(v: number) => void} props.onChange
+ * @param {(v: number) => void} [props.onChange]
+ * @param {(v: string) => void} [props.onInputChange]
* @returns {JSX.Element}
*/
const IntegerField = ({
className,
isDisabled = false,
+ readOnly = true,
+ displayButtons = true,
name,
+ onInputChange,
onChange,
value,
maxValue = Infinity,
minValue = -Infinity,
-}) => (
-
-
-
-);
+}) => {
+ const isInvalid = useMemo(
+ () =>
+ !!value &&
+ (isNaN(value) ||
+ !Number.isInteger(+value) ||
+ +value > maxValue ||
+ +value < minValue),
+ [value, minValue, maxValue]
+ );
+
+ const errorPopupContent = useMemo(() => {
+ if (value && (isNaN(value) || !Number.isInteger(+value))) {
+ return <>You must enter a valid integer.>;
+ }
+ if (+value > maxValue) {
+ return (
+ <>
+ You must enter an integer less than or equal to{" "}
+ {maxValue}.
+ >
+ );
+ }
+ if (+value < minValue) {
+ return (
+ <>
+ You must enter an integer greater than or equal to{" "}
+ {minValue}.
+ >
+ );
+ }
+ }, [value, minValue, maxValue]);
+
+ return (
+
+ {isInvalid && (
+
+
+
+ )}
+
onInputChange && onInputChange(event.target.value)}
+ disabled={isDisabled}
+ readOnly={readOnly}
+ className={cn(styles.input, {
+ error: isInvalid,
+ })}
+ name={name}
+ value={value}
+ />
+ {displayButtons && (
+ <>
+
{
+ event.stopPropagation();
+ if (!isDisabled) {
+ onChange(Math.max(value - 1, minValue));
+ }
+ }}
+ />
+ {
+ event.stopPropagation();
+ if (!isDisabled) {
+ onChange(Math.min(+value + 1, maxValue));
+ }
+ }}
+ />
+ >
+ )}
+
+ );
+};
IntegerField.propTypes = {
className: PT.string,
isDisabled: PT.bool,
+ readOnly: PT.bool,
+ displayButtons: PT.bool,
name: PT.string.isRequired,
maxValue: PT.number,
minValue: PT.number,
- onChange: PT.func.isRequired,
+ onChange: PT.func,
+ onInputChange: PT.func,
value: PT.number.isRequired,
};
diff --git a/src/components/IntegerField/styles.module.scss b/src/components/IntegerField/styles.module.scss
index 32cd095..0663b6f 100644
--- a/src/components/IntegerField/styles.module.scss
+++ b/src/components/IntegerField/styles.module.scss
@@ -19,6 +19,11 @@ input.input {
outline: none !important;
box-shadow: none !important;
text-align: center;
+ appearance: textfield;
+ &::-webkit-outer-spin-button, &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
&:disabled {
border-color: $control-disabled-border-color;
@@ -94,3 +99,17 @@ input.input {
height: 9px;
}
}
+
+.popup {
+ margin-right: 5px;
+ max-width: 400px;
+ max-height: 200px;
+ line-height: $line-height-px;
+ white-space: normal;
+}
+
+.icon {
+ padding-top: 1px;
+ width: 15px;
+ height: 15px;
+}
diff --git a/src/components/Typeahead/index.jsx b/src/components/Typeahead/index.jsx
index 9d71979..80c243c 100644
--- a/src/components/Typeahead/index.jsx
+++ b/src/components/Typeahead/index.jsx
@@ -74,6 +74,8 @@ const selectComponents = {
* @param {function} props.onChange function called when value changes
* @param {function} [props.onInputChange] function called when input value changes
* @param {function} [props.onBlur] function called on input blur
+ * @param {number} [props.minLengthForSuggestions] the minimum string lenth for displaying suggestions (default 3)
+ * @param {Boolean} [props.enforceListOnlySelection] enforces user to select from the list - manual inputs (if not in the list) won't affect the selection
* @param {string} props.value input value
* @param {function} props.getSuggestions the function to get suggestions
* @param {string} props.targetProp the target property of the returned object from getSuggestions
@@ -87,6 +89,8 @@ const Typeahead = ({
onChange,
onInputChange,
onBlur,
+ minLengthForSuggestions = 3,
+ enforceListOnlySelection = false,
placeholder,
value,
getSuggestions,
@@ -139,7 +143,12 @@ const Typeahead = ({
setIsMenuFocused(false);
setIsMenuOpen(false);
setIsLoading(false);
- onChange(inputValue);
+ // fire onChange event
+ // - if `enforceListOnlySelection` is not set,
+ // - or if it's set and options list contains the value
+ if (!enforceListOnlySelection || options.includes(inputValue)) {
+ onChange(inputValue);
+ }
}
} else if (key === "ArrowDown") {
if (!isMenuFocused) {
@@ -158,7 +167,12 @@ const Typeahead = ({
const onSelectBlur = () => {
setIsMenuFocused(false);
setIsMenuOpen(false);
- onChange(inputValue);
+ // fire onChange event
+ // - if `enforceListOnlySelection` is not set,
+ // - or if it's set and options list contains the value
+ if (!enforceListOnlySelection || options.includes(inputValue)) {
+ onChange(inputValue);
+ }
onBlur && onBlur();
};
@@ -170,11 +184,10 @@ const Typeahead = ({
}
setIsLoading(true);
setIsMenuOpen(true);
- const options = await loadSuggestions(
- getSuggestions,
- value,
- targetProp
- );
+ const options =
+ value.length < minLengthForSuggestions
+ ? [] // no suggestions yet if value length is less than `minLengthForSuggestions`
+ : await loadSuggestions(getSuggestions, value, targetProp);
if (!isChangeAppliedRef.current) {
setOptions(options);
setIsLoading(false);
@@ -233,9 +246,6 @@ const Typeahead = ({
const loadSuggestions = async (getSuggestions, inputValue, targetProp) => {
let options = [];
- if (inputValue.length < 3) {
- return options;
- }
try {
const res = await getSuggestions(inputValue);
const items = res.data.slice(0, 100);
@@ -266,6 +276,8 @@ Typeahead.propTypes = {
onChange: PT.func.isRequired,
onInputChange: PT.func,
onBlur: PT.func,
+ minLengthForSuggestions: PT.number,
+ enforceListOnlySelection: PT.bool,
placeholder: PT.string,
value: PT.oneOfType([PT.number, PT.string]),
getSuggestions: PT.func,
diff --git a/src/routes/Roles/components/RoleForm/index.jsx b/src/routes/Roles/components/RoleForm/index.jsx
index 70605f0..b4375b1 100644
--- a/src/routes/Roles/components/RoleForm/index.jsx
+++ b/src/routes/Roles/components/RoleForm/index.jsx
@@ -8,7 +8,10 @@ import cn from "classnames";
import { getRolesModalRole, getRolesModalError } from "store/selectors/roles";
import { setModalRole, setModalError } from "store/actions/roles";
import { searchSkills } from "services/roles";
+import { humanReadableTimeToHours } from "utils/misc";
import FallbackIcon from "../../../../assets/images/icon-role-fallback.svg";
+import IconExclamationMark from "components/Icons/ExclamationMarkCircled";
+import Popover from "components/Popover";
import Typeahead from "components/Typeahead";
import IntegerField from "components/IntegerField";
import IconArrowSmall from "components/Icons/ArrowSmall";
@@ -69,18 +72,18 @@ function RoleForm() {
rates: [
...roleState.rates,
{
- global: 0,
- inCountry: 0,
- offShore: 0,
- niche: 0,
- rate30Niche: 0,
- rate30Global: 0,
- rate30InCountry: 0,
- rate30OffShore: 0,
- rate20Niche: 0,
- rate20Global: 0,
- rate20InCountry: 0,
- rate20OffShore: 0,
+ global: 10,
+ inCountry: 10,
+ offShore: 10,
+ niche: 10,
+ rate30Niche: 10,
+ rate30Global: 10,
+ rate30InCountry: 10,
+ rate30OffShore: 10,
+ rate20Niche: 10,
+ rate20Global: 10,
+ rate20InCountry: 10,
+ rate20OffShore: 10,
},
],
})
@@ -89,14 +92,6 @@ function RoleForm() {
const editRate = useCallback(
(index, changes) => {
- // a num field is `null` but user trying to increase/decrease it
- // start with 0
- for (const key in changes) {
- if (isNaN(changes[key])) {
- changes[key] = 0;
- }
- }
- // apply changes
dispatch(
setModalRole({
...roleState,
@@ -194,8 +189,10 @@ function RoleForm() {
id="skills"
name="skills"
placeholder="Search skills..."
+ enforceListOnlySelection={true}
onChange={(val) => addSkill(val)}
onInputChange={(val) => setTypeaheadInputValue(val)}
+ minLengthForSuggestions={1} // retrieve suggestions with min. 1 characters. Useful for skills like "C"
value={typeaheadInputValue}
getSuggestions={searchSkills}
targetProp="name"
@@ -223,8 +220,11 @@ function RoleForm() {
onChange({ numberOfMembers: num })}
+ onInputChange={(val) => onChange({ numberOfMembers: val })}
/>
@@ -232,33 +232,96 @@ function RoleForm() {
onChange({ numberOfMembersAvailable: num })}
+ onInputChange={(val) => onChange({ numberOfMembersAvailable: val })}
/>
-
Time to Candidate |
-
onChange({ timeToCandidate: num })}
- />
+ Time to Interview |
+
+ {!!roleState.timeToInterview &&
+ humanReadableTimeToHours(roleState.timeToInterview) === 0 && (
+
+ Example values:
+
+ - 12 hours
+ - 3 days
+ - 2 weeks
+ - 2 weeks 3 days
+ - 1 month
+ - ...
+
+ >
+ }
+ strategy="fixed"
+ >
+
+
+ )}
+
+ onChange({
+ timeToInterview: event.target.value,
+ })
+ }
+ />
+
-
Time to Interview |
-
onChange({ timeToInterview: num })}
- />
+ Time to Candidate |
+
+ {!!roleState.timeToCandidate &&
+ humanReadableTimeToHours(roleState.timeToCandidate) === 0 && (
+
+ Example values:
+
+ - 12 hours
+ - 3 days
+ - 2 weeks
+ - 2 weeks 3 days
+ - 1 month
+ - ...
+
+ >
+ }
+ strategy="fixed"
+ >
+
+
+ )}
+
+ onChange({
+ timeToCandidate: event.target.value,
+ })
+ }
+ />
+