Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ const config: StorybookConfig = {
};
}

return config;
return {
...config,
plugins: config.plugins?.filter(plugin => {
if (plugin.constructor.name === 'ESLintWebpackPlugin') {
return false
}
return true
}),
};
}
};
export default config;
2 changes: 1 addition & 1 deletion .vscode/components.code-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"interface ${1:ComponentName}Props {",
"}",
"",
"const ${1:ComponentName}: FC<${1:ComponentName}Props> = (props: ${1:ComponentName}Props) => {",
"const ${1:ComponentName}: FC<${1:ComponentName}Props> = props => {",
"",
" return (",
" <div className={styles['wrap']}>",
Expand Down
2 changes: 1 addition & 1 deletion src/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ module.exports = {
parameter: true,
memberVariableDeclaration: true,
callSignature: true,
variableDeclaration: true,
variableDeclaration: false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss this...

arrayDestructuring: false,
objectDestructuring: true,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ChangeEvent, FC } from 'react'
import { noop } from 'lodash'

import { InputMultiselect } from '~/libs/ui'

import { autoCompleteSkills } from '../../services/emsi-skills'

interface Option {
label: string
value: string
}

const fetchSkills = (queryTerm: string): Promise<Option[]> => (
autoCompleteSkills(queryTerm)
.then(skills => (
skills.map(skill => ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we here filter out the already selected skills so they do not appear in the results?

label: skill.name,
value: skill.emsiId,
}))
))
)

interface InputSkillSelectorProps {
readonly onChange?: (event: ChangeEvent<HTMLInputElement>) => void
}

const InputSkillSelector: FC<InputSkillSelectorProps> = props => (
<InputMultiselect
label='Select Skills'
placeholder='Type to add a skill...'
onFetchOptions={fetchSkills}
name='skills'
onChange={props.onChange ?? noop}
/>
)

export default InputSkillSelector
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as InputSkillSelector } from './InputSkillSelector'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { EnvironmentConfig } from '~/config'
import { xhrGetAsync } from '~/libs/core'

import Skill from './skill.model'

