From 5a08a7065e73d0e2170c64846651ba5c66557e7a Mon Sep 17 00:00:00 2001 From: Vasilica Date: Mon, 17 Jul 2023 18:56:10 +0300 Subject: [PATCH] TAL-26 - update talent search input: reuse skill search from shared lib --- .../talent-search/TalentSearch.module.scss | 4 +- .../src/routes/talent-search/TalentSearch.tsx | 222 +++++------------- src/libs/shared/lib/components/index.ts | 1 + .../InputSkillSelector.tsx | 18 +- src/libs/shared/lib/index.ts | 1 + src/libs/shared/lib/services/index.ts | 1 + .../InputMultiselect.module.scss | 72 +++++- .../input-multiselect/InputMultiselect.tsx | 74 ++++-- .../form-input/input-multiselect/index.ts | 2 +- 9 files changed, 191 insertions(+), 204 deletions(-) create mode 100644 src/libs/shared/lib/services/index.ts diff --git a/src/apps/talent-search/src/routes/talent-search/TalentSearch.module.scss b/src/apps/talent-search/src/routes/talent-search/TalentSearch.module.scss index f31c707a3..10e437eca 100644 --- a/src/apps/talent-search/src/routes/talent-search/TalentSearch.module.scss +++ b/src/apps/talent-search/src/routes/talent-search/TalentSearch.module.scss @@ -6,7 +6,7 @@ background-image: url("../../assets/background.png"); background-size: cover; background-repeat: no-repeat; - + .contentLayout-outer { width: 100%; @@ -92,5 +92,5 @@ height: 24px; color: $black-40; stroke: $black-40; - margin-right: 24px; + margin-right: 10px; } diff --git a/src/apps/talent-search/src/routes/talent-search/TalentSearch.tsx b/src/apps/talent-search/src/routes/talent-search/TalentSearch.tsx index bc4888e30..7dadbdcb6 100644 --- a/src/apps/talent-search/src/routes/talent-search/TalentSearch.tsx +++ b/src/apps/talent-search/src/routes/talent-search/TalentSearch.tsx @@ -1,46 +1,40 @@ -/* eslint-disable react/jsx-no-bind */ -import { - CSSProperties, - FC, - useState, -} from 'react' -import { components, ControlProps, GroupBase, - MultiValue, Options, SingleValue, StylesConfig } from 'react-select' -import AsyncSelect from 'react-select/async' -import PropTypes from 'prop-types' +import { FC, useMemo, useState } from 'react' -import { SearchIcon } from '@heroicons/react/outline' -import { ContentLayout } from '~/libs/ui' +import { ContentLayout, IconOutline, InputMultiselectOption } from '~/libs/ui' +import { EmsiSkill, EmsiSkillSources, InputSkillSelector } from '~/libs/shared' import { Skill } from '@talentSearch/lib/models/' -import MatcherService from '@talentSearch/lib/services/MatcherService' import SkillPill from './components/SkillPill' import styles from './TalentSearch.module.scss' -function search(skills:Options): void { - alert(JSON.stringify(skills)) -} - -// eslint-disable-next-line react/destructuring-assignment, @typescript-eslint/typedef -const Control: React.FC>> = ({ children, ...props }) => ( - - {children} - search(props.getValue())} - className={styles.searchIconSpan} - > - - - -) - -Control.propTypes = { - children: PropTypes.node.isRequired, - getValue: PropTypes.func.isRequired, -} +// TODO: Make this configurable, or read from a service. We need to discuss +// how we want to handle this. +const popularSkills:Skill[][] = [ + [{ emsiId: 'KS441LF7187KS0CV4B6Y', name: 'Typescript' }, + { emsiId: 'KS1244K6176NLVWV02B6', name: 'Front-End Engineering' }, + { emsiId: 'KS1214R5XG4X4PY7LGY6', name: 'Bootstrap (Front-End Framework)' }], + [{ emsiId: 'KS121F45VPV8C9W3QFYH', name: 'Cascading Style Sheets (CSS)' }, + { emsiId: 'KS1200771D9CR9LB4MWW', name: 'JavaScript (Programming Language)' }], + [{ emsiId: 'KS1200578T5QCYT0Z98G', name: 'HyperText Markup Language (HTML)' }, + { emsiId: 'ES86A20379CD2AD061F3', name: 'IOS Development' }, + { emsiId: 'KS127296VDYS7ZFWVC46', name: 'Node.js' }], + [{ emsiId: 'ES50D03AC9CFC1A0BC93', name: '.NET Development' }, + { emsiId: 'KS1219W70LY1GXZDSKW5', name: 'C++ (Programming Language)' }, + { emsiId: 'KS127SZ60YZR8B5CQKV1', name: 'PHP Development' }], + [{ emsiId: 'KS1206V6K46N1SDVJGBD', name: 'Adobe Illustrator' }, + { emsiId: 'ESD07FEE22E7EC094EB8', name: 'Ruby (Programming Language)' }, + { emsiId: 'KS120076FGP5WGWYMP0F', name: 'Java (Programming Language)' }], + [{ emsiId: 'KSPSGF5MXB6568UIQ4BK', name: 'React Native' }, + { emsiId: 'KS441PL6JPXW200W0GRQ', name: 'User Experience (UX)' }], +] export const TalentSearch: FC = () => { const [skillsFilter, setSkillsFilter] = useState>([]) + const emsiSkills: EmsiSkill[] = useMemo(() => skillsFilter.map(s => ({ + name: s.name, + skillId: s.emsiId, + skillSources: [EmsiSkillSources.selfPicked], + })), [skillsFilter]) function toggleSkill(skill:Skill): void { let newFilter: Array = [] @@ -63,12 +57,12 @@ export const TalentSearch: FC = () => { } } - function onChange(options:MultiValue | SingleValue): void { - if (Array.isArray(options)) { - setSkillsFilter(options) - } else { - setSkillsFilter([]) - } + function onChange(ev: any): void { + const options = (ev.target.value as unknown) as InputMultiselectOption[] + setSkillsFilter(options.map(v => ({ + emsiId: v.value, + name: v.label as string, + }))) } function filteringSkill(skill:Skill): boolean { @@ -82,105 +76,6 @@ export const TalentSearch: FC = () => { return result } - // TODO: Make this configurable, or read from a service. We need to discuss - // how we want to handle this. - const popularSkills:Skill[][] = [ - [{ emsiId: 'KS441LF7187KS0CV4B6Y', name: 'Typescript' }, - { emsiId: 'KS1244K6176NLVWV02B6', name: 'Front-End Engineering' }, - { emsiId: 'KS1214R5XG4X4PY7LGY6', name: 'Bootstrap (Front-End Framework)' }], - [{ emsiId: 'KS121F45VPV8C9W3QFYH', name: 'Cascading Style Sheets (CSS)' }, - { emsiId: 'KS1200771D9CR9LB4MWW', name: 'JavaScript (Programming Language)' }], - [{ emsiId: 'KS1200578T5QCYT0Z98G', name: 'HyperText Markup Language (HTML)' }, - { emsiId: 'ES86A20379CD2AD061F3', name: 'IOS Development' }, - { emsiId: 'KS127296VDYS7ZFWVC46', name: 'Node.js' }], - [{ emsiId: 'ES50D03AC9CFC1A0BC93', name: '.NET Development' }, - { emsiId: 'KS1219W70LY1GXZDSKW5', name: 'C++ (Programming Language)' }, - { emsiId: 'KS127SZ60YZR8B5CQKV1', name: 'PHP Development' }], - [{ emsiId: 'KS1206V6K46N1SDVJGBD', name: 'Adobe Illustrator' }, - { emsiId: 'ESD07FEE22E7EC094EB8', name: 'Ruby (Programming Language)' }, - { emsiId: 'KS120076FGP5WGWYMP0F', name: 'Java (Programming Language)' }], - [{ emsiId: 'KSPSGF5MXB6568UIQ4BK', name: 'React Native' }, - { emsiId: 'KS441PL6JPXW200W0GRQ', name: 'User Experience (UX)' }], - ] - - const controlStyle: CSSProperties = { - borderColor: 'black', - paddingBottom: '10px', - paddingTop: '10px', - } - - const placeholderStyle: CSSProperties = { - color: '#2A2A2A', - fontFamily: 'Roboto', - fontSize: '16', - fontWeight: 400, - height: '36px', - paddingTop: '4px', - } - - const multiValueStyle: CSSProperties = { - backgroundColor: 'white', - border: '1px solid #d4d4d4', - borderRadius: '24px', - color: '#333', - fontFamily: 'Roboto', - fontSize: '14', - fontWeight: '400', - height: '32px', - marginRight: '10px', - paddingLeft: '8px', - paddingRight: '8px', - } - - const multiValueRemoveStyle: CSSProperties = { - backgroundColor: '#d9d9d9', - border: '1px solid #d4d4d4', - borderRadius: '11px', - color: '#333', - fontSize: '12', - height: '12px', - marginBottom: 'auto', - marginLeft: '5px', - marginRight: '5px', - marginTop: 'auto', - padding: '0px', - width: '12px', - } - - const hiddenStyle: CSSProperties = { - display: 'none', - } - - const selectStyle: StylesConfig = { - clearIndicator: provided => ({ - ...provided, - ...hiddenStyle, - }), - control: provided => ({ - ...provided, - ...controlStyle, - }), - dropdownIndicator: provided => ({ - ...provided, - ...hiddenStyle, - }), - indicatorSeparator: provided => ({ - ...provided, - ...hiddenStyle, - }), - multiValue: provided => ({ - ...provided, - ...multiValueStyle, - }), - multiValueRemove: provided => ({ - ...provided, - ...multiValueRemoveStyle, - }), - placeholder: provided => ({ - ...provided, - ...placeholderStyle, - }), - } return ( {
Search by skills - skill.name} - getOptionValue={(skill: Skill) => skill.emsiId} - components={{ Control }} - openMenuOnClick={false} - value={skillsFilter} - onChange={( - newValue: MultiValue | SingleValue, - ) => onChange(newValue)} + useWrapper={false} + theme='clear' + dropdownIcon={} + value={emsiSkills} + onChange={onChange} />
Popular Skills - {popularSkills.map( - row => ( -
- {row.map(skill => ( - - ))} -
- ), - )} + {popularSkills.map((row, i) => ( + // eslint-disable-next-line react/no-array-index-key +
+ {row.map(skill => ( + + ))} +
+ ))}
) diff --git a/src/libs/shared/lib/components/index.ts b/src/libs/shared/lib/components/index.ts index e3a4f8034..9299758bc 100644 --- a/src/libs/shared/lib/components/index.ts +++ b/src/libs/shared/lib/components/index.ts @@ -1,3 +1,4 @@ export * from './contact-support-form' export * from './modals' +export * from './input-skill-selector' export * from './member-skill-editor' diff --git a/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx b/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx index 5109749ab..e90789408 100644 --- a/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx +++ b/src/libs/shared/lib/components/input-skill-selector/InputSkillSelector.tsx @@ -1,7 +1,7 @@ -import { ChangeEvent, FC } from 'react' +import { ChangeEvent, FC, ReactNode } from 'react' import { noop } from 'lodash' -import { InputMultiselect, InputMultiselectOption } from '~/libs/ui' +import { InputMultiselect, InputMultiselectOption, InputMultiselectThemes } from '~/libs/ui' import { autoCompleteSkills, EmsiSkill, EmsiSkillSources } from '../../services/emsi-skills' @@ -29,21 +29,29 @@ const fetchSkills = (queryTerm: string): Promise => ( interface InputSkillSelectorProps { readonly limit?: number + readonly label?: string readonly loading?: boolean - readonly value?: EmsiSkill[] readonly onChange?: (event: ChangeEvent) => void + readonly placeholder?: string + readonly value?: EmsiSkill[] + readonly theme?: InputMultiselectThemes + readonly useWrapper?: boolean + readonly dropdownIcon?: ReactNode } const InputSkillSelector: FC = props => ( ) diff --git a/src/libs/shared/lib/index.ts b/src/libs/shared/lib/index.ts index c57239ed8..58ba4d800 100644 --- a/src/libs/shared/lib/index.ts +++ b/src/libs/shared/lib/index.ts @@ -1,4 +1,5 @@ export * from './components' export * from './containers' export * from './hooks' +export * from './services' export * from './utils' diff --git a/src/libs/shared/lib/services/index.ts b/src/libs/shared/lib/services/index.ts new file mode 100644 index 000000000..ef94db4f5 --- /dev/null +++ b/src/libs/shared/lib/services/index.ts @@ -0,0 +1 @@ +export * from './emsi-skills' diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.module.scss b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.module.scss index d1b667f2c..ba0c8249d 100644 --- a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.module.scss +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.module.scss @@ -1,5 +1,29 @@ @import '../../../../../styles/includes'; +.multiselect { + margin: 8px -10px 0; +} + +.multiSelectWrap { + margin: 0; + background: $tc-white; + padding: $sp-4 $sp-3; + + border-radius: $sp-1; + &.multiselect .ms { + &:global(__placeholder) { + line-height: $sp-6; + color: $black-100; + } + &:global(__control) { + min-height: $sp-8; + } + &:global(__indicators) { + display: flex; + } + } +} + .multiselect .ms { display: block; @@ -99,6 +123,7 @@ &:global(__menu) { top: 100%; + left: 0; position: absolute; width: 100%; z-index: 1; @@ -141,6 +166,49 @@ } } -.multiselect { - margin: 8px -10px 0; +.theme-clear.multiselect .ms { + &:global(__placeholder) { + color: $black-100; + } + &:global(__input-container) { + color: $black-100; + } + + &:global(__clear-indicator) { + display: none; + } + + &:global(__multi-value) { + background: $tc-white; + color: $black-20; + border-radius: $sp-6; + border: 1px solid $black-20; + + + &:global(__remove) { + svg { + display: block; + width: 16px; + height: 16px; + color: $black-40; + transition: 0.25s ease; + } + &:hover svg { + color: $black-100; + } + } + &:global(__label) { + color: $black-80; + padding: 6px $sp-3; + padding-right: 2px; + font-weight: $font-weight-normal; + } + } + + &:global(__option) { + &:global(--is-focused) { + background-color: $teal-50; + color: $black-100; + } + } } diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx index 883134f11..4b1ba1cb2 100644 --- a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/InputMultiselect.tsx @@ -6,6 +6,7 @@ import { import { noop } from 'lodash' import { components } from 'react-select' import AsyncSelect from 'react-select/async' +import classNames from 'classnames' import { InputWrapper } from '../input-wrapper' import { IconSolid } from '../../../../svgs' @@ -18,22 +19,27 @@ export interface InputMultiselectOption { verified?: boolean } -interface InputMultiselectProps { +export type InputMultiselectThemes = 'tc-green' | 'clear' + +export interface InputMultiselectProps { readonly dirty?: boolean - readonly loading?: boolean readonly disabled?: boolean + readonly dropdownIcon?: ReactNode readonly error?: string readonly hideInlineErrors?: boolean readonly hint?: string readonly label?: string readonly limit?: number + readonly loading?: boolean readonly name: string readonly onChange: (event: ChangeEvent) => void + readonly onFetchOptions?: (query: string) => Promise readonly options?: ReadonlyArray readonly placeholder?: string readonly tabIndex?: number + readonly theme?: InputMultiselectThemes + readonly useWrapper?: boolean readonly value?: InputMultiselectOption[] - readonly onFetchOptions?: (query: string) => Promise } const MultiValueRemove: FC = (props: any) => ( @@ -48,6 +54,13 @@ const MultiValueRemove: FC = (props: any) => ( ) +// eslint-disable-next-line react/function-component-definition +const dropdownIndicator = (dropdownIcon: ReactNode): FC => (props: any) => ( + + {dropdownIcon} + +) + const InputMultiselect: FC = (props: InputMultiselectProps) => { function handleOnChange(options: readonly InputMultiselectOption[]): void { @@ -60,7 +73,37 @@ const InputMultiselect: FC = (props: InputMultiselectProp return !!props.limit && (props.value?.length as number) >= props.limit } - return ( + const selectInputElement = ( + + ) + + return (props.useWrapper || props.useWrapper === undefined) ? ( = (props: InputMultiselectProp type='text' hint={props.limit ? ` (max ${props.limit})` : undefined} > - + {selectInputElement} - ) + ) : selectInputElement } export default InputMultiselect diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/index.ts b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/index.ts index 38d4054f9..bdb8b7a32 100644 --- a/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/index.ts +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-multiselect/index.ts @@ -1,2 +1,2 @@ export { default as InputMultiselect } from './InputMultiselect' -export { type InputMultiselectOption } from './InputMultiselect' +export type { InputMultiselectOption, InputMultiselectProps, InputMultiselectThemes } from './InputMultiselect'