Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1d436b8
TSJR-314 - register admin app
vas3a Nov 21, 2023
a63c12a
TSJR-314 - add skills manager routes
vas3a Nov 22, 2023
9b4fd6d
TSJR-314 - skill manager work
vas3a Nov 24, 2023
01a2ad0
TSJR-314 - optimize accordion
vas3a Nov 24, 2023
e26a983
TSJR-314 - category modal
vas3a Nov 24, 2023
b79fa38
TSJR-314 - implement new select input and use it in form definition
vas3a Nov 27, 2023
0fc9fa3
add modals & action menus for handling add & edit categories & skills
vas3a Nov 27, 2023
aa29a02
TSJR-314 - work for bulk edit
vas3a Nov 28, 2023
be2ebd0
TSJR-314 - bulk archive skills
vas3a Nov 28, 2023
498d687
TSJR-314 - edit existing skill
vas3a Nov 28, 2023
ee4355c
TSJR-314 fixes to skills modal
vas3a Nov 28, 2023
0eed469
TSJR-314 - improve validation for skills modal
vas3a Nov 29, 2023
f6012f6
TSJR-314 - update form validators
vas3a Nov 29, 2023
077f6e6
TSJR-314 - more updates to admin skill
vas3a Nov 30, 2023
0d8e64f
TSJR-314 - improvments to bulk editor
vas3a Nov 30, 2023
638cb68
TSJR-314 - add suggestions drodpdown
vas3a Dec 1, 2023
4a32e9d
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into T…
vas3a Dec 4, 2023
e9dc2f7
TSJR-314 - Add bulk replace skills
vas3a Dec 6, 2023
cbd8e8c
TSJR-314 - update route
vas3a Dec 6, 2023
471acf8
TSJR-314 fix linting issues
vas3a Dec 6, 2023
1ced5fd
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into T…
vas3a Dec 13, 2023
6c7a4ca
TSJR-314 - deploy to dev env
vas3a Dec 13, 2023
e4bb311
TSJR-314 - minor skills admin fixes
vas3a Dec 20, 2023
3c1fbb7
TSJR-314 - throw error when creating duplicate skill
vas3a Jan 8, 2024
abe59cd
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into T…
vas3a Feb 20, 2024
ed11e65
Move action menu
vas3a Feb 21, 2024
99cbe32
Merge pull request #953 from topcoder-platform/TSJR-314_skill-manager…
vas3a Feb 22, 2024
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
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ workflows:
branches:
only:
- dev
- TSJR-314_skill-manager_landing-page

