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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ def process_module(self, module: Module) -> None:
.distinct()
)

if not module.labels:
self.stdout.write(
self.style.WARNING(f"Skipping module '{module.name}' - no labels configured.")
)
return

if not module_repos.exists():
self.stdout.write(
self.style.WARNING(f"Skipping. Module '{module.name}' has no repositories.")
Expand Down
7 changes: 7 additions & 0 deletions frontend/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ const eslintConfig = [
'nest/no-global-nan': 'error',
'nest/no-global-parsefloat': 'error',
'nest/no-global-parseint': 'error',
'no-restricted-syntax': [
'error',
{
selector: "TSAsExpression[expression.type='CallExpression'][expression.callee.name='useParams']",
message: "Do not cast useParams(). Use generic type instead: useParams<{ param: string }>()",
},
],
quotes: ['error', 'single', { avoidEscape: true }],
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { GetProgramAdminsAndModulesDocument } from 'types/__generated__/moduleQu
import type { Module } from 'types/mentorship'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
import DetailsCardSkeleton from 'components/DetailsCardSkeleton'
import { getSimpleDuration } from 'components/ModuleCard'

const ModuleDetailsPage = () => {
Expand All @@ -36,7 +36,7 @@ const ModuleDetailsPage = () => {
}
}, [data, error])

if (isLoading) return <LoadingSpinner />
if (isLoading) return <DetailsCardSkeleton />

if (!module) {
return (
Expand All @@ -48,8 +48,19 @@ const ModuleDetailsPage = () => {
)
}

// @ts-ignore
if (data?.getProgram?.status !== 'PUBLISHED') {
return (
<ErrorDisplay
statusCode={404}
title="Program Not Found"
message="Sorry, the program for this module is not published."
/>
)
}

const moduleDetails = [
{ label: 'Experience Level', value: upperFirst(module.experienceLevel) },
{ label: 'Experience Level', value: upperFirst(module.experienceLevel.toLowerCase()) },
{ label: 'Start Date', value: formatDate(module.startedAt) },
{ label: 'End Date', value: formatDate(module.endedAt) },
{
Expand All @@ -63,7 +74,6 @@ const ModuleDetailsPage = () => {
admins={admins}
details={moduleDetails}
domains={module.domains}
labels={module.labels}
mentors={module.mentors}
summary={module.description}
tags={module.tags}
Expand Down
29 changes: 19 additions & 10 deletions frontend/src/app/mentorship/programs/[programKey]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
import { ErrorDisplay } from 'app/global-error'
import { GetProgramAndModulesDocument } from 'types/__generated__/programsQueries.generated'

import type { Module, Program } from 'types/mentorship'

Check warning on line 9 in frontend/src/app/mentorship/programs/[programKey]/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'Module'.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZq1NHsIZfwgLsnkaLF3&open=AZq1NHsIZfwgLsnkaLF3&pullRequest=2708

Check warning on line 9 in frontend/src/app/mentorship/programs/[programKey]/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'Program'.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZq1NHsIZfwgLsnkaLF4&open=AZq1NHsIZfwgLsnkaLF4&pullRequest=2708
import { titleCaseWord } from 'utils/capitalize'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
import DetailsCardSkeleton from 'components/DetailsCardSkeleton'

const ProgramDetailsPage = () => {
const { programKey } = useParams() as { programKey: string }
const params = useParams<{ programKey: string }>()
const searchParams = useSearchParams()
const router = useRouter()
const shouldRefresh = searchParams.get('refresh') === 'true'
Expand All @@ -22,16 +22,16 @@
refetch,
loading: isQueryLoading,
} = useQuery(GetProgramAndModulesDocument, {
variables: { programKey },
skip: !programKey,
variables: { programKey: params.programKey },
skip: !params.programKey,
notifyOnNetworkStatusChange: true,
})

const [program, setProgram] = useState<Program | null>(null)
const [modules, setModules] = useState<Module[]>([])
const program = data?.getProgram
const modules = data?.getProgramModules || []
const [isRefetching, setIsRefetching] = useState(false)

const isLoading = isQueryLoading || isRefetching
const isLoading = isQueryLoading || isRefetching || !program

useEffect(() => {
const processResult = async () => {
Expand All @@ -50,15 +50,14 @@
}

if (data?.getProgram) {
setProgram(data.getProgram)
setModules(data.getProgramModules || [])
// Data is already assigned to variables above
}
}

processResult()
}, [shouldRefresh, data, refetch, router, searchParams])

if (isLoading) return <LoadingSpinner />
if (isLoading) return <DetailsCardSkeleton />

if (!program && !isLoading) {
return (
Expand All @@ -70,6 +69,16 @@
)
}

if (program && program.status !== 'PUBLISHED') {
return (
<ErrorDisplay
statusCode={404}
title="Program Not Found"
message="Sorry, the program you're looking for doesn't exist."
/>
)
}

const programDetails = [
{ label: 'Status', value: titleCaseWord(program.status) },
{ label: 'Start Date', value: formatDate(program.startedAt) },
Expand Down
170 changes: 113 additions & 57 deletions frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,130 @@
import { ErrorDisplay, handleAppError } from 'app/global-error'
import { ProgramStatusEnum } from 'types/__generated__/graphql'
import { UpdateProgramDocument } from 'types/__generated__/programsMutations.generated'
import { GetProgramDetailsDocument } from 'types/__generated__/programsQueries.generated'
import { GetProgramAndModulesDocument } from 'types/__generated__/programsQueries.generated'
import type { ExtendedSession } from 'types/auth'
import { formatDateForInput } from 'utils/dateFormatter'
import { parseCommaSeparated } from 'utils/parser'
import slugify from 'utils/slugify'
import { validateTags } from 'utils/validators'
import LoadingSpinner from 'components/LoadingSpinner'
import ProgramForm from 'components/ProgramForm'
import { useForm } from 'hooks/useForm'
const EditProgramPage = () => {
const router = useRouter()
const { programKey } = useParams() as { programKey: string }
const { programKey } = useParams<{ programKey: string }>()
const { data: session, status: sessionStatus } = useSession()
const [updateProgram, { loading: mutationLoading }] = useMutation(UpdateProgramDocument)

Check warning on line 24 in frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless assignment to variable "mutationLoading".

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZq1NHrjZfwgLsnkaLF1&open=AZq1NHrjZfwgLsnkaLF1&pullRequest=2708
const {
data,
error,
loading: queryLoading,
} = useQuery(GetProgramDetailsDocument, {
} = useQuery(GetProgramAndModulesDocument, {
variables: { programKey },
skip: !programKey,
fetchPolicy: 'network-only',
})
const [formData, setFormData] = useState({
name: '',
description: '',
menteesLimit: 5,
startedAt: '',
endedAt: '',
tags: '',
domains: '',
adminLogins: '',
status: ProgramStatusEnum.Draft,
})

const [accessStatus, setAccessStatus] = useState<'checking' | 'allowed' | 'denied'>('checking')

const validate = (values: any) => {

Check failure on line 37 in frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZq1NHrjZfwgLsnkaLF2&open=AZq1NHrjZfwgLsnkaLF2&pullRequest=2708
const errors: Record<string, string> = {}

if (!values.name) errors.name = 'Name is required'
if (!values.description) errors.description = 'Description is required'
if (!values.startedAt) errors.startedAt = 'Start date is required'
if (!values.endedAt) errors.endedAt = 'End date is required'

if (values.startedAt && values.endedAt) {
if (new Date(values.startedAt) > new Date(values.endedAt)) {
errors.startedAt = 'Start date cannot be after end date'
}
}

if (data?.getProgramModules) {
const programStart = new Date(values.startedAt).getTime()
const programEnd = new Date(values.endedAt).getTime()

for (const module of data.getProgramModules) {
const moduleStart = new Date(module.startedAt).getTime()
const moduleEnd = new Date(module.endedAt).getTime()

if (moduleStart < programStart) {
errors.startedAt = `Module "${module.name}" starts before the program.`
}

if (moduleEnd > programEnd) {
errors.endedAt = `Module "${module.name}" ends after the program.`
}
}
}

const tagError = validateTags(values.tags)
if (tagError) errors.tags = tagError

return errors
}

const { values, errors, isSubmitting, handleSubmit, setValues, setErrors } = useForm({
initialValues: {
name: '',
description: '',
menteesLimit: 5,
startedAt: '',
endedAt: '',
tags: '',
domains: '',
adminLogins: '',
status: ProgramStatusEnum.Draft,
},
validate,
onSubmit: async (values) => {
try {
const input = {
key: programKey,
name: values.name,
description: values.description,
menteesLimit: Number(values.menteesLimit),
startedAt: values.startedAt,
endedAt: values.endedAt,
tags: parseCommaSeparated(values.tags),
domains: parseCommaSeparated(values.domains),
adminLogins: parseCommaSeparated(values.adminLogins),
status: values.status as ProgramStatusEnum,
}

await updateProgram({ variables: { input } })

addToast({
title: 'Program Updated',
description: 'The program has been successfully updated.',
color: 'success',
variant: 'solid',
timeout: 3000,
})

router.push(`/my/mentorship/programs/${slugify(values.name)}`)
} catch (err: any) {
let errorMessage = 'There was an error updating the program.'
if (err?.message?.toLowerCase().includes('duplicate') || err?.message?.toLowerCase().includes('unique')) {
errorMessage = 'A program with this name already exists. Please choose a different name.'
setErrors((prev) => ({ ...prev, name: errorMessage }))
}

addToast({
title: 'Update Failed',
description: errorMessage,
color: 'danger',
variant: 'solid',
timeout: 3000,
})
if (errorMessage !== 'A program with this name already exists. Please choose a different name.') {
handleAppError(err)
}
}
},
})

useEffect(() => {
if (sessionStatus === 'loading' || queryLoading) {
return
Expand Down Expand Up @@ -68,10 +159,11 @@
setTimeout(() => router.replace('/my/mentorship/programs'), 1500)
}
}, [sessionStatus, session, data, queryLoading, router])

useEffect(() => {
if (accessStatus === 'allowed' && data?.getProgram) {
const { getProgram: program } = data
setFormData({
setValues({
name: program.name || '',
description: program.description || '',
menteesLimit: program.menteesLimit ?? 5,
Expand All @@ -87,46 +179,9 @@
} else if (error) {
handleAppError(error)
}
}, [accessStatus, data, error])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
const input = {
key: programKey,
name: formData.name,
description: formData.description,
menteesLimit: Number(formData.menteesLimit),
startedAt: formData.startedAt,
endedAt: formData.endedAt,
tags: parseCommaSeparated(formData.tags),
domains: parseCommaSeparated(formData.domains),
adminLogins: parseCommaSeparated(formData.adminLogins),
status: formData.status,
}

await updateProgram({ variables: { input } })

addToast({
title: 'Program Updated',
description: 'The program has been successfully updated.',
color: 'success',
variant: 'solid',
timeout: 3000,
})
}, [accessStatus, data, error, setValues])

router.push(`/my/mentorship/programs/${slugify(formData.name)}`)
} catch (err) {
addToast({
title: 'Update Failed',
description: 'There was an error updating the program.',
color: 'danger',
variant: 'solid',
timeout: 3000,
})
handleAppError(err)
}
}
if (accessStatus === 'checking') {
if (accessStatus === 'checking' || (queryLoading && !values.name)) {
return <LoadingSpinner />
}
if (accessStatus === 'denied') {
Expand All @@ -140,13 +195,14 @@
}
return (
<ProgramForm
formData={formData}
setFormData={setFormData}
formData={values}
setFormData={setValues}
onSubmit={handleSubmit}
loading={mutationLoading}
loading={isSubmitting}
title="Edit Program"
submitText="Save"
isEdit={true}
errors={errors}
/>
)
}
Expand Down
Loading