Skip to content

Commit e9dc2f7

Browse files
committed
TSJR-314 - Add bulk replace skills
1 parent 4a32e9d commit e9dc2f7

File tree

16 files changed

+368
-41
lines changed

16 files changed

+368
-41
lines changed

src/apps/admin/src/skills-manager/components/bulk-editor/BulkEditor.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { SkillsManagerContextValue, useSkillsManagerContext } from '../../contex
77

88
import { ArchiveSkillsModal } from './archive-skills-modal'
99
import { MoveSkillsModal } from './move-skills-modal'
10+
import { ReplaceSkillsModal } from './replace-skills-modal'
1011
import styles from './BulkEditor.module.scss'
1112

1213
interface BulkEditorProps {
@@ -17,9 +18,11 @@ const BulkEditor: FC<BulkEditorProps> = props => {
1718
const {
1819
bulkEditorCtx: context,
1920
refetchSkills,
21+
skillsList,
2022
}: SkillsManagerContextValue = useSkillsManagerContext()
2123

2224
const [showArchive, setShowArchive] = useState(false)
25+
const [showReplaceSkills, setShowReplaceSkills] = useState(false)
2326
const [showMoveSkills, setShowMoveSkills] = useState(false)
2427

2528
function openArchiveModal(): void {
@@ -30,8 +33,12 @@ const BulkEditor: FC<BulkEditorProps> = props => {
3033
setShowMoveSkills(true)
3134
}
3235

36+
function openReplaceSkillsModal(): void {
37+
setShowReplaceSkills(true)
38+
}
39+
3340
function closeArchiveModal(archived?: boolean): void {
34-
if (archived) {
41+
if (archived === true) {
3542
refetchSkills()
3643
context.toggleAll()
3744
}
@@ -40,14 +47,23 @@ const BulkEditor: FC<BulkEditorProps> = props => {
4047
}
4148

4249
function closeMoveSkillsModal(moved?: boolean): void {
43-
if (moved) {
50+
if (moved === true) {
4451
refetchSkills()
4552
context.toggleAll()
4653
}
4754

4855
setShowMoveSkills(false)
4956
}
5057

58+
function closeReplaceSkillsModal(replaced?: boolean): void {
59+
if (replaced === true) {
60+
refetchSkills()
61+
context.toggleAll()
62+
}
63+
64+
setShowReplaceSkills(false)
65+
}
66+
5167
const hasSelection = context.selectedSkills.length > 0
5268

5369
return (
@@ -69,14 +85,13 @@ const BulkEditor: FC<BulkEditorProps> = props => {
6985
/>
7086
<Button
7187
primary
72-
variant='linkblue'
7388
label='Replace selected'
7489
size='lg'
7590
disabled={!hasSelection}
91+
onClick={openReplaceSkillsModal}
7692
/>
7793
<Button
7894
primary
79-
variant='linkblue'
8095
label='Move selected'
8196
size='lg'
8297
disabled={!hasSelection}
@@ -87,6 +102,14 @@ const BulkEditor: FC<BulkEditorProps> = props => {
87102
<ArchiveSkillsModal skills={context.selectedSkills} onClose={closeArchiveModal} />
88103
)}
89104

105+
{showReplaceSkills && (
106+
<ReplaceSkillsModal
107+
allSkills={skillsList}
108+
skills={context.selectedSkills}
109+
onClose={closeReplaceSkillsModal}
110+
/>
111+
)}
112+
90113
{showMoveSkills && (
91114
<MoveSkillsModal skills={context.selectedSkills} onClose={closeMoveSkillsModal} />
92115
)}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@import '@libs/ui/styles/includes';
2+
3+
.modalBody {
4+
.skillsList {
5+
column-count: 2;
6+
margin: -$sp-2;
7+
margin-bottom: $sp-3;
8+
}
9+
}
10+
11+
.formInput {
12+
&Label {
13+
display: flex;
14+
align-items: center;
15+
gap: $sp-6;
16+
margin-bottom: $sp-2;
17+
}
18+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { ChangeEvent, FC, useState } from 'react'
2+
import { find } from 'lodash'
3+
4+
import { BaseModal, Button, InputRadio, LoadingSpinner } from '~/libs/ui'
5+
6+
import { SkillsManagerContextValue, useSkillsManagerContext } from '../../../context'
7+
import { StandardizedSkill } from '../../../services'
8+
import { SkillsList } from '../../skills-list'
9+
import { SearchSkillInput } from '../search-skill-input'
10+
import { SkillForm } from '../../skill-modals'
11+
12+
import styles from './ReplaceSkillsModal.module.scss'
13+
14+
interface ReplaceSkillsModalProps {
15+
allSkills: StandardizedSkill[]
16+
skills: StandardizedSkill[]
17+
onClose: (archived?: boolean) => void
18+
}
19+
20+
const ReplaceSkillsModal: FC<ReplaceSkillsModalProps> = props => {
21+
const [isLoading, setIsLoading] = useState(false)
22+
const [type, setType] = useState<'existing'|'new'>('existing')
23+
const [replacingSkill, setReplacingSkill] = useState<StandardizedSkill>()
24+
25+
const {
26+
bulkEditorCtx: context,
27+
}: SkillsManagerContextValue = useSkillsManagerContext()
28+
29+
async function replaceAll(): Promise<void> {
30+
setIsLoading(true)
31+
// TODO: call api to replace skills
32+
props.onClose(true)
33+
setIsLoading(false)
34+
}
35+
36+
function close(): void {
37+
props.onClose()
38+
}
39+
40+
function handleSkillSelect(event: ChangeEvent<HTMLInputElement>): void {
41+
setReplacingSkill(find(props.allSkills, { id: event.target.value }))
42+
}
43+
44+
function handleNewForm(skillData: Partial<StandardizedSkill>, dataIsValid: boolean):void {
45+
setReplacingSkill(dataIsValid ? skillData as StandardizedSkill : undefined)
46+
}
47+
48+
function toggleType(t: 'existing'|'new'): void {
49+
setReplacingSkill(undefined)
50+
setType(t)
51+
}
52+
53+
return (
54+
<BaseModal
55+
onClose={props.onClose}
56+
open
57+
size='lg'
58+
title='Replace These Skills'
59+
bodyClassName={styles.modalBody}
60+
buttons={(
61+
<>
62+
<Button primary light label='Cancel' onClick={close} size='lg' />
63+
<Button
64+
primary
65+
label='Replace'
66+
onClick={replaceAll}
67+
size='lg'
68+
disabled={!replacingSkill}
69+
/>
70+
</>
71+
)}
72+
>
73+
<SkillsList
74+
className={styles.skillsList}
75+
skills={props.skills}
76+
onSelect={context.toggleSkill}
77+
isSelected={context.isSkillSelected}
78+
editMode={!!context.isEditing}
79+
/>
80+
81+
<div className={styles.formInput}>
82+
<div className={styles.formInputLabel}>
83+
<h2>With:</h2>
84+
<InputRadio
85+
label='Existing Skill'
86+
name='replace-with'
87+
id='replace-with-existing'
88+
value='existing'
89+
checked={type === 'existing'}
90+
onChange={function t() { toggleType('existing') }}
91+
/>
92+
<InputRadio
93+
label='New Skill'
94+
name='replace-with'
95+
id='replace-with-new'
96+
value='new'
97+
checked={type === 'new'}
98+
onChange={function t() { toggleType('new') }}
99+
/>
100+
</div>
101+
{type === 'existing' && (
102+
<SearchSkillInput
103+
skills={props.allSkills}
104+
onChange={handleSkillSelect}
105+
/>
106+
)}
107+
{type === 'new' && (
108+
<SkillForm
109+
onChange={handleNewForm}
110+
onLoading={setIsLoading as (l?: boolean) => void}
111+
hideCancelBtn
112+
hideSaveBtn
113+
/>
114+
)}
115+
</div>
116+
117+
<LoadingSpinner hide={!isLoading} overlay />
118+
</BaseModal>
119+
)
120+
}
121+
122+
export default ReplaceSkillsModal
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as ReplaceSkillsModal } from './ReplaceSkillsModal'
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ChangeEvent, FC, useMemo } from 'react'
2+
import { escapeRegExp } from 'lodash'
3+
import { FilterOptionOption } from 'react-select/dist/declarations/src/filters'
4+
5+
import { InputSelectReact } from '~/libs/ui'
6+
7+
import { mapSkillToSelectOption } from '../../../lib'
8+
import { StandardizedSkill } from '../../../services'
9+
10+
interface SearchSkillInputProps {
11+
skills: StandardizedSkill[]
12+
onChange: (event: ChangeEvent<HTMLInputElement>) => void
13+
}
14+
15+
const normalize = (s: string): string => (s || '')
16+
.trim()
17+
.toLowerCase()
18+
19+
const SearchSkillInput: FC<SearchSkillInputProps> = props => {
20+
const skillsOptionsList = useMemo(() => (
21+
mapSkillToSelectOption(props.skills)
22+
), [props.skills])
23+
24+
function filterOptions(o: FilterOptionOption<any>, v: string): boolean {
25+
const normValue = normalize(v)
26+
const normLabel = normalize(o.label)
27+
28+
if (v.length < 3 && normValue !== normLabel) {
29+
return false
30+
}
31+
32+
const m = new RegExp(escapeRegExp(normValue), 'i')
33+
return !!normLabel.match(m)
34+
}
35+
36+
return (
37+
<InputSelectReact
38+
placeholder='Search skills'
39+
options={skillsOptionsList}
40+
name='select-skill'
41+
onChange={props.onChange}
42+
openMenuOnClick={false}
43+
openMenuOnFocus={false}
44+
filterOption={filterOptions}
45+
/>
46+
)
47+
}
48+
49+
export default SearchSkillInput
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as SearchSkillInput } from './SearchSkillInput'

src/apps/admin/src/skills-manager/components/skill-modals/SkillModal.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
BaseModal,
1111
Button,
1212
LoadingSpinner,
13+
useConfirmationModal,
1314
} from '~/libs/ui'
1415

1516
import {
@@ -33,6 +34,8 @@ const SkillModal: FC<SkillModalProps> = props => {
3334
setEditSkill,
3435
}: SkillsManagerContextValue = useSkillsManagerContext()
3536

37+
const confirmModal = useConfirmationModal()
38+
3639
const [isLoading, setIsLoading] = useState(false)
3740
const isArchived = useMemo(() => isSkillArchived(props.skill), [props.skill])
3841

@@ -62,8 +65,16 @@ const SkillModal: FC<SkillModalProps> = props => {
6265
}, [setEditSkill])
6366

6467
const archiveSkill = useCallback(async (): Promise<void> => {
65-
setIsLoading(true)
68+
const confirmed = await confirmModal.confirm({
69+
content: 'Are you sure you want to archive this skill?',
70+
title: 'Confirm Archive',
71+
})
72+
73+
if (!confirmed) {
74+
return undefined
75+
}
6676

77+
setIsLoading(true)
6778
return archiveStandardizedSkill(props.skill)
6879
.then(() => {
6980
refetchSkills()
@@ -74,7 +85,7 @@ const SkillModal: FC<SkillModalProps> = props => {
7485
setIsLoading(false)
7586
return Promise.reject(e)
7687
})
77-
}, [setEditSkill, props.skill, refetchSkills])
88+
}, [confirmModal, props.skill, refetchSkills, setEditSkill])
7889

7990
const restoreSkill = useCallback(async (): Promise<void> => {
8091
setIsLoading(true)
@@ -137,6 +148,7 @@ const SkillModal: FC<SkillModalProps> = props => {
137148
primaryButtons={renderSaveAndAddBtn}
138149
/>
139150
<LoadingSpinner hide={!isLoading} overlay />
151+
{confirmModal.modal}
140152
</BaseModal>
141153
)
142154
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { default as SkillModal } from './SkillModal'
2+
export * from './skill-form'

0 commit comments

Comments
 (0)