From 7d3d9223194460993456f4609e35abeec060cd24 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 13 Jan 2025 17:42:37 +0200 Subject: [PATCH 001/102] PM-462 - update urls --- .../platform-ui-app/getting-started/GettingStartedGuide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/dev-center/src/dev-center-pages/platform-ui-app/getting-started/GettingStartedGuide.md b/src/apps/dev-center/src/dev-center-pages/platform-ui-app/getting-started/GettingStartedGuide.md index b9017d491..291640e86 100644 --- a/src/apps/dev-center/src/dev-center-pages/platform-ui-app/getting-started/GettingStartedGuide.md +++ b/src/apps/dev-center/src/dev-center-pages/platform-ui-app/getting-started/GettingStartedGuide.md @@ -17,8 +17,8 @@ Preferably, use the VS Code IDE for development. Install git on your local machine. This is trivial for working in the community. You can follow these guides for installing GIT: -- [Windows](https://local.topcoder-dev.com/devcenter/getting-started#23-install-git) -- [Linux](https://local.topcoder-dev.com/devcenter/getting-started#197-install-git) +- [Windows](https://devcenter.topcoder.com/getting-started#23-install-git) +- [Linux](https://devcenter.topcoder.com/getting-started#197-install-git) ### nvm From 2dc5180e35e486011ee226faaf10c142adcfda8a Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Wed, 15 Jan 2025 22:40:55 +0530 Subject: [PATCH 002/102] Fix default values on onboarding form --- .../onboarding/src/components/InputTextAutoSave/index.tsx | 2 +- src/apps/onboarding/src/pages/open-to-work/index.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx index bb98db09b..96c601346 100644 --- a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx +++ b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx @@ -28,7 +28,7 @@ export interface InputTextProps { } const InputTextAutoSave: FC = (props: InputTextProps) => { - const [value, setValue] = useState('') + const [value, setValue] = useState(props.value || '') useEffect(() => { setValue(props.value) }, [props.value]) diff --git a/src/apps/onboarding/src/pages/open-to-work/index.tsx b/src/apps/onboarding/src/pages/open-to-work/index.tsx index 817cebfd9..194653815 100644 --- a/src/apps/onboarding/src/pages/open-to-work/index.tsx +++ b/src/apps/onboarding/src/pages/open-to-work/index.tsx @@ -1,7 +1,6 @@ import { useNavigate } from 'react-router-dom' import { FC, MutableRefObject, useEffect, useRef, useState } from 'react' import { connect } from 'react-redux' -import { pick } from 'lodash' import classNames from 'classnames' import { Button, IconOutline, PageDivider } from '~/libs/ui' @@ -104,7 +103,9 @@ export const PageOpenToWorkContent: FC = props => { ) } -const mapStateToProps: any = (state: any) => pick(state.member, 'availableForGigs') +const mapStateToProps: any = (state: any) => ({ + availableForGigs: state.member.memberInfo?.availableForGigs, +}) const mapDispatchToProps: any = { updateMemberOpenForWork, From da59c8709725266fa31775db0a82c533ed4e553c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 20 Jan 2025 11:30:18 +0100 Subject: [PATCH 003/102] fix: breadcrumb url --- .../dev-center-pages/platform-ui-app/storybook/Storybook.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/dev-center/src/dev-center-pages/platform-ui-app/storybook/Storybook.tsx b/src/apps/dev-center/src/dev-center-pages/platform-ui-app/storybook/Storybook.tsx index 4acac412d..b619cf3fb 100644 --- a/src/apps/dev-center/src/dev-center-pages/platform-ui-app/storybook/Storybook.tsx +++ b/src/apps/dev-center/src/dev-center-pages/platform-ui-app/storybook/Storybook.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { Breadcrumb, BreadcrumbItemModel, ContentLayout } from '~/libs/ui' -import { toolTitle } from '../../../dev-center.routes' +import { rootRoute, toolTitle } from '../../../dev-center.routes' import { LayoutDocHeader, MarkdownDoc } from '../../../dev-center-lib/MarkdownDoc' import useMarkdown from '../../../dev-center-lib/hooks/useMarkdown' @@ -12,7 +12,7 @@ import styles from './Storybook.module.scss' export const Storybook: React.FC = () => { const { doc, toc, title }: ReturnType = useMarkdown({ uri: storybookMarkdown }) const breadcrumb: Array = React.useMemo(() => [ - { name: toolTitle, url: '/dev-center' }, + { name: toolTitle, url: rootRoute || '/' }, { name: title, url: '#' }, ], [title]) From 26b7ffc5c9724151fadad20a1e712892da343fbb Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 20 Jan 2025 18:50:08 +0530 Subject: [PATCH 004/102] PM-505 Fix QA feedback --- .../onboarding/src/components/InputTextAutoSave/index.tsx | 1 + src/apps/onboarding/src/redux/reducers/member.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx index 96c601346..f00e0a2d0 100644 --- a/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx +++ b/src/apps/onboarding/src/components/InputTextAutoSave/index.tsx @@ -37,6 +37,7 @@ const InputTextAutoSave: FC = (props: InputTextProps) => { ) { setValue(event.target.value) }} diff --git a/src/apps/onboarding/src/redux/reducers/member.ts b/src/apps/onboarding/src/redux/reducers/member.ts index f1d7442be..94f02b02f 100644 --- a/src/apps/onboarding/src/redux/reducers/member.ts +++ b/src/apps/onboarding/src/redux/reducers/member.ts @@ -20,7 +20,6 @@ const initialState: { connectInfo?: ConnectInfo loadingMemberTraits?: boolean loadingMemberInfo?: boolean - availableForGigs?: boolean } = { } @@ -40,7 +39,10 @@ const memberReducer: any = ( case ACTIONS.MEMBER.SET_OPEN_FOR_WORK: return { ...state, - availableForGigs: action.payload, + memberInfo: { + ...state.memberInfo, + availableForGigs: action.payload, + } } case ACTIONS.MEMBER.SET_WORKS: return { From 48a3b1a374f98c2e89f65c1684ef6a8499e04213 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 20 Jan 2025 19:07:09 +0530 Subject: [PATCH 005/102] lint fix --- src/apps/onboarding/src/redux/reducers/member.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/onboarding/src/redux/reducers/member.ts b/src/apps/onboarding/src/redux/reducers/member.ts index 94f02b02f..66885bccf 100644 --- a/src/apps/onboarding/src/redux/reducers/member.ts +++ b/src/apps/onboarding/src/redux/reducers/member.ts @@ -42,7 +42,7 @@ const memberReducer: any = ( memberInfo: { ...state.memberInfo, availableForGigs: action.payload, - } + }, } case ACTIONS.MEMBER.SET_WORKS: return { From 91359f5cb06d5d4633ed929fffa5feea1f7f6c3a Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 27 Jan 2025 17:39:26 +0530 Subject: [PATCH 006/102] PM-579 Add copilot request UI --- src/apps/copilots/README.md | 1 + src/apps/copilots/index.ts | 1 + src/apps/copilots/src/CopilotsApp.tsx | 22 + src/apps/copilots/src/constants/index.ts | 17 + src/apps/copilots/src/copilots.routes.tsx | 34 ++ src/apps/copilots/src/index.ts | 1 + src/apps/copilots/src/models/Project.ts | 4 + .../src/pages/copilot-request-form/index.tsx | 394 ++++++++++++++++++ .../copilot-request-form/styles.module.scss | 48 +++ src/apps/copilots/src/services/projects.ts | 16 + src/apps/platform/src/platform.routes.tsx | 2 + src/config/constants.ts | 2 + .../button/base-button/BaseButton.module.scss | 14 + .../button/base-button/BaseButton.tsx | 8 +- 14 files changed, 563 insertions(+), 1 deletion(-) create mode 100644 src/apps/copilots/README.md create mode 100644 src/apps/copilots/index.ts create mode 100644 src/apps/copilots/src/CopilotsApp.tsx create mode 100644 src/apps/copilots/src/constants/index.ts create mode 100644 src/apps/copilots/src/copilots.routes.tsx create mode 100644 src/apps/copilots/src/index.ts create mode 100644 src/apps/copilots/src/models/Project.ts create mode 100644 src/apps/copilots/src/pages/copilot-request-form/index.tsx create mode 100644 src/apps/copilots/src/pages/copilot-request-form/styles.module.scss create mode 100644 src/apps/copilots/src/services/projects.ts diff --git a/src/apps/copilots/README.md b/src/apps/copilots/README.md new file mode 100644 index 000000000..90e48136e --- /dev/null +++ b/src/apps/copilots/README.md @@ -0,0 +1 @@ +# Copilots app \ No newline at end of file diff --git a/src/apps/copilots/index.ts b/src/apps/copilots/index.ts new file mode 100644 index 000000000..6f39cd49b --- /dev/null +++ b/src/apps/copilots/index.ts @@ -0,0 +1 @@ +export * from './src' diff --git a/src/apps/copilots/src/CopilotsApp.tsx b/src/apps/copilots/src/CopilotsApp.tsx new file mode 100644 index 000000000..5c7198c2a --- /dev/null +++ b/src/apps/copilots/src/CopilotsApp.tsx @@ -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 './copilots.routes' + +const CopilotsApp: FC<{}> = () => { + const { getChildRoutes }: RouterContextData = useContext(routerContext) + + return ( + + + + {getChildRoutes(toolTitle)} + + + ) +} + +export default CopilotsApp diff --git a/src/apps/copilots/src/constants/index.ts b/src/apps/copilots/src/constants/index.ts new file mode 100644 index 000000000..54de6e1cd --- /dev/null +++ b/src/apps/copilots/src/constants/index.ts @@ -0,0 +1,17 @@ +export enum ProjectType { + design = 'Design', + developement = 'Development', + marathonCopilot = 'Data Science (Marathon Copilot)', + sprintCopilot = 'Data Science (Sprint Copilot)', + marathonTester = 'Data Science (Marathon Tester)', + qa = 'QA', +} + +export const ProjectTypes = [ + ProjectType.design, + ProjectType.developement, + ProjectType.marathonCopilot, + ProjectType.marathonTester, + ProjectType.sprintCopilot, + ProjectType.qa, +] diff --git a/src/apps/copilots/src/copilots.routes.tsx b/src/apps/copilots/src/copilots.routes.tsx new file mode 100644 index 000000000..c53cf738d --- /dev/null +++ b/src/apps/copilots/src/copilots.routes.tsx @@ -0,0 +1,34 @@ +import { lazyLoad, LazyLoadedComponent, PlatformRoute, UserRole } from '~/libs/core' +import { AppSubdomain, EnvironmentConfig, ToolTitle } from '~/config' + +const CopilotsApp: LazyLoadedComponent = lazyLoad(() => import('./CopilotsApp')) +const CopilotsRequestForm: LazyLoadedComponent = lazyLoad(() => import('./pages/copilot-request-form/index')) + +export const rootRoute: string = ( + EnvironmentConfig.SUBDOMAIN === AppSubdomain.copilots ? '' : `/${AppSubdomain.copilots}` +) + +export const toolTitle: string = ToolTitle.copilots +export const absoluteRootRoute: string = `${window.location.origin}${rootRoute}` + +export const copilotsRoutes: ReadonlyArray = [ + { + authRequired: true, + children: [ + { + element: , + id: 'CopilotRequestForm', + route: '/request', + }, + + ], + domain: AppSubdomain.copilots, + element: , + id: toolTitle, + rolesRequired: [ + UserRole.administrator, + UserRole.connectManager, + ], + route: rootRoute, + }, +] diff --git a/src/apps/copilots/src/index.ts b/src/apps/copilots/src/index.ts new file mode 100644 index 000000000..344c36c9b --- /dev/null +++ b/src/apps/copilots/src/index.ts @@ -0,0 +1 @@ +export { copilotsRoutes } from './copilots.routes' diff --git a/src/apps/copilots/src/models/Project.ts b/src/apps/copilots/src/models/Project.ts new file mode 100644 index 000000000..80148cd37 --- /dev/null +++ b/src/apps/copilots/src/models/Project.ts @@ -0,0 +1,4 @@ +export interface Project{ + id: string, + name: string, +} diff --git a/src/apps/copilots/src/pages/copilot-request-form/index.tsx b/src/apps/copilots/src/pages/copilot-request-form/index.tsx new file mode 100644 index 000000000..99dcbf4c8 --- /dev/null +++ b/src/apps/copilots/src/pages/copilot-request-form/index.tsx @@ -0,0 +1,394 @@ +import { FC, useContext, useState } from 'react' +import { SWRResponse } from 'swr' +import { bind, isEmpty } from 'lodash' +import classNames from 'classnames' + +import { profileContext, ProfileContextData } from '~/libs/core' +import { Button, InputDatePicker, InputMultiselectOption, + InputRadio, InputSelect, InputText, InputTextarea } from '~/libs/ui' +import { InputSkillSelector } from '~/libs/shared' + +import { useFetchProjects } from '../../services/projects' +import { ProjectTypes } from '../../constants' +import { Project } from '../../models/Project' + +import styles from './styles.module.scss' + +const CopilotRequestForm: FC<{}> = () => { + const { profile }: ProfileContextData = useContext(profileContext) + + const [formValues, setFormValues] = useState({}) + const [isFormChanged, setIsFormChanged] = useState(false) + const [formErrors, setFormErrors] = useState({}) + const { data: projectsData }: SWRResponse = useFetchProjects() + const [existingCopilot, setExistingCopilot] = useState('') + const [paymentType, setPaymentType] = useState('') + + const projects = projectsData + ? projectsData.map(project => ({ + label: project.name, + value: project.id, + })) + : [] + + const projectTypes = ProjectTypes ? ProjectTypes.map(project => ({ + label: project, + value: project, + })) + : [] + + function exisitingCopilotToggle(t: 'yes'|'no'): void { + setExistingCopilot(t) + setIsFormChanged(true) + } + + function togglePaymentType(t: 'standard'|'other'): void { + setFormValues((prevFormValues: any) => ({ + ...prevFormValues, + paymentType: t, + })) + setPaymentType(t) + } + + function handleFormValueChange( + key: string, + event: React.ChangeEvent | React.ChangeEvent, + ): void { + const oldFormValues = { ...formValues } + let value: string | boolean | Date | undefined + switch (key) { + case 'startDate': + value = event as unknown as Date + break + case 'skills': + oldFormValues[key] = Array.isArray(value) ? [...value] : [] + break + default: + value = event.target.value + break + } + + setFormValues({ + ...oldFormValues, + [key]: value, + }) + setIsFormChanged(true) + } + + function handleSkillsChange(ev: any): void { + const options = (ev.target.value as unknown) as InputMultiselectOption[] + const updatedSkills = options.map(v => ({ + id: v.value, + name: v.label as string, + })) + setFormValues((prevFormValues: any) => ({ + ...prevFormValues, + skills: updatedSkills, + })) + setIsFormChanged(true) + } + + function handleFormAction(): void { + const updatedFormErrors: { [key: string]: string } = {} + + if (!formValues.projectId) { + updatedFormErrors.project = 'Project is required' + } + + if (!formValues.projectType) { + updatedFormErrors.projectType = 'Selecting project type is required' + } + + if (!formValues.projectOverview) { + updatedFormErrors.projectOverview = 'Providing a project overview is required' + } + + if (!formValues.skills) { + updatedFormErrors.skills = 'Providing skills is required' + } + + if (!formValues.startDate) { + updatedFormErrors.startDate = 'Providing a start date for copilot is required' + } + + if (!formValues.numWeeks) { + updatedFormErrors.numWeeks = 'Providing number of weeks is required' + } + + if (!formValues.tzRestrictions) { + updatedFormErrors.tzRestrictions = 'Providing timezone restrictions is required. Type No if no restrictions' + } + + if (!formValues.numHoursPerWeek) { + updatedFormErrors.numHoursPerWeek = 'Providing commitment per week is required' + } + + if (isEmpty(updatedFormErrors)) { + // call the API to update the trait based on action type + } + + setFormErrors(updatedFormErrors) + } + + return ( +
+
+
+

Copilot Request

+

+ {' '} + Hi, + {profile?.firstName} + ! + This form is to request a copilot for your project. Please fill in the details below. +

+

Select the project you want the copilot for

+ +

+ Are you already working with a copilot that you'd love to work with on this project + as well? +

+ +
+ + +
+ { + existingCopilot === 'yes' + && ( +
+

+ Great! What is the username of the copilot you'd like to work with again? +

+ + +
+ ) + } + +

What type of project are you working on?

+ + +

How would you rank the complexity of the project?

+
+
+

+ Please provide an overview of the project the copilot will undertake +

+ +

Any specific skills or technology requirements that come to mind?

+ +

What's the planned start date for the copilot?

+ +

How many weeks will you need the copilot for?

+ +

Are there any timezone requirements or restrictions?

+ +

What do you expect the commitment to be per week in hours?

+ +

+ Will this project require direct spoken communication with the customer + (i.e. phone calls, WebEx, etc.) +

+
+ + +
+

Will this role be standard payments or something else?

+
+ + + {paymentType === 'other' + && ( + + )} +
+
+
+ ) +} + +export default CopilotRequestForm diff --git a/src/apps/copilots/src/pages/copilot-request-form/styles.module.scss b/src/apps/copilots/src/pages/copilot-request-form/styles.module.scss new file mode 100644 index 000000000..02b33b612 --- /dev/null +++ b/src/apps/copilots/src/pages/copilot-request-form/styles.module.scss @@ -0,0 +1,48 @@ +$gradient: linear-gradient( + to top right, // This approximates the diagonal direction + #086093, // Start color + #028367 // End color +); + +.container { + display: flex; + justify-content: center; + align-items: center; + background: $gradient; + padding: 20px; +} + +.form { + background: white; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 1000px; + box-sizing: border-box; + + .heading { + margin-bottom: 1rem; + } + + .subheading { + margin-bottom: 2rem; + } + + .formRow { + margin-top: 2rem; + margin-bottom: 1rem; + } + + .formRadioBtn { + display: flex; + gap: 2rem; + margin-bottom: 1rem; + } + + .complexity { + display: flex; + flex-direction: column; + gap: 1rem; + } +} \ No newline at end of file diff --git a/src/apps/copilots/src/services/projects.ts b/src/apps/copilots/src/services/projects.ts new file mode 100644 index 000000000..cce9bb980 --- /dev/null +++ b/src/apps/copilots/src/services/projects.ts @@ -0,0 +1,16 @@ +import useSWR, { SWRResponse } from 'swr' + +import { xhrGetAsync } from '~/libs/core' +import { EnvironmentConfig } from '~/config' + +import { Project } from '../models/Project' + +const baseUrl = `${EnvironmentConfig.API.V5}/projects` + +export const useFetchProjects = (): SWRResponse => { + const response = useSWR(baseUrl, xhrGetAsync, { + refreshInterval: 0, + revalidateOnFocus: false, + }) + return response +} diff --git a/src/apps/platform/src/platform.routes.tsx b/src/apps/platform/src/platform.routes.tsx index 137f9be0e..0da61cb8e 100644 --- a/src/apps/platform/src/platform.routes.tsx +++ b/src/apps/platform/src/platform.routes.tsx @@ -11,6 +11,7 @@ import { onboardingRoutes } from '~/apps/onboarding' import { skillsManagerRoutes } from '~/apps/skills-manager' import { walletRoutes } from '~/apps/wallet' import { walletAdminRoutes } from '~/apps/wallet-admin' +import { copilotsRoutes } from '~/apps/copilots' const Home: LazyLoadedComponent = lazyLoad( () => import('./routes/home'), @@ -32,6 +33,7 @@ export const platformRoutes: Array = [ ...selfServiceRoutes, ...onboardingRoutes, ...devCenterRoutes, + ...copilotsRoutes, ...learnRoutes, ...gamificationAdminRoutes, ...talentSearchRoutes, diff --git a/src/config/constants.ts b/src/config/constants.ts index cab50f7eb..bf939bd2c 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -11,6 +11,7 @@ export enum AppSubdomain { talentSearch = 'talent-search', wallet = 'wallet', walletAdmin = 'wallet-admin', + copilots = 'copilots', } export enum ToolTitle { @@ -26,6 +27,7 @@ export enum ToolTitle { talentSearch = 'Expert Talent', wallet = 'Wallet', walletAdmin = 'Wallet Admin', + copilots = 'Copilots' } export const PageSubheaderPortalId: string = 'page-subheader-portal-el' diff --git a/src/libs/ui/lib/components/button/base-button/BaseButton.module.scss b/src/libs/ui/lib/components/button/base-button/BaseButton.module.scss index aee3a86b4..661feddbf 100644 --- a/src/libs/ui/lib/components/button/base-button/BaseButton.module.scss +++ b/src/libs/ui/lib/components/button/base-button/BaseButton.module.scss @@ -50,6 +50,15 @@ $btn-secondary-border-width: $border; &:global(.btn-size-full) { width: 100%; } + &:global(.btn-custom-radius) { + border-radius: $sp-1; + } + &:global(.btn-no-caps) { + text-transform: none; + } + &:global(.btn-left-align-text) { + justify-content: left; + } // Define variants &:global(.btn-variant-danger) { @@ -67,6 +76,11 @@ $btn-secondary-border-width: $border; --btn-variant--hover: #{lighten($link-blue-dark, 10%)}; --btn-variant--active: #{darken($link-blue-dark, 10%)}; } + &:global(.btn-variant-tcgreen) { + --btn-variant: #{$turq-180}; + --btn-variant--hover: #{lighten($turq-180, 10%)}; + --btn-variant--active: #{darken($turq-180, 10%)}; + } &:global(.btn-variant-round) { @include pad(0); diff --git a/src/libs/ui/lib/components/button/base-button/BaseButton.tsx b/src/libs/ui/lib/components/button/base-button/BaseButton.tsx index 80b81cb40..0f44c347f 100644 --- a/src/libs/ui/lib/components/button/base-button/BaseButton.tsx +++ b/src/libs/ui/lib/components/button/base-button/BaseButton.tsx @@ -6,7 +6,7 @@ import classNames from 'classnames' import styles from './BaseButton.module.scss' export type ButtonSize = 'sm'| 'md'| 'lg'| 'xl' -export type ButtonVariants = 'danger' | 'warning' | 'linkblue' | 'round' +export type ButtonVariants = 'danger' | 'warning' | 'linkblue' | 'round' | 'tc-green' export type ButtonTypes = 'primary' | 'secondary' export interface BaseButtonProps extends ButtonHTMLAttributes { @@ -24,6 +24,9 @@ export interface BaseButtonProps extends ButtonHTMLAttributes fullWidth?: boolean variant?: ButtonVariants active?: boolean + customRadius?: boolean + noCaps?: boolean + leftAlignText?: boolean } const BaseButton: FC = props => { @@ -32,9 +35,12 @@ const BaseButton: FC = props => { const className: string = classNames(styles.btn, props.className, { 'btn-active': props.active, + 'btn-custom-radius': props.customRadius, 'btn-disabled': props.disabled, + 'btn-left-align-text': props.leftAlignText, 'btn-light': props.light, 'btn-loading': props.loading, + 'btn-no-caps': props.noCaps, 'btn-size-full': props.fullWidth, [`btn-size-${props.size}`]: !!props.size, 'btn-style-link': props.link, From c4999cf3dc1bcf7c24f833b55d9f2202c3659b56 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 27 Jan 2025 21:02:14 +0530 Subject: [PATCH 007/102] PR feedback --- src/apps/copilots/src/copilots.routes.tsx | 2 +- .../src/pages/copilot-request-form/index.tsx | 16 +++++++++------- .../copilot-request-form/styles.module.scss | 5 +++++ .../profile-factory/user-role.enum.ts | 1 + 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/apps/copilots/src/copilots.routes.tsx b/src/apps/copilots/src/copilots.routes.tsx index c53cf738d..eec1db6d0 100644 --- a/src/apps/copilots/src/copilots.routes.tsx +++ b/src/apps/copilots/src/copilots.routes.tsx @@ -27,7 +27,7 @@ export const copilotsRoutes: ReadonlyArray = [ id: toolTitle, rolesRequired: [ UserRole.administrator, - UserRole.connectManager, + UserRole.projectManager, ], route: rootRoute, }, diff --git a/src/apps/copilots/src/pages/copilot-request-form/index.tsx b/src/apps/copilots/src/pages/copilot-request-form/index.tsx index 99dcbf4c8..dd6f15c9e 100644 --- a/src/apps/copilots/src/pages/copilot-request-form/index.tsx +++ b/src/apps/copilots/src/pages/copilot-request-form/index.tsx @@ -267,13 +267,15 @@ const CopilotRequestForm: FC<{}> = () => { tabIndex={0} />

Any specific skills or technology requirements that come to mind?

- +
+ +

What's the planned start date for the copilot?

Date: Tue, 28 Jan 2025 17:51:19 +0530 Subject: [PATCH 008/102] Integrate save request API --- .../copilots/src/models/CopilotRequest.ts | 18 +++ .../src/pages/copilot-request-form/index.tsx | 108 ++++++++++++++++-- .../copilot-request-form/styles.module.scss | 20 ++++ src/apps/copilots/src/services/projects.ts | 13 ++- 4 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 src/apps/copilots/src/models/CopilotRequest.ts diff --git a/src/apps/copilots/src/models/CopilotRequest.ts b/src/apps/copilots/src/models/CopilotRequest.ts new file mode 100644 index 000000000..5e15e4590 --- /dev/null +++ b/src/apps/copilots/src/models/CopilotRequest.ts @@ -0,0 +1,18 @@ +import { UserSkill } from '~/libs/core' + +import { ProjectType } from '../constants' + +export interface CopilotRequest { + projectId: string, + projectType: ProjectType, + complexity: 'high' | 'medium' | 'low', + copilotUsername: string, + numHoursPerWeek: number, + numWeeks: number, + overview: string, + paymentType: string, + requiresCommunicatn: 'yes' | 'no', + skills: UserSkill[], + startDate: Date, + tzRestrictions: 'yes' | 'no', +} \ No newline at end of file diff --git a/src/apps/copilots/src/pages/copilot-request-form/index.tsx b/src/apps/copilots/src/pages/copilot-request-form/index.tsx index dd6f15c9e..7696b575f 100644 --- a/src/apps/copilots/src/pages/copilot-request-form/index.tsx +++ b/src/apps/copilots/src/pages/copilot-request-form/index.tsx @@ -1,14 +1,15 @@ import { FC, useContext, useState } from 'react' import { SWRResponse } from 'swr' import { bind, isEmpty } from 'lodash' +import { toast } from 'react-toastify' import classNames from 'classnames' import { profileContext, ProfileContextData } from '~/libs/core' -import { Button, InputDatePicker, InputMultiselectOption, +import { Button, IconSolid, InputDatePicker, InputMultiselectOption, InputRadio, InputSelect, InputText, InputTextarea } from '~/libs/ui' import { InputSkillSelector } from '~/libs/shared' -import { useFetchProjects } from '../../services/projects' +import { saveCopilotRequest, useFetchProjects } from '../../services/projects' import { ProjectTypes } from '../../constants' import { Project } from '../../models/Project' @@ -47,6 +48,11 @@ const CopilotRequestForm: FC<{}> = () => { ...prevFormValues, paymentType: t, })) + setFormErrors((prevFormErrors: any) => { + const updatedErrors = { ...prevFormErrors } + delete updatedErrors.paymentType + return updatedErrors + }) setPaymentType(t) } @@ -72,6 +78,24 @@ const CopilotRequestForm: FC<{}> = () => { ...oldFormValues, [key]: value, }) + + // Clear specific field error + setFormErrors((prevFormErrors: any) => { + const updatedErrors = { ...prevFormErrors } + let errorKey: string + switch (key) { + case 'copilotUsername': + errorKey = 'existingCopilot' + break + default: + errorKey = key + break + } + + // Remove the error from the updatedErrors object + delete updatedErrors[errorKey] + return updatedErrors + }) setIsFormChanged(true) } @@ -85,6 +109,12 @@ const CopilotRequestForm: FC<{}> = () => { ...prevFormValues, skills: updatedSkills, })) + + setFormErrors((prevFormErrors: any) => { + const updatedErrors = { ...prevFormErrors } + delete updatedErrors.skills + return updatedErrors + }) setIsFormChanged(true) } @@ -92,15 +122,31 @@ const CopilotRequestForm: FC<{}> = () => { const updatedFormErrors: { [key: string]: string } = {} if (!formValues.projectId) { - updatedFormErrors.project = 'Project is required' + updatedFormErrors.projectId = 'Project is required' + } + + if (!existingCopilot) { + updatedFormErrors.existingCopilot = 'Selection is required' + } + + if (!formValues.complexity) { + updatedFormErrors.complexity = 'Selection is required' + } + + if (!formValues.requiresCommunicatn) { + updatedFormErrors.requiresCommunicatn = 'Selection is required' + } + + if (!formValues.paymentType) { + updatedFormErrors.paymentType = 'Selection is required' } if (!formValues.projectType) { updatedFormErrors.projectType = 'Selecting project type is required' } - if (!formValues.projectOverview) { - updatedFormErrors.projectOverview = 'Providing a project overview is required' + if (!formValues.overview) { + updatedFormErrors.overview = 'Providing a project overview is required' } if (!formValues.skills) { @@ -124,7 +170,19 @@ const CopilotRequestForm: FC<{}> = () => { } if (isEmpty(updatedFormErrors)) { - // call the API to update the trait based on action type + saveCopilotRequest(formValues) + .then(() => { + toast.success('Subscription updated successfully') + // Reset form after successful submission + setFormValues({}) + setIsFormChanged(false) + setFormErrors({}) + setExistingCopilot('') + setPaymentType('') + }) + .catch(() => { + toast.error('Error updating subscription') + }) } setFormErrors(updatedFormErrors) @@ -152,7 +210,7 @@ const CopilotRequestForm: FC<{}> = () => { label='Project' placeholder='Select the project you wish to request a copilot for' dirty - error={formErrors.project} + error={formErrors.projectId} />