export async function autoCompleteSkills(queryTerm: string): Promise<Skill[]> {
return xhrGetAsync(`${EnvironmentConfig.API.V5}/emsi-skills/skills/auto-complete?term=${queryTerm}`)
}
2 changes: 2 additions & 0 deletions src/libs/shared/lib/services/emsi-skills/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './skill.model'
export * from './emsi-skills.service'
4 changes: 4 additions & 0 deletions src/libs/shared/lib/services/emsi-skills/skill.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default interface Skill {
name: string;
emsiId: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './input-image-picker'
export * from './form-input-autcomplete-option.enum'
export * from './input-rating'
export * from './input-select'
export * from './input-multiselect'
export * from './input-text'
export * from './input-textarea'
export { inputOptional, InputWrapper } from './input-wrapper'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
@import '../../../../../styles/includes';

.multiselect .ms {
display: block;

&:global(__value-container) {
display: flex;
align-items: center;
flex: 1;
flex-wrap: wrap;
position: relative;
overflow: hidden;
margin: 0 10px;
padding: 0;
gap: 8px;
}

&:global(__indicators) {
display: none;
}

&:global(__placeholder) {
position: absolute;
font-size: 14px;
line-height: 16px;
color: $black-60;
}

&:global(__control) {
border: 0 none;
box-shadow: none;

align-items: center;
cursor: default;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
min-height: 0;
outline: 0!important;
position: relative;
transition: all 100ms;
background: none;
border-radius: 4px;
}

&:global(__input-container) {
font-size: 14px;
line-height: 16px;
color: $black-60;
display: inline-grid;
flex: 1 1 auto;
margin: 0;
grid-template-columns: 0 min-content;
padding: 0;
visibility: visible;
}

&:global(__multi-value) {
margin: 0;
background: $teal-140;
color: $tc-white;
border-radius: 4px;

&:global(__remove) {
cursor: pointer;
border: 0 none;
background: none;
outline: none;
appearance: none;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
padding: 2px;
margin: 2px 6px 2px 0;

svg {
display: block;
width: 16px;
height: 16px;
}
}
&:global(__label) {
color: $tc-white;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: 4px;
padding: 4px;
padding-left: 8px;
padding-right: 2px;
font-size: 14px;
line-height: 16px;
letter-spacing: 0.5px;
font-family: $font-roboto;
font-weight: $font-weight-medium;
}
}

&:global(__menu) {
top: 100%;
position: absolute;
width: 100%;
z-index: 1;
background-color: $tc-white;
border-radius: 4px;
box-shadow: 0px 4px 4px 0px rgba(0,0,0,0.25);
margin-bottom: 5px;
margin-top: 5px;
border: 1px solid $black-40;
&:global(-list) {
max-height: 300px;
overflow-y: auto;
position: relative;
-webkit-overflow-scrolling: touch;
padding: 8px 0;
}
&:global(-notice) {
text-align: center;
color: #999;
padding: 8px 12px;
}
}
&:global(__option) {
cursor: default;
display: block;
width: 100%;
user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
background-color: transparent;
color: $black-100;
padding: 8px 16px;

font-size: 16px;
line-height: 24px;

&:global(--is-focused) {
background-color: $turq-160;
color: $tc-white;
}
}
}

.multiselect {
margin: 8px -10px 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-disable no-underscore-dangle */
/* eslint-disable camelcase */
import { Meta, StoryObj } from '@storybook/react'

import { InputMultiselect } from '.'

const meta: Meta<typeof InputMultiselect> = {
argTypes: {
},
component: InputMultiselect,
excludeStories: /.*Decorator$/,
// tags: ['autodocs'],
title: 'Forms/InputMultiselect',
}

export default meta

type Story = StoryObj<typeof InputMultiselect>;

export const Basic: Story = {
args: {
onChange: d => console.log(d),
onFetchOptions: d => Promise.resolve(d ? [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
{ label: 'Option 3', value: '3' },
{ label: 'Option 4', value: '4' },
{ label: 'Option 5', value: '5' },
{ label: 'Option 6', value: '6' },
{ label: 'Option 7', value: '7' },
{ label: 'Option 8', value: '8' },
{ label: 'Option 9', value: '9' },
{ label: 'Option 10', value: '10' },
{ label: 'Option 11', value: '11' },
{ label: 'Option 12', value: '12' },
{ label: 'Option 13', value: '13' },
{ label: 'Option 14', value: '14' },
{ label: 'Option 15', value: '15' },
{ label: 'Option 16', value: '16' },
{ label: 'Option 17', value: '17' },
{ label: 'Option 18', value: '18' },
{ label: 'Option 19', value: '19' },
] : []),
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
ChangeEvent,
FC,
ReactNode,
} from 'react'
import { noop } from 'lodash'
import { components } from 'react-select'
import AsyncSelect from 'react-select/async'

import { InputWrapper } from '../input-wrapper'
import { IconSolid } from '../../../../svgs'

import styles from './InputMultiselect.module.scss'

export interface InputMultiselectOption {
label?: ReactNode
value: string
}

interface InputMultiselectProps {
readonly dirty?: boolean
readonly disabled?: boolean
readonly error?: string
readonly hideInlineErrors?: boolean
readonly hint?: string
readonly label?: string
readonly name: string
readonly onChange: (event: ChangeEvent<HTMLInputElement>) => void
readonly options?: ReadonlyArray<InputMultiselectOption>
readonly placeholder?: string
readonly tabIndex?: number
readonly value?: string
readonly onFetchOptions?: (query: string) => Promise<InputMultiselectOption[]>
}

const MultiValueRemove: FC = (props: any) => (
<components.MultiValueRemove {...props}>
<IconSolid.XCircleIcon />
</components.MultiValueRemove>
)

const InputMultiselect: FC<InputMultiselectProps> = (props: InputMultiselectProps) => {

function handleOnChange(options: readonly InputMultiselectOption[]): void {
props.onChange({
target: { value: options },
} as unknown as ChangeEvent<HTMLInputElement>)
}

return (
<InputWrapper
{...props}
dirty={!!props.dirty}
disabled={!!props.disabled}
label={(props.label || props.name) ?? 'Select Option'}
hideInlineErrors={props.hideInlineErrors}
type='text'
>
<AsyncSelect
className={styles.multiselect}
classNamePrefix={styles.ms}
unstyled
isMulti
cacheOptions
autoFocus
defaultOptions
placeholder={props.placeholder}
loadOptions={props.onFetchOptions}
name={props.name}
onChange={handleOnChange}
onBlur={noop}
blurInputOnSelect={false}
components={{
// MultiValueLabel: () =>
MultiValueRemove,
}}
/>
</InputWrapper>
)
}

export default InputMultiselect
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as InputMultiselect } from './InputMultiselect'
export { type InputMultiselectOption } from './InputMultiselect'