- deployQa:
context: org-global
Expand Down
42 changes: 35 additions & 7 deletions .vscode/components.code-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// ],
// "description": "Log output to console"
// }
"[MFE] React component": {
"[PLAT] React component": {
"scope": "typescript,typescriptreact",
"prefix": "rfc",
"body": [
Expand All @@ -28,18 +28,39 @@
"",
"const ${1:ComponentName}: FC<${1:ComponentName}Props> = props => {",
"",
" return (",
" return (",
" <div className={styles.wrap}>",
" </div>",
" )",
"}",
"",
"export default ${1:ComponentName}",
""
],
"description": "Create a react functional component"
},
"[PLAT] Simple React component": {
"scope": "typescript,typescriptreact",
"prefix": "rfc",
"body": [
"import { FC } from 'react'",
"",
"import styles from './${1:ComponentName}.module.scss'",
"",
"interface ${1:ComponentName}Props {",
"}",
"",
"const ${1:ComponentName}: FC<${1:ComponentName}Props> = props => (",
" <div className={styles.wrap}>",
" </div>",
" )",
"}",
")",
"",
"export default ${1:ComponentName}",
""
],
"description": "Create a react functional component"
},
"[MFE] export comp": {
"[PLAT] export comp": {
"scope": "typescript,typescriptreact",
"prefix": "exp",
"body": [
Expand All @@ -48,14 +69,21 @@
],
"description": "Export module"
},
"[MFE] use state": {
"[PLAT] use state": {
"scope": "typescript,typescriptreact",
"prefix": "usest",
"body": [
"const [$1, set$2]: [$3, Dispatch<SetStateAction<$3>>] = useState($4)$0",
]
},
"[MFE] Storybook Template": {
"[PLAT] includes": {
"scope": "css,scss",
"prefix": "includes",
"body": [
"@import '@libs/ui/styles/includes';",
]
},
"[PLAT] Storybook Template": {
"scope": "typescript,typescriptreact",
"prefix": "sb",
"body": [
Expand Down
1 change: 1 addition & 0 deletions src/apps/admin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Admin App
1 change: 1 addition & 0 deletions src/apps/admin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src'
22 changes: 22 additions & 0 deletions src/apps/admin/src/AdminApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FC, useContext } from 'react'
import { Outlet, Routes } from 'react-router-dom'

import { routerContext, RouterContextData } from '~/libs/core'
import { SharedSwrConfig } from '~/libs/shared'

import { toolTitle } from './admin.routes'

const AdminApp: FC<{}> = () => {
const { getChildRoutes }: RouterContextData = useContext(routerContext)

return (
<SharedSwrConfig>
<Outlet />
<Routes>
{getChildRoutes(toolTitle)}
</Routes>
</SharedSwrConfig>
)
}

export default AdminApp
34 changes: 34 additions & 0 deletions src/apps/admin/src/admin.routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Navigate } from 'react-router-dom'

import { lazyLoad, LazyLoadedComponent, PlatformRoute, UserRole } from '~/libs/core'
import { AppSubdomain, EnvironmentConfig, ToolTitle } from '~/config'

import { skillsManagerRootRoute, skillsManagerRoutes } from './skills-manager'

const AdminApp: LazyLoadedComponent = lazyLoad(() => import('./AdminApp'))

export const rootRoute: string = (
EnvironmentConfig.SUBDOMAIN === AppSubdomain.admin ? '' : `/${AppSubdomain.admin}`
)

export const toolTitle: string = ToolTitle.admin
export const absoluteRootRoute: string = `${window.location.origin}${rootRoute}`

export const adminRoutes: ReadonlyArray<PlatformRoute> = [
{
authRequired: true,
children: [
...skillsManagerRoutes,
{
element: <Navigate to={`${rootRoute}${skillsManagerRootRoute}`} />,
id: 'Default Admin Route',
route: '',
},
],
domain: AppSubdomain.admin,
element: <AdminApp />,
id: toolTitle,
rolesRequired: [UserRole.administrator],
route: rootRoute,
},
]
4 changes: 4 additions & 0 deletions src/apps/admin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
adminRoutes,
rootRoute as adminRootRoute,
} from './admin.routes'
22 changes: 22 additions & 0 deletions src/apps/admin/src/skills-manager/SkillsManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FC, useContext } from 'react'
import { Outlet, Routes } from 'react-router-dom'

import { routerContext, RouterContextData } from '~/libs/core'

import { skillsManagerRoutes } from './skills-manager.routes'
import { SkillsManagerContext } from './context'

const SkillsManager: FC<{}> = () => {
const { getRouteElement }: RouterContextData = useContext(routerContext)

return (
<SkillsManagerContext>
<Outlet />
<Routes>
{skillsManagerRoutes.map(getRouteElement)}
</Routes>
</SkillsManagerContext>
)
}

export default SkillsManager
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import '@libs/ui/styles/includes';
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
Children,
cloneElement,
FC,
isValidElement,
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react'

import { AccordionItemProps } from './accordion-item'
import styles from './Accordion.module.scss'

interface AccordionProps {
children: JSX.Element[] | JSX.Element
defaultOpen?: boolean
}

function computeOpenSectionsState(props: AccordionProps): {[key: string]: boolean} {
const newOpenState: {[key: string]: boolean} = {}

Children.forEach<ReactNode>(props.children, child => {
if (!isValidElement(child)) {
return
}

const childKey = child.key as string
newOpenState[childKey] = child.props.open ?? props.defaultOpen
})

return newOpenState
}

const Accordion: FC<AccordionProps> = props => {
const prevProps = useRef({ ...props })
const [openedSections, setOpenedSections] = useState<{[key: string]: boolean}>({})

const handleToggle = useCallback((key: string) => {
setOpenedSections(all => ({ ...all, [key]: !all[key] }))
}, [])

// check if props have changed and update the openedSections synchronously
if (prevProps.current.defaultOpen !== props.defaultOpen) {
prevProps.current = { ...props }
Object.assign(openedSections, computeOpenSectionsState(props))
}

// use an effect to make sure the changes are propagated in the state
useEffect(() => {
setOpenedSections(computeOpenSectionsState(props))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.defaultOpen])

const renderAccordions = (children: JSX.Element[] | JSX.Element): ReactNode => (
Children.map<ReactNode, ReactNode>(children, child => {
if (isValidElement(child)) {
const childKey = child.key as string
openedSections[childKey] = openedSections[childKey] ?? child.props.open ?? props.defaultOpen

return cloneElement(
child,
{
open: !!openedSections[childKey],
toggle: function toggle() { handleToggle(childKey) },
} as AccordionItemProps,
)
}

return child
})
)

return (
<div className={styles.wrap}>
{renderAccordions(props.children)}
</div>
)
}

export default Accordion
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
@import '@libs/ui/styles/includes';

.wrap {
+ .wrap {
margin-top: $sp-8;
}
}

.itemHeader {
display: flex;
align-items: center;
}

.icon {
color: $turq-120;
transition: 0.2s ease-in-out;
margin-right: $sp-2;
}

.menuIcon {
color: $turq-120;
}

.titleBar {
display: flex;
align-items: center;
gap: $sp-2;
}

.textLabel {
font-family: $font-roboto;
font-size: 20px;
font-weight: $font-weight-medium;
line-height: $sp-5;
letter-spacing: 0.05px;
}

.textLabel, .icon {
cursor: pointer;
}

.badge {
display: block;
font-family: $font-roboto;
font-weight: $font-weight-bold;
font-size: 14px;
line-height: 22px;
padding: 0 $sp-2;

color: $black-100;

background: $black-10;
border-radius: $sp-4;
}

.content {
padding: $sp-4 $sp-6;
}

.open {
.icon {
transform: rotateZ(180deg);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { FC, useMemo } from 'react'
import classNames from 'classnames'

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

import { ActionsMenu, ActionsMenuItem } from '../../actions-menu'

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

export interface AccordionItemProps {
label?: string
badgeCount?: number
open?: boolean
toggle?: () => void
children: JSX.Element[] | JSX.Element | (() => JSX.Element[] | JSX.Element)
menuActions: ActionsMenuItem[]
onMenuAction: (a: string) => void
}

const AccordionItem: FC<AccordionItemProps> = props => {
const content = useMemo(() => (!props.open ? <></> : (
<div className={styles.content}>
{typeof props.children === 'function' ? props.children.call(undefined) : props.children}
</div>
)), [props.children, props.open])

return (
<div className={classNames(styles.wrap, props.open && styles.open)}>
<div className={styles.itemHeader}>
<span className={styles.icon} onClick={props.toggle}>
<IconOutline.ChevronDownIcon className='icon-lg' />
</span>
<div className={styles.titleBar}>
{props.label && (
<div className={styles.textLabel} onClick={props.toggle}>
{props.label}
</div>
)}
{props.badgeCount !== undefined && (
<div className={styles.badge}>
{props.badgeCount}
</div>
)}
{props.menuActions?.length > 0 && (
<ActionsMenu
items={props.menuActions}
onAction={props.onMenuAction}
className={styles.menu}
>
<IconOutline.DotsVerticalIcon className={classNames('icon-lg', styles.menuIcon)} />
</ActionsMenu>
)}
</div>
</div>
{content}
</div>
)
}

export default AccordionItem
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as AccordionItem, type AccordionItemProps } from './AccordionItem'
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Accordion } from './Accordion'
export * from './accordion-item'
Loading