Are you already working with a copilot that you'd love to work with on this project @@ -198,6 +256,12 @@ const CopilotRequestForm: FC<{}> = () => { ) } + {formErrors.existingCopilot && ( +

+ + {formErrors.existingCopilot} +

+ )}

What type of project are you working on?

= () => { noCaps leftAlignText /> + {formErrors.complexity && ( +

+ + {formErrors.complexity} +

+ )}

Please provide an overview of the project the copilot will undertake @@ -263,11 +333,11 @@ const CopilotRequestForm: FC<{}> = () => { type of work and project which is to be undertaken.' value={formValues.overview} onChange={bind(handleFormValueChange, this, 'overview')} - error={formErrors.projectOverview} - tabIndex={0} + error={formErrors.overview} + dirty />

Any specific skills or technology requirements that come to mind?

-
+
= () => { onChange={handleSkillsChange} />
+ {formErrors.skills && ( +

+ + {formErrors.skills} +

+ )}

What's the planned start date for the copilot?

= () => { onChange={bind(handleFormValueChange, this, 'requiresCommunicatn')} />
+ {formErrors.requiresCommunicatn && ( +

+ + {formErrors.requiresCommunicatn} +

+ )}

Will this role be standard payments or something else?

= () => { /> )}
+ {formErrors.paymentType && ( +

+ + {formErrors.paymentType} +

+ )} + + + + + + + {filtering && ( +
+ +
+ )} + {filtered && users.length === 0 &&

No users.

} + {filtered && users.length !== 0 && } +
+ + + ) +} + +/// ///////////////// +// Filter reducer +/// ///////////////// + +const FilterActionType = { + FILTER_INIT: 'FILTER_INIT' as const, + FILTER_DONE: 'FILTER_DONE' as const, + FILTER_FAILED: 'FILTER_FAILED' as const, +} + +type FilterState = { + isLoading: boolean + filtered: boolean + totalUsers: number +} + +type FilterReducerAction = + | { + type: typeof FilterActionType.FILTER_INIT | typeof FilterActionType.FILTER_FAILED + } + | { + type: typeof FilterActionType.FILTER_DONE + payload: { + totalUsers: number + } + } + +const filterReducer = (previousState: FilterState, action: FilterReducerAction): FilterState => { + const { type } = action + + switch (type) { + case FilterActionType.FILTER_INIT: { + return { + ...previousState, + isLoading: true, + filtered: false, + totalUsers: 0, + } + } + + case FilterActionType.FILTER_DONE: { + return { + ...previousState, + isLoading: false, + filtered: true, + totalUsers: action.payload.totalUsers, + } + } + + case FilterActionType.FILTER_FAILED: { + return { + ...previousState, + isLoading: false, + } + } + + default: { + return previousState + } + } +} + +function useFilter({ + filterCriteria, + challengeId, +}: { + filterCriteria: ChallengeResourceFilterCriteria + challengeId: string +}) { + const [state, dispatch] = useReducer(filterReducer, { + isLoading: false, + filtered: false, + totalUsers: 0, + }) + + const filter = async () => { + const checkIfLegacyId = async (id: string): Promise => { + if (/^[0-9]+$/.test(`${id}`)) { + return (await getChallengeByLegacyId(+id)).id + } + + return id + } + + dispatch({ type: FilterActionType.FILTER_INIT }) + try { + const id = await checkIfLegacyId(challengeId) + const { data, total } = await getChallengeResources(id, filterCriteria) + dispatch({ type: FilterActionType.FILTER_DONE, payload: { totalUsers: total } }) + return data + } catch (error) { + dispatch({ type: FilterActionType.FILTER_FAILED }) + handleError(error) + return [] + } + } + + return { + filtering: state.isLoading, + filtered: state.filtered, + filter, + } +} + +/// ///////////////// +// Remove reducer +/// ///////////////// + +const RemoveActionType = { + REMOVE_INIT: 'REMOVE_INIT' as const, + REMOVE_DONE: 'REMOVE_DONE' as const, + REMOVE_FAILED: 'REMOVE_FAILED' as const, +} + +type RemoveActionType = { + type: + | typeof RemoveActionType.REMOVE_INIT + | typeof RemoveActionType.REMOVE_FAILED + | typeof RemoveActionType.REMOVE_DONE +} + +type RemoveState = { + isRemoving: number + removed: boolean +} + +const removeReducer = (previousState: RemoveState, action: RemoveActionType): RemoveState => { + const { type } = action + + switch (type) { + case RemoveActionType.REMOVE_INIT: { + return { + ...previousState, + isRemoving: previousState.isRemoving + 1, + removed: false, + } + } + + case RemoveActionType.REMOVE_DONE: { + return { + ...previousState, + isRemoving: previousState.isRemoving - 1, + removed: true, + } + } + + case RemoveActionType.REMOVE_FAILED: { + return { + ...previousState, + isRemoving: previousState.isRemoving - 1, + } + } + + default: { + return previousState + } + } +} + +function useRemove({ challengeId }: { challengeId: string }) { + const [state, dispatch] = useReducer(removeReducer, { + isRemoving: 0, + removed: false, + }) + + const remove = async (user: ChallengeResource) => { + dispatch({ type: RemoveActionType.REMOVE_INIT }) + + try { + await deleteChallengeResource({ challengeId, memberHandle: user.memberHandle, roleId: user.roleId }) + dispatch({ type: RemoveActionType.REMOVE_DONE }) + return true + } catch (error) { + dispatch({ type: RemoveActionType.REMOVE_FAILED }) + handleError(error) + return false + } + } + + return { + remove, + removing: state.isRemoving !== 0, + removed: state.removed, + } +} + +export default ManageUserPage diff --git a/src/apps/admin/challenge-management/ManageUserPage/index.ts b/src/apps/admin/challenge-management/ManageUserPage/index.ts new file mode 100644 index 000000000..f1aa8824f --- /dev/null +++ b/src/apps/admin/challenge-management/ManageUserPage/index.ts @@ -0,0 +1 @@ +export { default as ManageUserPage } from './ManageUserPage' diff --git a/src/apps/admin/declarations.d.ts b/src/apps/admin/declarations.d.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/admin/index.ts b/src/apps/admin/index.ts new file mode 100644 index 000000000..5acea567f --- /dev/null +++ b/src/apps/admin/index.ts @@ -0,0 +1 @@ +export { adminRoutes, rootRoute as adminRootRoute } from './admin-app.routes' diff --git a/src/apps/admin/lib/assets/i/bars-menu-icon.svg b/src/apps/admin/lib/assets/i/bars-menu-icon.svg new file mode 100644 index 000000000..5baf49df5 --- /dev/null +++ b/src/apps/admin/lib/assets/i/bars-menu-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/apps/admin/lib/assets/i/cloud-upload-icon.svg b/src/apps/admin/lib/assets/i/cloud-upload-icon.svg new file mode 100644 index 000000000..26a0e903f --- /dev/null +++ b/src/apps/admin/lib/assets/i/cloud-upload-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/apps/admin/lib/assets/i/registrant-user-icon.svg b/src/apps/admin/lib/assets/i/registrant-user-icon.svg new file mode 100644 index 000000000..c35a87000 --- /dev/null +++ b/src/apps/admin/lib/assets/i/registrant-user-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/apps/admin/lib/assets/i/submission-icon.svg b/src/apps/admin/lib/assets/i/submission-icon.svg new file mode 100644 index 000000000..e90a3e237 --- /dev/null +++ b/src/apps/admin/lib/assets/i/submission-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/apps/admin/lib/assets/i/user-group-icon.svg b/src/apps/admin/lib/assets/i/user-group-icon.svg new file mode 100644 index 000000000..c44f9cc1f --- /dev/null +++ b/src/apps/admin/lib/assets/i/user-group-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/apps/admin/lib/components/ChallengeAddUserDialog/ChallengeAddUserDialog.module.scss b/src/apps/admin/lib/components/ChallengeAddUserDialog/ChallengeAddUserDialog.module.scss new file mode 100644 index 000000000..d77d07da2 --- /dev/null +++ b/src/apps/admin/lib/components/ChallengeAddUserDialog/ChallengeAddUserDialog.module.scss @@ -0,0 +1,42 @@ +@import '@libs/ui/styles/includes'; + +.addUserForm { + .actionButtons { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + margin-top: 20px; + } +} + +.selectUserHandles { + margin-bottom: 16px; + + .selectUserHandlesTitle { + font-weight: 700; + font-size: 14px; + padding-bottom: 2px; + } +} + +.selectUserHandlesCustomMultiValue { + font-size: 13px; + padding: 0 8px; + margin-right: 6px; + color: $black-60; + background-color: $black-10; + border-radius: 4px; + + &.invalid { + color: $red-100; + } + + .label { + font-weight: 700; + } +} + +.selectUserHandlesDropdownContainer { + z-index: 9999 !important; +} diff --git a/src/apps/admin/lib/components/ChallengeAddUserDialog/ChallengeAddUserDialog.tsx b/src/apps/admin/lib/components/ChallengeAddUserDialog/ChallengeAddUserDialog.tsx new file mode 100644 index 000000000..034df8373 --- /dev/null +++ b/src/apps/admin/lib/components/ChallengeAddUserDialog/ChallengeAddUserDialog.tsx @@ -0,0 +1,209 @@ +import { createContext, FC, KeyboardEventHandler, useContext, useMemo, useRef, useState } from 'react' +import { BaseModal, Button, IconOutline, InputSelect, InputSelectOption } from '~/libs/ui' +import { MultiValue, MultiValueProps } from 'react-select' +import { ResourceRole } from '../../models' +import { getMembersByHandle, getMemberSuggestionsByHandle } from '../../services' +import _ from 'lodash' +import CreatableSelect from 'react-select/creatable' +import cn from 'classnames' +import { ChallengeManagementContext } from '../../contexts' +import styles from './ChallengeAddUserDialog.module.scss' + +interface Option { + readonly label: string + readonly value: string +} + +export interface ChallengeAddUserDialogProps { + open: boolean + setOpen: (isOpen: boolean) => void + onAdd: (payload: { handles: string[]; roleId: string }) => void +} + +const CustomMultiValue = (props: MultiValueProps): JSX.Element => { + const { data, removeProps } = props + const { invalidHandles } = useContext(SelectUserHandlesContext) + + return ( +
+ {data.label} + + + +
+ ) +} + +const SelectUserHandlesContext = createContext<{ invalidHandles: Set }>({ invalidHandles: new Set() }) + +const SelectUserHandles: FC<{ onUsersSelect: (handles: string[]) => void }> = ({ onUsersSelect }) => { + const separatorRegEx = /[ ,\n\t;]+/ + + const components = { + DropdownIndicator: null, + MultiValue: CustomMultiValue, + } + + const [inputValue, setInputValue] = useState('') + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const invalidHandles = useRef(new Set()) + + const handleKeyDown: KeyboardEventHandler = event => { + if (!inputValue) return + + switch (event.key) { + case 'Enter': { + const words = inputValue + .trim() + .split(separatorRegEx) + // remove '' + .filter(Boolean) + // remove duplicate + .filter((word, index, arr) => arr.indexOf(word) === index) + + const newValue = [...value.map(i => i.value), ...words] + // remove duplicate + .filter((val, index, arr) => arr.indexOf(val) === index) + .map(i => ({ label: i, value: i })) + + setValue(newValue) + setInputValue('') + event.preventDefault() + onUsersSelect(newValue.map(i => i.value)) + checkHandles(newValue) + break + } + + case 'Tab': + setInputValue('') + event.preventDefault() + } + } + + const getSuggestions = _.debounce(async (inputValue: string) => { + if (!inputValue || !inputValue.trim() || inputValue.trim().length < 2) { + return + } + + inputValue = inputValue.trim() + if (inputValue.split(separatorRegEx).length == 1) { + const members = await getMemberSuggestionsByHandle(inputValue) + const newOptions: Option[] = members.map(i => ({ label: i.handle, value: i.handle })) + setOptions(newOptions) + } + }, 750) + + const checkHandles = async (value: MultiValue