diff --git a/backend/api/completions.ts b/backend/api/completions.ts index 11e158d09..e19f31926 100644 --- a/backend/api/completions.ts +++ b/backend/api/completions.ts @@ -152,6 +152,7 @@ export class CompletionController { const { user } = getUserResult.value const { slug } = req.params + // TODO: typing let tierData: any = [] const course = ( diff --git a/backend/bin/kafkaConsumer/common/createKafkaConsumer.ts b/backend/bin/kafkaConsumer/common/createKafkaConsumer.ts index 5c872253f..87c21a128 100644 --- a/backend/bin/kafkaConsumer/common/createKafkaConsumer.ts +++ b/backend/bin/kafkaConsumer/common/createKafkaConsumer.ts @@ -16,7 +16,8 @@ import { KafkaError } from "../../lib/errors" import checkConnectionInInterval from "./connectedChecker" const logCommit = - (logger: winston.Logger) => (err: any, topicPartitions: any) => { + (logger: winston.Logger) => + (err: any, topicPartitions: Kafka.TopicPartition[]) => { if (err) { logger.error(new KafkaError("Error in commit", err)) } else { diff --git a/backend/bin/kafkaConsumer/common/userCourseProgress/interfaces.ts b/backend/bin/kafkaConsumer/common/userCourseProgress/interfaces.ts index 7f1d8cd64..3ea034c21 100644 --- a/backend/bin/kafkaConsumer/common/userCourseProgress/interfaces.ts +++ b/backend/bin/kafkaConsumer/common/userCourseProgress/interfaces.ts @@ -3,7 +3,7 @@ export interface Message { user_id: number course_id: string service_id: string - progress: [PointsByGroup] + progress: PointsByGroup[] message_format_version: Number } diff --git a/backend/bin/kafkaConsumer/common/userCourseProgress/validate.ts b/backend/bin/kafkaConsumer/common/userCourseProgress/validate.ts index d4ca523fe..c4bc42a40 100644 --- a/backend/bin/kafkaConsumer/common/userCourseProgress/validate.ts +++ b/backend/bin/kafkaConsumer/common/userCourseProgress/validate.ts @@ -1,6 +1,8 @@ import { Message as KafkaMessage } from "node-rdkafka" import * as yup from "yup" +import { Message } from "./interfaces" + const CURRENT_MESSAGE_FORMAT_VERSION = 1 const PointsByGroupYupSchema = yup.object().shape({ @@ -32,14 +34,14 @@ export const MessageYupSchema = yup.object().shape({ .required(), }) -const handleNullProgressImpl = (value: any) => ({ +const handleNullProgressImpl = (value: Message) => ({ ...value, - progress: value?.progress?.map((progress: any) => ({ - ...progress, + progress: value?.progress?.map((pointsByGroup) => ({ + ...pointsByGroup, progress: - progress.progress === null || isNaN(progress.progress) + pointsByGroup.progress === null || isNaN(pointsByGroup.progress) ? 0 - : progress.progress, + : pointsByGroup.progress, })), }) diff --git a/backend/bin/kafkaConsumer/common/userFunctions.ts b/backend/bin/kafkaConsumer/common/userFunctions.ts index 770a0c132..7bb101ae1 100644 --- a/backend/bin/kafkaConsumer/common/userFunctions.ts +++ b/backend/bin/kafkaConsumer/common/userFunctions.ts @@ -21,15 +21,20 @@ import { ServiceProgressType, } from "./userCourseProgress/interfaces" +interface WithKafkaContext { + context: KafkaContext +} + +interface GetCombinedUserCourseProgressArgs extends WithKafkaContext { + user: User + course: Course +} + export const getCombinedUserCourseProgress = async ({ user, course, context: { prisma }, -}: { - user: User - course: Course - context: KafkaContext -}): Promise => { +}: GetCombinedUserCourseProgressArgs): Promise => { const userCourseServiceProgresses = await prisma.user .findUnique({ where: { id: user.id } }) .user_course_service_progresses({ @@ -61,15 +66,16 @@ export const getCombinedUserCourseProgress = async ({ return combined } +interface CheckRequiredExerciseCompletionsArgs extends WithKafkaContext { + user: User + course: Course +} + export const checkRequiredExerciseCompletions = async ({ user, course, context: { knex }, -}: { - user: User - course: Course - context: KafkaContext -}): Promise => { +}: CheckRequiredExerciseCompletionsArgs): Promise => { if (course.exercise_completions_needed) { const exercise_completions = await knex("exercise_completion") .countDistinct("exercise_completion.exercise_id") @@ -85,15 +91,16 @@ export const checkRequiredExerciseCompletions = async ({ return true } +interface GetExerciseCompletionsForCoursesArgs extends WithKafkaContext { + user: User + courseIds: string[] +} + export const getExerciseCompletionsForCourses = async ({ user, courseIds, context: { knex }, -}: { - user: User - courseIds: string[] - context: KafkaContext -}) => { +}: GetExerciseCompletionsForCoursesArgs) => { // picks only one exercise completion per exercise/user: // the one with the latest timestamp and latest updated_at const exercise_completions: ExerciseCompletionPart[] = await knex( @@ -118,15 +125,16 @@ export const getExerciseCompletionsForCourses = async ({ return exercise_completions // ?.rows ?? [] } +interface PruneDuplicateExerciseCompletionsArgs extends WithKafkaContext { + user_id: string + course_id: string +} + export const pruneDuplicateExerciseCompletions = async ({ user_id, course_id, context: { knex }, -}: { - user_id: string - course_id: string - context: KafkaContext -}) => { +}: PruneDuplicateExerciseCompletionsArgs) => { // variation: only prune those with the latest timestamp but older updated_at /*const deleted: Array> = await knex( "exercise_completion", @@ -198,9 +206,7 @@ export const pruneDuplicateExerciseCompletions = async ({ export const pruneOrphanedExerciseCompletionRequiredActions = async ({ context: { knex }, -}: { - context: KafkaContext -}) => { +}: WithKafkaContext) => { const deleted: Array> = await knex("exercise_completion_required_actions") .whereNull("exercise_completion_id") @@ -210,15 +216,16 @@ export const pruneOrphanedExerciseCompletionRequiredActions = async ({ return deleted } +interface GetUserCourseSettingsArgs extends WithKafkaContext { + user_id: string + course_id: string +} + export const getUserCourseSettings = async ({ user_id, course_id, context: { prisma }, -}: { - user_id: string - course_id: string - context: KafkaContext -}): Promise => { +}: GetUserCourseSettingsArgs): Promise => { // - if the course inherits user course settings from some course, get settings from that one // - if not, get from the course itself or null if none exists const result = await prisma.course.findUnique({ @@ -256,12 +263,11 @@ export const getUserCourseSettings = async ({ ) } -interface CheckCompletionArgs { +interface CheckCompletionArgs extends WithKafkaContext { user: User course: Course handler?: Course | null combinedProgress?: CombinedUserCourseProgress - context: KafkaContext } export const checkCompletion = async ({ @@ -301,12 +307,11 @@ export const checkCompletion = async ({ } } -interface CreateCompletionArgs { +interface CreateCompletionArgs extends WithKafkaContext { user: User course: Course handler?: Course | null tier?: number - context: KafkaContext } export const createCompletion = async ({ diff --git a/backend/bin/kafkaConsumer/exerciseConsumer/interfaces.ts b/backend/bin/kafkaConsumer/exerciseConsumer/interfaces.ts index 53356a802..92a82601d 100644 --- a/backend/bin/kafkaConsumer/exerciseConsumer/interfaces.ts +++ b/backend/bin/kafkaConsumer/exerciseConsumer/interfaces.ts @@ -2,7 +2,7 @@ export interface Message { timestamp: string course_id: string service_id: string - data: ExerciseData[] //[ExerciseData] + data: ExerciseData[] message_format_version: number } diff --git a/backend/bin/seedPoints.ts b/backend/bin/seedPoints.ts index 12171a068..c3d13ffdb 100644 --- a/backend/bin/seedPoints.ts +++ b/backend/bin/seedPoints.ts @@ -5,21 +5,20 @@ import { Prisma } from "@prisma/client" import prisma from "../prisma" //Generate integer id which is not already taken -function generateUniqueUpstreamId({ ExistingIds }: { ExistingIds: number[] }) { +function generateUniqueUpstreamId(existingIds: number[]) { //take the largest possible integer - const LargestPossibleUpstreamId = 2147483647 - let UniqueIntId = 0 + const MAX_INTEGER = 2147483647 + let uniqueIntId = 0 //Go down from the largest possible integer //until value not already in use is found - let i: number - for (i = LargestPossibleUpstreamId; i > 0; i--) { - if (ExistingIds.indexOf(i) === -1) { - UniqueIntId = i - return UniqueIntId + for (let i = MAX_INTEGER; i > 0; i--) { + if (existingIds.indexOf(i) === -1) { + uniqueIntId = i + return uniqueIntId } } - return UniqueIntId + return uniqueIntId } function generateRandomString() { @@ -30,10 +29,12 @@ function generateRandomString() { } const addUsers = async () => { - //get existing users from database - const UsersInDatabase = await prisma.user.findMany() - //create a list of upstream ids already in use - let UpstreamIdsInUse = UsersInDatabase.map((user) => user.upstream_id) + //get existing upstream_ids + const upstreamIdsInUse = ( + await prisma.user.findMany({ + select: { upstream_id: true }, + }) + ).map((user) => user.upstream_id) //Generate random data for 100 users //and add them to the database let i = 0 @@ -42,7 +43,7 @@ const addUsers = async () => { const last_name = faker.name.lastName() const newUser = { - upstream_id: generateUniqueUpstreamId({ ExistingIds: UpstreamIdsInUse }), + upstream_id: generateUniqueUpstreamId(upstreamIdsInUse), first_name, last_name, username: faker.internet.userName(first_name, last_name), @@ -52,7 +53,7 @@ const addUsers = async () => { real_student_number: generateRandomString(), } //add new upstreamId to ids already in use - UpstreamIdsInUse = UpstreamIdsInUse.concat(newUser.upstream_id) + upstreamIdsInUse.push(newUser.upstream_id) await prisma.user.create({ data: newUser }) i += 1 @@ -71,10 +72,11 @@ const addServices = async () => { } } -const addUserCourseProgressess = async ({ courseId }: { courseId: string }) => { - const UsersInDb = await prisma.user.findMany({ take: 100 }) +const addUserCourseProgressess = async (courseId: string) => { + const usersInDb = await prisma.user.findMany({ take: 100 }) + return await Promise.all( - UsersInDb.map(async (user) => { + usersInDb.map(async (user) => { const progress = [ { group: "week1", @@ -129,7 +131,7 @@ const addUserCourseProgressess = async ({ courseId }: { courseId: string }) => { ) } -const addUserCourseSettingses = async ({ courseId }: { courseId: string }) => { +const addUserCourseSettingses = async (courseId: string) => { const UsersInDb = await prisma.user.findMany({ take: 100 }) return await Promise.all( UsersInDb.map(async (user) => { @@ -160,11 +162,14 @@ const seedPointsData = async () => { const course = await prisma.course.findUnique({ where: { slug: "elements-of-ai" }, }) - console.log("course", course) + await addUsers() await addServices() - course && (await addUserCourseProgressess({ courseId: course.id })) - course && (await addUserCourseSettingses({ courseId: course.id })) + + if (course) { + await addUserCourseProgressess(course.id) + await addUserCourseSettingses(course.id) + } } seedPointsData().finally(() => process.exit(0)) diff --git a/backend/graphql/CompletionRegistered.ts b/backend/graphql/CompletionRegistered.ts index 77bab901a..fd2167871 100644 --- a/backend/graphql/CompletionRegistered.ts +++ b/backend/graphql/CompletionRegistered.ts @@ -1,6 +1,15 @@ import { ForbiddenError } from "apollo-server-express" import { chunk } from "lodash" -import { arg, extendType, intArg, list, objectType, stringArg } from "nexus" +import { + arg, + extendType, + intArg, + list, + nonNull, + objectType, + stringArg, +} from "nexus" +import { type NexusGenInputs } from "nexus-typegen" import { Prisma } from "@prisma/client" @@ -109,11 +118,11 @@ export const CompletionRegisteredMutations = extendType({ t.field("registerCompletion", { type: "String", args: { - completions: list(arg({ type: "CompletionArg" })), + completions: nonNull(list(nonNull(arg({ type: "CompletionArg" })))), }, authorize: isOrganization, resolve: async (_, args, ctx: Context) => { - let queue = chunk(args.completions, 500) + const queue = chunk(args.completions, 500) for (let i = 0; i < queue.length; i++) { const promises = buildPromises(queue[i], ctx) @@ -125,7 +134,10 @@ export const CompletionRegisteredMutations = extendType({ }, }) -const buildPromises = (array: any[], ctx: Context) => { +const buildPromises = ( + array: Array, + ctx: Context, +) => { return array.map(async (entry) => { const { user_id, course_id } = (await ctx.prisma.completion.findUnique({ diff --git a/backend/graphql/Course/mutations.ts b/backend/graphql/Course/mutations.ts index 8c0a3d1a4..12003926e 100644 --- a/backend/graphql/Course/mutations.ts +++ b/backend/graphql/Course/mutations.ts @@ -10,6 +10,7 @@ import { Context } from "../../context" import KafkaProducer, { ProducerMessage } from "../../services/kafkaProducer" import { invalidate } from "../../services/redis" import { convertUpdate } from "../../util/db-functions" +import { notEmpty } from "../../util/notEmpty" import { deleteImage, uploadImage } from "../Image" const isNotNull = (value: T | null | undefined): value is T => @@ -374,18 +375,16 @@ export const CourseMutations = extendType({ }, }) -const getIds = (arr: any[]) => (arr || []).map((t) => t.id) -const filterNotIncluded = (arr1: any[], arr2: any[], mapToId = true) => { +type WithIdOrNull = (object & { id?: string | null }) | null +const getIds = (arr: WithIdOrNull[]) => (arr || []).map((t) => t?.id) + +function filterNotIncluded(arr1: WithIdOrNull[], arr2: WithIdOrNull[]) { const ids1 = getIds(arr1) const ids2 = getIds(arr2) - const filtered = ids1.filter((id) => !ids2.includes(id)) - - if (mapToId) { - return filtered.map((id) => ({ id })) - } + const filtered = ids1.filter((id) => !ids2.includes(id)).filter(notEmpty) - return filtered + return filtered.map((id) => ({ id })) } interface ICreateMutation { @@ -395,7 +394,7 @@ interface ICreateMutation { field: keyof Prisma.Prisma__CourseClient } -const createMutation = async ({ +const createMutation = async ({ ctx, slug, data, @@ -420,7 +419,7 @@ const createMutation = async ({ const updated = (data || []) .filter(hasId) // (t) => !!t.id) .map((t) => ({ - where: { id: t.id } as { id: string }, + where: { id: t.id }, data: t, //{ ...t, id: undefined }, })) const removed = filterNotIncluded(existing!, data) @@ -432,8 +431,6 @@ const createMutation = async ({ } } -const hasId = ( - data: T, -): data is any & { id: string | null } => Boolean(data?.id) -const hasNotId = (data: T) => - !hasId(data) +const hasId = (data: T): data is T & { id: string } => + Boolean(data?.id) +const hasNotId = (data: T) => !hasId(data) diff --git a/backend/graphql/Image.ts b/backend/graphql/Image.ts index e6636f599..1285b2b33 100644 --- a/backend/graphql/Image.ts +++ b/backend/graphql/Image.ts @@ -1,10 +1,13 @@ +import { ReadStream } from "fs" + +import { FileUpload } from "graphql-upload" import { arg, booleanArg, extendType, idArg, nonNull, objectType } from "nexus" import { isAdmin } from "../accessControl" import { Context } from "../context" import { - deleteImage as deleteStorageImage, - uploadImage as uploadStorageImage, + deleteStorageImage, + uploadStorageImage, } from "../services/google-cloud" const sharp = require("sharp") @@ -58,35 +61,29 @@ export const ImageMutations = extendType({ }, }) -const readFS = (stream: NodeJS.ReadStream): Promise => { - let chunkList: any[] | Uint8Array[] = [] +const readFS = (stream: ReadStream): Promise => { + const chunkList: Uint8Array[] = [] return new Promise((resolve, reject) => stream - .on("data", (data) => chunkList.push(data)) + .on("data", (data: Buffer) => chunkList.push(data)) .on("error", (err) => reject(err)) .on("end", () => resolve(Buffer.concat(chunkList))), ) } +interface UploadImageArgs { + ctx: Context + file: Promise + base64: boolean +} + export const uploadImage = async ({ ctx, file, base64 = false, -}: { - ctx: Context - file: any - base64: boolean -}) => { - const { - createReadStream, - mimetype, - filename, - }: { - createReadStream: Function - mimetype: string - filename: string - } = await file +}: UploadImageArgs) => { + const { createReadStream, mimetype, filename } = await file const image: Buffer = await readFS(createReadStream()) const filenameWithoutExtension = /(.+?)(\.[^.]*$|$)$/.exec(filename)?.[1] @@ -145,13 +142,15 @@ export const uploadImage = async ({ return newImage } +interface DeleteImageArgs { + ctx: Context + id: string +} + export const deleteImage = async ({ ctx, id, -}: { - ctx: Context - id: string -}): Promise => { +}: DeleteImageArgs): Promise => { const image = await ctx.prisma.image.findUnique({ where: { id } }) if (!image) { diff --git a/backend/graphql/Upload.ts b/backend/graphql/Upload.ts index dd6cf724c..66389b22f 100644 --- a/backend/graphql/Upload.ts +++ b/backend/graphql/Upload.ts @@ -5,5 +5,6 @@ export type UploadRoot = Promise export const Upload = scalarType({ ...GraphQLUpload!, + name: "Upload", rootTyping: "UploadRoot", }) diff --git a/backend/graphql/UserCourseProgress.ts b/backend/graphql/UserCourseProgress.ts index c1b6bfaa7..c41dfa307 100644 --- a/backend/graphql/UserCourseProgress.ts +++ b/backend/graphql/UserCourseProgress.ts @@ -93,7 +93,7 @@ export const UserCourseProgress = objectType({ throw new Error("no course or user found") } - const courseProgress: any = normalizeProgress(progress) + const courseProgress = normalizeProgress(progress) // TODO: this should probably also only count completed exercises! const exercises = await ctx.prisma.course diff --git a/backend/schema.ts b/backend/schema.ts index da141cbde..1eb26408d 100644 --- a/backend/schema.ts +++ b/backend/schema.ts @@ -69,7 +69,11 @@ export default makeSchema({ module: require.resolve(".prisma/client/index.d.ts"), alias: "prisma", }, + { module: "@types/graphql-upload/index.d.ts", alias: "upload" }, ], + mapping: { + Upload: "upload.Upload['promise']", + }, }, plugins: createPlugins(), outputs: { diff --git a/backend/services/google-cloud.ts b/backend/services/google-cloud.ts index bfe20a05f..4ce9b167d 100644 --- a/backend/services/google-cloud.ts +++ b/backend/services/google-cloud.ts @@ -38,19 +38,21 @@ const storage = const bucket = storage.bucket(GOOGLE_CLOUD_STORAGE_BUCKET ?? "") // this shouldn't ever happen in production -export const uploadImage = async ({ - imageBuffer, - mimeType, - name = "", - directory = "", - base64 = false, -}: { +interface UploadStorageImageArgs { imageBuffer: Buffer mimeType: string name?: string directory?: string base64?: boolean -}): Promise => { +} + +export const uploadStorageImage = async ({ + imageBuffer, + mimeType, + name = "", + directory = "", + base64 = false, +}: UploadStorageImageArgs): Promise => { const filename = `${directory ? directory + "/" : ""}${shortid.generate()}${ name && name !== "" ? "-" + name : "" }.${mime.extension(mimeType)}` @@ -83,7 +85,9 @@ export const uploadImage = async ({ }) } -export const deleteImage = async (filename: string): Promise => { +export const deleteStorageImage = async ( + filename: string, +): Promise => { if (!filename || filename === "") { return Promise.resolve(false) } diff --git a/backend/util/server-functions.ts b/backend/util/server-functions.ts index 9cb857a01..c470ee044 100644 --- a/backend/util/server-functions.ts +++ b/backend/util/server-functions.ts @@ -13,13 +13,15 @@ interface GetUserReturn { details: UserInfo } +interface RequireCourseOwnershipArgs { + course_id: string + ctx: ApiContext +} + export function requireCourseOwnership({ course_id, ctx, -}: { - course_id: string - ctx: ApiContext -}) { +}: RequireCourseOwnershipArgs) { return async function ( req: Request, res: Response, diff --git a/docs/graphql.md b/docs/graphql.md index 04531252e..fef7093e5 100644 --- a/docs/graphql.md +++ b/docs/graphql.md @@ -2,13 +2,13 @@ ## Frontend types -If you make changes to the GraphQL schema, resolvers etc. in the backend and/or the queries/mutations in the frontend, you probably need to regenerate the Typescript types for the frontend. +If you make changes to the GraphQL schema, resolvers etc. in the backend and/or the operations/definitions in the frontend, you probably need to regenerate the Typescript types for the frontend. ### In brief: -Ensure you have a fresh GraphQL schema in the backend by running `npm run generate` in the backend folder. +Ensure you have a fresh GraphQL schema by running `npm run generate` in the backend folder. -Then run `npm run graphql-codegen` in the frontend folder. +Then run `npm run graphql-codegen` in the frontend folder. You can also run `npm run graphql-codegen --watch` to watch for changes and regenerate the frontend types automatically. ## Detailed example from backend to frontend: diff --git a/frontend/components/Dashboard/CompletionCard.tsx b/frontend/components/Dashboard/CompletionCard.tsx index c1b8376d6..29e8d6f24 100644 --- a/frontend/components/Dashboard/CompletionCard.tsx +++ b/frontend/components/Dashboard/CompletionCard.tsx @@ -36,11 +36,11 @@ const ListItemArea = styled.div` margin: 1rem auto 1rem auto; ` -function CompletionCard({ - completer, -}: { +interface CompletionCardProps { completer: CompletionsQueryNodeFieldsFragment -}) { +} + +function CompletionCard({ completer }: CompletionCardProps) { const completionLanguage = MapLangToLanguage[completer?.completion_language ?? ""] ?? "No language available" diff --git a/frontend/components/Dashboard/Editor/Course/CourseLanguageSelector.tsx b/frontend/components/Dashboard/Editor/Course/CourseLanguageSelector.tsx index dc92e53dd..051f80548 100644 --- a/frontend/components/Dashboard/Editor/Course/CourseLanguageSelector.tsx +++ b/frontend/components/Dashboard/Editor/Course/CourseLanguageSelector.tsx @@ -33,7 +33,7 @@ const StyledLanguageButton = styled(Button)` border: 1px solid #83acda; color: black; } - &: disabled { + &:disabled { box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.5); background-color: #354b45; color: white; @@ -43,8 +43,9 @@ const StyledLanguageButton = styled(Button)` ` interface LanguageSelectorProps { selectedLanguage: string - setSelectedLanguage: any + setSelectedLanguage: React.Dispatch> } + const CourseLanguageSelector = (props: LanguageSelectorProps) => { const { selectedLanguage, setSelectedLanguage } = props const t = useTranslator(CoursesTranslations) diff --git a/frontend/components/Dashboard/Editor/Course/form-validation.ts b/frontend/components/Dashboard/Editor/Course/form-validation.ts index 4daeb8fb6..ef8ed2e17 100644 --- a/frontend/components/Dashboard/Editor/Course/form-validation.ts +++ b/frontend/components/Dashboard/Editor/Course/form-validation.ts @@ -11,6 +11,8 @@ import { UserCourseSettingsVisibilityFormValues, } from "./types" import { FormValues } from "/components/Dashboard/Editor/types" +import { Translator } from "/translations" +import { type CoursesTranslations } from "/translations/courses" import { CourseFromSlugDocument, @@ -85,7 +87,7 @@ export const initialVisibility: UserCourseSettingsVisibilityFormValues = { course: undefined, } -export const statuses = (t: Function) => [ +export const statuses = (t: Translator) => [ { value: CourseStatus.Upcoming, label: t("courseUpcoming"), @@ -100,7 +102,7 @@ export const statuses = (t: Function) => [ }, ] -export const languages = (t: Function) => [ +export const languages = (t: Translator) => [ { value: "fi_FI", label: t("courseFinnish"), @@ -149,15 +151,13 @@ const testUnique = ( return otherValues.indexOf(value) === -1 } -const courseEditSchema = ({ - client, - initialSlug, - t, -}: { +interface CourseEditSchemaArgs { client: ApolloClient initialSlug: string | null - t: (key: any) => string -}) => + t: Translator +} + +const courseEditSchema = ({ client, initialSlug, t }: CourseEditSchemaArgs) => Yup.object().shape({ name: Yup.string().required(t("validationRequired")), new_slug: Yup.string() @@ -268,13 +268,12 @@ const courseEditSchema = ({ .min(0), }) -const validateSlug = ({ - client, - initialSlug, -}: { +interface ValidateSlugArgs { client: ApolloClient initialSlug: string | null -}) => +} + +const validateSlug = ({ client, initialSlug }: ValidateSlugArgs) => async function ( this: Yup.TestContext, value?: string | null, diff --git a/frontend/components/Dashboard/Editor/Course/serialization.ts b/frontend/components/Dashboard/Editor/Course/serialization.ts index bdecdfe2b..3a643240c 100644 --- a/frontend/components/Dashboard/Editor/Course/serialization.ts +++ b/frontend/components/Dashboard/Editor/Course/serialization.ts @@ -15,13 +15,15 @@ import { const isProduction = process.env.NODE_ENV === "production" +interface ToCourseFormArgs { + course?: EditorCourseDetailedFieldsFragment + modules?: StudyModuleDetailedFieldsFragment[] +} + export const toCourseForm = ({ course, modules, -}: { - course?: EditorCourseDetailedFieldsFragment - modules?: StudyModuleDetailedFieldsFragment[] -}): CourseFormValues => { +}: ToCourseFormArgs): CourseFormValues => { const courseStudyModules = course?.study_modules?.map((module) => module.id) ?? [] @@ -85,13 +87,15 @@ export const toCourseForm = ({ : initialValues } +interface FromCourseFormArgs { + values: CourseFormValues + initialValues: CourseFormValues +} + export const fromCourseForm = ({ values, initialValues, -}: { - values: CourseFormValues - initialValues: CourseFormValues -}): CourseCreateArg | CourseUpsertArg => { +}: FromCourseFormArgs): CourseCreateArg | CourseUpsertArg => { const newCourse = !values.id const course_translations = diff --git a/frontend/components/Dashboard/Editor/StudyModule/StudyModuleEditForm.tsx b/frontend/components/Dashboard/Editor/StudyModule/StudyModuleEditForm.tsx index bd552b310..6cd6f3d2f 100644 --- a/frontend/components/Dashboard/Editor/StudyModule/StudyModuleEditForm.tsx +++ b/frontend/components/Dashboard/Editor/StudyModule/StudyModuleEditForm.tsx @@ -285,13 +285,7 @@ const RenderForm = () => { ) } -const StudyModuleEditForm = ({ - module, - validationSchema, - onSubmit, - onCancel, - onDelete, -}: { +interface StudyModuleEditFormProps { module: StudyModuleFormValues validationSchema: Yup.ObjectSchema onSubmit: ( @@ -300,7 +294,15 @@ const StudyModuleEditForm = ({ ) => void onCancel: () => void onDelete: (values: StudyModuleFormValues) => void -}) => { +} + +const StudyModuleEditForm = ({ + module, + validationSchema, + onSubmit, + onCancel, + onDelete, +}: StudyModuleEditFormProps) => { const validate = useCallback(async (values: StudyModuleFormValues) => { try { await validationSchema.validate(values, { diff --git a/frontend/components/Dashboard/Editor/StudyModule/form-validation.ts b/frontend/components/Dashboard/Editor/StudyModule/form-validation.ts index 4f054ef25..0980ada09 100644 --- a/frontend/components/Dashboard/Editor/StudyModule/form-validation.ts +++ b/frontend/components/Dashboard/Editor/StudyModule/form-validation.ts @@ -6,6 +6,8 @@ import { StudyModuleFormValues, StudyModuleTranslationFormValues, } from "./types" +import { Translator } from "/translations" +import { type StudyModulesTranslations } from "/translations/study-modules" import { StudyModuleExistsDocument } from "/graphql/generated" @@ -25,7 +27,7 @@ export const initialValues: StudyModuleFormValues = { study_module_translations: [initialTranslation], } -export const languages = (t: Function) => [ +export const languages = (t: Translator) => [ { value: "fi_FI", label: t("moduleFinnish"), @@ -40,15 +42,17 @@ export const languages = (t: Function) => [ }, ] +interface StudyModuleEditSchemaArgs { + client: ApolloClient + initialSlug: string | null + t: Translator +} + const studyModuleEditSchema = ({ client, initialSlug, t, -}: { - client: ApolloClient - initialSlug: string | null - t: (key: any) => string -}) => +}: StudyModuleEditSchemaArgs) => Yup.object().shape({ new_slug: Yup.string() .required(t("validationRequired")) @@ -111,13 +115,12 @@ const studyModuleEditSchema = ({ .integer(t("validationInteger")), }) -const validateSlug = ({ - client, - initialSlug, -}: { +interface ValidateSlugArgs { client: ApolloClient initialSlug: string | null -}) => +} + +const validateSlug = ({ client, initialSlug }: ValidateSlugArgs) => async function ( this: Yup.TestContext, value?: string | null, diff --git a/frontend/components/Dashboard/Editor/StudyModule/index.tsx b/frontend/components/Dashboard/Editor/StudyModule/index.tsx index 07f1c98dc..89466820a 100644 --- a/frontend/components/Dashboard/Editor/StudyModule/index.tsx +++ b/frontend/components/Dashboard/Editor/StudyModule/index.tsx @@ -23,11 +23,11 @@ import { UpdateStudyModuleDocument, } from "/graphql/generated" -const StudyModuleEdit = ({ - module, -}: { +interface StudyModuleEditProps { module?: StudyModuleDetailedFieldsFragment -}) => { +} + +const StudyModuleEdit = ({ module }: StudyModuleEditProps) => { const t = useTranslator(ModulesTranslations) const [addStudyModule] = useMutation(AddStudyModuleDocument) diff --git a/frontend/components/Dashboard/Editor/StudyModule/serialization.ts b/frontend/components/Dashboard/Editor/StudyModule/serialization.ts index 97734a596..cee343a04 100644 --- a/frontend/components/Dashboard/Editor/StudyModule/serialization.ts +++ b/frontend/components/Dashboard/Editor/StudyModule/serialization.ts @@ -12,13 +12,13 @@ import { StudyModuleUpsertArg, } from "/graphql/generated" -interface ToStudyModuleFormProps { +interface ToStudyModuleFormArgs { module?: StudyModuleDetailedFieldsFragment } export const toStudyModuleForm = ({ module, -}: ToStudyModuleFormProps): StudyModuleFormValues => +}: ToStudyModuleFormArgs): StudyModuleFormValues => module ? { ...module, @@ -29,11 +29,13 @@ export const toStudyModuleForm = ({ } : initialValues +interface FromStudyModuleFormArgs { + values: StudyModuleFormValues +} + export const fromStudyModuleForm = ({ values, -}: { - values: StudyModuleFormValues -}): StudyModuleCreateArg | StudyModuleUpsertArg => { +}: FromStudyModuleFormArgs): StudyModuleCreateArg | StudyModuleUpsertArg => { const study_module_translations = values?.study_module_translations?.map( (c: StudyModuleTranslationFormValues) => ({ ...omit(c, "__typename"), diff --git a/frontend/components/Dashboard/Editor/common.tsx b/frontend/components/Dashboard/Editor/common.tsx index ca502dbd9..befbbaf47 100644 --- a/frontend/components/Dashboard/Editor/common.tsx +++ b/frontend/components/Dashboard/Editor/common.tsx @@ -78,15 +78,13 @@ export const AdjustingAnchorLink = styled.a<{ id: string }>` visibility: hidden; ` -export const CheckboxField = ({ - id, - label, - checked, -}: { +interface CheckboxFieldProps { id: string label: string checked: boolean -}) => { +} + +export const CheckboxField = ({ id, label, checked }: CheckboxFieldProps) => { const { setFieldValue } = useFormikContext() return ( diff --git a/frontend/components/Dashboard/Editor2/Common/Fields/ControlledHiddenField.tsx b/frontend/components/Dashboard/Editor2/Common/Fields/ControlledHiddenField.tsx index ad0a5b36d..a5d160ef7 100644 --- a/frontend/components/Dashboard/Editor2/Common/Fields/ControlledHiddenField.tsx +++ b/frontend/components/Dashboard/Editor2/Common/Fields/ControlledHiddenField.tsx @@ -2,13 +2,15 @@ import { Controller, useFormContext } from "react-hook-form" import notEmpty from "/util/notEmpty" +interface ControlledHiddenFieldProps { + name: string + defaultValue: any +} + export const ControlledHiddenField = ({ name, defaultValue, -}: { - name: string - defaultValue: any -}) => { +}: ControlledHiddenFieldProps) => { const { control } = useFormContext() return ( diff --git a/frontend/components/Dashboard/Editor2/Common/Fields/FieldController.tsx b/frontend/components/Dashboard/Editor2/Common/Fields/FieldController.tsx index 4d6f2bc7e..3316d1df3 100644 --- a/frontend/components/Dashboard/Editor2/Common/Fields/FieldController.tsx +++ b/frontend/components/Dashboard/Editor2/Common/Fields/FieldController.tsx @@ -48,7 +48,7 @@ export function FieldController({ ( + render={({ message }) => ( {message} diff --git a/frontend/components/Dashboard/Editor2/Course/CourseLanguageSelector.tsx b/frontend/components/Dashboard/Editor2/Course/CourseLanguageSelector.tsx index d6c90a016..6e96fd2d4 100644 --- a/frontend/components/Dashboard/Editor2/Course/CourseLanguageSelector.tsx +++ b/frontend/components/Dashboard/Editor2/Course/CourseLanguageSelector.tsx @@ -50,8 +50,9 @@ const StyledLanguageButton = styled(Button)` ` interface LanguageSelectorProps { selectedLanguage: string - setSelectedLanguage: any + setSelectedLanguage: React.Dispatch> } + function CourseLanguageSelector(props: LanguageSelectorProps) { const { selectedLanguage, setSelectedLanguage } = props diff --git a/frontend/components/Dashboard/Editor2/Course/form-validation.tsx b/frontend/components/Dashboard/Editor2/Course/form-validation.tsx index 4a2b78b3a..5d8eb6657 100644 --- a/frontend/components/Dashboard/Editor2/Course/form-validation.tsx +++ b/frontend/components/Dashboard/Editor2/Course/form-validation.tsx @@ -11,6 +11,8 @@ import { UserCourseSettingsVisibilityFormValues, } from "./types" import { testUnique } from "/components/Dashboard/Editor2/Common" +import { Translator } from "/translations" +import { type CoursesTranslations } from "/translations/courses" import { CourseFromSlugDocument, CourseStatus } from "/graphql/generated" @@ -84,15 +86,13 @@ export const initialVisibility: UserCourseSettingsVisibilityFormValues = { export const study_modules: { value: any; label: any }[] = [] -const courseEditSchema = ({ - client, - initialSlug, - t, -}: { +interface CourseEditSchemaArgs { client: ApolloClient initialSlug: string | null - t: (key: any) => string -}) => { + t: Translator +} + +const courseEditSchema = ({ client, initialSlug, t }: CourseEditSchemaArgs) => { return Yup.object().shape({ name: Yup.string().required(t("validationRequired")), new_slug: Yup.string() @@ -207,13 +207,12 @@ const courseEditSchema = ({ }) } -const validateSlug = ({ - client, - initialSlug, -}: { +interface ValidateSlugArgs { client: ApolloClient initialSlug: string | null -}) => +} + +const validateSlug = ({ client, initialSlug }: ValidateSlugArgs) => async function ( this: Yup.TestContext, value?: string | null, diff --git a/frontend/components/Dashboard/Editor2/Course/index.tsx b/frontend/components/Dashboard/Editor2/Course/index.tsx index 8ba470e98..657d8f628 100644 --- a/frontend/components/Dashboard/Editor2/Course/index.tsx +++ b/frontend/components/Dashboard/Editor2/Course/index.tsx @@ -77,7 +77,7 @@ function CourseEditor({ course, courses, studyModules }: CourseEditProps) { ], }) - const onSubmit = useCallback(async (values: CourseFormValues, _?: any) => { + const onSubmit = useCallback(async (values: CourseFormValues) => { const newCourse = !values.id const mutationVariables = fromCourseForm({ values, @@ -120,7 +120,7 @@ function CourseEditor({ course, courses, studyModules }: CourseEditProps) { }, []) const onError: SubmitErrorHandler = useCallback( - (errors: Record, _?: any) => { + (errors: Record) => { const { anchor, anchorLink } = getFirstErrorAnchor(anchors, errors) setTab(anchor?.tab ?? 0) diff --git a/frontend/components/Dashboard/Editor2/Course/serialization.ts b/frontend/components/Dashboard/Editor2/Course/serialization.ts index 934cc2171..fe219f251 100644 --- a/frontend/components/Dashboard/Editor2/Course/serialization.ts +++ b/frontend/components/Dashboard/Editor2/Course/serialization.ts @@ -14,13 +14,15 @@ import { const isProduction = process.env.NODE_ENV === "production" +interface ToCourseFormArgs { + course?: EditorCourseDetailedFieldsFragment + modules?: StudyModuleDetailedFieldsFragment[] +} + export const toCourseForm = ({ course, modules, -}: { - course?: EditorCourseDetailedFieldsFragment - modules?: StudyModuleDetailedFieldsFragment[] -}): CourseFormValues => { +}: ToCourseFormArgs): CourseFormValues => { const courseStudyModules = course?.study_modules?.map((module) => module.id) ?? [] @@ -105,7 +107,7 @@ export const toCourseForm = ({ : initialValues } -interface FromCourseFormProps { +interface FromCourseFormArgs { values: CourseFormValues initialValues: CourseFormValues } @@ -113,7 +115,7 @@ interface FromCourseFormProps { export const fromCourseForm = ({ values, initialValues, -}: FromCourseFormProps): CourseCreateArg | CourseUpsertArg => { +}: FromCourseFormArgs): CourseCreateArg | CourseUpsertArg => { const newCourse = !values.id console.log(values) diff --git a/frontend/components/Dashboard/Editor2/StudyModule/form-validation.ts b/frontend/components/Dashboard/Editor2/StudyModule/form-validation.ts index 929a6fb57..3a72d91d1 100644 --- a/frontend/components/Dashboard/Editor2/StudyModule/form-validation.ts +++ b/frontend/components/Dashboard/Editor2/StudyModule/form-validation.ts @@ -7,6 +7,8 @@ import { StudyModuleTranslationFormValues, } from "./types" import { testUnique } from "/components/Dashboard/Editor2/Common" +import { Translator } from "/translations" +import { type StudyModulesTranslations } from "/translations/study-modules" import { StudyModuleExistsDocument } from "/graphql/generated" @@ -26,7 +28,7 @@ export const initialValues: StudyModuleFormValues = { study_module_translations: [initialTranslation], } -export const languages = (t: Function) => [ +export const languages = (t: Translator) => [ { value: "fi_FI", label: t("moduleFinnish"), @@ -55,15 +57,17 @@ function validateImage(this: Yup.TestContext, _value?: any): boolean { return true } +interface StudyModuleEditSchemaArgs { + client: ApolloClient + initialSlug: string | null + t: Translator +} + const studyModuleEditSchema = ({ client, initialSlug, t, -}: { - client: ApolloClient - initialSlug: string | null - t: (key: any) => string -}) => +}: StudyModuleEditSchemaArgs) => Yup.object().shape({ new_slug: Yup.string() .required(t("validationRequired")) @@ -102,13 +106,12 @@ const studyModuleEditSchema = ({ .integer(t("validationInteger")), }) -const validateSlug = ({ - client, - initialSlug, -}: { +interface ValidateSlugArgs { client: ApolloClient initialSlug: string | null -}) => +} + +const validateSlug = ({ client, initialSlug }: ValidateSlugArgs) => async function ( this: Yup.TestContext, value?: string | null, diff --git a/frontend/components/Dashboard/Editor2/StudyModule/index.tsx b/frontend/components/Dashboard/Editor2/StudyModule/index.tsx index a1d86833d..1cb05c4c2 100644 --- a/frontend/components/Dashboard/Editor2/StudyModule/index.tsx +++ b/frontend/components/Dashboard/Editor2/StudyModule/index.tsx @@ -29,11 +29,11 @@ import { UpdateStudyModuleDocument, } from "/graphql/generated" -const StudyModuleEdit = ({ - module, -}: { +interface StudyModuleEditProps { module?: StudyModuleDetailedFieldsFragment -}) => { +} + +const StudyModuleEdit = ({ module }: StudyModuleEditProps) => { const t = useTranslator(ModulesTranslations) const [status, setStatus] = useState({ message: null }) const client = useApolloClient() diff --git a/frontend/components/Dashboard/Editor2/StudyModule/serialization.ts b/frontend/components/Dashboard/Editor2/StudyModule/serialization.ts index d6c255bb2..9fde5cb86 100644 --- a/frontend/components/Dashboard/Editor2/StudyModule/serialization.ts +++ b/frontend/components/Dashboard/Editor2/StudyModule/serialization.ts @@ -12,11 +12,13 @@ import { StudyModuleUpsertArg, } from "/graphql/generated" +interface ToStudyModuleFormArgs { + module?: StudyModuleDetailedFieldsFragment +} + export const toStudyModuleForm = ({ module, -}: { - module?: StudyModuleDetailedFieldsFragment -}): StudyModuleFormValues => +}: ToStudyModuleFormArgs): StudyModuleFormValues => module ? { ...module, @@ -31,11 +33,13 @@ export const toStudyModuleForm = ({ } : initialValues +interface FromStudyModuleFormArgs { + values: StudyModuleFormValues +} + export const fromStudyModuleForm = ({ values, -}: { - values: StudyModuleFormValues -}): StudyModuleCreateArg | StudyModuleUpsertArg => { +}: FromStudyModuleFormArgs): StudyModuleCreateArg | StudyModuleUpsertArg => { const study_module_translations = values?.study_module_translations?.map( (c: StudyModuleTranslationFormValues) => ({ ...omit(c, ["__typename", "_id"]), diff --git a/frontend/components/Dashboard/ImagePreview.tsx b/frontend/components/Dashboard/ImagePreview.tsx index aac9bde05..8375b36c3 100644 --- a/frontend/components/Dashboard/ImagePreview.tsx +++ b/frontend/components/Dashboard/ImagePreview.tsx @@ -35,17 +35,19 @@ const CloseButton = styled(ButtonBase)` } ` +interface ImagePreviewProps { + file: string | undefined + onClose: Function | null + height?: number + [key: string]: any +} + const ImagePreview = ({ file, onClose = null, height = 250, ...rest -}: { - file: string | undefined - onClose: Function | null - height?: number - [key: string]: any -}) => { +}: ImagePreviewProps) => { if (!file) { return null } diff --git a/frontend/components/Dashboard/PointsExportButton.tsx b/frontend/components/Dashboard/PointsExportButton.tsx index cbdf09d0f..5f5449148 100644 --- a/frontend/components/Dashboard/PointsExportButton.tsx +++ b/frontend/components/Dashboard/PointsExportButton.tsx @@ -81,7 +81,7 @@ async function flatten( const { course_variant, country, language } = datum?.user_course_settings ?? {} - const newDatum: any = { + const newDatum = { user_id: upstream_id, first_name: first_name?.replace(/\s+/g, " ").trim() ?? "", last_name: last_name?.replace(/\s+/g, " ").trim() ?? "", @@ -108,7 +108,7 @@ async function flatten( async function downloadInChunks( courseSlug: string, client: ApolloClient, - setMessage: any, + setMessage: React.Dispatch>, ): Promise { const res: ExportUserCourseProgressesQuery["userCourseProgresses"] = [] // let after: string | undefined = undefined diff --git a/frontend/components/Dashboard/PointsProgress.tsx b/frontend/components/Dashboard/PointsProgress.tsx index 649a1202a..25356cd36 100644 --- a/frontend/components/Dashboard/PointsProgress.tsx +++ b/frontend/components/Dashboard/PointsProgress.tsx @@ -25,7 +25,12 @@ const ChartContainer = styled.div` margin-bottom: 1rem; ` -const PointsProgress = ({ total, title }: { total: number; title: string }) => ( +interface PointsProgressProps { + total: number + title: string +} + +const PointsProgress = ({ total, title }: PointsProgressProps) => ( <> = () => { } = useContext(UserSearchContext) const handleChangeRowsPerPage = useCallback( - async ({ eventValue }: { eventValue: string }) => { + async (eventValue: string) => { const newRowsPerPage = parseInt(eventValue, 10) setSearchVariables({ @@ -186,7 +186,7 @@ const Pagination: React.FC = () => { onPageChange={() => null} onRowsPerPageChange={( event: ChangeEvent, - ) => handleChangeRowsPerPage({ eventValue: event.target.value })} + ) => handleChangeRowsPerPage(event.target.value)} ActionsComponent={() => } /> ) diff --git a/frontend/components/HeaderBar/UserOptionsMenu.tsx b/frontend/components/HeaderBar/UserOptionsMenu.tsx index c7d89d5d3..4f794f2f9 100644 --- a/frontend/components/HeaderBar/UserOptionsMenu.tsx +++ b/frontend/components/HeaderBar/UserOptionsMenu.tsx @@ -9,11 +9,12 @@ import { signOut } from "/lib/authentication" import CommonTranslations from "/translations/common" import { useTranslator } from "/util/useTranslator" -interface Props { +interface UserOptionsMenuProps { isSignedIn: boolean - logInOrOut: any + logInOrOut: Function } -const UserOptionsMenu = (props: Props) => { + +const UserOptionsMenu = (props: UserOptionsMenuProps) => { const client = useApolloClient() const { isSignedIn, logInOrOut } = props const t = useTranslator(CommonTranslations) diff --git a/frontend/components/Home/ModuleDisplay/Common.tsx b/frontend/components/Home/ModuleDisplay/Common.tsx index 9225416e9..83eed70cb 100644 --- a/frontend/components/Home/ModuleDisplay/Common.tsx +++ b/frontend/components/Home/ModuleDisplay/Common.tsx @@ -60,7 +60,12 @@ const ModuleImageBase = styled.img` width: 100%; ` -export const ModuleImage = ({ src, alt }: { src: string; alt?: string }) => ( +interface ModuleImageProps { + src: string + alt?: string +} + +export const ModuleImage = ({ src, alt }: ModuleImageProps) => ( ` ` interface DisplayBackgroundProps { backgroundColor: string - children: any hueRotateAngle: number brightness: number } -const ModuleDisplayBackground = (props: DisplayBackgroundProps) => { + +const ModuleDisplayBackground = ( + props: React.PropsWithChildren, +) => { const { backgroundColor, children, hueRotateAngle, brightness } = props // webp for svg? diff --git a/frontend/components/ImportantNotice.tsx b/frontend/components/ImportantNotice.tsx index 59a6a94b3..dee4da135 100644 --- a/frontend/components/ImportantNotice.tsx +++ b/frontend/components/ImportantNotice.tsx @@ -1,5 +1,5 @@ import styled from "@emotion/styled" -import { Paper, SvgIcon, Typography } from "@mui/material" +import { Paper, SvgIcon, SvgIconProps, Typography } from "@mui/material" import RegisterCompletionTranslations from "/translations/register-completion" import { useTranslator } from "/util/useTranslator" @@ -21,7 +21,7 @@ const AlertSvgIcon = styled(SvgIcon)` color: white; ` -function AlertIcon(props: any) { +function AlertIcon(props: SvgIconProps) { return ( diff --git a/frontend/components/Installation/OSSelectorButton.tsx b/frontend/components/Installation/OSSelectorButton.tsx index 7884f26df..b5b524f74 100644 --- a/frontend/components/Installation/OSSelectorButton.tsx +++ b/frontend/components/Installation/OSSelectorButton.tsx @@ -1,6 +1,7 @@ import { useContext } from "react" import styled from "@emotion/styled" +import { IconProp } from "@fortawesome/fontawesome-svg-core" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import ButtonBase from "@mui/material/ButtonBase" import Typography from "@mui/material/Typography" @@ -29,12 +30,13 @@ const StyledIcon = styled(FontAwesomeIcon)` const StyledTypography = styled(Typography)` margin-bottom: 0.3rem; ` -interface Props { +interface OSSelectorButtonProps { OSName: userOsType - Icon: any + Icon: IconProp active: boolean } -const OSSelectorButton = (props: Props) => { + +const OSSelectorButton = (props: OSSelectorButtonProps) => { const { OSName, Icon, active } = props const { changeOS } = useContext(UserOSContext) return ( diff --git a/frontend/components/MobileBottomNavigation.tsx b/frontend/components/MobileBottomNavigation.tsx index 781da0302..5b9357eb4 100644 --- a/frontend/components/MobileBottomNavigation.tsx +++ b/frontend/components/MobileBottomNavigation.tsx @@ -17,9 +17,10 @@ const StyledBottomNavigation = styled(AppBar)` ` const MobileBottomNavigation = () => { - const { loggedIn } = useContext(LoginStateContext) + const { loggedIn, admin } = useContext(LoginStateContext) - return loggedIn ? ( + // there's currently nothing to show for non-admin users here, so don't show an empty toolbar + return loggedIn && admin ? ( diff --git a/frontend/graphql/generated/index.ts b/frontend/graphql/generated/index.ts index 222a62f25..fbc178c58 100644 --- a/frontend/graphql/generated/index.ts +++ b/frontend/graphql/generated/index.ts @@ -970,7 +970,7 @@ export type MutationrecheckCompletionsArgs = { } export type MutationregisterCompletionArgs = { - completions?: InputMaybe>> + completions: Array } export type MutationupdateAbEnrollmentArgs = { diff --git a/frontend/lib/authentication.ts b/frontend/lib/authentication.ts index a9edef059..d2c9d8452 100644 --- a/frontend/lib/authentication.ts +++ b/frontend/lib/authentication.ts @@ -69,7 +69,7 @@ export const signIn = async ({ return details } -export const signOut = async (apollo: ApolloClient, cb: any) => { +export const signOut = async (apollo: ApolloClient, cb: Function) => { document.cookie = "access_token" + "=; expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/" document.cookie = "admin" + "=; expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/" diff --git a/frontend/package.json b/frontend/package.json index c19b44e88..94e35ef70 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,8 @@ "generate-page-stubs": "node util/createStubs.js", "typecheck": "tsc --noEmit", "start:inspect": "NODE_ENV=production PORT=3021 NODE_OPTIONS=--inspect node server.js", - "graphql-codegen": "graphql-codegen --config codegen.yml" + "graphql-codegen": "graphql-codegen --config codegen.yml", + "graphql-codegen:watch": "graphql-codegen --config codegen.yml --watch" }, "keywords": [], "author": "", diff --git a/frontend/pages/_layout.tsx b/frontend/pages/_layout.tsx index 0a2473932..5966333f8 100644 --- a/frontend/pages/_layout.tsx +++ b/frontend/pages/_layout.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from "react" +import { PropsWithChildren } from "react" import { useRouter } from "next/router" @@ -18,7 +18,7 @@ const FooterDownPusherWrapper = styled.div` justify-content: space-between; ` -const Layout = ({ children }: { children: ReactNode }) => { +const Layout = ({ children }: PropsWithChildren<{}>) => { const router = useRouter() const isHomePage = !!router?.asPath?.replace(/#(.*)/, "").match(/^\/?$/) diff --git a/frontend/pages/courses/[slug]/manual-completions.tsx b/frontend/pages/courses/[slug]/manual-completions.tsx index 67075f57e..32b20c098 100644 --- a/frontend/pages/courses/[slug]/manual-completions.tsx +++ b/frontend/pages/courses/[slug]/manual-completions.tsx @@ -11,7 +11,7 @@ import styled from "@emotion/styled" import AdapterLuxon from "@mui/lab/AdapterLuxon" import DatePicker from "@mui/lab/DatePicker" import LocalizationProvider from "@mui/lab/LocalizationProvider" -import { Button, Container, TextField } from "@mui/material" +import { Button, Container, TextField, TextFieldProps } from "@mui/material" import Alert from "@mui/material/Alert" import AlertTitle from "@mui/material/AlertTitle" import Typography from "@mui/material/Typography" @@ -233,7 +233,7 @@ const ManualCompletions = () => { inputFormat="yyyy-MM-dd" onChange={setCompletionDate} value={completionDate} - renderInput={(props: any) => ( + renderInput={(props: TextFieldProps) => ( )} /> diff --git a/frontend/pages/users/[id].tsx b/frontend/pages/users/[id].tsx index f2a53effb..296d960e0 100644 --- a/frontend/pages/users/[id].tsx +++ b/frontend/pages/users/[id].tsx @@ -19,7 +19,8 @@ const UserPage = () => { const client = useApolloClient() const t = useTranslator(CommonTranslations) - const [more, setMore]: any[] = useState([]) + // TODO: typing, this "more" isn't actually used anywhere? + const [more, setMore] = useState([]) const { loading, error, data } = useQuery( UserProfileUserCourseSettingsDocument, diff --git a/frontend/static/types/tmc-client-js.d.ts b/frontend/static/types/tmc-client-js.d.ts index ed829a45f..8e4a1bb65 100644 --- a/frontend/static/types/tmc-client-js.d.ts +++ b/frontend/static/types/tmc-client-js.d.ts @@ -1,3 +1,14 @@ +namespace TMCClient { + interface AuthenticateArgs { + username: string + password: string + } + + interface AuthenticatedUser { + username: string + accessToken: string + } +} declare module "tmc-client-js" { class TMCClient { constructor(clientId: string, clientSecret: string, oAuthSite?: string) @@ -5,14 +16,11 @@ declare module "tmc-client-js" { authenticate({ username, password, - }: { - username: string - password: string - }): Promise + }: TMCClient.AuthenticateArgs): Promise - unauthenticate(): any + unauthenticate(): TMCClient - getUser(): any + getUser(): TMCClient.AuthenticatedUser } export = TMCClient diff --git a/frontend/translations/index.ts b/frontend/translations/index.ts index 57b5099f3..16c606196 100644 --- a/frontend/translations/index.ts +++ b/frontend/translations/index.ts @@ -30,7 +30,7 @@ const isStringTranslation = ( ): translation is BaseTranslation => typeof translation === "string" const getTranslator = - (dicts: Record) => + (dicts: TranslationDictionary) => (lng: string, router?: NextRouter): Translator => memoize( (key: keyof T, variables?: Record) => { @@ -70,7 +70,7 @@ const substitute = ({ translation, variables, router, -}: Substitute): any => { +}: Substitute): Translation | TranslationEntry => { if (isObjectTranslation(translation)) { return Object.keys(translation).reduce( (obj, key) => ({ @@ -99,7 +99,7 @@ const substitute = ({ const replaceGroups = translation.match(/{{(.*?)}}/gm) const keyGroups = translation.match(/\[\[(.*?)\]\]/gm) - let ret: string = translation + let ret = translation if (!replaceGroups && !keyGroups) { return ret @@ -125,7 +125,7 @@ const substitute = ({ ret = ret.replace( replaceRegExp, Array.isArray(queryParam) ? queryParam[0] : queryParam, - ) + ) as T[keyof T] & string }) } @@ -146,7 +146,7 @@ const substitute = ({ ) } else { const replaceRegExp = new RegExp(`{{${key}}}`, "g") - ret = ret.replace(replaceRegExp, `${variable}`) + ret = ret.replace(replaceRegExp, `${variable}`) as T[keyof T] & string } })