),
@@ -81,6 +86,8 @@ const CopilotApplications: FC<{
fulfilment: member?.copilotFulfillment || 0,
handle: member?.handle,
opportunityStatus: props.opportunity.status,
+ pastProjects: member?.pastProjects || 0,
+ projectName: props.opportunity.projectName,
}
})
.sort((a, b) => (b.fulfilment || 0) - (a.fulfilment || 0)) : [])
@@ -90,7 +97,7 @@ const CopilotApplications: FC<{
return (
diff --git a/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/copilot-applications/styles.module.scss b/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/copilot-applications/styles.module.scss
index d31ba3f75..43efc6329 100644
--- a/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/copilot-applications/styles.module.scss
+++ b/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/copilot-applications/styles.module.scss
@@ -12,3 +12,22 @@
color: $teal-100;
}
}
+
+.noApplications {
+ margin-top: $sp-6;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: large;
+}
+
+.notes {
+ text-align: left;
+ max-width: 200px;
+}
+
+@media (max-width: 767px) {
+ .notes {
+ min-width: 200px;
+ }
+}
\ No newline at end of file
diff --git a/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/opportunity-details/OpportunityDetails.tsx b/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/opportunity-details/OpportunityDetails.tsx
index 8cd3afd44..c0376b784 100644
--- a/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/opportunity-details/OpportunityDetails.tsx
+++ b/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/opportunity-details/OpportunityDetails.tsx
@@ -11,8 +11,7 @@ const OpportunityDetails: FC<{
Required skills
- {props.opportunity?.skills.map(item => item.name)
- .join(',')}
+ {props.opportunity?.skills.map(item => ({item.name}))}
Description
diff --git a/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/opportunity-details/styles.module.scss b/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/opportunity-details/styles.module.scss
index 8debc451e..aad91a31d 100644
--- a/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/opportunity-details/styles.module.scss
+++ b/src/apps/copilots/src/pages/copilot-opportunity-details/tabs/opportunity-details/styles.module.scss
@@ -11,6 +11,13 @@
gap: 100px;
}
+@media (max-width: 767px) {
+ .content {
+ flex-direction: column;
+ gap: 15px;
+ }
+}
+
.content > div:first-child {
flex: 3;
}
diff --git a/src/apps/copilots/src/pages/copilot-opportunity-list/index.tsx b/src/apps/copilots/src/pages/copilot-opportunity-list/index.tsx
index 9c9bf4d3e..045ff0192 100644
--- a/src/apps/copilots/src/pages/copilot-opportunity-list/index.tsx
+++ b/src/apps/copilots/src/pages/copilot-opportunity-list/index.tsx
@@ -22,10 +22,10 @@ import styles from './styles.module.scss'
const tableColumns: TableColumn[] = [
{
label: 'Title',
- propertyName: 'projectName',
+ propertyName: 'opportunityTitle',
renderer: (copilotOpportunity: CopilotOpportunity) => (
- {copilotOpportunity.projectName}
+ {copilotOpportunity.opportunityTitle}
),
type: 'element',
@@ -100,9 +100,16 @@ const tableColumns: TableColumn[] = [
type: 'number',
},
{
+ isSortable: false,
label: 'Payment',
propertyName: 'paymentType',
- type: 'text',
+ renderer: (copilotOpportunity: CopilotOpportunity) => (
+
+ {copilotOpportunity.paymentType === 'standard'
+ ? copilotOpportunity.paymentType : copilotOpportunity.otherPaymentType}
+
+ ),
+ type: 'element',
},
]
diff --git a/src/apps/copilots/src/pages/copilot-opportunity-list/styles.module.scss b/src/apps/copilots/src/pages/copilot-opportunity-list/styles.module.scss
index fabdab748..ed569a42f 100644
--- a/src/apps/copilots/src/pages/copilot-opportunity-list/styles.module.scss
+++ b/src/apps/copilots/src/pages/copilot-opportunity-list/styles.module.scss
@@ -7,6 +7,12 @@
gap: 8px;
}
+@media (max-width: 767px) {
+ .title {
+ min-width: 200px;
+ }
+}
+
.skillPill {
background-color: #d6d6d6;
color: #333;
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 53d98eecb..b93dd89e9 100644
--- a/src/apps/copilots/src/pages/copilot-request-form/index.tsx
+++ b/src/apps/copilots/src/pages/copilot-request-form/index.tsx
@@ -1,7 +1,7 @@
-import { FC, useContext, useMemo, useState } from 'react'
+import { FC, useContext, useEffect, useMemo, useState } from 'react'
import { bind, debounce, isEmpty } from 'lodash'
import { toast } from 'react-toastify'
-import { useNavigate } from 'react-router-dom'
+import { Params, useNavigate, useParams, useSearchParams } from 'react-router-dom'
import classNames from 'classnames'
import { profileContext, ProfileContextData } from '~/libs/core'
@@ -9,20 +9,87 @@ import { Button, IconSolid, InputDatePicker, InputMultiselectOption,
InputRadio, InputSelect, InputSelectReact, InputText, InputTextarea } from '~/libs/ui'
import { InputSkillSelector } from '~/libs/shared'
-import { getProjects } from '../../services/projects'
+import { getProject, getProjects, ProjectsResponse, useProjects } from '../../services/projects'
import { ProjectTypes, ProjectTypeValues } from '../../constants'
-import { saveCopilotRequest } from '../../services/copilot-requests'
+import { CopilotRequestResponse, saveCopilotRequest, useCopilotRequest } from '../../services/copilot-requests'
+import { Project } from '../../models/Project'
import styles from './styles.module.scss'
+
+const editableFields = [
+ 'projectId',
+ 'opportunityTitle',
+ 'copilotUsername',
+ 'complexity',
+ 'requiresCommunication',
+ 'paymentType',
+ 'otherPaymentType',
+ 'projectType',
+ 'overview',
+ 'skills',
+ 'startDate',
+ 'numWeeks',
+ 'tzRestrictions',
+ 'numHoursPerWeek',
+]
+
// eslint-disable-next-line
const CopilotRequestForm: FC<{}> = () => {
const { profile }: ProfileContextData = useContext(profileContext)
const navigate = useNavigate()
+ const routeParams: Params = useParams()
+ const [params] = useSearchParams()
const [formValues, setFormValues] = useState({})
const [isFormChanged, setIsFormChanged] = useState(false)
const [formErrors, setFormErrors] = useState({})
const [paymentType, setPaymentType] = useState('')
+ const [projectFromQuery, setProjectFromQuery] = useState()
+
+ const { data: copilotRequestData }: CopilotRequestResponse = useCopilotRequest(routeParams.requestId)
+
+ useEffect(() => {
+ if (copilotRequestData) {
+ setFormValues(copilotRequestData)
+ }
+ }, [copilotRequestData])
+
+ const fetchProject = async (): Promise => {
+ const projectId = params.get('projectId')
+
+ if (!projectId) {
+ return
+ }
+
+ const project = await getProject(projectId as string)
+
+ setFormValues((prevValues: any) => ({
+ ...prevValues,
+ projectId: project.id,
+ }))
+ setIsFormChanged(true)
+ setProjectFromQuery(project)
+ }
+
+ useEffect(() => {
+ fetchProject()
+ }, [params])
+
+ const { data: projects = [] }: ProjectsResponse = useProjects(undefined, {
+ filter: { id: copilotRequestData?.projectId },
+ isPaused: () => !copilotRequestData?.projectId,
+ })
+
+ const projectOptions = useMemo(() => {
+ const projectsFromResponse = projects.map(p => ({
+ label: p.name,
+ value: p.id,
+ }))
+
+ return projectFromQuery
+ ? [...projectsFromResponse, { label: projectFromQuery.name, value: projectFromQuery.id }]
+ : projectsFromResponse
+ }, [projects, projectFromQuery])
const projectTypes = ProjectTypes ? ProjectTypes.map(project => ({
label: project,
@@ -63,6 +130,8 @@ const CopilotRequestForm: FC<{}> = () => {
return updatedErrors
})
+
+ setIsFormChanged(true)
}
function handleFormValueChange(
@@ -79,7 +148,12 @@ const CopilotRequestForm: FC<{}> = () => {
oldFormValues[key] = Array.isArray(value) ? [...value] : []
break
default:
- value = event.target.value
+ if (event.type === 'blur') {
+ value = event.target.value?.trim()
+ } else {
+ value = event.target.value
+ }
+
break
}
@@ -131,6 +205,11 @@ const CopilotRequestForm: FC<{}> = () => {
const updatedFormErrors: { [key: string]: string } = {}
const fieldValidations: { condition: boolean; key: string; message: string }[] = [
+ {
+ condition: (formValues.opportunityTitle?.trim().length ?? 0) < 7,
+ key: 'opportunityTitle',
+ message: 'The title for the opportunity must be at least 7 characters',
+ },
{ condition: !formValues.projectId, key: 'projectId', message: 'Project is required' },
{ condition: !formValues.complexity, key: 'complexity', message: 'Selection is required' },
{
@@ -141,7 +220,7 @@ const CopilotRequestForm: FC<{}> = () => {
{ condition: !formValues.paymentType, key: 'paymentType', message: 'Selection is required' },
{ condition: !formValues.projectType, key: 'projectType', message: 'Selecting project type is required' },
{
- condition: !formValues.overview || formValues.overview.length < 10,
+ condition: !formValues.overview || formValues.overview.trim().length < 10,
key: 'overview',
message: 'Project overview must be at least 10 characters',
},
@@ -171,7 +250,7 @@ const CopilotRequestForm: FC<{}> = () => {
message: 'Number of weeks should be a positive number',
},
{
- condition: !formValues.tzRestrictions,
+ condition: !formValues.tzRestrictions || formValues.tzRestrictions.trim().length === 0,
key: 'tzRestrictions',
message: 'Providing timezone restrictions is required. Type No if no restrictions',
},
@@ -185,6 +264,11 @@ const CopilotRequestForm: FC<{}> = () => {
key: 'numHoursPerWeek',
message: 'Number of hours per week should be a positive number',
},
+ {
+ condition: formValues.otherPaymentType && formValues.otherPaymentType.trim().length === 0,
+ key: 'otherPaymentType',
+ message: 'Field cannot be left empty',
+ },
]
fieldValidations.forEach(
@@ -198,11 +282,15 @@ const CopilotRequestForm: FC<{}> = () => {
if (isEmpty(updatedFormErrors)) {
const cleanedFormValues: any = Object.fromEntries(
Object.entries(formValues)
- .filter(([, value]) => value !== ''), // Excludes null and undefined
+ // Excludes null and undefined
+ .filter(([field, value]) => editableFields.includes(field) && value !== ''),
)
- saveCopilotRequest(cleanedFormValues)
+ saveCopilotRequest({ ...cleanedFormValues, id: copilotRequestData?.id })
.then(() => {
- toast.success('Copilot request sent successfully')
+ toast.success(
+ copilotRequestData ? 'Copilot request updated successfully'
+ : 'Copilot request sent successfully',
+ )
setFormValues({
complexity: '',
numHoursPerWeek: '',
@@ -248,12 +336,19 @@ const CopilotRequestForm: FC<{}> = () => {
+
+
Title
+
{props.request.opportunityTitle}
+
Opportunity details
= props => {
navigate(copilotRoutesMap.CopilotRequestDetails.replace(':requestId', `${props.request.id}`))
}, [navigate, props.request.id])
+ const isEditable = useMemo(() => !['canceled', 'fulfilled'].includes(props.request.status), [props.request.status])
+
+ const editRequest = useCallback(() => {
+ if (!isEditable) {
+ return
+ }
+
+ navigate(copilotRoutesMap.CopilotRequestEditForm.replace(':requestId', `${props.request.id}`))
+ }, [navigate, props.request.id, isEditable])
+
const copilotOpportunityId = props.request.opportunity?.id
const navigateToOpportunity = useCallback(() => {
@@ -78,6 +89,21 @@ const CopilotTableActions: FC<{request: CopilotRequest}> = props => {
+
+ {isEditable ? (
+
+
+
+ ) : (
+
+ )}
+
{props.request.status === 'approved'
&& (
@@ -170,6 +196,12 @@ const CopilotRequestsPage: FC = () => {
},
type: 'element',
},
+ {
+ className: styles.opportunityTitle,
+ label: 'Title',
+ propertyName: 'opportunityTitle',
+ type: 'text',
+ },
{
label: 'Type',
propertyName: 'type',
diff --git a/src/apps/copilots/src/services/copilot-requests.ts b/src/apps/copilots/src/services/copilot-requests.ts
index be46ffa75..af45f6276 100644
--- a/src/apps/copilots/src/services/copilot-requests.ts
+++ b/src/apps/copilots/src/services/copilot-requests.ts
@@ -1,7 +1,7 @@
import useSWR, { SWRResponse } from 'swr'
import { EnvironmentConfig } from '~/config'
-import { xhrGetAsync, xhrPostAsync } from '~/libs/core'
+import { xhrGetAsync, xhrPatchAsync, xhrPostAsync } from '~/libs/core'
import { buildUrl } from '~/libs/shared/lib/utils/url'
import { CopilotRequest } from '../models/CopilotRequest'
@@ -23,6 +23,7 @@ function copilotRequestFactory(data: any): CopilotRequest {
createdAt: new Date(data.createdAt),
data: undefined,
opportunity: data.copilotOpportunity?.[0],
+ startDate: new Date(data.data?.startDate),
}
}
@@ -54,8 +55,8 @@ export type CopilotRequestResponse = SWRResponse
* @param {string} requestId - The unique identifier of the copilot request.
* @returns {CopilotRequestResponse} - The response containing the copilot request data.
*/
-export const useCopilotRequest = (requestId: string): CopilotRequestResponse => {
- const url = buildUrl(`${baseUrl}/copilots/requests/${requestId}`)
+export const useCopilotRequest = (requestId?: string): CopilotRequestResponse => {
+ const url = requestId && buildUrl(`${baseUrl}/copilots/requests/${requestId}`)
const fetcher = (urlp: string): Promise => xhrGetAsync(urlp)
.then(copilotRequestFactory)
@@ -74,12 +75,14 @@ export const useCopilotRequest = (requestId: string): CopilotRequestResponse =>
*/
export const saveCopilotRequest = (request: CopilotRequest)
: Promise => {
- const url = `${baseUrl}/${request.projectId}/copilots/requests`
+ const url = request.id
+ ? `${baseUrl}/copilots/requests/${request.id}` : `${baseUrl}/${request.projectId}/copilots/requests`
+
const requestData = {
- data: request,
+ data: { ...request, id: undefined },
}
- return xhrPostAsync(url, requestData, {})
+ return request.id ? xhrPatchAsync(url, requestData) : xhrPostAsync(url, requestData, {})
}
/**
diff --git a/src/apps/copilots/src/services/members.ts b/src/apps/copilots/src/services/members.ts
index 85b05a16c..3ab23b867 100644
--- a/src/apps/copilots/src/services/members.ts
+++ b/src/apps/copilots/src/services/members.ts
@@ -12,6 +12,7 @@ interface Member {
COPILOT: {
activeProjects: number,
fulfillment: number,
+ projects: number,
}
}[]
}
@@ -19,6 +20,7 @@ interface Member {
export interface FormattedMembers extends Member {
copilotFulfillment: number,
activeProjects: number,
+ pastProjects: number;
}
export type MembersResponse = SWRResponse
@@ -40,11 +42,16 @@ export const getMembersByUserIds = async (
)
}
-const membersFactory = (members: Member[]): FormattedMembers[] => members.map(member => ({
- ...member,
- activeProjects: member.stats?.find(item => item.COPILOT?.activeProjects)?.COPILOT?.activeProjects || 0,
- copilotFulfillment: member.stats?.find(item => item.COPILOT?.fulfillment)?.COPILOT?.fulfillment || 0,
-}))
+const membersFactory = (members: Member[]): FormattedMembers[] => members.map(member => {
+ const copilotStats = member.stats?.find(item => item.COPILOT)?.COPILOT ?? {} as Member['stats'][0]['COPILOT']
+
+ return {
+ ...member,
+ activeProjects: copilotStats.activeProjects || 0,
+ copilotFulfillment: copilotStats.fulfillment || 0,
+ pastProjects: copilotStats.projects || 0,
+ }
+})
/**
* Custom hook to fetch members by list of user ids
diff --git a/src/apps/copilots/src/services/projects.ts b/src/apps/copilots/src/services/projects.ts
index 45b26131e..096c373ef 100644
--- a/src/apps/copilots/src/services/projects.ts
+++ b/src/apps/copilots/src/services/projects.ts
@@ -11,6 +11,8 @@ const baseUrl = `${EnvironmentConfig.API.V5}/projects`
export type ProjectsResponse = SWRResponse
+const sleep = (ms: number): Promise<()=> void> => new Promise(resolve => { setTimeout(resolve, ms) })
+
/**
* Custom hook to fetch and manage projects data.
*
@@ -24,13 +26,26 @@ export const useProjects = (search?: string, config?: {isPaused?: () => boolean,
const params = { name: search, ...config?.filter }
const url = buildUrl(baseUrl, params)
- const fetcher = (): Promise => {
- if (config?.filter?.id && Array.isArray(config.filter.id)) {
- const chunks = chunk(config.filter.id, 20)
- return Promise.all(
- chunks.map(page => xhrGetAsync(buildUrl(baseUrl, { ...params, id: page }))),
- )
- .then(responses => responses.flat())
+ const fetcher = async (): Promise => {
+ const ids = config?.filter?.id
+
+ if (Array.isArray(ids)) {
+ const idChunks = chunk(ids, 20)
+ const allResults: Project[] = []
+
+ for (const chunkIds of idChunks) {
+ // eslint-disable-next-line no-await-in-loop
+ const response = await xhrGetAsync(
+ buildUrl(baseUrl, { ...params, id: chunkIds }),
+ )
+ allResults.push(...response)
+
+ // Rate limit: delay 200ms between calls
+ // eslint-disable-next-line no-await-in-loop
+ await sleep(200)
+ }
+
+ return allResults
}
return xhrGetAsync(url)
@@ -43,6 +58,11 @@ export const useProjects = (search?: string, config?: {isPaused?: () => boolean,
})
}
+export const getProject = (projectId: string): Promise => {
+ const url = `${baseUrl}/${projectId}`
+ return xhrGetAsync(url)
+}
+
export const getProjects = (search?: string, filter?: any): Promise => {
const params = { name: `"${search}"`, ...filter }
const url = buildUrl(baseUrl, params)
diff --git a/src/libs/ui/lib/components/content-layout/ContentLayout.module.scss b/src/libs/ui/lib/components/content-layout/ContentLayout.module.scss
index 7cc8a3ac9..628902a35 100644
--- a/src/libs/ui/lib/components/content-layout/ContentLayout.module.scss
+++ b/src/libs/ui/lib/components/content-layout/ContentLayout.module.scss
@@ -36,6 +36,12 @@
color: $black-100;
}
}
+
+ @media (max-width: 767px) {
+ .page-header {
+ flex-direction: column;
+ }
+ }
}
}
}
diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.tsx b/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.tsx
index 2c4840898..ce15c0cf3 100644
--- a/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.tsx
+++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-date-picker/InputDatePicker.tsx
@@ -26,6 +26,7 @@ interface InputDatePickerProps {
readonly maxTime?: Date | undefined
readonly minDate?: Date | null | undefined
readonly minTime?: Date | undefined
+ readonly minYear?: Date | null |undefined
readonly placeholder?: string
readonly showMonthPicker?: boolean
readonly showYearPicker?: boolean
@@ -76,7 +77,8 @@ const InputDatePicker: FC = (props: InputDatePickerProps)
const datePickerRef = useRef>(null)
const years = useMemo(() => {
const maxYear = getYear(props.maxDate ? props.maxDate : new Date()) + 1
- return range(1979, maxYear, 1)
+ const minYear = getYear(props.minYear ? props.minYear : 1979)
+ return range(minYear, maxYear, 1)
}, [props.maxDate])
const [stateHasFocus, setStateHasFocus] = useState(false)
diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.module.scss b/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.module.scss
index 3fe20ea51..761a49e51 100644
--- a/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.module.scss
+++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.module.scss
@@ -77,7 +77,7 @@
&:global(__single-value) {
@extend .body-small;
- color: $black-60;
+ color: $black-100;
white-space: break-spaces;
word-break: break-all;
text-align: left;
diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.tsx b/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.tsx
index 224f73928..0aea4daa5 100644
--- a/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.tsx
+++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.tsx
@@ -48,6 +48,7 @@ interface InputSelectReactProps {
readonly async?: boolean
readonly loadOptions?: (inputValue: string, callback: (option: any) => void) => void
readonly filterOption?: (option: InputSelectOption, value: string) => boolean
+ readonly isClearable?: boolean
}
/**
@@ -105,11 +106,12 @@ const InputSelectReact: FC = props => {
// throw the proper event type to the form handler (needs name & form element on target)
function handleSelect(option: unknown): void {
+ const selectedOption = option as InputSelectOption | null
props.onChange({
target: {
form: findParentFrom(wrapRef.current as HTMLDivElement),
name: props.name,
- value: (option as InputSelectOption).value,
+ value: selectedOption?.value || '',
},
} as ChangeEvent)
}
@@ -162,6 +164,7 @@ const InputSelectReact: FC = props => {
formatCreateLabel={props.createLabel}
onCreateOption={props.onCreateOption}
onBlur={handleBlur}
+ isClearable={props.isClearable}
backspaceRemovesValue
isDisabled={props.disabled}
filterOption={props.filterOption}
diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-select/InputSelect.module.scss b/src/libs/ui/lib/components/form/form-groups/form-input/input-select/InputSelect.module.scss
index d3673be51..16505e78b 100644
--- a/src/libs/ui/lib/components/form/form-groups/form-input/input-select/InputSelect.module.scss
+++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-select/InputSelect.module.scss
@@ -5,7 +5,7 @@
align-items: center;
margin-top: $sp-1;
cursor: pointer;
- color: $black-60;
+ color: $black-100;
&-icon {
margin-left: auto;
diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText.module.scss b/src/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText.module.scss
index 555e29b8e..2ef89273c 100644
--- a/src/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText.module.scss
+++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-text/InputText.module.scss
@@ -3,7 +3,7 @@
.form-input-text {
@extend .body-small;
- color: $black-60;
+ color: $black-100;
box-sizing: border-box;
border: 0;
width: 100%;
diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-textarea/InputTextarea.module.scss b/src/libs/ui/lib/components/form/form-groups/form-input/input-textarea/InputTextarea.module.scss
index 0b5e6eee4..078aa5a13 100644
--- a/src/libs/ui/lib/components/form/form-groups/form-input/input-textarea/InputTextarea.module.scss
+++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-textarea/InputTextarea.module.scss
@@ -4,7 +4,7 @@
.form-input-textarea {
@include font-roboto;
@extend .body-small;
- color: $black-60;
+ color: $black-100;
box-sizing: border-box;
border: none;
outline: none;
diff --git a/src/libs/ui/lib/components/table/table-functions/table.functions.ts b/src/libs/ui/lib/components/table/table-functions/table.functions.ts
index 10fd8bbde..736b58eff 100644
--- a/src/libs/ui/lib/components/table/table-functions/table.functions.ts
+++ b/src/libs/ui/lib/components/table/table-functions/table.functions.ts
@@ -65,6 +65,11 @@ export function getSorted(
.sort((a: T, b: T) => {
const aField: string = a[sort.fieldName]
const bField: string = b[sort.fieldName]
+
+ // Handle undefined/null values safely
+ if (aField === undefined && bField === undefined) return 0
+ if (aField === undefined) return 1
+ if (bField === undefined) return -1
return sort.direction === 'asc'
? aField.localeCompare(bField)
: bField.localeCompare(aField)
diff --git a/src/libs/ui/lib/components/tabs-navbar/TabsNavbar.tsx b/src/libs/ui/lib/components/tabs-navbar/TabsNavbar.tsx
index 0b2d8f019..9d0ea8c76 100644
--- a/src/libs/ui/lib/components/tabs-navbar/TabsNavbar.tsx
+++ b/src/libs/ui/lib/components/tabs-navbar/TabsNavbar.tsx
@@ -125,7 +125,7 @@ const TabsNavbar: FC = (props: TabsNavbarProps) => {
handleActivateTab={handleActivateTab}
handleActivateChildTab={handleActivateChildTab}
/>
-
+ {props.tabs.length > 1 && }
diff --git a/yarn.lock b/yarn.lock
index 6fd04826d..afe2bf405 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2529,6 +2529,18 @@
resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
+"@isaacs/balanced-match@^4.0.1":
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29"
+ integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==
+
+"@isaacs/brace-expansion@^5.0.0":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3"
+ integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==
+ dependencies:
+ "@isaacs/balanced-match" "^4.0.1"
+
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -7722,6 +7734,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
+cross-spawn@^7.0.6:
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+ integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
crypto-js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
@@ -9995,6 +10016,14 @@ foreground-child@^3.1.0:
cross-spawn "^7.0.0"
signal-exit "^4.0.1"
+foreground-child@^3.3.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
+ integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
+ dependencies:
+ cross-spawn "^7.0.6"
+ signal-exit "^4.0.1"
+
fork-ts-checker-webpack-plugin@^6.5.0:
version "6.5.2"
resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz#4f67183f2f9eb8ba7df7177ce3cf3e75cdafb340"
@@ -10314,6 +10343,18 @@ glob@^10.0.0:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
+glob@^11.0.0:
+ version "11.0.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6"
+ integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==
+ dependencies:
+ foreground-child "^3.3.1"
+ jackspeak "^4.1.1"
+ minimatch "^10.0.3"
+ minipass "^7.1.2"
+ package-json-from-dist "^1.0.0"
+ path-scurry "^2.0.0"
+
glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -11410,6 +11451,13 @@ jackspeak@^3.1.2:
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
+jackspeak@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae"
+ integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==
+ dependencies:
+ "@isaacs/cliui" "^8.0.2"
+
jake@^10.8.5:
version "10.8.5"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
@@ -12814,6 +12862,11 @@ lru-cache@^10.2.0:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
+lru-cache@^11.0.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.1.0.tgz#afafb060607108132dbc1cf8ae661afb69486117"
+ integrity sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==
+
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -13542,6 +13595,13 @@ minimatch@3.1.2, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch
dependencies:
brace-expansion "^1.1.7"
+minimatch@^10.0.3:
+ version "10.0.3"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa"
+ integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==
+ dependencies:
+ "@isaacs/brace-expansion" "^5.0.0"
+
minimatch@^5.0.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.1.tgz#6c9dffcf9927ff2a31e74b5af11adf8b9604b022"
@@ -14261,6 +14321,14 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+path-scurry@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580"
+ integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==
+ dependencies:
+ lru-cache "^11.0.0"
+ minipass "^7.1.2"
+
path-to-regexp@0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
@@ -16543,6 +16611,14 @@ rimraf@^3.0.0, rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
+rimraf@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.1.tgz#ffb8ad8844dd60332ab15f52bc104bc3ed71ea4e"
+ integrity sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==
+ dependencies:
+ glob "^11.0.0"
+ package-json-from-dist "^1.0.0"
+
rimraf@~2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
@@ -17439,7 +17515,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -17453,6 +17529,13 @@ strip-ansi@^3.0.0:
dependencies:
ansi-regex "^2.0.0"
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
strip-ansi@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
@@ -19291,7 +19374,7 @@ workbox-window@6.5.4:
"@types/trusted-types" "^2.0.2"
workbox-core "6.5.4"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -19309,6 +19392,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.0.1.tgz#2101e861777fec527d0ea90c57c6b03aac56a5b3"