diff --git a/.eslintrc.js b/.eslintrc.js index b4eb1b7d6..d1cd8c7ff 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -48,6 +48,7 @@ module.exports = { "react-hooks/rules-of-hooks": "error", "@typescript-eslint/prefer-nullish-coalescing": "warn", "@typescript-eslint/prefer-optional-chain": "warn", + // complexity: "warn", // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs // e.g. "@typescript-eslint/explicit-function-return-type": "off", }, diff --git a/.prettierrc.js b/.prettierrc.js index 64d2380c2..afdc64e2a 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -11,7 +11,9 @@ module.exports = { "", "^[@]", "", - "^[./]", + "^[./](?!graphql)", + "", + "^/graphql", ], plugins: [require.resolve("@ianvs/prettier-plugin-sort-imports")], importOrderBuiltinModulesToTop: true, diff --git a/README.md b/README.md index 5c5c3dd63..50bd70a0e 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,58 @@ -## Development environment +# Development environment -Note: Before starting development, please run the following command on the repo root: +## Requirements -```bash -npm ci -``` +Create `.env` files for backend and frontend. See examples and ask your local boffin for details. -frontend: +Install `docker-compose`, if not already installed. -```bash -cd frontend -npm ci -npm run dev -``` +## Development workflow -backend: +Run `npm ci` in the each of the root, backend and frontend directories to install dependencies. + +Create separate shells for the database container, backend and frontend: ```bash cd backend docker-compose up ``` +```bash +cd frontend +npm run dev +``` + ```bash cd backend -npm ci npm run migrate npm run dev ``` -## Generating GraphQL types for frontend +If the database doesn't seem to do anything, ie. no messages after the initial ones after running `docker-compose up` and the database queries are not getting through, run `docker-compose down` and try again. You can always run the database container in detached mode (`-d`) but then you won't see the logs live. -If you make changes to the GraphQL schema or the queries in the frontend, you probably need to regenerate the Typescript types. Run `npm run generate-graphql-types` in the frontend folder. +Run `npm run prettier` in the root directory before committing. The commit runs hooks to check this as well as some linters, type checks etc. -If you're getting errors about mismatching GraphQL versions, then try `npm i --legacy-peer-deps --save-dev apollo` to force it. This is because `apollo-tooling` keeps on packaging some old versions of dependencies instead of using the newer ones available. -## Using installed `librdkafka` to speed up backend development + +Using pre-built librdkafka to speed up backend development -By default, `node-rdkafka` builds `librdkafka` from the source. This can take minutes on a bad day and can slow development down quite considerably. However, there's an option to use the version installed locally. +By default, `node-rdkafka` builds `librdkafka` from the source. This can take minutes on a bad day and can slow development down quite considerably, especially when you're working with different branches with different dependencies and need to run `npm ci` often. However, there's an option to use the version installed locally. Do this in some other directory than the project one: -```bash -wget https://github.com/edenhill/librdkafka/archive/v1.4.0.tar.gz -O - | tar -xz -cd librdkafka-1.4.0 +```bash +wget https://github.com/edenhill/librdkafka/archive/v1.8.2.tar.gz -O - | tar -xz +cd librdkafka-1.8.2 ./configure --prefix=/usr make && make install ``` -(Ubuntu 20.04 and later seem to require v1.5.0, so change accordingly.) - -You may have to do some of that as root. Alternatively, you can install a prebuilt package - see [here](https://github.com/edenhill/librdkafka) for more information. Just be sure to install version >1.4.0. +You may have to do some of that as root. Alternatively, you can install a prebuilt package - see [here](https://github.com/edenhill/librdkafka) for more information. Set the env `BUILD_LIBRDKAFKA=0` when doing `npm ci` or similar on the backend to skip the build. -## Documentation + + +## More documentation -[Kafka](backend/docs/kafka.md) +- [Kafka](docs/kafka.md) +- [GraphQL](docs/graphql.md) diff --git a/backend/.env.example b/backend/.env.example index c2d8a5a24..35ab79155 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -20,4 +20,5 @@ GOOGLE_CLOUD_STORAGE_BUCKET=x GOOGLE_CLOUD_STORAGE_PROJECT=x GOOGLE_CLOUD_STORAGE_KEYFILE=x -UPDATE_USER_SECRET=secret \ No newline at end of file +UPDATE_USER_SECRET=secret +BACKEND_URL=http://localhost:4000 \ No newline at end of file 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/Completion/index.ts b/backend/graphql/Completion/index.ts index 8a66e27d7..e617b1941 100644 --- a/backend/graphql/Completion/index.ts +++ b/backend/graphql/Completion/index.ts @@ -1,4 +1,4 @@ -// generated Mon Jul 11 2022 17:59:37 GMT+0300 (Itä-Euroopan kesäaika) +// generated Thu Aug 11 2022 17:12:07 GMT+0300 (Itä-Euroopan kesäaika) export * from "./input" export * from "./model" diff --git a/backend/graphql/Completion/model.ts b/backend/graphql/Completion/model.ts index b9dd8ecce..61d7d6554 100644 --- a/backend/graphql/Completion/model.ts +++ b/backend/graphql/Completion/model.ts @@ -14,6 +14,7 @@ export const Completion = objectType({ t.model.completion_language() t.model.email() t.model.student_number() + t.model.user_id() t.model.user_upstream_id() t.model.completions_registered() t.model.course_id() 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/index.ts b/backend/graphql/Course/index.ts index 8a66e27d7..e617b1941 100644 --- a/backend/graphql/Course/index.ts +++ b/backend/graphql/Course/index.ts @@ -1,4 +1,4 @@ -// generated Mon Jul 11 2022 17:59:37 GMT+0300 (Itä-Euroopan kesäaika) +// generated Thu Aug 11 2022 17:12:07 GMT+0300 (Itä-Euroopan kesäaika) export * from "./input" export * from "./model" diff --git a/backend/graphql/Course/mutations.ts b/backend/graphql/Course/mutations.ts index 8c0a3d1a4..8307073ca 100644 --- a/backend/graphql/Course/mutations.ts +++ b/backend/graphql/Course/mutations.ts @@ -10,11 +10,15 @@ 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 => value !== null && value !== undefined +const nullToUndefined = (value: T | null | undefined): T | undefined => + value ?? undefined + export const CourseMutations = extendType({ type: "Mutation", definition(t) { @@ -77,7 +81,7 @@ export const CourseMutations = extendType({ study_modules: !!study_modules ? { connect: study_modules.map((s) => ({ - id: s?.id ?? undefined, + id: nullToUndefined(s?.id), })), } : undefined, @@ -105,7 +109,7 @@ export const CourseMutations = extendType({ }, }) - const kafkaProducer = await new KafkaProducer() + const kafkaProducer = new KafkaProducer() const producerMessage: ProducerMessage = { message: JSON.stringify(newCourse), partition: null, @@ -246,14 +250,15 @@ export const CourseMutations = extendType({ .findUnique({ where: { slug } }) .study_modules() //const addedModules: StudyModuleWhereUniqueInput[] = pullAll(study_modules, existingStudyModules.map(module => module.id)) - const removedModuleIds = (existingStudyModules || []) - .filter((module) => !getIds(study_modules ?? []).includes(module.id)) - .map((module) => ({ id: module.id } as { id: string })) + const removedModuleIds = + existingStudyModules + ?.filter((module) => !getIds(study_modules).includes(module.id)) + .map((module) => ({ id: module.id })) ?? [] const connectModules = study_modules?.map((s) => ({ ...s, - id: s?.id ?? undefined, - slug: s?.slug ?? undefined, + id: nullToUndefined(s?.id), + slug: nullToUndefined(s?.slug), })) ?? [] const studyModuleMutation: @@ -294,7 +299,7 @@ export const CourseMutations = extendType({ const updatedCourse = await ctx.prisma.course.update({ where: { - id: id ?? undefined, + id: nullToUndefined(id), slug, }, data: convertUpdate({ @@ -308,7 +313,7 @@ export const CourseMutations = extendType({ "completion_email_id", "course_stats_email_id", ]), - slug: new_slug ? new_slug : slug, + slug: new_slug ?? slug, end_date, // FIXME: disconnect removed photos? photo: !!photo ? { connect: { id: photo } } : undefined, @@ -374,18 +379,16 @@ export const CourseMutations = extendType({ }, }) -const getIds = (arr: any[]) => (arr || []).map((t) => t.id) -const filterNotIncluded = (arr1: any[], arr2: any[], mapToId = true) => { +type WithIdOrNull = { id?: string | null; [key: string]: any } | null +const getIds = (arr?: WithIdOrNull[] | null) => 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 +398,7 @@ interface ICreateMutation { field: keyof Prisma.Prisma__CourseClient } -const createMutation = async ({ +const createMutation = async ({ ctx, slug, data, @@ -420,7 +423,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 +435,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 5789178b7..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") @@ -24,7 +27,7 @@ export const Image = objectType({ t.model.original_mimetype() t.model.uncompressed() t.model.uncompressed_mimetype() - t.model.courses() + // t.model.courses() }, }) @@ -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/StudyModule/index.ts b/backend/graphql/StudyModule/index.ts index 8a66e27d7..e617b1941 100644 --- a/backend/graphql/StudyModule/index.ts +++ b/backend/graphql/StudyModule/index.ts @@ -1,4 +1,4 @@ -// generated Mon Jul 11 2022 17:59:37 GMT+0300 (Itä-Euroopan kesäaika) +// generated Thu Aug 11 2022 17:12:07 GMT+0300 (Itä-Euroopan kesäaika) export * from "./input" export * from "./model" 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/User/index.ts b/backend/graphql/User/index.ts index 8a66e27d7..e617b1941 100644 --- a/backend/graphql/User/index.ts +++ b/backend/graphql/User/index.ts @@ -1,4 +1,4 @@ -// generated Mon Jul 11 2022 17:59:37 GMT+0300 (Itä-Euroopan kesäaika) +// generated Thu Aug 11 2022 17:12:07 GMT+0300 (Itä-Euroopan kesäaika) export * from "./input" export * from "./model" 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/graphql/index.ts b/backend/graphql/index.ts index 177e86aea..4e792c5e6 100644 --- a/backend/graphql/index.ts +++ b/backend/graphql/index.ts @@ -1,4 +1,4 @@ -// generated Mon Jul 11 2022 17:59:37 GMT+0300 (Itä-Euroopan kesäaika) +// generated Thu Aug 11 2022 17:12:07 GMT+0300 (Itä-Euroopan kesäaika) export * from "./ABEnrollment" export * from "./ABStudy" diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 8231fc2d9..a54d9f3c9 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -1,6 +1,5 @@ generator client { provider = "prisma-client-js" - previewFeatures = ["aggregateApi"] } datasource db { 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 new file mode 100644 index 000000000..8b71b3cca --- /dev/null +++ b/docs/graphql.md @@ -0,0 +1,93 @@ +# Working with GraphQL + +## Frontend types + +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 by running `npm run generate` in the backend 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: + +You've created a model called `StaffMember` in the Prisma schema: + +```prisma +model StaffMember { + id String @id @default(uuid()) + user_id String? + user User? @relation(fields: [user_id], references: [id]) + work_email String? + work_phone String? + created_at DateTime? @default(now()) + updated_at DateTime? @updatedAt + + @@map("staff_member) +} +``` + +You've also created the appropriate database migration and created a resolver for a query called `staffMember`, taking a single required parameter `id` of type `ID` and returing a `StaffMember` or `null` if none found. + +First, ensure that you have a fresh GraphQL schema by running `npm run generate` in the backend folder. The generated schema is automatically linked to the frontend folder. + +You then might define the query in the frontend as follows in a `.graphql` file situated somewhere in the `graphql` folder: + +```graphql +query StaffMemberDetails($id: ID!) { + staffMember(id: $id) { + id + user { + id + email + name + } + work_email + work_phone + } +} +``` + +Note that even if you could leave the query unnamed, it would then be assigned a random type name which would be less helpful. + +Run `npm run graphql-codegen` in the frontend folder. This will generate a bucketload of useful typings and other code. + +The most useful is the `StaffMemberDetailsDocument`, which is a correctly typed document node for the GraphQL operation, also providing the result type automatically. The result type is available separately as `StaffMemberDetailsQuery` and the operation variables as `StaffMemberDetailsQueryVariables`. Also, the model definition is available as `StaffMember` -- do note that it includes all the fields exposed in the GraphQL schema, not just the ones that are actually used in the query. If you're using only a subselection of fields for a query and passing the result to a component, you might want to create a fragment for the fields you're using and use the generated type. + +You can now use the query in the frontend as follows: + +```typescript +import { StaffMemberDetailsDocument } from "/graphql/generated" + +// in React components using the Apollo hooks: +function SayHello(id: string) { + const { data, loading, error } = useQuery(StaffMemberDetailsDocument, { + variables: { + id, + }, + }) + + return ( + + {loading && Loading...} + {error && Error: {error.message}} + {data(Hello, {data.staffMember?.user.name ?? "stranger"}} + + ) +} + +// or, if you're outside React components, assuming client is an ApolloClient instance +async function getStaffMemberDetails(client: ApolloClient, id: string) { + const { data } = await client.query({ + query: StaffMemberDetailsDocument, + variables: { id }, + }) + + return data?.staffMember +} +``` + +In both cases, the results of the query in `data` are typed correctly, so you would get `data.staffMember.work_email` for example -- provided that there is a user with the id that given as parameter. + +The flow is similar with mutations and fragments. Mutation result and operation variable types are named similarly, only with `Mutation` instead of `Query`. Fragments do not produce result types; the type of the fragment is the defined name + `Fragment`. diff --git a/backend/docs/kafka.md b/docs/kafka.md similarity index 100% rename from backend/docs/kafka.md rename to docs/kafka.md diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 000000000..78b10c9b3 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,3 @@ +BACKEND_URL=http://localhost:4000 +FRONTEND_URL=http://localhost:3000 +PORT=3000 diff --git a/frontend/apollo.config.js b/frontend/apollo.config.js index 55315592a..ceb8e5925 100644 --- a/frontend/apollo.config.js +++ b/frontend/apollo.config.js @@ -1,16 +1,7 @@ const path = require("path") module.exports = { client: { - tagName: "gql", - includes: [ - "./components/**/*.tsx", - "./pages/**/*.tsx", - "./components/**/*.ts", - "./pages/**/*.ts", - "./graphql/**/*.ts", - "./lib/**/*.tsx", - "./lib/**/*.ts", - ], + includes: ["./graphql/**/*.graphql"], excludes: ["node_modules"], service: { name: "backend", diff --git a/frontend/codegen.yml b/frontend/codegen.yml new file mode 100644 index 000000000..3770903ef --- /dev/null +++ b/frontend/codegen.yml @@ -0,0 +1,31 @@ +schema: schema.graphql +documents: "graphql/**/*.{ts,tsx,graphql}" +config: + preResolveTypes: true + namingConvention: keep + avoidOptionals: + field: true + nonOptionalTypeName: true + dedupeFragments: true +hooks: + afterAllFileWrite: + - prettier --write +generates: + ./graphql/generated/index.ts: + pluckConfig: + modules: + - name: "@apollo/client" + identifier: gql + plugins: + - add: + placement: "prepend" + content: + - "/**" + - " * This is an automatically generated file." + - " * Run `npm run graphql-codegen` to regenerate." + - " **/" + - time + - typescript + - typescript-operations + - fragment-matcher + - typed-document-node diff --git a/frontend/components/CertificateButton.tsx b/frontend/components/CertificateButton.tsx index ce528ed1d..976112339 100644 --- a/frontend/components/CertificateButton.tsx +++ b/frontend/components/CertificateButton.tsx @@ -1,6 +1,6 @@ import { useContext, useEffect, useReducer, useState } from "react" -import { gql, useMutation } from "@apollo/client" +import { useMutation } from "@apollo/client" import styled from "@emotion/styled" import { CircularProgress, @@ -15,16 +15,20 @@ import DialogTitle from "@mui/material/DialogTitle" import AlertContext from "/contexts/AlertContext" import LoginStateContext from "/contexts/LoginStateContext" -import { UserOverViewQuery as CompletionsUserOverViewQuery } from "/graphql/queries/currentUser" import { updateAccount } from "/lib/account" import { checkCertificate, createCertificate } from "/lib/certificates" -import { UserDetailQuery } from "/lib/with-apollo-client/fetch-user-details" -import { UserOverViewQuery } from "/pages/profile" -import { ProfileUserOverView_currentUser_completions_course } from "/static/types/generated/ProfileUserOverView" -import { UserOverView_currentUser } from "/static/types/generated/UserOverView" import CompletionsTranslations from "/translations/completions" import { useTranslator } from "/util/useTranslator" +import { + CourseCoreFieldsFragment, + CurrentUserDetailedDocument, + CurrentUserDocument, + CurrentUserOverviewDocument, + UpdateUserNameDocument, + UserOverviewFieldsFragment, +} from "/graphql/generated" + const StyledButton = styled(Button)` margin: auto; background-color: #005361; @@ -37,18 +41,8 @@ const StyledTextField = styled(TextField)` margin-bottom: 1rem; ` -const updateUserNameMutation = gql` - mutation updateUserName($first_name: String, $last_name: String) { - updateUserName(first_name: $first_name, last_name: $last_name) { - id - first_name - last_name - } - } -` - interface CertificateProps { - course: ProfileUserOverView_currentUser_completions_course + course: CourseCoreFieldsFragment } type Status = @@ -132,9 +126,9 @@ const reducer = (state: CertificateState, action: Action): CertificateState => { status: "ERROR", error: action.payload, } + default: + return state } - - return state } const CertificateButton = ({ course }: CertificateProps) => { @@ -146,11 +140,11 @@ const CertificateButton = ({ course }: CertificateProps) => { const [firstName, setFirstName] = useState(currentUser?.first_name ?? "") const [lastName, setLastName] = useState(currentUser?.last_name ?? "") - const [updateUserName] = useMutation(updateUserNameMutation, { + const [updateUserName] = useMutation(UpdateUserNameDocument, { refetchQueries: [ - { query: UserDetailQuery }, - { query: UserOverViewQuery }, - { query: CompletionsUserOverViewQuery }, + { query: CurrentUserDocument }, + { query: CurrentUserDetailedDocument }, + { query: CurrentUserOverviewDocument }, ], }) @@ -204,7 +198,7 @@ const CertificateButton = ({ course }: CertificateProps) => { ...(currentUser || { email: "", id: "" }), first_name: firstName, last_name: lastName, - } as UserOverView_currentUser) + } as UserOverviewFieldsFragment) dispatch({ type: "UPDATED_NAME", payload: res }) } diff --git a/frontend/components/CompletedCourseCard.tsx b/frontend/components/CompletedCourseCard.tsx index cbab6c318..3d18d3678 100644 --- a/frontend/components/CompletedCourseCard.tsx +++ b/frontend/components/CompletedCourseCard.tsx @@ -6,11 +6,15 @@ import Typography from "@mui/material/Typography" import { mapLangToLanguage } from "/components/DataFormatFunctions" import { ClickableDiv } from "/components/Surfaces/ClickableCard" -import { ProfileUserOverView_currentUser_completions } from "/static/types/generated/ProfileUserOverView" import CompletionsTranslations from "/translations/completions" import ProfileTranslations from "/translations/profile" import { useTranslator } from "/util/useTranslator" +import { + CompletionDetailedFieldsFragment, + CourseWithPhotoCoreFieldsFragment, +} from "/graphql/generated" + const Background = styled(ClickableDiv)` display: flex; flex-direction: column; @@ -38,7 +42,9 @@ const RegistrationDetails = styled.div` ` interface CourseCardProps { - completion: ProfileUserOverView_currentUser_completions + completion: CompletionDetailedFieldsFragment & { + course: CourseWithPhotoCoreFieldsFragment + } } function formatDateTime(date: string) { diff --git a/frontend/components/CourseImage.tsx b/frontend/components/CourseImage.tsx index 446773e42..e2c21beff 100644 --- a/frontend/components/CourseImage.tsx +++ b/frontend/components/CourseImage.tsx @@ -3,9 +3,10 @@ import { memo } from "react" import styled from "@emotion/styled" import { Typography } from "@mui/material" -import { AllCourses_courses_photo } from "/static/types/generated/AllCourses" import { addDomain } from "/util/imageUtils" +import { ImageCoreFieldsFragment } from "/graphql/generated" + const ComponentStyle = ` width: 100%; height: 100%; @@ -23,7 +24,7 @@ const PlaceholderComponent = styled.div` align-items: center; ` interface CourseImageProps { - photo?: AllCourses_courses_photo | null + photo?: ImageCoreFieldsFragment | null [k: string]: any } diff --git a/frontend/components/CreateEmailTemplateDialog.tsx b/frontend/components/CreateEmailTemplateDialog.tsx index 107831fe0..a49458e18 100644 --- a/frontend/components/CreateEmailTemplateDialog.tsx +++ b/frontend/components/CreateEmailTemplateDialog.tsx @@ -3,12 +3,7 @@ import { useState } from "react" import { omit } from "lodash" import Router from "next/router" -import { - gql, - OperationVariables, - useApolloClient, - useQuery, -} from "@apollo/client" +import { useApolloClient, useQuery } from "@apollo/client" import { Button, Dialog, @@ -23,34 +18,17 @@ import { import CustomSnackbar from "/components/CustomSnackbar" import Spinner from "/components/Spinner" -import { UpdateCourseMutation } from "/graphql/mutations/courses" -import { AddEmailTemplateMutation } from "/graphql/mutations/email-templates" -import { AddEmailTemplate } from "/static/types/generated/AddEmailTemplate" -import { CourseDetailsFromSlugQuery_course as CourseDetailsData } from "/static/types/generated/CourseDetailsFromSlugQuery" -import { updateCourse } from "/static/types/generated/updateCourse" - -export const AllCoursesDetails = gql` - query AllCoursesDetails { - courses { - id - slug - name - teacher_in_charge_name - teacher_in_charge_email - start_date - completion_email { - name - id - } - course_stats_email { - id - } - } - } -` + +import { + AddEmailTemplateDocument, + CourseCoreFieldsFragment, + CourseUpsertArg, + EmailTemplateEditorCoursesDocument, + UpdateCourseDocument, +} from "/graphql/generated" interface CreateEmailTemplateDialogParams { - course?: CourseDetailsData + course?: CourseCoreFieldsFragment buttonText: string type?: string } @@ -63,13 +41,10 @@ const CreateEmailTemplateDialog = ({ const [openDialog, setOpenDialog] = useState(false) const [nameInput, setNameInput] = useState("") const [templateType, setTemplateType] = useState(type) - const [selectedCourse, setSelectedCourse] = useState< - CourseDetailsData | undefined - >(undefined) + const [selectedCourse, setSelectedCourse] = + useState(null) const [isErrorSnackbarOpen, setIsErrorSnackbarOpen] = useState(false) - const { loading, error, data } = useQuery<{ courses: CourseDetailsData[] }>( - AllCoursesDetails, - ) + const { loading, error, data } = useQuery(EmailTemplateEditorCoursesDocument) const client = useApolloClient() if (loading) { @@ -90,8 +65,8 @@ const CreateEmailTemplateDialog = ({ const courseOptions = templateType === "completion" - ? data.courses - .filter((c) => c?.completion_email === null) + ? data!.courses + ?.filter((c) => c?.completion_email === null) .map((c, i) => { return ( @@ -99,7 +74,7 @@ const CreateEmailTemplateDialog = ({ ) }) - : data.courses.map((c, i) => { + : data!.courses?.map((c, i) => { return ( {c?.name} @@ -109,8 +84,8 @@ const CreateEmailTemplateDialog = ({ const handleCreate = async () => { try { - const { data } = await client.mutate({ - mutation: AddEmailTemplateMutation, + const { data } = await client.mutate({ + mutation: AddEmailTemplateDocument, variables: { name: nameInput, template_type: templateType, @@ -122,7 +97,7 @@ const CreateEmailTemplateDialog = ({ const updateableCourse = course ?? selectedCourse if (updateableCourse) { - const connectVariables = {} as OperationVariables + const connectVariables = {} as CourseUpsertArg if (templateType === "completion") { connectVariables.completion_email_id = data?.addEmailTemplate?.id @@ -131,8 +106,8 @@ const CreateEmailTemplateDialog = ({ connectVariables.course_stats_email_id = data?.addEmailTemplate?.id } - await client.mutate({ - mutation: UpdateCourseMutation, + await client.mutate({ + mutation: UpdateCourseDocument, variables: { course: { // - already has slug and can't have both @@ -203,7 +178,9 @@ const CreateEmailTemplateDialog = ({ { e.preventDefault() - setSelectedCourse(data.courses[Number(e.target.value)]) + setSelectedCourse( + data!.courses?.[Number(e.target.value)] ?? null, + ) }} id="selectCourse" defaultValue="Select course" diff --git a/frontend/components/Dashboard/CompletionCard.tsx b/frontend/components/Dashboard/CompletionCard.tsx index 9a7c95a8f..29e8d6f24 100644 --- a/frontend/components/Dashboard/CompletionCard.tsx +++ b/frontend/components/Dashboard/CompletionCard.tsx @@ -12,7 +12,7 @@ import { Typography, } from "@mui/material" -import { AllCompletions_completionsPaginated_edges_node } from "/static/types/generated/AllCompletions" +import { CompletionsQueryNodeFieldsFragment } from "/graphql/generated" //map language code stored to database to human readable language const MapLangToLanguage: Record = { @@ -36,11 +36,11 @@ const ListItemArea = styled.div` margin: 1rem auto 1rem auto; ` -function CompletionCard({ - completer, -}: { - completer: AllCompletions_completionsPaginated_edges_node -}) { +interface CompletionCardProps { + completer: CompletionsQueryNodeFieldsFragment +} + +function CompletionCard({ completer }: CompletionCardProps) { const completionLanguage = MapLangToLanguage[completer?.completion_language ?? ""] ?? "No language available" diff --git a/frontend/components/Dashboard/CompletionsList.tsx b/frontend/components/Dashboard/CompletionsList.tsx index be933395f..1a9102f83 100644 --- a/frontend/components/Dashboard/CompletionsList.tsx +++ b/frontend/components/Dashboard/CompletionsList.tsx @@ -1,130 +1,35 @@ import { useContext, useState } from "react" -import { gql } from "@apollo/client" import { useQuery } from "@apollo/client" import { CircularProgress } from "@mui/material" import CompletionsListWithData from "./CompletionsListWithData" import ModifiableErrorMessage from "/components/ModifiableErrorMessage" import CourseLanguageContext from "/contexts/CourseLanguageContext" -import { AllCompletions as AllCompletionsData } from "/static/types/generated/AllCompletions" -import { AllCompletionsPrevious as AllCompletionsPreviousData } from "/static/types/generated/AllCompletionsPrevious" import notEmpty from "/util/notEmpty" import { useQueryParameter } from "/util/useQueryParameter" -export const AllCompletionsQuery = gql` - query AllCompletions( - $course: String! - $cursor: String - $completionLanguage: String - $search: String - ) { - completionsPaginated( - course: $course - completion_language: $completionLanguage - search: $search - first: 50 - after: $cursor - ) { - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - edges { - node { - id - email - completion_language - created_at - user { - id - first_name - last_name - student_number - } - course { - id - name - } - completions_registered { - id - organization { - id - slug - } - } - } - } - } - } -` -export const PreviousPageCompletionsQuery = gql` - query AllCompletionsPrevious( - $course: String! - $cursor: String - $completionLanguage: String - $search: String - ) { - completionsPaginated( - course: $course - completion_language: $completionLanguage - search: $search - last: 50 - before: $cursor - ) { - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - edges { - node { - id - email - completion_language - created_at - user { - id - first_name - last_name - student_number - } - course { - id - name - } - completions_registered { - id - organization { - id - slug - } - } - } - } - } - } -` +import { + PaginatedCompletionsDocument, + PaginatedCompletionsPreviousPageDocument, +} from "/graphql/generated" interface CompletionsListProps { search?: string } +interface QueryDetails { + start?: string | null + end?: string | null + back: boolean + page: number +} + const CompletionsList = ({ search }: CompletionsListProps) => { const completionLanguage = useContext(CourseLanguageContext) const course = useQueryParameter("slug") - interface queryDetailsInterface { - start?: string | null - end?: string | null - back: boolean - page: number - } - - const [queryDetails, setQueryDetails] = useState({ + const [queryDetails, setQueryDetails] = useState({ start: null, end: null, back: false, @@ -132,19 +37,12 @@ const CompletionsList = ({ search }: CompletionsListProps) => { }) const query = queryDetails.back - ? PreviousPageCompletionsQuery - : AllCompletionsQuery + ? PaginatedCompletionsPreviousPageDocument + : PaginatedCompletionsDocument const cursor = queryDetails.back ? queryDetails.end : queryDetails.start - interface Variables { - cursor?: string | null - course: string | string[] - completionLanguage?: string - search?: string - } - - const variables: Variables = { + const variables = { cursor, course, completionLanguage: @@ -152,9 +50,7 @@ const CompletionsList = ({ search }: CompletionsListProps) => { search: search !== "" ? search : undefined, } - const { data, loading, error } = useQuery< - AllCompletionsData | AllCompletionsPreviousData - >(query, { + const { data, loading, error } = useQuery(query, { variables, fetchPolicy: "network-only", }) diff --git a/frontend/components/Dashboard/CompletionsListWithData.tsx b/frontend/components/Dashboard/CompletionsListWithData.tsx index 079007fea..ab8d0b7ec 100644 --- a/frontend/components/Dashboard/CompletionsListWithData.tsx +++ b/frontend/components/Dashboard/CompletionsListWithData.tsx @@ -2,10 +2,11 @@ import { List } from "@mui/material" import CompletionCard from "./CompletionCard" import CompletionPaginator from "./CompletionPaginator" -import { AllCompletions_completionsPaginated_edges_node } from "/static/types/generated/AllCompletions" + +import { CompletionsQueryNodeFieldsFragment } from "/graphql/generated" interface CompletionsListWithDataProps { - completions: AllCompletions_completionsPaginated_edges_node[] + completions: CompletionsQueryNodeFieldsFragment[] onLoadMore: () => void onGoBack: () => void hasPrevious: boolean diff --git a/frontend/components/Dashboard/CourseCard.tsx b/frontend/components/Dashboard/CourseCard.tsx index b2ddcb67c..17f7d7881 100644 --- a/frontend/components/Dashboard/CourseCard.tsx +++ b/frontend/components/Dashboard/CourseCard.tsx @@ -18,8 +18,8 @@ import CourseStatusBadge from "./CourseStatusBadge" import { ButtonWithPaddingAndMargin as StyledButton } from "/components/Buttons/ButtonWithPaddingAndMargin" import CourseImage from "/components/CourseImage" import { CardTitle } from "/components/Text/headers" -import { AllEditorCourses_courses } from "/static/types/generated/AllEditorCourses" -import { CourseStatus } from "/static/types/generated/globalTypes" + +import { CourseStatus, EditorCourseFieldsFragment } from "/graphql/generated" const CardBase = styled.div<{ ishidden?: number }>` position: relative; @@ -159,7 +159,7 @@ const formatDate = (date?: string | null) => date ? new Date(date).toLocaleDateString() : "-" interface CourseCardProps { - course?: AllEditorCourses_courses + course?: EditorCourseFieldsFragment loading?: boolean onClickStatus?: (value: CourseStatus | null) => (_: any) => void } diff --git a/frontend/components/Dashboard/CourseGrid.tsx b/frontend/components/Dashboard/CourseGrid.tsx index bd85ea80c..62958b8ea 100644 --- a/frontend/components/Dashboard/CourseGrid.tsx +++ b/frontend/components/Dashboard/CourseGrid.tsx @@ -3,11 +3,11 @@ import { range } from "lodash" import styled from "@emotion/styled" import CourseCard from "./CourseCard" -import { AllEditorCourses_courses } from "/static/types/generated/AllEditorCourses" -import { CourseStatus } from "/static/types/generated/globalTypes" + +import { CourseStatus, EditorCourseFieldsFragment } from "/graphql/generated" interface CourseGridProps { - courses?: AllEditorCourses_courses[] + courses?: EditorCourseFieldsFragment[] loading: boolean onClickStatus?: ( value: CourseStatus | null, diff --git a/frontend/components/Dashboard/CourseStatusBadge.tsx b/frontend/components/Dashboard/CourseStatusBadge.tsx index 674344502..796239775 100644 --- a/frontend/components/Dashboard/CourseStatusBadge.tsx +++ b/frontend/components/Dashboard/CourseStatusBadge.tsx @@ -5,7 +5,7 @@ import Error from "@mui/icons-material/Error" import Schedule from "@mui/icons-material/Schedule" import { Chip, ChipProps } from "@mui/material" -import { CourseStatus } from "/static/types/generated/globalTypes" +import { CourseStatus } from "/graphql/generated" const StatusBadge = styled(Chip)<{ status?: CourseStatus | null }>` background-color: ${({ status }) => diff --git a/frontend/components/Dashboard/DashboardPointsList.tsx b/frontend/components/Dashboard/DashboardPointsList.tsx index 0e321aada..66791bd32 100644 --- a/frontend/components/Dashboard/DashboardPointsList.tsx +++ b/frontend/components/Dashboard/DashboardPointsList.tsx @@ -1,40 +1,39 @@ import { Grid } from "@mui/material" import PointsListItemCard from "./PointsListItemCard" -import { UserCourseSettings_userCourseSettings_edges as Points } from "/static/types/generated/UserCourseSettings" import notEmpty from "/util/notEmpty" -interface Props { - pointsForUser: Points[] +import { StudentProgressesQueryNodeFieldsFragment } from "/graphql/generated" + +interface PointsListProps { + data: StudentProgressesQueryNodeFieldsFragment[] cutterValue: number } -const PointsList = (props: Props) => { - const { pointsForUser, cutterValue } = props +const PointsList = (props: PointsListProps) => { + const { data, cutterValue } = props return ( - {pointsForUser.filter(notEmpty).map((p: Points) => - p?.node?.user?.progress ? ( + {data.filter(notEmpty).map((p) => + p?.user?.progress ? ( ) : null, )} diff --git a/frontend/components/Dashboard/Editor/Course/CourseEditForm.tsx b/frontend/components/Dashboard/Editor/Course/CourseEditForm.tsx index da8235c6c..7f0843c9f 100644 --- a/frontend/components/Dashboard/Editor/Course/CourseEditForm.tsx +++ b/frontend/components/Dashboard/Editor/Course/CourseEditForm.tsx @@ -46,12 +46,15 @@ import { } from "/components/Dashboard/Editor/common" import UserCourseSettingsVisibilityEditForm from "/components/Dashboard/Editor/Course/UserCourseSettingsVisibilityEditForm" import FormWrapper from "/components/Dashboard/Editor/FormWrapper" -import { CourseEditorCourses_courses } from "/static/types/generated/CourseEditorCourses" -import { CourseEditorStudyModules_study_modules } from "/static/types/generated/CourseEditorStudyModules" import CoursesTranslations from "/translations/courses" import { useQueryParameter } from "/util/useQueryParameter" import { useTranslator } from "/util/useTranslator" +import { + EditorCourseOtherCoursesFieldsFragment, + StudyModuleDetailedFieldsFragment, +} from "/graphql/generated" + interface CoverProps { covered: boolean } @@ -87,8 +90,8 @@ export const FormFieldGroup = styled.div` interface RenderFormProps { initialValues?: CourseFormValues - courses?: CourseEditorCourses_courses[] - studyModules?: CourseEditorStudyModules_study_modules[] + courses?: EditorCourseOtherCoursesFieldsFragment[] + studyModules?: StudyModuleDetailedFieldsFragment[] } interface RenderProps { @@ -116,11 +119,8 @@ const renderForm = const sortedCourses = useMemo( () => courses - ?.filter((c: CourseEditorCourses_courses) => c.id !== values?.id) - .sort( - (a: CourseEditorCourses_courses, b: CourseEditorCourses_courses) => - a?.name < b?.name ? -1 : 1, - ), + ?.filter((c) => c.id !== values?.id) + .sort((a, b) => (a?.name < b?.name ? -1 : 1)), [courses], ) @@ -283,19 +283,17 @@ const renderForm = - {studyModules?.map( - (module: CourseEditorStudyModules_study_modules) => ( - - - - ), - )} + {studyModules?.map((module) => ( + + + + ))} @@ -433,16 +431,14 @@ const renderForm = (no choice) - {sortedCourses?.map( - (course: CourseEditorCourses_courses) => ( - - {course.name} - - ), - )} + {sortedCourses?.map((course) => ( + + {course.name} + + ))} (no choice) - {sortedCourses?.map( - (course: CourseEditorCourses_courses) => ( - - {course.name} - - ), - )} + {sortedCourses?.map((course) => ( + + {course.name} + + ))} @@ -517,6 +511,19 @@ const renderForm = ) } +interface CourseEditFormProps { + course: CourseFormValues + studyModules?: StudyModuleDetailedFieldsFragment[] + courses?: EditorCourseOtherCoursesFieldsFragment[] + validationSchema: Yup.ObjectSchema + onSubmit: ( + values: CourseFormValues, + FormikHelpers: FormikHelpers, + ) => void + onCancel: () => void + onDelete: (values: CourseFormValues) => void +} + const CourseEditForm = memo( ({ course, @@ -526,18 +533,7 @@ const CourseEditForm = memo( onSubmit, onCancel, onDelete, - }: { - course: CourseFormValues - studyModules?: CourseEditorStudyModules_study_modules[] - courses?: CourseEditorCourses_courses[] - validationSchema: Yup.ObjectSchema - onSubmit: ( - values: CourseFormValues, - FormikHelpers: FormikHelpers, - ) => void - onCancel: () => void - onDelete: (values: CourseFormValues) => void - }) => { + }: CourseEditFormProps) => { const validate = useCallback(async (values: CourseFormValues) => { try { await validationSchema.validate(values, { diff --git a/frontend/components/Dashboard/Editor/Course/CourseImageInput.tsx b/frontend/components/Dashboard/Editor/Course/CourseImageInput.tsx index 90e887f49..d162b5a0d 100644 --- a/frontend/components/Dashboard/Editor/Course/CourseImageInput.tsx +++ b/frontend/components/Dashboard/Editor/Course/CourseImageInput.tsx @@ -11,13 +11,14 @@ import { FormSubtitle } from "/components/Dashboard/Editor/common" import ImportPhotoDialog from "/components/Dashboard/Editor/Course/ImportPhotoDialog" import ImageDropzoneInput from "/components/Dashboard/ImageDropzoneInput" import ImagePreview from "/components/Dashboard/ImagePreview" -import { CourseEditorCourses_courses } from "/static/types/generated/CourseEditorCourses" import CoursesTranslations from "/translations/courses" import { addDomain } from "/util/imageUtils" import { useTranslator } from "/util/useTranslator" +import { EditorCourseOtherCoursesFieldsFragment } from "/graphql/generated" + interface ImageInputProps { - courses: CourseEditorCourses_courses[] | undefined + courses: EditorCourseOtherCoursesFieldsFragment[] | undefined } const CourseImageInput = (props: ImageInputProps) => { @@ -31,8 +32,7 @@ const CourseImageInput = (props: ImageInputProps) => { const coursesWithPhotos = courses ?.filter( - (course: CourseEditorCourses_courses) => - course.slug !== values.slug && !!course?.photo?.compressed, + (course) => course.slug !== values.slug && !!course?.photo?.compressed, ) .map((course) => { const translation = (course.course_translations?.filter( 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/ImportPhotoDialog.tsx b/frontend/components/Dashboard/Editor/Course/ImportPhotoDialog.tsx index a6fef696f..3b01009db 100644 --- a/frontend/components/Dashboard/Editor/Course/ImportPhotoDialog.tsx +++ b/frontend/components/Dashboard/Editor/Course/ImportPhotoDialog.tsx @@ -15,14 +15,15 @@ import { import { StyledTextField } from "/components/Dashboard/Editor/common" import { CourseFormValues } from "/components/Dashboard/Editor/Course/types" -import { - CourseEditorCourses_courses, - CourseEditorCourses_courses_photo, -} from "/static/types/generated/CourseEditorCourses" import CoursesTranslations from "/translations/courses" import { addDomain } from "/util/imageUtils" import { useTranslator } from "/util/useTranslator" +import { + EditorCourseOtherCoursesFieldsFragment, + ImageCoreFieldsFragment, +} from "/graphql/generated" + const ImageContainer = styled.div` display: flex; width: 100%; @@ -43,7 +44,7 @@ const ImagePlaceholder = styled.div` interface ImportPhotoDialogProps { open: boolean onClose: () => void - courses: CourseEditorCourses_courses[] + courses: EditorCourseOtherCoursesFieldsFragment[] } const ImportPhotoDialog = ({ @@ -67,10 +68,7 @@ const ImportPhotoDialog = ({ ) }, [values.import_photo]) - const fetchBase64 = ( - photo: CourseEditorCourses_courses_photo, - filename: string, - ) => { + const fetchBase64 = (photo: ImageCoreFieldsFragment, filename: string) => { fetch(filename, { mode: "no-cors", cache: "no-cache", @@ -85,10 +83,7 @@ const ImportPhotoDialog = ({ }) } - const fetchURL = ( - photo: CourseEditorCourses_courses_photo, - filename: string, - ) => { + const fetchURL = (photo: ImageCoreFieldsFragment, filename: string) => { const req = new XMLHttpRequest() req.open("GET", filename, true) req.responseType = "blob" @@ -134,7 +129,7 @@ const ImportPhotoDialog = ({ autoComplete="off" component={StyledTextField} > - {courses?.map((course: CourseEditorCourses_courses) => ( + {courses?.map((course) => ( {course.name} diff --git a/frontend/components/Dashboard/Editor/Course/form-validation.ts b/frontend/components/Dashboard/Editor/Course/form-validation.ts index 682086819..ef8ed2e17 100644 --- a/frontend/components/Dashboard/Editor/Course/form-validation.ts +++ b/frontend/components/Dashboard/Editor/Course/form-validation.ts @@ -1,7 +1,7 @@ import { DateTime } from "luxon" import * as Yup from "yup" -import { ApolloClient, DocumentNode } from "@apollo/client" +import { ApolloClient } from "@apollo/client" import { CourseAliasFormValues, @@ -11,8 +11,14 @@ import { UserCourseSettingsVisibilityFormValues, } from "./types" import { FormValues } from "/components/Dashboard/Editor/types" -import { CourseDetails_course_open_university_registration_links } from "/static/types/generated/CourseDetails" -import { CourseStatus } from "/static/types/generated/globalTypes" +import { Translator } from "/translations" +import { type CoursesTranslations } from "/translations/courses" + +import { + CourseFromSlugDocument, + CourseStatus, + OpenUniversityRegistrationLinkCoreFieldsFragment, +} from "/graphql/generated" export const initialTranslation: CourseTranslationFormValues = { id: undefined, @@ -23,7 +29,7 @@ export const initialTranslation: CourseTranslationFormValues = { open_university_course_link: { course_code: "", link: "", - } as CourseDetails_course_open_university_registration_links, + } as OpenUniversityRegistrationLinkCoreFieldsFragment, } export const initialVariant: CourseVariantFormValues = { @@ -81,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"), @@ -96,7 +102,7 @@ export const statuses = (t: Function) => [ }, ] -export const languages = (t: Function) => [ +export const languages = (t: Translator) => [ { value: "fi_FI", label: t("courseFinnish"), @@ -145,17 +151,13 @@ const testUnique = ( return otherValues.indexOf(value) === -1 } -const courseEditSchema = ({ - client, - checkSlug, - initialSlug, - t, -}: { +interface CourseEditSchemaArgs { client: ApolloClient - checkSlug: DocumentNode 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() @@ -165,7 +167,7 @@ const courseEditSchema = ({ .test( "unique", t("validationSlugInUse"), - validateSlug({ client, checkSlug, initialSlug }), + validateSlug({ client, initialSlug }), ), status: Yup.mixed() .oneOf(statuses(t).map((s) => s.value)) @@ -266,15 +268,12 @@ const courseEditSchema = ({ .min(0), }) -const validateSlug = ({ - checkSlug, - client, - initialSlug, -}: { - checkSlug: DocumentNode +interface ValidateSlugArgs { client: ApolloClient initialSlug: string | null -}) => +} + +const validateSlug = ({ client, initialSlug }: ValidateSlugArgs) => async function ( this: Yup.TestContext, value?: string | null, @@ -289,7 +288,7 @@ const validateSlug = ({ try { const { data } = await client.query({ - query: checkSlug, + query: CourseFromSlugDocument, variables: { slug: value }, }) diff --git a/frontend/components/Dashboard/Editor/Course/index.tsx b/frontend/components/Dashboard/Editor/Course/index.tsx index 948be4b86..bc18389e5 100644 --- a/frontend/components/Dashboard/Editor/Course/index.tsx +++ b/frontend/components/Dashboard/Editor/Course/index.tsx @@ -9,45 +9,43 @@ import CourseEditForm from "./CourseEditForm" import courseEditSchema from "./form-validation" import { fromCourseForm, toCourseForm } from "./serialization" import { CourseFormValues } from "./types" -import { - AddCourseMutation, - DeleteCourseMutation, - UpdateCourseMutation, -} from "/graphql/mutations/courses" -import { - AllCoursesQuery, - AllEditorCoursesQuery, - CheckSlugQuery, - CourseEditorCoursesQuery, - CourseQuery, -} from "/graphql/queries/courses" -import { CourseDetails_course } from "/static/types/generated/CourseDetails" -import { CourseEditorCourses_courses } from "/static/types/generated/CourseEditorCourses" -import { CourseEditorStudyModules_study_modules } from "/static/types/generated/CourseEditorStudyModules" import CoursesTranslations from "/translations/courses" import { useTranslator } from "/util/useTranslator" -const CourseEdit = ({ - course, - modules, - courses, -}: { - course?: CourseDetails_course - modules?: CourseEditorStudyModules_study_modules[] - courses?: CourseEditorCourses_courses[] -}) => { +import { + AddCourseDocument, + CourseEditorOtherCoursesDocument, + CourseFromSlugDocument, + CoursesDocument, + CourseUpsertArg, + DeleteCourseDocument, + EditorCourseDetailedFieldsFragment, + EditorCourseOtherCoursesFieldsFragment, + EditorCoursesDocument, + EmailTemplateEditorCoursesDocument, + StudyModuleDetailedFieldsFragment, + UpdateCourseDocument, +} from "/graphql/generated" + +interface CourseEditProps { + course?: EditorCourseDetailedFieldsFragment + courses?: EditorCourseOtherCoursesFieldsFragment[] + modules?: StudyModuleDetailedFieldsFragment[] +} + +const CourseEdit = ({ course, modules, courses }: CourseEditProps) => { const t = useTranslator(CoursesTranslations) - const [addCourse] = useMutation(AddCourseMutation) - const [updateCourse] = useMutation(UpdateCourseMutation) - const [deleteCourse] = useMutation(DeleteCourseMutation, { + const [addCourse] = useMutation(AddCourseDocument) + const [updateCourse] = useMutation(UpdateCourseDocument) + const [deleteCourse] = useMutation(DeleteCourseDocument, { refetchQueries: [ - { query: AllCoursesQuery }, - { query: AllEditorCoursesQuery }, - { query: CourseEditorCoursesQuery }, + { query: CoursesDocument }, + { query: EditorCoursesDocument }, + { query: CourseEditorOtherCoursesDocument }, + { query: EmailTemplateEditorCoursesDocument }, ], }) - const checkSlug = CheckSlugQuery const client = useApolloClient() @@ -55,7 +53,6 @@ const CourseEdit = ({ const validationSchema = courseEditSchema({ client, - checkSlug, initialSlug: course?.slug && course.slug !== "" ? course.slug : null, t, }) @@ -68,27 +65,41 @@ const CourseEdit = ({ const newCourse = !values.id const mutationVariables = fromCourseForm({ values, initialValues }) + // - if we create a new course, we refetch all courses so the new one is on the list // - if we update, we also need to refetch that course with a potentially updated slug const refetchQueries = [ - { query: AllCoursesQuery }, - { query: AllEditorCoursesQuery }, - { query: CourseEditorCoursesQuery }, + { query: CoursesDocument }, + { query: EditorCoursesDocument }, + { query: CourseEditorOtherCoursesDocument }, + { query: EmailTemplateEditorCoursesDocument }, !newCourse - ? { query: CourseQuery, variables: { slug: values.new_slug } } + ? { + query: CourseFromSlugDocument, + variables: { slug: values.new_slug }, + } : undefined, ].filter((v) => !!v) as PureQueryOptions[] - const courseMutation = newCourse ? addCourse : updateCourse - try { setStatus({ message: t("statusSaving") }) // TODO/FIXME: return value? - await courseMutation({ - variables: { course: mutationVariables }, - refetchQueries: () => refetchQueries, - }) + if (newCourse) { + await addCourse({ + variables: { + course: mutationVariables, + }, + refetchQueries: () => refetchQueries, + }) + } else { + await updateCourse({ + variables: { + course: mutationVariables as CourseUpsertArg, + }, + refetchQueries: () => refetchQueries, + }) + } setStatus({ message: null }) Router.push(`/courses`, undefined, { shallow: true }) diff --git a/frontend/components/Dashboard/Editor/Course/serialization.ts b/frontend/components/Dashboard/Editor/Course/serialization.ts index 924f2e731..dcbbc86a3 100644 --- a/frontend/components/Dashboard/Editor/Course/serialization.ts +++ b/frontend/components/Dashboard/Editor/Course/serialization.ts @@ -4,34 +4,32 @@ import { DateTime } from "luxon" import { initialValues } from "./form-validation" import { CourseFormValues, CourseTranslationFormValues } from "./types" -import { - CourseDetails_course, - CourseDetails_course_photo, -} from "/static/types/generated/CourseDetails" -import { CourseEditorStudyModules_study_modules } from "/static/types/generated/CourseEditorStudyModules" + import { CourseCreateArg, CourseStatus, CourseUpsertArg, -} from "/static/types/generated/globalTypes" + EditorCourseDetailedFieldsFragment, + StudyModuleDetailedFieldsFragment, +} from "/graphql/generated" const isProduction = process.env.NODE_ENV === "production" +interface ToCourseFormArgs { + course?: EditorCourseDetailedFieldsFragment + modules?: StudyModuleDetailedFieldsFragment[] +} + export const toCourseForm = ({ course, modules, -}: { - course?: CourseDetails_course - modules?: CourseEditorStudyModules_study_modules[] -}): CourseFormValues => { +}: ToCourseFormArgs): CourseFormValues => { const courseStudyModules = course?.study_modules?.map((module) => module.id) ?? [] return course ? { ...omit(course, ["__typename"]), - teacher_in_charge_name: course.teacher_in_charge_name ?? "", - teacher_in_charge_email: course.teacher_in_charge_email ?? "", support_email: course.support_email ?? "", start_date: course.start_date ? DateTime.fromISO(course.start_date) @@ -67,7 +65,7 @@ export const toCourseForm = ({ })) ?? [], course_aliases: course?.course_aliases ?? [], new_slug: course.slug, - thumbnail: (course?.photo as CourseDetails_course_photo)?.compressed, + thumbnail: course?.photo?.compressed, ects: course.ects ?? undefined, import_photo: "", inherit_settings_from: course.inherit_settings_from?.id, @@ -87,13 +85,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 = @@ -203,8 +203,6 @@ export const fromCourseForm = ({ inherit_settings_from: values.inherit_settings_from, completions_handled_by: values.completions_handled_by, user_course_settings_visibilities, - teacher_in_charge_email: values.teacher_in_charge_email ?? "", - teacher_in_charge_name: values.teacher_in_charge_name ?? "", status, //values.status as CourseStatus upcoming_active_link: values.upcoming_active_link ?? false, automatic_completions: values.automatic_completions ?? false, diff --git a/frontend/components/Dashboard/Editor/Course/types.ts b/frontend/components/Dashboard/Editor/Course/types.ts index b322e885c..dc6f26dcd 100644 --- a/frontend/components/Dashboard/Editor/Course/types.ts +++ b/frontend/components/Dashboard/Editor/Course/types.ts @@ -1,11 +1,12 @@ import { DateTime } from "luxon" import { FormValues } from "../types" + import { - CourseDetails_course_open_university_registration_links, - CourseDetails_course_photo, -} from "/static/types/generated/CourseDetails" -import { CourseStatus } from "/static/types/generated/globalTypes" + CourseStatus, + ImageCoreFieldsFragment, + OpenUniversityRegistrationLinkCoreFieldsFragment, +} from "/graphql/generated" export interface CourseFormValues extends FormValues { id?: string | null @@ -17,7 +18,7 @@ export interface CourseFormValues extends FormValues { start_date: string | DateTime end_date?: string | DateTime ects?: string - photo?: string | CourseDetails_course_photo | null + photo?: string | ImageCoreFieldsFragment | null start_point: boolean promote: boolean hidden: boolean @@ -25,7 +26,7 @@ export interface CourseFormValues extends FormValues { status: CourseStatus course_translations: CourseTranslationFormValues[] open_university_registration_links?: - | CourseDetails_course_open_university_registration_links[] + | OpenUniversityRegistrationLinkCoreFieldsFragment[] | null study_modules?: { [key: string]: boolean } | null course_variants: CourseVariantFormValues[] @@ -59,7 +60,7 @@ export interface CourseTranslationFormValues extends FormValues { link?: string | null course?: string // open_university_course_code?: string - open_university_course_link?: CourseDetails_course_open_university_registration_links + open_university_course_link?: OpenUniversityRegistrationLinkCoreFieldsFragment } export interface OpenUniversityRegistrationValues extends FormValues { 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 9e84f0e39..0980ada09 100644 --- a/frontend/components/Dashboard/Editor/StudyModule/form-validation.ts +++ b/frontend/components/Dashboard/Editor/StudyModule/form-validation.ts @@ -1,11 +1,15 @@ import * as Yup from "yup" -import { ApolloClient, DocumentNode } from "@apollo/client" +import { ApolloClient } from "@apollo/client" import { StudyModuleFormValues, StudyModuleTranslationFormValues, } from "./types" +import { Translator } from "/translations" +import { type StudyModulesTranslations } from "/translations/study-modules" + +import { StudyModuleExistsDocument } from "/graphql/generated" export const initialTranslation: StudyModuleTranslationFormValues = { id: undefined, @@ -23,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"), @@ -38,17 +42,17 @@ export const languages = (t: Function) => [ }, ] +interface StudyModuleEditSchemaArgs { + client: ApolloClient + initialSlug: string | null + t: Translator +} + const studyModuleEditSchema = ({ client, - checkSlug, initialSlug, t, -}: { - client: ApolloClient - checkSlug: DocumentNode - initialSlug: string | null - t: (key: any) => string -}) => +}: StudyModuleEditSchemaArgs) => Yup.object().shape({ new_slug: Yup.string() .required(t("validationRequired")) @@ -57,7 +61,7 @@ const studyModuleEditSchema = ({ .test( "unique", t("validationSlugInUse"), - validateSlug({ client, checkSlug, initialSlug }), + validateSlug({ client, initialSlug }), ), name: Yup.string().required(t("validationRequired")), study_module_translations: Yup.array().of( @@ -111,15 +115,12 @@ const studyModuleEditSchema = ({ .integer(t("validationInteger")), }) -const validateSlug = ({ - checkSlug, - client, - initialSlug, -}: { - checkSlug: DocumentNode +interface ValidateSlugArgs { client: ApolloClient initialSlug: string | null -}) => +} + +const validateSlug = ({ client, initialSlug }: ValidateSlugArgs) => async function ( this: Yup.TestContext, value?: string | null, @@ -134,7 +135,7 @@ const validateSlug = ({ try { const { data } = await client.query({ - query: checkSlug, + query: StudyModuleExistsDocument, variables: { slug: value }, }) diff --git a/frontend/components/Dashboard/Editor/StudyModule/index.tsx b/frontend/components/Dashboard/Editor/StudyModule/index.tsx index 46168f5ed..89466820a 100644 --- a/frontend/components/Dashboard/Editor/StudyModule/index.tsx +++ b/frontend/components/Dashboard/Editor/StudyModule/index.tsx @@ -9,37 +9,35 @@ import studyModuleEditSchema from "./form-validation" import { fromStudyModuleForm, toStudyModuleForm } from "./serialization" import StudyModuleEditForm from "./StudyModuleEditForm" import { StudyModuleFormValues } from "./types" -import { - AddStudyModuleMutation, - DeleteStudyModuleMutation, - UpdateStudyModuleMutation, -} from "/graphql/mutations/study-modules" -import { - AllEditorModulesQuery, - AllModulesQuery, - CheckModuleSlugQuery, -} from "/graphql/queries/study-modules" -import { StudyModuleQuery } from "/pages/study-modules/[slug]/edit" -import { StudyModuleDetails_study_module } from "/static/types/generated/StudyModuleDetails" import ModulesTranslations from "/translations/study-modules" import { useTranslator } from "/util/useTranslator" -const StudyModuleEdit = ({ - module, -}: { - module?: StudyModuleDetails_study_module -}) => { +import { + AddStudyModuleDocument, + DeleteStudyModuleDocument, + EditorStudyModuleDetailsDocument, + EditorStudyModulesDocument, + StudyModuleDetailedFieldsFragment, + StudyModuleExistsDocument, + StudyModulesDocument, + UpdateStudyModuleDocument, +} from "/graphql/generated" + +interface StudyModuleEditProps { + module?: StudyModuleDetailedFieldsFragment +} + +const StudyModuleEdit = ({ module }: StudyModuleEditProps) => { const t = useTranslator(ModulesTranslations) - const [addStudyModule] = useMutation(AddStudyModuleMutation) - const [updateStudyModule] = useMutation(UpdateStudyModuleMutation) - const [deleteStudyModule] = useMutation(DeleteStudyModuleMutation, { + const [addStudyModule] = useMutation(AddStudyModuleDocument) + const [updateStudyModule] = useMutation(UpdateStudyModuleDocument) + const [deleteStudyModule] = useMutation(DeleteStudyModuleDocument, { refetchQueries: [ - { query: AllModulesQuery }, - { query: AllEditorModulesQuery }, + { query: StudyModulesDocument }, + { query: EditorStudyModulesDocument }, ], }) - const checkSlug = CheckModuleSlugQuery const client = useApolloClient() @@ -47,7 +45,6 @@ const StudyModuleEdit = ({ const validationSchema = studyModuleEditSchema({ client, - checkSlug, initialSlug: module?.slug && module.slug !== "" ? module.slug : null, t, }) @@ -61,12 +58,17 @@ const StudyModuleEdit = ({ const mutationVariables = fromStudyModuleForm({ values }) const refetchQueries = [ - { query: AllModulesQuery }, - { query: AllEditorModulesQuery }, - !newStudyModule - ? { query: StudyModuleQuery, variables: { slug: values.new_slug } } - : undefined, - ].filter((v) => !!v) as PureQueryOptions[] + { query: StudyModulesDocument }, + { query: EditorStudyModulesDocument }, + ...(!newStudyModule + ? [StudyModuleExistsDocument, EditorStudyModuleDetailsDocument].map( + (query) => ({ + query, + variables: { slug: values.new_slug }, + }), + ) + : []), + ] as PureQueryOptions[] const moduleMutation = newStudyModule ? addStudyModule : updateStudyModule diff --git a/frontend/components/Dashboard/Editor/StudyModule/serialization.ts b/frontend/components/Dashboard/Editor/StudyModule/serialization.ts index 74d622e36..cee343a04 100644 --- a/frontend/components/Dashboard/Editor/StudyModule/serialization.ts +++ b/frontend/components/Dashboard/Editor/StudyModule/serialization.ts @@ -5,17 +5,20 @@ import { StudyModuleFormValues, StudyModuleTranslationFormValues, } from "./types" + import { StudyModuleCreateArg, + StudyModuleDetailedFieldsFragment, StudyModuleUpsertArg, -} from "/static/types/generated/globalTypes" -import { StudyModuleDetails_study_module } from "/static/types/generated/StudyModuleDetails" +} from "/graphql/generated" + +interface ToStudyModuleFormArgs { + module?: StudyModuleDetailedFieldsFragment +} export const toStudyModuleForm = ({ module, -}: { - module?: StudyModuleDetails_study_module -}): StudyModuleFormValues => +}: ToStudyModuleFormArgs): StudyModuleFormValues => module ? { ...module, @@ -26,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/ControlledModuleList.tsx b/frontend/components/Dashboard/Editor2/Common/Fields/ControlledModuleList.tsx index 066fa3161..8995ad99f 100644 --- a/frontend/components/Dashboard/Editor2/Common/Fields/ControlledModuleList.tsx +++ b/frontend/components/Dashboard/Editor2/Common/Fields/ControlledModuleList.tsx @@ -23,7 +23,8 @@ import { ControlledFieldProps, FieldController, } from "/components/Dashboard/Editor2/Common/Fields" -import { CourseEditorStudyModules_study_modules } from "/static/types/generated/CourseEditorStudyModules" + +import { StudyModuleDetailedFieldsFragment } from "/graphql/generated" const ModuleList = styled(List)` padding: 0px; @@ -35,7 +36,7 @@ const ModuleListItem = styled(ListItem)` ` interface ControlledModuleListProps extends ControlledFieldProps { - modules?: CourseEditorStudyModules_study_modules[] + modules?: StudyModuleDetailedFieldsFragment[] } export function ControlledModuleList(props: ControlledModuleListProps) { 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/CourseEditForm.tsx b/frontend/components/Dashboard/Editor2/Course/CourseEditForm.tsx index 8fdd749a6..e70513c61 100644 --- a/frontend/components/Dashboard/Editor2/Course/CourseEditForm.tsx +++ b/frontend/components/Dashboard/Editor2/Course/CourseEditForm.tsx @@ -29,23 +29,26 @@ import UserCourseSettingsVisibilityForm from "/components/Dashboard/Editor2/Cour import EditorContainer from "/components/Dashboard/Editor2/EditorContainer" import { useEditorContext } from "/components/Dashboard/Editor2/EditorContext" import DisableAutoComplete from "/components/DisableAutoComplete" -import { CourseDetails_course } from "/static/types/generated/CourseDetails" -import { CourseEditorCourses_courses } from "/static/types/generated/CourseEditorCourses" -import { CourseEditorStudyModules_study_modules } from "/static/types/generated/CourseEditorStudyModules" -import { CourseStatus } from "/static/types/generated/globalTypes" import CommonTranslations from "/translations/common" import CoursesTranslations from "/translations/courses" import { useQueryParameter } from "/util/useQueryParameter" import { useTranslator } from "/util/useTranslator" +import { + CourseStatus, + EditorCourseDetailedFieldsFragment, + EditorCourseOtherCoursesFieldsFragment, + StudyModuleDetailedFieldsFragment, +} from "/graphql/generated" + const SelectLanguageFirstCover = styled.div<{ covered: boolean }>` ${(props) => `opacity: ${props.covered ? `0.2` : `1`}`} ` interface CourseEditFormProps { - course?: CourseDetails_course - courses?: CourseEditorCourses_courses[] - studyModules?: CourseEditorStudyModules_study_modules[] + course?: EditorCourseDetailedFieldsFragment + courses?: EditorCourseOtherCoursesFieldsFragment[] + studyModules?: StudyModuleDetailedFieldsFragment[] } export default function CourseEditForm({ @@ -73,11 +76,8 @@ export default function CourseEditForm({ const sortedCourses = useMemo( () => courses - ?.filter((c: CourseEditorCourses_courses) => c.id !== course?.id) - .sort( - (a: CourseEditorCourses_courses, b: CourseEditorCourses_courses) => - a?.name < b?.name ? -1 : 1, - ), + ?.filter((c) => c.id !== course?.id) + .sort((a, b) => (a?.name < b?.name ? -1 : 1)), [courses], ) diff --git a/frontend/components/Dashboard/Editor2/Course/CourseImageForm.tsx b/frontend/components/Dashboard/Editor2/Course/CourseImageForm.tsx index 3d1cff4cc..8dd070cbf 100644 --- a/frontend/components/Dashboard/Editor2/Course/CourseImageForm.tsx +++ b/frontend/components/Dashboard/Editor2/Course/CourseImageForm.tsx @@ -16,14 +16,16 @@ import { } from "/components/Dashboard/Editor2/Common/Fields" import { CourseFormValues } from "/components/Dashboard/Editor2/Course/types" import { useEditorContext } from "/components/Dashboard/Editor2/EditorContext" -import { CourseEditorCourses_courses } from "/static/types/generated/CourseEditorCourses" import CoursesTranslations from "/translations/courses" import { addDomain } from "/util/imageUtils" import { useTranslator } from "/util/useTranslator" +import { EditorCourseOtherCoursesFieldsFragment } from "/graphql/generated" + interface CourseImageFormProps { - courses?: CourseEditorCourses_courses[] + courses?: EditorCourseOtherCoursesFieldsFragment[] } + export default function CourseImageForm({ courses }: CourseImageFormProps) { const { locale = "fi" } = useRouter() const t = useTranslator(CoursesTranslations) @@ -54,10 +56,7 @@ export default function CourseImageForm({ courses }: CourseImageFormProps) { const coursesWithPhotos = courses - ?.filter( - (course: CourseEditorCourses_courses) => - course.slug !== slug && !!course?.photo?.compressed, - ) + ?.filter((course) => course.slug !== slug && !!course?.photo?.compressed) .map((course) => { const translation = (course.course_translations?.filter( (t) => t.language === locale, 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/ImportPhotoDialog.tsx b/frontend/components/Dashboard/Editor2/Course/ImportPhotoDialog.tsx index 798fb3410..26d41e6af 100644 --- a/frontend/components/Dashboard/Editor2/Course/ImportPhotoDialog.tsx +++ b/frontend/components/Dashboard/Editor2/Course/ImportPhotoDialog.tsx @@ -13,14 +13,15 @@ import { } from "@mui/material" import { ControlledSelect } from "/components/Dashboard/Editor2/Common/Fields" -import { - CourseEditorCourses_courses, - CourseEditorCourses_courses_photo, -} from "/static/types/generated/CourseEditorCourses" import CoursesTranslations from "/translations/courses" import { addDomain } from "/util/imageUtils" import { useTranslator } from "/util/useTranslator" +import { + EditorCourseOtherCoursesFieldsFragment, + ImageCoreFieldsFragment, +} from "/graphql/generated" + const ImageContainer = styled.div` display: flex; width: 100%; @@ -39,7 +40,7 @@ const ImagePlaceholder = styled.div` ` interface ImportPhotoDialogProps { - courses?: CourseEditorCourses_courses[] + courses?: EditorCourseOtherCoursesFieldsFragment[] open: boolean onClose: () => void } @@ -50,15 +51,11 @@ export default function ImportPhotoDialog({ courses = [], }: ImportPhotoDialogProps) { const { setValue, getValues, watch } = useFormContext() - const [selected, setSelected] = useState( - null, - ) + const [selected, setSelected] = + useState(null) const t = useTranslator(CoursesTranslations) - const fetchBase64 = ( - photo: CourseEditorCourses_courses_photo, - filename: string, - ) => { + const fetchBase64 = (photo: ImageCoreFieldsFragment, filename: string) => { fetch(filename, { mode: "no-cors", cache: "no-cache", @@ -73,10 +70,7 @@ export default function ImportPhotoDialog({ }) } - const fetchURL = ( - photo: CourseEditorCourses_courses_photo, - filename: string, - ) => { + const fetchURL = (photo: ImageCoreFieldsFragment, filename: string) => { const req = new XMLHttpRequest() req.open("GET", filename, true) req.responseType = "blob" @@ -92,9 +86,9 @@ export default function ImportPhotoDialog({ const photo = watch("import_photo") const handleSelection = () => { - const photo = ( - courses?.find((course) => course.id === getValues("import_photo")) ?? {} - ).photo + const photo = courses?.find( + (course) => course.id === getValues("import_photo"), + )?.photo if (!photo) { return diff --git a/frontend/components/Dashboard/Editor2/Course/form-validation.tsx b/frontend/components/Dashboard/Editor2/Course/form-validation.tsx index 416924a3c..5d8eb6657 100644 --- a/frontend/components/Dashboard/Editor2/Course/form-validation.tsx +++ b/frontend/components/Dashboard/Editor2/Course/form-validation.tsx @@ -1,7 +1,7 @@ import { DateTime } from "luxon" import * as Yup from "yup" -import { ApolloClient, DocumentNode } from "@apollo/client" +import { ApolloClient } from "@apollo/client" import { CourseAliasFormValues, @@ -11,7 +11,10 @@ import { UserCourseSettingsVisibilityFormValues, } from "./types" import { testUnique } from "/components/Dashboard/Editor2/Common" -import { CourseStatus } from "/static/types/generated/globalTypes" +import { Translator } from "/translations" +import { type CoursesTranslations } from "/translations/courses" + +import { CourseFromSlugDocument, CourseStatus } from "/graphql/generated" export const initialTranslation: CourseTranslationFormValues = { _id: undefined, @@ -83,17 +86,13 @@ export const initialVisibility: UserCourseSettingsVisibilityFormValues = { export const study_modules: { value: any; label: any }[] = [] -const courseEditSchema = ({ - client, - checkSlug, - initialSlug, - t, -}: { +interface CourseEditSchemaArgs { client: ApolloClient - checkSlug: DocumentNode 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() @@ -103,7 +102,7 @@ const courseEditSchema = ({ .test( "unique", t("validationSlugInUse"), - validateSlug({ client, checkSlug, initialSlug }), + validateSlug({ client, initialSlug }), ), status: Yup.mixed() .oneOf(Object.keys(CourseStatus)) @@ -208,15 +207,12 @@ const courseEditSchema = ({ }) } -const validateSlug = ({ - checkSlug, - client, - initialSlug, -}: { - checkSlug: DocumentNode +interface ValidateSlugArgs { client: ApolloClient initialSlug: string | null -}) => +} + +const validateSlug = ({ client, initialSlug }: ValidateSlugArgs) => async function ( this: Yup.TestContext, value?: string | null, @@ -231,7 +227,7 @@ const validateSlug = ({ try { const { data } = await client.query({ - query: checkSlug, + query: CourseFromSlugDocument, variables: { slug: value }, }) diff --git a/frontend/components/Dashboard/Editor2/Course/index.tsx b/frontend/components/Dashboard/Editor2/Course/index.tsx index 7275a6ee3..657d8f628 100644 --- a/frontend/components/Dashboard/Editor2/Course/index.tsx +++ b/frontend/components/Dashboard/Editor2/Course/index.tsx @@ -12,34 +12,33 @@ import { customValidationResolver } from "/components/Dashboard/Editor2/Common" import courseEditSchema from "/components/Dashboard/Editor2/Course/form-validation" import { FormStatus } from "/components/Dashboard/Editor2/types" import { useAnchorContext } from "/contexts/AnchorContext" -import { - AddCourseMutation, - DeleteCourseMutation, - UpdateCourseMutation, -} from "/graphql/mutations/courses" -import { - AllCoursesQuery, - AllEditorCoursesQuery, - CheckSlugQuery, - CourseEditorCoursesQuery, - CourseQuery, -} from "/graphql/queries/courses" import withEnumeratingAnchors from "/lib/with-enumerating-anchors" -import { CourseDetails_course } from "/static/types/generated/CourseDetails" -import { CourseEditorCourses_courses } from "/static/types/generated/CourseEditorCourses" -import { CourseEditorStudyModules_study_modules } from "/static/types/generated/CourseEditorStudyModules" import CoursesTranslations from "/translations/courses" import notEmpty from "/util/notEmpty" import { getFirstErrorAnchor } from "/util/useEnumeratingAnchors" import { useTranslator } from "/util/useTranslator" -interface CourseEditorProps { - course?: CourseDetails_course - courses?: CourseEditorCourses_courses[] - studyModules?: CourseEditorStudyModules_study_modules[] +import { + AddCourseDocument, + CourseEditorOtherCoursesDocument, + CourseFromSlugDocument, + CoursesDocument, + CourseUpsertArg, + DeleteCourseDocument, + EditorCourseDetailedFieldsFragment, + EditorCourseOtherCoursesFieldsFragment, + EditorCoursesDocument, + StudyModuleDetailedFieldsFragment, + UpdateCourseDocument, +} from "/graphql/generated" + +interface CourseEditProps { + course?: EditorCourseDetailedFieldsFragment + courses?: EditorCourseOtherCoursesFieldsFragment[] + studyModules?: StudyModuleDetailedFieldsFragment[] } -function CourseEditor({ course, courses, studyModules }: CourseEditorProps) { +function CourseEditor({ course, courses, studyModules }: CourseEditProps) { const t = useTranslator(CoursesTranslations) const [status, setStatus] = useState({ message: null }) const [tab, setTab] = useState(0) @@ -52,7 +51,6 @@ function CourseEditor({ course, courses, studyModules }: CourseEditorProps) { }) const validationSchema = courseEditSchema({ client, - checkSlug: CheckSlugQuery, initialSlug: course?.slug && course.slug !== "" ? course.slug : null, t, }) @@ -69,17 +67,17 @@ function CourseEditor({ course, courses, studyModules }: CourseEditorProps) { trigger() }, []) - const [addCourse] = useMutation(AddCourseMutation) - const [updateCourse] = useMutation(UpdateCourseMutation) - const [deleteCourse] = useMutation(DeleteCourseMutation, { + const [addCourse] = useMutation(AddCourseDocument) + const [updateCourse] = useMutation(UpdateCourseDocument) + const [deleteCourse] = useMutation(DeleteCourseDocument, { refetchQueries: [ - { query: AllCoursesQuery }, - { query: AllEditorCoursesQuery }, - { query: CourseEditorCoursesQuery }, + { query: CoursesDocument }, + { query: EditorCoursesDocument }, + { query: CourseEditorOtherCoursesDocument }, ], }) - const onSubmit = useCallback(async (values: CourseFormValues, _?: any) => { + const onSubmit = useCallback(async (values: CourseFormValues) => { const newCourse = !values.id const mutationVariables = fromCourseForm({ values, @@ -88,25 +86,33 @@ function CourseEditor({ course, courses, studyModules }: CourseEditorProps) { // - if we create a new course, we refetch all courses so the new one is on the list // - if we update, we also need to refetch that course with a potentially updated slug const refetchQueries = [ - { query: AllCoursesQuery }, - { query: AllEditorCoursesQuery }, - { query: CourseEditorCoursesQuery }, + { query: CoursesDocument }, + { query: EditorCoursesDocument }, + { query: CourseEditorOtherCoursesDocument }, !newCourse - ? { query: CourseQuery, variables: { slug: values.new_slug } } + ? { + query: CourseFromSlugDocument, + variables: { slug: values.new_slug }, + } : undefined, ].filter(notEmpty) as PureQueryOptions[] - const courseMutation = newCourse ? addCourse : updateCourse - console.log("would mutate", mutationVariables) try { setStatus({ message: t("statusSaving") }) console.log("trying to save") - await courseMutation({ - variables: { course: mutationVariables }, - refetchQueries: () => refetchQueries, - }) + if (newCourse) { + await addCourse({ + variables: { course: mutationVariables }, + refetchQueries: () => refetchQueries, + }) + } else { + await updateCourse({ + variables: { course: mutationVariables as CourseUpsertArg }, + refetchQueries: () => refetchQueries, + }) + } setStatus({ message: null }) } catch (err: any) { setStatus({ message: err.message, error: true }) @@ -114,7 +120,7 @@ function CourseEditor({ course, courses, studyModules }: CourseEditorProps) { }, []) 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 2addd587d..fe219f251 100644 --- a/frontend/components/Dashboard/Editor2/Course/serialization.ts +++ b/frontend/components/Dashboard/Editor2/Course/serialization.ts @@ -3,26 +3,26 @@ import { DateTime } from "luxon" import { initialValues } from "./form-validation" import { CourseFormValues, CourseTranslationFormValues } from "./types" -import { - CourseDetails_course, - CourseDetails_course_photo, -} from "/static/types/generated/CourseDetails" -import { CourseEditorStudyModules_study_modules } from "/static/types/generated/CourseEditorStudyModules" + import { CourseCreateArg, CourseStatus, CourseUpsertArg, -} from "/static/types/generated/globalTypes" + EditorCourseDetailedFieldsFragment, + StudyModuleDetailedFieldsFragment, +} from "/graphql/generated" const isProduction = process.env.NODE_ENV === "production" +interface ToCourseFormArgs { + course?: EditorCourseDetailedFieldsFragment + modules?: StudyModuleDetailedFieldsFragment[] +} + export const toCourseForm = ({ course, modules, -}: { - course?: CourseDetails_course - modules?: CourseEditorStudyModules_study_modules[] -}): CourseFormValues => { +}: ToCourseFormArgs): CourseFormValues => { const courseStudyModules = course?.study_modules?.map((module) => module.id) ?? [] @@ -81,7 +81,7 @@ export const toCourseForm = ({ _id: c.id ?? undefined, })) ?? [], new_slug: course.slug, - thumbnail: (course?.photo as CourseDetails_course_photo)?.compressed, + thumbnail: course?.photo?.compressed, ects: course.ects ?? undefined, import_photo: "", inherit_settings_from: course.inherit_settings_from?.id, @@ -107,13 +107,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 console.log(values) diff --git a/frontend/components/Dashboard/Editor2/Course/types.ts b/frontend/components/Dashboard/Editor2/Course/types.ts index ece35103a..b254818ab 100644 --- a/frontend/components/Dashboard/Editor2/Course/types.ts +++ b/frontend/components/Dashboard/Editor2/Course/types.ts @@ -1,7 +1,6 @@ import { DateTime } from "luxon" -import { CourseDetails_course_photo } from "/static/types/generated/CourseDetails" -import { CourseStatus } from "/static/types/generated/globalTypes" +import { CourseStatus, ImageCoreFieldsFragment } from "/graphql/generated" interface FormValues { id?: string | null @@ -16,7 +15,7 @@ export interface CourseFormValues extends FormValues { start_date: string | DateTime end_date?: string | DateTime ects?: string - photo?: string | CourseDetails_course_photo | null + photo?: string | ImageCoreFieldsFragment | null start_point: boolean promote: boolean hidden: boolean diff --git a/frontend/components/Dashboard/Editor2/StudyModule/form-validation.ts b/frontend/components/Dashboard/Editor2/StudyModule/form-validation.ts index 6e367ba36..3a72d91d1 100644 --- a/frontend/components/Dashboard/Editor2/StudyModule/form-validation.ts +++ b/frontend/components/Dashboard/Editor2/StudyModule/form-validation.ts @@ -1,12 +1,16 @@ import * as Yup from "yup" -import { ApolloClient, DocumentNode } from "@apollo/client" +import { ApolloClient } from "@apollo/client" import { StudyModuleFormValues, 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" export const initialTranslation: StudyModuleTranslationFormValues = { _id: undefined, @@ -24,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"), @@ -53,17 +57,17 @@ function validateImage(this: Yup.TestContext, _value?: any): boolean { return true } +interface StudyModuleEditSchemaArgs { + client: ApolloClient + initialSlug: string | null + t: Translator +} + const studyModuleEditSchema = ({ client, - checkSlug, initialSlug, t, -}: { - client: ApolloClient - checkSlug: DocumentNode - initialSlug: string | null - t: (key: any) => string -}) => +}: StudyModuleEditSchemaArgs) => Yup.object().shape({ new_slug: Yup.string() .required(t("validationRequired")) @@ -72,7 +76,7 @@ const studyModuleEditSchema = ({ .test( "unique", t("validationSlugInUse"), - validateSlug({ client, checkSlug, initialSlug }), + validateSlug({ client, initialSlug }), ), image: Yup.string().test("exists", t("moduleImageError"), validateImage), name: Yup.string().required(t("validationRequired")), @@ -102,15 +106,12 @@ const studyModuleEditSchema = ({ .integer(t("validationInteger")), }) -const validateSlug = ({ - checkSlug, - client, - initialSlug, -}: { - checkSlug: DocumentNode +interface ValidateSlugArgs { client: ApolloClient initialSlug: string | null -}) => +} + +const validateSlug = ({ client, initialSlug }: ValidateSlugArgs) => async function ( this: Yup.TestContext, value?: string | null, @@ -125,7 +126,7 @@ const validateSlug = ({ try { const { data } = await client.query({ - query: checkSlug, + query: StudyModuleExistsDocument, variables: { slug: value }, }) diff --git a/frontend/components/Dashboard/Editor2/StudyModule/index.tsx b/frontend/components/Dashboard/Editor2/StudyModule/index.tsx index 2f45c3f3d..1cb05c4c2 100644 --- a/frontend/components/Dashboard/Editor2/StudyModule/index.tsx +++ b/frontend/components/Dashboard/Editor2/StudyModule/index.tsx @@ -13,39 +13,35 @@ import { StudyModuleFormValues } from "./types" import { customValidationResolver } from "/components/Dashboard/Editor2/Common" import { FormStatus } from "/components/Dashboard/Editor2/types" import { useAnchorContext } from "/contexts/AnchorContext" -import { - AddStudyModuleMutation, - DeleteStudyModuleMutation, - UpdateStudyModuleMutation, -} from "/graphql/mutations/study-modules" -import { - AllEditorModulesQuery, - AllModulesQuery, - CheckModuleSlugQuery, -} from "/graphql/queries/study-modules" import withEnumeratingAnchors from "/lib/with-enumerating-anchors" -import { StudyModuleQuery } from "/pages/study-modules/[slug]/edit" -import { StudyModuleDetails_study_module } from "/static/types/generated/StudyModuleDetails" import ModulesTranslations from "/translations/study-modules" import { getFirstErrorAnchor } from "/util/useEnumeratingAnchors" import { useTranslator } from "/util/useTranslator" -const StudyModuleEdit = ({ - module, -}: { - module?: StudyModuleDetails_study_module -}) => { +import { + AddStudyModuleDocument, + DeleteStudyModuleDocument, + EditorStudyModuleDetailsDocument, + EditorStudyModulesDocument, + StudyModuleDetailedFieldsFragment, + StudyModuleExistsDocument, + StudyModulesDocument, + UpdateStudyModuleDocument, +} from "/graphql/generated" + +interface StudyModuleEditProps { + module?: StudyModuleDetailedFieldsFragment +} + +const StudyModuleEdit = ({ module }: StudyModuleEditProps) => { const t = useTranslator(ModulesTranslations) const [status, setStatus] = useState({ message: null }) const client = useApolloClient() const { anchors } = useAnchorContext() - const checkSlug = CheckModuleSlugQuery - const defaultValues = toStudyModuleForm({ module }) const validationSchema = studyModuleEditSchema({ client, - checkSlug, initialSlug: module?.slug && module.slug !== "" ? module.slug : null, t, }) @@ -60,12 +56,12 @@ const StudyModuleEdit = ({ trigger() }, []) - const [addStudyModule] = useMutation(AddStudyModuleMutation) - const [updateStudyModule] = useMutation(UpdateStudyModuleMutation) - const [deleteStudyModule] = useMutation(DeleteStudyModuleMutation, { + const [addStudyModule] = useMutation(AddStudyModuleDocument) + const [updateStudyModule] = useMutation(UpdateStudyModuleDocument) + const [deleteStudyModule] = useMutation(DeleteStudyModuleDocument, { refetchQueries: [ - { query: AllModulesQuery }, - { query: AllEditorModulesQuery }, + { query: StudyModulesDocument }, + { query: EditorStudyModulesDocument }, ], }) @@ -75,12 +71,17 @@ const StudyModuleEdit = ({ const mutationVariables = fromStudyModuleForm({ values }) const refetchQueries = [ - { query: AllModulesQuery }, - { query: AllEditorModulesQuery }, - !newStudyModule - ? { query: StudyModuleQuery, variables: { slug: values.new_slug } } - : undefined, - ].filter((v) => !!v) as PureQueryOptions[] + { query: StudyModulesDocument }, + { query: EditorStudyModulesDocument }, + ...(!newStudyModule + ? [StudyModuleExistsDocument, EditorStudyModuleDetailsDocument].map( + (query) => ({ + query, + variables: { slug: values.new_slug }, + }), + ) + : []), + ] as PureQueryOptions[] const moduleMutation = newStudyModule ? addStudyModule : updateStudyModule diff --git a/frontend/components/Dashboard/Editor2/StudyModule/serialization.ts b/frontend/components/Dashboard/Editor2/StudyModule/serialization.ts index 381968d97..9fde5cb86 100644 --- a/frontend/components/Dashboard/Editor2/StudyModule/serialization.ts +++ b/frontend/components/Dashboard/Editor2/StudyModule/serialization.ts @@ -5,17 +5,20 @@ import { StudyModuleFormValues, StudyModuleTranslationFormValues, } from "./types" + import { StudyModuleCreateArg, + StudyModuleDetailedFieldsFragment, StudyModuleUpsertArg, -} from "/static/types/generated/globalTypes" -import { StudyModuleDetails_study_module } from "/static/types/generated/StudyModuleDetails" +} from "/graphql/generated" + +interface ToStudyModuleFormArgs { + module?: StudyModuleDetailedFieldsFragment +} export const toStudyModuleForm = ({ module, -}: { - module?: StudyModuleDetails_study_module -}): StudyModuleFormValues => +}: ToStudyModuleFormArgs): StudyModuleFormValues => module ? { ...module, @@ -30,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/PaginatedPointsList.tsx b/frontend/components/Dashboard/PaginatedPointsList.tsx index 14eec09b0..f93461a0d 100644 --- a/frontend/components/Dashboard/PaginatedPointsList.tsx +++ b/frontend/components/Dashboard/PaginatedPointsList.tsx @@ -2,64 +2,16 @@ import { ChangeEvent, useEffect, useState } from "react" import { range } from "lodash" -import { gql, useLazyQuery } from "@apollo/client" +import { useLazyQuery } from "@apollo/client" import styled from "@emotion/styled" import { Button, Grid, Skeleton, Slider, TextField } from "@mui/material" import PointsList from "./DashboardPointsList" import ErrorBoundary from "/components/ErrorBoundary" -import { ProgressUserCourseProgressFragment } from "/graphql/fragments/userCourseProgress" -import { ProgressUserCourseServiceProgressFragment } from "/graphql/fragments/userCourseServiceProgress" -import { UserCourseSettings as StudentProgressData } from "/static/types/generated/UserCourseSettings" import notEmpty from "/util/notEmpty" import useDebounce from "/util/useDebounce" -export const StudentProgresses = gql` - query UserCourseSettings( - $course_id: ID! - $skip: Int - $after: String - $search: String - ) { - userCourseSettings( - course_id: $course_id - first: 15 - after: $after - skip: $skip - search: $search - ) { - pageInfo { - hasNextPage - endCursor - } - edges { - node { - id - user { - id - first_name - last_name - email - student_number - real_student_number - progress(course_id: $course_id) { - course { - name - id - } - ...ProgressUserCourseProgressFragment - ...ProgressUserCourseServiceProgressFragment - } - } - } - } - totalCount - } - } - ${ProgressUserCourseProgressFragment} - ${ProgressUserCourseServiceProgressFragment} -` -// count(course_id: $course_id, search: $search) +import { StudentProgressesDocument } from "/graphql/generated" const LoadingPointCardSkeleton = styled(Skeleton)` width: 100%; @@ -80,12 +32,14 @@ function PaginatedPointsList(props: Props) { const [search, setSearch] = useDebounce(searchString, 1000) // use lazy query to prevent running query on each render - const [getData, { data, loading, error, fetchMore }] = - useLazyQuery(StudentProgresses, { + const [getData, { data, loading, error, fetchMore }] = useLazyQuery( + StudentProgressesDocument, + { // fetchPolicy: "cache-first", ssr: false, // notifyOnNetworkStatusChange :true - }) + }, + ) useEffect(() => { getData({ @@ -106,7 +60,9 @@ function PaginatedPointsList(props: Props) { label: value, })) - const edges = (data?.userCourseSettings?.edges ?? []).filter(notEmpty) + const users = (data?.userCourseSettings?.edges ?? []) + .map((e) => e?.node) + .filter(notEmpty) return ( @@ -158,7 +114,7 @@ function PaginatedPointsList(props: Props) { {data?.userCourseSettings?.totalCount || 0} results - + { fetchMore({ diff --git a/frontend/components/Dashboard/PointsExportButton.tsx b/frontend/components/Dashboard/PointsExportButton.tsx index 39785a3dc..5f5449148 100644 --- a/frontend/components/Dashboard/PointsExportButton.tsx +++ b/frontend/components/Dashboard/PointsExportButton.tsx @@ -2,14 +2,15 @@ import { useState } from "react" import { utils, type WorkBook, writeFile } from "xlsx" -import { ApolloClient, gql, useApolloClient } from "@apollo/client" +import { ApolloClient, useApolloClient } from "@apollo/client" import styled from "@emotion/styled" import { ButtonWithPaddingAndMargin as StyledButton } from "/components/Buttons/ButtonWithPaddingAndMargin" + import { - ExportUserCourseProgesses, - ExportUserCourseProgesses_userCourseProgresses, -} from "/static/types/generated/ExportUserCourseProgesses" + ExportUserCourseProgressesDocument, + ExportUserCourseProgressesQuery, +} from "/graphql/generated" const PointsExportButtonContainer = styled.div` margin-bottom: 1rem; @@ -59,9 +60,15 @@ function PointsExportButton(props: PointsExportButtonProps) { ) } -async function flatten(data: ExportUserCourseProgesses_userCourseProgresses[]) { +async function flatten( + data: ExportUserCourseProgressesQuery["userCourseProgresses"], +) { console.log("data in flatten", data) + if (!data) { + return [] + } + const newData = data.map((datum) => { const { upstream_id, @@ -74,7 +81,7 @@ async function flatten(data: ExportUserCourseProgesses_userCourseProgresses[]) { 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() ?? "", @@ -101,15 +108,15 @@ async function flatten(data: ExportUserCourseProgesses_userCourseProgresses[]) { async function downloadInChunks( courseSlug: string, client: ApolloClient, - setMessage: any, -): Promise { - const res = [] + setMessage: React.Dispatch>, +): Promise { + const res: ExportUserCourseProgressesQuery["userCourseProgresses"] = [] // let after: string | undefined = undefined let skip = 0 while (1 === 1) { - const { data } = await client.query({ - query: GET_DATA, + const { data } = await client.query({ + query: ExportUserCourseProgressesDocument, variables: { course_slug: courseSlug, skip, @@ -118,7 +125,7 @@ async function downloadInChunks( first: 100*/ }, }) - let downloaded: any = data?.userCourseProgresses ?? [] + let downloaded = data?.userCourseProgresses ?? [] if (downloaded.length === 0) { break } @@ -130,34 +137,7 @@ async function downloadInChunks( const nDownLoaded = res.push(...downloaded) setMessage(`Downloaded progress for ${nDownLoaded} users...`) } - return res as unknown as ExportUserCourseProgesses_userCourseProgresses[] + return res } export default PointsExportButton - -const GET_DATA = gql` - query ExportUserCourseProgesses( - $course_slug: String! - $skip: Int - $take: Int - ) { - userCourseProgresses(course_slug: $course_slug, skip: $skip, take: $take) { - id - user { - id - email - student_number - real_student_number - upstream_id - first_name - last_name - } - progress - user_course_settings { - course_variant - country - language - } - } - } -` diff --git a/frontend/components/Dashboard/PointsItemTable.tsx b/frontend/components/Dashboard/PointsItemTable.tsx index e3cb4674d..35a8a9310 100644 --- a/frontend/components/Dashboard/PointsItemTable.tsx +++ b/frontend/components/Dashboard/PointsItemTable.tsx @@ -1,8 +1,8 @@ import PointsListItemTableChart from "/components/Dashboard/PointsListItemTableChart" -import { formattedGroupPointsDictionary } from "/util/formatPointsData" +import { FormattedGroupPointsDictionary } from "/util/formatPointsData" interface TableProps { - studentPoints: formattedGroupPointsDictionary["groups"] + studentPoints: FormattedGroupPointsDictionary["groups"] showDetailedBreakdown: boolean cutterValue: number } diff --git a/frontend/components/Dashboard/PointsListItemCard.tsx b/frontend/components/Dashboard/PointsListItemCard.tsx index 69ca91113..5be9ea3cc 100644 --- a/frontend/components/Dashboard/PointsListItemCard.tsx +++ b/frontend/components/Dashboard/PointsListItemCard.tsx @@ -1,6 +1,5 @@ import { useState } from "react" -import { gql } from "@apollo/client" import styled from "@emotion/styled" import { Grid } from "@mui/material" @@ -8,34 +7,13 @@ import PointsItemTable from "./PointsItemTable" import { FormSubmitButton } from "/components/Buttons/FormSubmitButton" import PointsProgress from "/components/Dashboard/PointsProgress" import { CardSubtitle, CardTitle } from "/components/Text/headers" -import { ProgressUserCourseProgressFragment } from "/graphql/fragments/userCourseProgress" -import { ProgressUserCourseServiceProgressFragment } from "/graphql/fragments/userCourseServiceProgress" -import { UserCourseProgressFragment } from "/static/types/generated/UserCourseProgressFragment" -import { UserCourseServiceProgressFragment } from "/static/types/generated/UserCourseServiceProgressFragment" -import { UserPoints_currentUser_progresses_course } from "/static/types/generated/UserPoints" -import formatPointsData, { - formattedGroupPointsDictionary, -} from "/util/formatPointsData" +import formatPointsData from "/util/formatPointsData" -const UserFragment = gql` - fragment UserPointsFragment on User { - id - first_name - last_name - email - student_number - progresses { - course { - name - id - } - ...ProgressUserCourseProgressFragment - ...ProgressUserCourseServiceProgressFragment - } - } - ${ProgressUserCourseProgressFragment} - ${ProgressUserCourseServiceProgressFragment} -` +import { + CourseCoreFieldsFragment, + UserCourseProgressCoreFieldsFragment, + UserCourseServiceProgressCoreFieldsFragment, +} from "/graphql/generated" const Root = styled(Grid)` background-color: white; @@ -43,9 +21,28 @@ const Root = styled(Grid)` padding: 1rem; ` +interface PersonalDetails { + firstName: string + lastName: string + email: string + sid: string +} +interface PointsListItemCardProps { + course?: CourseCoreFieldsFragment | null + userCourseProgress?: UserCourseProgressCoreFieldsFragment | null + userCourseServiceProgresses?: + | UserCourseServiceProgressCoreFieldsFragment[] + | null + cutterValue?: number + showPersonalDetails?: boolean + personalDetails?: PersonalDetails + showProgress?: boolean +} + interface PersonalDetailsDisplayProps { personalDetails: PersonalDetails } + const PersonalDetailsDisplay = (props: PersonalDetailsDisplayProps) => { const { personalDetails } = props return ( @@ -58,24 +55,7 @@ const PersonalDetailsDisplay = (props: PersonalDetailsDisplayProps) => { > ) } -interface PersonalDetails { - firstName: string - lastName: string - email: string - sid: string -} - -interface Props { - course?: UserPoints_currentUser_progresses_course | null - userCourseProgress?: UserCourseProgressFragment | null - userCourseServiceProgresses?: UserCourseServiceProgressFragment[] | null - cutterValue?: number - showPersonalDetails?: boolean - personalDetails?: PersonalDetails - showProgress?: boolean -} - -function PointsListItemCard(props: Props) { +function PointsListItemCard(props: PointsListItemCardProps) { const { course, userCourseProgress, @@ -87,7 +67,8 @@ function PointsListItemCard(props: Props) { } = props const [showDetails, setShowDetails] = useState(false) - const formattedPointsData: formattedGroupPointsDictionary = formatPointsData({ + // TODO: do this in the backend + const formattedPointsData = formatPointsData({ userCourseProgress, userCourseServiceProgresses, }) @@ -136,8 +117,4 @@ function PointsListItemCard(props: Props) { ) } -PointsListItemCard.fragments = { - user: UserFragment, -} - export default PointsListItemCard diff --git a/frontend/components/Dashboard/PointsListItemTableChart.tsx b/frontend/components/Dashboard/PointsListItemTableChart.tsx index 5d3aa4215..4b6870110 100644 --- a/frontend/components/Dashboard/PointsListItemTableChart.tsx +++ b/frontend/components/Dashboard/PointsListItemTableChart.tsx @@ -3,7 +3,7 @@ import { CardSubtitle } from "components/Text/headers" import styled from "@emotion/styled" import LinearProgress from "@mui/material/LinearProgress" -import { formattedGroupPoints } from "/util/formatPointsData" +import { FormattedGroupPoints } from "/util/formatPointsData" const ChartContainer = styled.div` display: flex; @@ -29,7 +29,7 @@ const ColoredProgressBar = styled(({ ...props }) => ( interface Props { title: string - points: formattedGroupPoints + points: FormattedGroupPoints cuttervalue: Number showDetailed: Boolean } 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) => ( <> ` flex: 1; ` interface ModuleCardProps { - module?: AllEditorModulesWithTranslations_study_modules + module?: StudyModuleDetailedFieldsFragment loading?: boolean } diff --git a/frontend/components/Dashboard/StudyModules/ModuleGrid.tsx b/frontend/components/Dashboard/StudyModules/ModuleGrid.tsx index bbf96863c..1ac3e6caf 100644 --- a/frontend/components/Dashboard/StudyModules/ModuleGrid.tsx +++ b/frontend/components/Dashboard/StudyModules/ModuleGrid.tsx @@ -3,10 +3,11 @@ import { range } from "lodash" import { Grid } from "@mui/material" import ModuleCard from "./ModuleCard" -import { AllEditorModulesWithTranslations_study_modules } from "/static/types/generated/AllEditorModulesWithTranslations" + +import { StudyModuleDetailedFieldsFragment } from "/graphql/generated" interface ModuleGridProps { - modules?: AllEditorModulesWithTranslations_study_modules[] + modules?: StudyModuleDetailedFieldsFragment[] loading: boolean } diff --git a/frontend/components/Dashboard/Users/MobileGrid.tsx b/frontend/components/Dashboard/Users/MobileGrid.tsx index 41fcd6e7b..16c02fdc9 100644 --- a/frontend/components/Dashboard/Users/MobileGrid.tsx +++ b/frontend/components/Dashboard/Users/MobileGrid.tsx @@ -22,13 +22,11 @@ import { import Pagination from "/components/Dashboard/Users/Pagination" import UserSearchContext from "/contexts/UserSearchContext" -import { - UserDetailsContains_userDetailsContains_edges, - UserDetailsContains_userDetailsContains_edges_node, -} from "/static/types/generated/UserDetailsContains" import UsersTranslations from "/translations/users" import { useTranslator } from "/util/useTranslator" +import { UserCoreFieldsFragment } from "/graphql/generated" + const UserCard = styled(Card)` margin-top: 0.5rem; margin-bottom: 0.5rem; @@ -90,22 +88,22 @@ const RenderCards: FC = () => { {data?.userDetailsContains?.edges?.map((row) => ( ))} > ) } -const DataCard = ({ - row, -}: { - row?: UserDetailsContains_userDetailsContains_edges -}) => { +interface DataCardProps { + row?: UserCoreFieldsFragment +} + +const DataCard = ({ row }: DataCardProps) => { const t = useTranslator(UsersTranslations) const { email, upstream_id, first_name, last_name, student_number } = - row?.node ?? ({} as UserDetailsContains_userDetailsContains_edges_node) + row || {} const fields = [ { diff --git a/frontend/components/Dashboard/Users/Pagination.tsx b/frontend/components/Dashboard/Users/Pagination.tsx index ebefc3e36..3018065f1 100644 --- a/frontend/components/Dashboard/Users/Pagination.tsx +++ b/frontend/components/Dashboard/Users/Pagination.tsx @@ -153,7 +153,7 @@ const Pagination: React.FC = () => { } = 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/Dashboard/Users/Summary/CollapseContext.tsx b/frontend/components/Dashboard/Users/Summary/CollapseContext.tsx index 953b1f656..04aee024a 100644 --- a/frontend/components/Dashboard/Users/Summary/CollapseContext.tsx +++ b/frontend/components/Dashboard/Users/Summary/CollapseContext.tsx @@ -2,7 +2,7 @@ import { createContext, Dispatch, useContext } from "react" import { produce } from "immer" -import { UserSummary_user_user_course_summary } from "/static/types/generated/UserSummary" +import { UserCourseSummaryCoreFieldsFragment } from "/graphql/generated" export type ExerciseState = Record export type CourseState = { @@ -199,7 +199,7 @@ export const collapseReducer = ( } export const createInitialState = ( - data?: UserSummary_user_user_course_summary[], + data?: UserCourseSummaryCoreFieldsFragment[], ) => data?.reduce( (collapseState, courseEntry) => ({ diff --git a/frontend/components/Dashboard/Users/Summary/Completion.tsx b/frontend/components/Dashboard/Users/Summary/Completion.tsx index 3a5b54d5b..05e7eab6d 100644 --- a/frontend/components/Dashboard/Users/Summary/Completion.tsx +++ b/frontend/components/Dashboard/Users/Summary/Completion.tsx @@ -18,16 +18,17 @@ import { import CollapseButton from "/components/Buttons/CollapseButton" import { formatDateTime } from "/components/DataFormatFunctions" import { CompletionListItem } from "/components/Home/Completions" -import { - UserSummary_user_user_course_summary_completion, - UserSummary_user_user_course_summary_course, -} from "/static/types/generated/UserSummary" import ProfileTranslations from "/translations/profile" import { useTranslator } from "/util/useTranslator" +import { + CompletionDetailedFieldsFragment, + UserCourseSummaryCourseFieldsFragment, +} from "/graphql/generated" + interface CompletionProps { - completion?: UserSummary_user_user_course_summary_completion - course: UserSummary_user_user_course_summary_course + completion?: CompletionDetailedFieldsFragment + course: UserCourseSummaryCourseFieldsFragment } export default function Completion({ completion, course }: CompletionProps) { diff --git a/frontend/components/Dashboard/Users/Summary/CourseEntry.tsx b/frontend/components/Dashboard/Users/Summary/CourseEntry.tsx index 4a8ce04cb..4a292d52e 100644 --- a/frontend/components/Dashboard/Users/Summary/CourseEntry.tsx +++ b/frontend/components/Dashboard/Users/Summary/CourseEntry.tsx @@ -15,11 +15,12 @@ import ExerciseList from "./ExerciseList" import ProgressEntry from "./ProgressEntry" import CollapseButton from "/components/Buttons/CollapseButton" import { CardTitle } from "/components/Text/headers" -import { UserSummary_user_user_course_summary } from "/static/types/generated/UserSummary" import notEmpty from "/util/notEmpty" +import { UserCourseSummaryCoreFieldsFragment } from "/graphql/generated" + interface CourseEntryProps { - data?: UserSummary_user_user_course_summary + data?: UserCourseSummaryCoreFieldsFragment } const CourseEntryCard = styled(Card)` diff --git a/frontend/components/Dashboard/Users/Summary/ExerciseEntry.tsx b/frontend/components/Dashboard/Users/Summary/ExerciseEntry.tsx index 1c669373b..c1dc79868 100644 --- a/frontend/components/Dashboard/Users/Summary/ExerciseEntry.tsx +++ b/frontend/components/Dashboard/Users/Summary/ExerciseEntry.tsx @@ -3,18 +3,18 @@ import React from "react" import { Chip, Collapse, TableCell, TableRow } from "@mui/material" import { useCollapseContext } from "./CollapseContext" -import { - UserSummary_user_user_course_summary_course_exercises, - UserSummary_user_user_course_summary_exercise_completions, - UserSummary_user_user_course_summary_exercise_completions_exercise_completion_required_actions, -} from "/static/types/generated/UserSummary" import ProfileTranslations from "/translations/profile" // import CollapseButton from "/components/Buttons/CollapseButton" import { useTranslator } from "/util/useTranslator" +import { + ExerciseCompletionCoreFieldsFragment, + ExerciseCoreFieldsFragment, +} from "/graphql/generated" + interface ExerciseEntryProps { - exercise: UserSummary_user_user_course_summary_course_exercises & { - exercise_completions: UserSummary_user_user_course_summary_exercise_completions[] + exercise: ExerciseCoreFieldsFragment & { + exercise_completions: ExerciseCompletionCoreFieldsFragment[] } } @@ -47,9 +47,7 @@ export default function ExerciseEntry({ exercise }: ExerciseEntryProps) { {exerciseCompletion?.exercise_completion_required_actions.map( - ( - action: UserSummary_user_user_course_summary_exercise_completions_exercise_completion_required_actions, - ) => ( + (action) => ( // @ts-ignore: translator key ), diff --git a/frontend/components/Dashboard/Users/Summary/ExerciseList.tsx b/frontend/components/Dashboard/Users/Summary/ExerciseList.tsx index 510c0b754..ff839cab5 100644 --- a/frontend/components/Dashboard/Users/Summary/ExerciseList.tsx +++ b/frontend/components/Dashboard/Users/Summary/ExerciseList.tsx @@ -9,16 +9,17 @@ import { } from "@mui/material" import ExerciseEntry from "./ExerciseEntry" -import { - UserSummary_user_user_course_summary_course_exercises, - UserSummary_user_user_course_summary_exercise_completions, -} from "/static/types/generated/UserSummary" import ProfileTranslations from "/translations/profile" import { useTranslator } from "/util/useTranslator" +import { + ExerciseCompletionCoreFieldsFragment, + ExerciseCoreFieldsFragment, +} from "/graphql/generated" + interface ExerciseListProps { - exercises: (UserSummary_user_user_course_summary_course_exercises & { - exercise_completions: UserSummary_user_user_course_summary_exercise_completions[] + exercises: (ExerciseCoreFieldsFragment & { + exercise_completions: ExerciseCompletionCoreFieldsFragment[] })[] } diff --git a/frontend/components/Dashboard/Users/Summary/ProgressEntry.tsx b/frontend/components/Dashboard/Users/Summary/ProgressEntry.tsx index e76389c52..b13c2aaff 100644 --- a/frontend/components/Dashboard/Users/Summary/ProgressEntry.tsx +++ b/frontend/components/Dashboard/Users/Summary/ProgressEntry.tsx @@ -18,16 +18,21 @@ import { import CollapseButton from "/components/Buttons/CollapseButton" import PointsListItemCard from "/components/Dashboard/PointsListItemCard" import PointsProgress from "/components/Dashboard/PointsProgress" -import { UserCourseProgressFragment } from "/static/types/generated/UserCourseProgressFragment" -import { UserCourseServiceProgressFragment } from "/static/types/generated/UserCourseServiceProgressFragment" -import { UserSummary_user_user_course_summary_course } from "/static/types/generated/UserSummary" import ProfileTranslations from "/translations/profile" import { useTranslator } from "/util/useTranslator" +import { + UserCourseProgressCoreFieldsFragment, + UserCourseServiceProgressCoreFieldsFragment, + UserCourseSummaryCourseFieldsFragment, +} from "/graphql/generated" + interface ProgressEntryProps { - userCourseProgress?: UserCourseProgressFragment | null - userCourseServiceProgresses?: UserCourseServiceProgressFragment[] | null - course: UserSummary_user_user_course_summary_course + userCourseProgress?: UserCourseProgressCoreFieldsFragment | null + userCourseServiceProgresses?: + | UserCourseServiceProgressCoreFieldsFragment[] + | null + course: UserCourseSummaryCourseFieldsFragment } export default function ProgressEntry({ diff --git a/frontend/components/Dashboard/Users/Summary/UserPointsSummary.tsx b/frontend/components/Dashboard/Users/Summary/UserPointsSummary.tsx index aeb59760a..b41314e6a 100644 --- a/frontend/components/Dashboard/Users/Summary/UserPointsSummary.tsx +++ b/frontend/components/Dashboard/Users/Summary/UserPointsSummary.tsx @@ -13,12 +13,13 @@ import { useCollapseContext, } from "/components/Dashboard/Users/Summary/CollapseContext" import RawView from "/components/Dashboard/Users/Summary/RawView" -import { UserSummary_user_user_course_summary } from "/static/types/generated/UserSummary" import CommonTranslations from "/translations/common" import { useTranslator } from "/util/useTranslator" +import { UserCourseSummaryCoreFieldsFragment } from "/graphql/generated" + interface UserPointsSummaryProps { - data?: UserSummary_user_user_course_summary[] + data?: UserCourseSummaryCoreFieldsFragment[] search?: string } diff --git a/frontend/components/FilterMenu.tsx b/frontend/components/FilterMenu.tsx index 31541982f..1a043b37e 100644 --- a/frontend/components/FilterMenu.tsx +++ b/frontend/components/FilterMenu.tsx @@ -16,10 +16,11 @@ import { TextField, } from "@mui/material" -import { HandlerCourses_handlerCourses } from "/static/types/generated/HandlerCourses" import CommonTranslations from "/translations/common" import { useTranslator } from "/util/useTranslator" +import { CourseCoreFieldsFragment, CourseStatus } from "/graphql/generated" + const Container = styled.div` background-color: white; padding: 0.5rem; @@ -55,7 +56,7 @@ interface SearchVariables { search?: string hidden?: boolean | null handledBy?: string | null - status?: string[] | null + status?: CourseStatus[] | null } interface FilterFields { @@ -66,9 +67,9 @@ interface FilterFields { interface FilterProps { searchVariables: SearchVariables setSearchVariables: React.Dispatch - handlerCourses?: HandlerCourses_handlerCourses[] + handlerCourses?: CourseCoreFieldsFragment[] status?: string[] - setStatus?: React.Dispatch> + setStatus?: React.Dispatch> loading: boolean fields?: FilterFields } @@ -116,9 +117,11 @@ export default function FilterMenu({ const handleStatusChange = (value: string) => (e: React.ChangeEvent) => { - const newStatus = e.target.checked - ? [...(searchVariables?.status || []), value] - : searchVariables?.status?.filter((v) => v !== value) || [] + const newStatus = ( + e.target.checked + ? [...(searchVariables?.status || []), value] + : searchVariables?.status?.filter((v) => v !== value) || [] + ) as CourseStatus[] setStatus(newStatus) setSearchVariables({ @@ -257,12 +260,12 @@ export default function FilterMenu({ onClick={() => { setHidden(true) setHandledBy("") - setStatus(["Active", "Upcoming"]) + setStatus([CourseStatus.Active, CourseStatus.Upcoming]) setSearchVariables({ search: "", hidden: true, handledBy: null, - status: ["Active", "Upcoming"], + status: [CourseStatus.Active, CourseStatus.Upcoming], }) }} style={{ marginLeft: "0.5rem" }} 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/Completions/CompletionListItem.tsx b/frontend/components/Home/Completions/CompletionListItem.tsx index 813d3948f..3086e9f75 100644 --- a/frontend/components/Home/Completions/CompletionListItem.tsx +++ b/frontend/components/Home/Completions/CompletionListItem.tsx @@ -10,19 +10,15 @@ import { formatDateTime, mapLangToLanguage, } from "/components/DataFormatFunctions" -import { CompletionsRegisteredFragment_completions_registered } from "/static/types/generated/CompletionsRegisteredFragment" -import { - ProfileUserOverView_currentUser_completions, - ProfileUserOverView_currentUser_completions_course, -} from "/static/types/generated/ProfileUserOverView" -import { - UserSummary_user_user_course_summary_completion, - UserSummary_user_user_course_summary_course, -} from "/static/types/generated/UserSummary" import ProfileTranslations from "/translations/profile" import { addDomain } from "/util/imageUtils" import { useTranslator } from "/util/useTranslator" +import { + CompletionDetailedFieldsFragment, + UserCourseSummaryCourseFieldsFragment, +} from "/graphql/generated" + const StyledButton = styled(Button)` //height: 50%; color: black; @@ -32,11 +28,15 @@ const StyledA = styled.a` margin: auto; ` +interface CompletionListItemProps { + completion: CompletionDetailedFieldsFragment + course: Omit +} + interface CourseAvatarProps { - course: - | UserSummary_user_user_course_summary_course - | ProfileUserOverView_currentUser_completions_course + course: CompletionListItemProps["course"] } + const CourseAvatar = ({ course }: CourseAvatarProps) => { return ( { +export const CompletionListItem = ({ + completion, + course, +}: CompletionListItemProps) => { const isRegistered = (completion?.completions_registered ?? []).length > 0 const t = useTranslator(ProfileTranslations) @@ -146,9 +140,7 @@ export const CompletionListItem = ({ completion, course }: ListItemProps) => { {isRegistered && completion.completions_registered - ? ( - completion.completions_registered as CompletionsRegisteredFragment_completions_registered[] - )?.map((r) => { + ? completion.completions_registered?.map((r) => { return ( diff --git a/frontend/components/Home/Completions/Completions.tsx b/frontend/components/Home/Completions/Completions.tsx index dee2f3e33..600cfb734 100644 --- a/frontend/components/Home/Completions/Completions.tsx +++ b/frontend/components/Home/Completions/Completions.tsx @@ -1,46 +1,15 @@ -import { gql } from "@apollo/client" import styled from "@emotion/styled" import { Typography } from "@mui/material" import { RegularContainer as Container } from "/components/Container" import { CompletionListItem } from "/components/Home/Completions" -import { ProfileUserOverView_currentUser_completions } from "/static/types/generated/ProfileUserOverView" import ProfileTranslations from "/translations/profile" import { useTranslator } from "/util/useTranslator" -const completionsFragment = gql` - fragment UserCompletions on User { - completions { - id - completion_language - student_number - created_at - tier - eligible_for_ects - completion_date - course { - id - slug - name - photo { - id - uncompressed - } - has_certificate - } - completions_registered { - id - created_at - organization { - slug - } - } - } - } -` +import { CompletionDetailedFieldsWithCourseFragment } from "/graphql/generated" export interface CompletionsProps { - completions: ProfileUserOverView_currentUser_completions[] + completions: CompletionDetailedFieldsWithCourseFragment[] } const Title = styled(Typography)` @@ -80,7 +49,3 @@ export const Completions = ({ completions = [] }: CompletionsProps) => { ) } - -Completions.fragments = { - completions: completionsFragment, -} diff --git a/frontend/components/Home/CourseAndModuleList.tsx b/frontend/components/Home/CourseAndModuleList.tsx index 988525ed5..d2f1a95bb 100644 --- a/frontend/components/Home/CourseAndModuleList.tsx +++ b/frontend/components/Home/CourseAndModuleList.tsx @@ -8,17 +8,17 @@ import CourseHighlights from "./CourseHighlights" import ModuleList from "./ModuleList" import ModuleNavi from "./ModuleNavi" import ModifiableErrorMessage from "/components/ModifiableErrorMessage" -import { AllCoursesQuery } from "/graphql/queries/courses" -import { AllModulesQuery } from "/graphql/queries/study-modules" -import { AllCourses as AllCoursesData } from "/static/types/generated/AllCourses" -import { AllModules as AllModulesData } from "/static/types/generated/AllModules" -import { CourseStatus } from "/static/types/generated/globalTypes" -import { AllModules_study_modules_with_courses } from "/static/types/moduleTypes" import HomeTranslations from "/translations/home" import { mapNextLanguageToLocaleCode } from "/util/moduleFunctions" import notEmpty from "/util/notEmpty" import { useTranslator } from "/util/useTranslator" +import { + CoursesDocument, + CourseStatus, + StudyModulesDocument, +} from "/graphql/generated" + // const highlightsBanner = "/static/images/backgroundPattern.svg" const CourseAndModuleList = () => { @@ -26,22 +26,23 @@ const CourseAndModuleList = () => { const t = useTranslator(HomeTranslations) const language = mapNextLanguageToLocaleCode(locale) + // TODO: do this in one query; get module courses already in backend const { loading: coursesLoading, error: coursesError, data: coursesData, - } = useQuery(AllCoursesQuery, { variables: { language } }) + } = useQuery(CoursesDocument, { variables: { language } }) const { loading: modulesLoading, error: modulesError, data: modulesData, - } = useQuery(AllModulesQuery, { variables: { language } }) + } = useQuery(StudyModulesDocument, { variables: { language } }) const courses = coursesData?.courses let study_modules = modulesData?.study_modules?.filter(notEmpty) const modulesWithCourses = useMemo( - (): AllModules_study_modules_with_courses[] => + () => (study_modules || []) .filter(notEmpty) .map((module) => { diff --git a/frontend/components/Home/CourseCard.tsx b/frontend/components/Home/CourseCard.tsx index 73a09840b..a0610773e 100644 --- a/frontend/components/Home/CourseCard.tsx +++ b/frontend/components/Home/CourseCard.tsx @@ -8,10 +8,11 @@ import { CourseImageBase } from "/components/Images/CardBackgroundFullCover" import { ClickableButtonBase } from "/components/Surfaces/ClickableCard" import { CardTitle } from "/components/Text/headers" import { CardText } from "/components/Text/paragraphs" -import { AllCourses_courses } from "/static/types/generated/AllCourses" import HomeTranslations from "/translations/home" import { useTranslator } from "/util/useTranslator" +import { CourseFieldsFragment } from "/graphql/generated" + const Background = styled(ClickableButtonBase)<{ component: any }>` display: flex; flex-direction: column; @@ -67,7 +68,7 @@ const CardLinkWithGA = styled(ReactGA.OutboundLink)` text-decoration: none; ` interface CourseCardProps { - course?: AllCourses_courses + course?: CourseFieldsFragment } export default function CourseCard({ course }: CourseCardProps) { diff --git a/frontend/components/Home/CourseHighlights.tsx b/frontend/components/Home/CourseHighlights.tsx index 9b4f2797e..bc340f16c 100644 --- a/frontend/components/Home/CourseHighlights.tsx +++ b/frontend/components/Home/CourseHighlights.tsx @@ -5,7 +5,8 @@ import CourseCard from "./CourseCard" import Container from "/components/Container" import { BackgroundImage } from "/components/Images/GraphicBackground" import { H2Background, SubtitleBackground } from "/components/Text/headers" -import { AllCourses_courses } from "/static/types/generated/AllCourses" + +import { CourseFieldsFragment } from "/graphql/generated" interface RootProps { backgroundColor: string @@ -22,7 +23,7 @@ const Root = styled.div` ` interface CourseHighlightsProps { - courses?: AllCourses_courses[] + courses?: CourseFieldsFragment[] loading: boolean title: string headerImage: any 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) => ( { diff --git a/frontend/components/Home/ModuleDisplay/ModuleCoursesDisplay.tsx b/frontend/components/Home/ModuleDisplay/ModuleCoursesDisplay.tsx index be8ffc7e9..714f07903 100644 --- a/frontend/components/Home/ModuleDisplay/ModuleCoursesDisplay.tsx +++ b/frontend/components/Home/ModuleDisplay/ModuleCoursesDisplay.tsx @@ -4,11 +4,11 @@ import ModuleCoursesListing, { ThreeOrLessCoursesListing, } from "/components/Home/ModuleDisplay/ModuleCourseCardList" import { H2Background } from "/components/Text/headers" -import { AllCourses_courses as CourseData } from "/static/types/generated/AllCourses" -import { CourseStatus } from "/static/types/generated/globalTypes" import HomeTranslations from "/translations/home" import { useTranslator } from "/util/useTranslator" +import { CourseFieldsFragment, CourseStatus } from "/graphql/generated" + const CoursesListContainer = styled.div` margin: 2rem 2em 2em 2rem; padding: 1rem; @@ -20,7 +20,7 @@ const CoursesListTitle = styled(H2Background)` margin-bottom: 3rem; ` interface ModuleCoursesProps { - courses: CourseData[] + courses: CourseFieldsFragment[] } const ModuleCoursesDisplay = (props: ModuleCoursesProps) => { diff --git a/frontend/components/Home/ModuleDisplay/ModuleDisplay.tsx b/frontend/components/Home/ModuleDisplay/ModuleDisplay.tsx index c157d0886..a4ffe35f8 100644 --- a/frontend/components/Home/ModuleDisplay/ModuleDisplay.tsx +++ b/frontend/components/Home/ModuleDisplay/ModuleDisplay.tsx @@ -5,11 +5,15 @@ import { orderBy } from "lodash" import ModuleDisplayBackground from "/components/Home/ModuleDisplay/ModuleDisplayBackground" import ModuleDisplayContent from "/components/Home/ModuleDisplay/ModuleDisplayContent" import ModuleDisplaySkeleton from "/components/Home/ModuleDisplay/ModuleDisplaySkeleton" -import { CourseStatus } from "/static/types/generated/globalTypes" -import { AllModules_study_modules_with_courses } from "/static/types/moduleTypes" +import notEmpty from "/util/notEmpty" + +import { + CourseStatus, + StudyModuleFieldsWithCoursesFragment, +} from "/graphql/generated" interface ModuleProps { - module?: AllModules_study_modules_with_courses + module?: StudyModuleFieldsWithCoursesFragment hueRotateAngle: number brightness: number backgroundColor: string @@ -17,10 +21,11 @@ interface ModuleProps { function Module(props: ModuleProps) { const { module, hueRotateAngle, brightness, backgroundColor } = props + const orderedCourses = useMemo( () => orderBy( - module?.courses || [], + (module?.courses || []).filter(notEmpty), [ (course) => course.study_module_order, (course) => course.study_module_start_point === true, @@ -32,6 +37,10 @@ function Module(props: ModuleProps) { [module?.courses], ) + if (!module) { + return null + } + return ( ` ` 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/Home/ModuleDisplay/ModuleDisplayContent.tsx b/frontend/components/Home/ModuleDisplay/ModuleDisplayContent.tsx index aac1c5d8a..ea24c49f7 100644 --- a/frontend/components/Home/ModuleDisplay/ModuleDisplayContent.tsx +++ b/frontend/components/Home/ModuleDisplay/ModuleDisplayContent.tsx @@ -1,12 +1,13 @@ import { CenteredContent } from "/components/Home/ModuleDisplay/Common" import ModuleCoursesDisplay from "/components/Home/ModuleDisplay/ModuleCoursesDisplay" import ModuleDescription from "/components/Home/ModuleDisplay/ModuleDescription" -import { AllCourses_courses as CourseData } from "/static/types/generated/AllCourses" + +import { CourseFieldsFragment } from "/graphql/generated" interface ModuleDisplayProps { name: string description: string | JSX.Element - orderedCourses?: CourseData[] + orderedCourses?: CourseFieldsFragment[] } const ModuleDisplayContent = (props: ModuleDisplayProps) => { const { name, description, orderedCourses = [] } = props diff --git a/frontend/components/Home/ModuleImage.tsx b/frontend/components/Home/ModuleImage.tsx index 469dcf4ef..b019b0201 100644 --- a/frontend/components/Home/ModuleImage.tsx +++ b/frontend/components/Home/ModuleImage.tsx @@ -1,13 +1,12 @@ import { BackgroundImage } from "/components/Images/CardBackgroundFullCover" -import { AllModules_study_modules } from "/static/types/generated/AllModules" -import { AllModules_study_modules_with_courses } from "/static/types/moduleTypes" import { mime } from "/util/imageUtils" -const ModuleImage = ({ - module, -}: { - module?: AllModules_study_modules | AllModules_study_modules_with_courses -}) => { +import { StudyModuleFieldsFragment } from "/graphql/generated" + +interface ModuleImageProps { + module: StudyModuleFieldsFragment +} +const ModuleImage = ({ module }: ModuleImageProps) => { const imageUrl = module?.image ?? (module ? `${module.slug}.jpg` : "") try { diff --git a/frontend/components/Home/ModuleList.tsx b/frontend/components/Home/ModuleList.tsx index fd5aaa2cb..9f4ae07cd 100644 --- a/frontend/components/Home/ModuleList.tsx +++ b/frontend/components/Home/ModuleList.tsx @@ -1,7 +1,8 @@ import Module from "./ModuleDisplay/ModuleDisplay" import PartnerDivider from "/components/PartnerDivider" import LUT from "/static/md_pages/lut_module.mdx" -import { AllModules_study_modules_with_courses } from "/static/types/moduleTypes" + +import { StudyModuleFieldsWithCoursesFragment } from "/graphql/generated" const moduleColors: Array<{ backgroundColor: string @@ -33,7 +34,7 @@ const moduleColors: Array<{ type ModuleComponent = | { type: "module" - module: AllModules_study_modules_with_courses + module: StudyModuleFieldsWithCoursesFragment } | { type: "custom-module" @@ -55,13 +56,11 @@ const customModuleComponents: Array = [ }, ] -const ModuleList = ({ - modules, - loading, -}: { - modules: AllModules_study_modules_with_courses[] +interface ModuleListProps { + modules: StudyModuleFieldsWithCoursesFragment[] loading: boolean -}) => { +} +const ModuleList = ({ modules, loading }: ModuleListProps) => { if (loading) { return } diff --git a/frontend/components/Home/ModuleNavi.tsx b/frontend/components/Home/ModuleNavi.tsx index db5e7d464..e34ce9e73 100644 --- a/frontend/components/Home/ModuleNavi.tsx +++ b/frontend/components/Home/ModuleNavi.tsx @@ -3,10 +3,11 @@ import styled from "@emotion/styled" import ModuleNaviCard from "./ModuleNaviCard" import Container from "/components/Container" import { H2Background } from "/components/Text/headers" -import { AllModules_study_modules } from "/static/types/generated/AllModules" import HomeTranslations from "/translations/home" import { useTranslator } from "/util/useTranslator" +import { StudyModuleFieldsFragment } from "/graphql/generated" + const NaviArea = styled.section` margin-bottom: 5em; margin-top: 5em; @@ -48,13 +49,12 @@ const Grid = styled.div` } ` -const ModuleNavi = ({ - modules, - loading, -}: { - modules?: AllModules_study_modules[] +interface ModuleNaviProps { + modules?: StudyModuleFieldsFragment[] loading: boolean -}) => { +} + +const ModuleNavi = ({ modules, loading }: ModuleNaviProps) => { const t = useTranslator(HomeTranslations) return ( diff --git a/frontend/components/Home/ModuleNaviCard.tsx b/frontend/components/Home/ModuleNaviCard.tsx index 7d71db357..2af655a17 100644 --- a/frontend/components/Home/ModuleNaviCard.tsx +++ b/frontend/components/Home/ModuleNaviCard.tsx @@ -8,7 +8,8 @@ import { FullCoverTextBackground } from "/components/Images/CardBackgroundFullCo import { ClickableButtonBase } from "/components/Surfaces/ClickableCard" import { CardTitle } from "/components/Text/headers" import { CardText } from "/components/Text/paragraphs" -import { AllModules_study_modules } from "/static/types/generated/AllModules" + +import { StudyModuleFieldsFragment } from "/graphql/generated" const SkeletonTitle = styled(Skeleton)` margin-top: 0.5rem; @@ -43,7 +44,11 @@ const GridItem = styled.div` } ` -const ModuleNaviCard = ({ module }: { module?: AllModules_study_modules }) => ( +interface ModuleNaviCardProps { + module?: StudyModuleFieldsFragment +} + +const ModuleNaviCard = ({ module }: ModuleNaviCardProps) => ( diff --git a/frontend/components/Home/ModuleSmallCourseCard.tsx b/frontend/components/Home/ModuleSmallCourseCard.tsx index 5deb7fc89..115b1ed53 100644 --- a/frontend/components/Home/ModuleSmallCourseCard.tsx +++ b/frontend/components/Home/ModuleSmallCourseCard.tsx @@ -8,11 +8,11 @@ import { ModuleCardTitle, } from "/components/Home/ModuleDisplay/Common" import { ClickableButtonBase } from "/components/Surfaces/ClickableCard" -import { AllCourses_courses } from "/static/types/generated/AllCourses" -import { CourseStatus } from "/static/types/generated/globalTypes" import HomeTranslations from "/translations/home" import { useTranslator } from "/util/useTranslator" +import { CourseFieldsFragment, CourseStatus } from "/graphql/generated" + const SkeletonTitle = styled(Skeleton)` margin-bottom: 0.5rem; ` @@ -96,7 +96,7 @@ const Header = styled.div` ` interface ModuleSmallCourseCardProps { - course?: AllCourses_courses + course?: CourseFieldsFragment showHeader?: boolean } @@ -123,10 +123,10 @@ function ModuleSmallCourseCard({ course!.status === CourseStatus.Upcoming) && ( - {course!.status === "Upcoming" + {course!.status === CourseStatus.Upcoming ? t("upcomingShort") : t("moduleCourseStartPoint")} 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/components/Profile/ProfilePointsDisplay.tsx b/frontend/components/Profile/ProfilePointsDisplay.tsx index f0c5f6334..d8f0cdbac 100644 --- a/frontend/components/Profile/ProfilePointsDisplay.tsx +++ b/frontend/components/Profile/ProfilePointsDisplay.tsx @@ -5,15 +5,14 @@ import { useQuery } from "@apollo/client" import { FormSubmitButton } from "/components/Buttons/FormSubmitButton" import ErrorMessage from "/components/ErrorMessage" import Spinner from "/components/Spinner" -import { studentHasPoints } from "/components/User/Points/PointsList" import PointsListGrid from "/components/User/Points/PointsListGrid" -import { UserPointsQuery } from "/components/User/Points/PointsQuery" -import { UserPoints as UserPointsData } from "/static/types/generated/UserPoints" import ProfileTranslations from "/translations/profile" import { useTranslator } from "/util/useTranslator" +import { CurrentUserProgressesDocument } from "/graphql/generated" + const ProfilePointsDisplay = () => { - const { data, error, loading } = useQuery(UserPointsQuery) + const { data, error, loading } = useQuery(CurrentUserProgressesDocument) const t = useTranslator(ProfileTranslations) if (loading) { @@ -24,9 +23,7 @@ const ProfilePointsDisplay = () => { return } - const hasPoints = studentHasPoints({ pointsData: data }) - - if (!hasPoints) { + if (!data?.currentUser?.progresses) { return <>> } diff --git a/frontend/components/Profile/ProfileSettings.tsx b/frontend/components/Profile/ProfileSettings.tsx index 4a08de44a..e7f787d79 100644 --- a/frontend/components/Profile/ProfileSettings.tsx +++ b/frontend/components/Profile/ProfileSettings.tsx @@ -1,24 +1,20 @@ import { ChangeEvent, useState } from "react" -import { gql, useMutation } from "@apollo/client" +import { useMutation } from "@apollo/client" import CustomSnackbar from "/components/CustomSnackbar" import ResearchConsent from "/components/Dashboard/ResearchConsent" -import { UserOverViewQuery } from "/pages/profile" -import { ProfileUserOverView_currentUser } from "/static/types/generated/ProfileUserOverView" import ProfileTranslations from "/translations/profile" import { useTranslator } from "/util/useTranslator" -const updateResearchConsentMutation = gql` - mutation updateUpdateAccountResearchConsent($value: Boolean!) { - updateResearchConsent(value: $value) { - id - } - } -` +import { + CurrentUserOverviewDocument, + UpdateResearchConsentDocument, + UserOverviewFieldsFragment, +} from "/graphql/generated" interface ProfileSettingsProps { - data?: ProfileUserOverView_currentUser + data?: UserOverviewFieldsFragment } interface SnackbarProps { @@ -43,9 +39,9 @@ const ProfileSettings = ({ data }: ProfileSettingsProps) => { : "0", ) const [updateResearchConsent, { loading }] = useMutation( - updateResearchConsentMutation, + UpdateResearchConsentDocument, { - refetchQueries: [{ query: UserOverViewQuery }], + refetchQueries: [{ query: CurrentUserOverviewDocument }], }, ) const [isSnackbarOpen, setIsSnackbarOpen] = useState(false) diff --git a/frontend/components/Profile/StudentDataDisplay.tsx b/frontend/components/Profile/StudentDataDisplay.tsx index 2c58027e7..3cf54afe1 100644 --- a/frontend/components/Profile/StudentDataDisplay.tsx +++ b/frontend/components/Profile/StudentDataDisplay.tsx @@ -7,12 +7,13 @@ import Box from "@mui/material/Box" import Typography from "@mui/material/Typography" import ProfileSettings from "/components/Profile/ProfileSettings" -import { ProfileUserOverView_currentUser } from "/static/types/generated/ProfileUserOverView" import notEmpty from "/util/notEmpty" +import { UserOverviewFieldsFragment } from "/graphql/generated" + interface TabPanelProps { - index: any - value: any + index: number + value: number } const TabPanel = ({ @@ -33,7 +34,7 @@ const TabPanel = ({ interface StudentDataDisplayProps { tab: number - data?: ProfileUserOverView_currentUser + data?: UserOverviewFieldsFragment } const StudentDataDisplay = ({ tab, data }: StudentDataDisplayProps) => { diff --git a/frontend/components/Profile/VerifiedUsers/VerifiedUser.tsx b/frontend/components/Profile/VerifiedUsers/VerifiedUser.tsx index 9704c99a6..7ffa8d2b1 100644 --- a/frontend/components/Profile/VerifiedUsers/VerifiedUser.tsx +++ b/frontend/components/Profile/VerifiedUsers/VerifiedUser.tsx @@ -1,4 +1,4 @@ -// import { ProfileUserOverView_currentUser_verified_users } from "/static/types/generated/ProfileUserOverView" +// import { ProfileUserOverView_currentUser_verified_users } from "/graphql/generated/ProfileUserOverView" import React from "react" import styled from "@emotion/styled" diff --git a/frontend/components/Profile/VerifiedUsers/VerifiedUsers.tsx b/frontend/components/Profile/VerifiedUsers/VerifiedUsers.tsx index 10de4bf69..9e53d40a6 100644 --- a/frontend/components/Profile/VerifiedUsers/VerifiedUsers.tsx +++ b/frontend/components/Profile/VerifiedUsers/VerifiedUsers.tsx @@ -1,4 +1,4 @@ -// import { ProfileUserOverView_currentUser_verified_users } from "/static/types/generated/ProfileUserOverView" +// import { ProfileUserOverView_currentUser_verified_users } from "/graphql/generated/ProfileUserOverView" import Link from "next/link" import { useRouter } from "next/router" diff --git a/frontend/components/RegisterCompletionText.tsx b/frontend/components/RegisterCompletionText.tsx index 65dd1fc6b..af589b0ac 100644 --- a/frontend/components/RegisterCompletionText.tsx +++ b/frontend/components/RegisterCompletionText.tsx @@ -62,7 +62,7 @@ function LinkButton({ link, onRegistrationClick }: LinkButtonProps) { ) } -type RegProps = { +interface RegisterCompletionTextProps { email: String link: string tiers: any @@ -73,8 +73,9 @@ function RegisterCompletionText({ link, tiers, onRegistrationClick, -}: RegProps) { +}: RegisterCompletionTextProps) { const t = useTranslator(RegisterCompletionTranslations) + return ( diff --git a/frontend/components/User/Points/PointsList.tsx b/frontend/components/User/Points/PointsList.tsx index 512b56156..1dcb4112a 100644 --- a/frontend/components/User/Points/PointsList.tsx +++ b/frontend/components/User/Points/PointsList.tsx @@ -2,39 +2,27 @@ import { useQuery } from "@apollo/client" import NoPointsErrorMessage from "./NoPointsErrorMessage" import PointsListGrid from "./PointsListGrid" -import { UserPointsQuery } from "./PointsQuery" import ErrorMessage from "/components/ErrorMessage" import Spinner from "/components/Spinner" -import { UserPoints as UserPointsData } from "/static/types/generated/UserPoints" -interface StudentHasPointsProps { - pointsData: UserPointsData -} -export function studentHasPoints(props: StudentHasPointsProps) { - const { pointsData } = props +import { CurrentUserProgressesDocument } from "/graphql/generated" - if (pointsData.currentUser?.progresses) { - return true - } else { - return false - } -} -interface Props { +interface PointsListProps { showOnlyTen?: boolean } -function PointsList(props: Props) { - const { data, error, loading } = useQuery(UserPointsQuery) +function PointsList(props: PointsListProps) { + const { data, error, loading } = useQuery(CurrentUserProgressesDocument) const { showOnlyTen } = props + if (error) { return } if (loading || !data) { return } - const hasPoints = studentHasPoints({ pointsData: data }) - if (hasPoints) { + if (data.currentUser?.progresses) { return ( ) diff --git a/frontend/components/User/Points/PointsListGrid.tsx b/frontend/components/User/Points/PointsListGrid.tsx index 9292e63ae..789215106 100644 --- a/frontend/components/User/Points/PointsListGrid.tsx +++ b/frontend/components/User/Points/PointsListGrid.tsx @@ -1,16 +1,15 @@ import Grid from "@mui/material/Grid" import PointsListItemCard from "/components/Dashboard/PointsListItemCard" -import { - UserPoints_currentUser_progresses, - UserPoints as UserPointsData, -} from "/static/types/generated/UserPoints" import notEmpty from "/util/notEmpty" +import { CurrentUserProgressesQuery } from "/graphql/generated" + interface GridProps { - data: UserPointsData + data: CurrentUserProgressesQuery showOnlyTen?: boolean } + function PointsListGrid(props: GridProps) { const { data, showOnlyTen } = props @@ -20,18 +19,16 @@ function PointsListGrid(props: GridProps) { return ( - {progressesToShow.map( - (progress: UserPoints_currentUser_progresses, index) => ( - - ), - )} + {progressesToShow.map((progress, index) => ( + + ))} ) } diff --git a/frontend/components/User/Points/PointsQuery.tsx b/frontend/components/User/Points/PointsQuery.tsx deleted file mode 100644 index b60891970..000000000 --- a/frontend/components/User/Points/PointsQuery.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { gql } from "@apollo/client" - -import PointsListItemCard from "/components/Dashboard/PointsListItemCard" - -export const UserPointsQuery = gql` - query UserPoints { - currentUser { - ...UserPointsFragment - } - } - ${PointsListItemCard.fragments.user} -` diff --git a/frontend/contexts/LoginStateContext.ts b/frontend/contexts/LoginStateContext.ts index 0157c2f47..126702632 100644 --- a/frontend/contexts/LoginStateContext.ts +++ b/frontend/contexts/LoginStateContext.ts @@ -1,13 +1,13 @@ import { createContext } from "react" -import { UserOverView_currentUser } from "/static/types/generated/UserOverView" +import { UserOverviewFieldsFragment } from "/graphql/generated" export interface LoginState { loggedIn: boolean logInOrOut: () => void admin: boolean - currentUser?: UserOverView_currentUser - updateUser: (user: UserOverView_currentUser) => void + currentUser?: UserOverviewFieldsFragment + updateUser: (user: UserOverviewFieldsFragment) => void } const LoginStateContext = createContext({ diff --git a/frontend/contexts/UserSearchContext.ts b/frontend/contexts/UserSearchContext.ts index e4dbaff81..c4806ed9b 100644 --- a/frontend/contexts/UserSearchContext.ts +++ b/frontend/contexts/UserSearchContext.ts @@ -1,6 +1,6 @@ import { createContext } from "react" -import { UserDetailsContains } from "/static/types/generated/UserDetailsContains" +import { UserDetailsContainsQuery } from "/graphql/generated" export interface SearchVariables { search: string @@ -13,7 +13,7 @@ export interface SearchVariables { } interface UserSearchContext { - data: UserDetailsContains + data?: UserDetailsContainsQuery loading: boolean page: number rowsPerPage: number @@ -24,7 +24,7 @@ interface UserSearchContext { } export default createContext({ - data: {} as UserDetailsContains, + data: {} as UserDetailsContainsQuery, loading: false, page: 0, rowsPerPage: 10, diff --git a/frontend/graphql/fragments/completion.fragments.graphql b/frontend/graphql/fragments/completion.fragments.graphql new file mode 100644 index 000000000..b86be895f --- /dev/null +++ b/frontend/graphql/fragments/completion.fragments.graphql @@ -0,0 +1,68 @@ +fragment CompletionCoreFields on Completion { + id + course_id + user_id + email + student_number + completion_language + completion_link + completion_date + tier + grade + eligible_for_ects + project_completion + registered + created_at + updated_at +} + +fragment CompletionCourseFields on Course { + ...CourseWithPhotoCoreFields +} + +fragment CompletionDetailedFields on Completion { + ...CompletionCoreFields + completions_registered { + ...CompletionRegisteredCoreFields + } +} + +fragment CompletionDetailedFieldsWithCourse on Completion { + ...CompletionDetailedFields + course { + ...CompletionCourseFields + } +} + +fragment CompletionsQueryNodeFields on Completion { + ...CompletionCoreFields + user { + ...UserCoreFields + } + course { + ...CourseCoreFields + id + name + } + completions_registered { + id + organization { + id + slug + } + } +} + +fragment CompletionsQueryConnectionFields on QueryCompletionsPaginated_type_Connection { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + node { + ...CompletionsQueryNodeFields + } + } +} diff --git a/frontend/graphql/fragments/completionRegistered.fragments.graphql b/frontend/graphql/fragments/completionRegistered.fragments.graphql new file mode 100644 index 000000000..6f6dde572 --- /dev/null +++ b/frontend/graphql/fragments/completionRegistered.fragments.graphql @@ -0,0 +1,11 @@ +fragment CompletionRegisteredCoreFields on CompletionRegistered { + id + completion_id + organization_id + organization { + id + slug + } + created_at + updated_at +} diff --git a/frontend/graphql/fragments/completionsRegistered.ts b/frontend/graphql/fragments/completionsRegistered.ts deleted file mode 100644 index 6ec383ebb..000000000 --- a/frontend/graphql/fragments/completionsRegistered.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { gql } from "@apollo/client" - -export const CompletionsRegisteredFragment = gql` - fragment CompletionsRegisteredFragment on Completion { - completions_registered { - id - created_at - organization { - slug - } - } - } -` diff --git a/frontend/graphql/fragments/course.fragments.graphql b/frontend/graphql/fragments/course.fragments.graphql new file mode 100644 index 000000000..de4c2fcc7 --- /dev/null +++ b/frontend/graphql/fragments/course.fragments.graphql @@ -0,0 +1,102 @@ +fragment CourseCoreFields on Course { + id + slug + name + ects + created_at + updated_at +} + +fragment CourseWithPhotoCoreFields on Course { + ...CourseCoreFields + photo { + ...ImageCoreFields + } +} + +fragment CourseTranslationCoreFields on CourseTranslation { + id + course_id + language + name + description + link + created_at + updated_at +} + +fragment CourseFields on Course { + ...CourseWithPhotoCoreFields + description + link + order + study_module_order + promote + status + start_point + study_module_start_point + hidden + upcoming_active_link + tier + support_email + teacher_in_charge_email + teacher_in_charge_name + start_date + end_date + has_certificate + course_translations { + ...CourseTranslationCoreFields + } + study_modules { + ...StudyModuleCoreFields + } +} + +fragment EditorCourseFields on Course { + ...CourseFields + instructions + upcoming_active_link + completions_handled_by { + ...CourseCoreFields + } + course_variants { + id + slug + description + } + course_aliases { + id + course_code + } + user_course_settings_visibilities { + id + language + } +} + +fragment EditorCourseDetailedFields on Course { + ...EditorCourseFields + course_translations { + ...CourseTranslationCoreFields + description + instructions + link + } + open_university_registration_links { + ...OpenUniversityRegistrationLinkCoreFields + } + inherit_settings_from { + id + } + automatic_completions + automatic_completions_eligible_for_ects + exercise_completions_needed + points_needed +} + +fragment EditorCourseOtherCoursesFields on Course { + ...CourseWithPhotoCoreFields + course_translations { + ...CourseTranslationCoreFields + } +} diff --git a/frontend/graphql/fragments/emailTemplate.fragments.graphql b/frontend/graphql/fragments/emailTemplate.fragments.graphql new file mode 100644 index 000000000..578e572f8 --- /dev/null +++ b/frontend/graphql/fragments/emailTemplate.fragments.graphql @@ -0,0 +1,17 @@ +fragment EmailTemplateCoreFields on EmailTemplate { + id + name + title + txt_body + html_body + template_type + created_at + updated_at +} + +fragment EmailTemplateFields on EmailTemplate { + ...EmailTemplateCoreFields + triggered_automatically_by_course_id + exercise_completions_threshold + points_threshold +} diff --git a/frontend/graphql/fragments/exercise.fragments.graphql b/frontend/graphql/fragments/exercise.fragments.graphql new file mode 100644 index 000000000..99aef658e --- /dev/null +++ b/frontend/graphql/fragments/exercise.fragments.graphql @@ -0,0 +1,10 @@ +fragment ExerciseCoreFields on Exercise { + id + name + custom_id + course_id + part + section + max_points + deleted +} diff --git a/frontend/graphql/fragments/exerciseCompletion.fragments.graphql b/frontend/graphql/fragments/exerciseCompletion.fragments.graphql new file mode 100644 index 000000000..d041d9d6f --- /dev/null +++ b/frontend/graphql/fragments/exerciseCompletion.fragments.graphql @@ -0,0 +1,16 @@ +fragment ExerciseCompletionCoreFields on ExerciseCompletion { + id + exercise_id + user_id + created_at + updated_at + attempted + completed + timestamp + n_points + exercise_completion_required_actions { + id + exercise_completion_id + value + } +} diff --git a/frontend/graphql/fragments/image.fragments.graphql b/frontend/graphql/fragments/image.fragments.graphql new file mode 100644 index 000000000..666f6ed3b --- /dev/null +++ b/frontend/graphql/fragments/image.fragments.graphql @@ -0,0 +1,12 @@ +fragment ImageCoreFields on Image { + id + name + original + original_mimetype + compressed + compressed_mimetype + uncompressed + uncompressed_mimetype + created_at + updated_at +} diff --git a/frontend/graphql/fragments/openUniversityRegistrationLink.fragments.graphql b/frontend/graphql/fragments/openUniversityRegistrationLink.fragments.graphql new file mode 100644 index 000000000..12a292d09 --- /dev/null +++ b/frontend/graphql/fragments/openUniversityRegistrationLink.fragments.graphql @@ -0,0 +1,6 @@ +fragment OpenUniversityRegistrationLinkCoreFields on OpenUniversityRegistrationLink { + id + course_code + language + link +} diff --git a/frontend/graphql/fragments/organization.fragments.graphql b/frontend/graphql/fragments/organization.fragments.graphql new file mode 100644 index 000000000..0ac8b4a58 --- /dev/null +++ b/frontend/graphql/fragments/organization.fragments.graphql @@ -0,0 +1,16 @@ +fragment OrganizationCoreFields on Organization { + id + slug + hidden + created_at + updated_at + # required_confirmation + # required_organization_email + organization_translations { + id + organization_id + language + name + information + } +} diff --git a/frontend/graphql/fragments/progress.fragments.graphql b/frontend/graphql/fragments/progress.fragments.graphql new file mode 100644 index 000000000..8da26d08b --- /dev/null +++ b/frontend/graphql/fragments/progress.fragments.graphql @@ -0,0 +1,11 @@ +fragment ProgressCoreFields on Progress { + course { + ...CourseCoreFields + } + user_course_progress { + ...UserCourseProgressCoreFields + } + user_course_service_progresses { + ...UserCourseServiceProgressCoreFields + } +} diff --git a/frontend/graphql/fragments/studyModule.fragments.graphql b/frontend/graphql/fragments/studyModule.fragments.graphql new file mode 100644 index 000000000..0a3abe01c --- /dev/null +++ b/frontend/graphql/fragments/studyModule.fragments.graphql @@ -0,0 +1,38 @@ +fragment StudyModuleCoreFields on StudyModule { + id + slug + name + created_at + updated_at +} + +fragment StudyModuleFields on StudyModule { + ...StudyModuleCoreFields + description + image + order +} + +fragment StudyModuleTranslationFields on StudyModuleTranslation { + id + study_module_id + language + name + description + created_at + updated_at +} + +fragment StudyModuleDetailedFields on StudyModule { + ...StudyModuleFields + study_module_translations { + ...StudyModuleTranslationFields + } +} + +fragment StudyModuleFieldsWithCourses on StudyModule { + ...StudyModuleFields + courses { + ...CourseFields + } +} diff --git a/frontend/graphql/fragments/user.fragments.graphql b/frontend/graphql/fragments/user.fragments.graphql new file mode 100644 index 000000000..a3527b86f --- /dev/null +++ b/frontend/graphql/fragments/user.fragments.graphql @@ -0,0 +1,36 @@ +fragment UserCoreFields on User { + id + upstream_id + first_name + last_name + username + email + student_number + real_student_number + created_at + updated_at +} + +fragment UserDetailedFields on User { + ...UserCoreFields + administrator + research_consent +} + +fragment UserProgressesFields on User { + ...UserCoreFields + progresses { + ...ProgressCoreFields + } +} + +fragment UserOverviewFields on User { + ...UserDetailedFields + completions { + ...CompletionDetailedFields + course { + ...CourseWithPhotoCoreFields + has_certificate + } + } +} diff --git a/frontend/graphql/fragments/userCourseProgress.fragments.graphql b/frontend/graphql/fragments/userCourseProgress.fragments.graphql new file mode 100644 index 000000000..cf1980cc5 --- /dev/null +++ b/frontend/graphql/fragments/userCourseProgress.fragments.graphql @@ -0,0 +1,15 @@ +fragment UserCourseProgressCoreFields on UserCourseProgress { + id + course_id + user_id + max_points + n_points + progress + extra + exercise_progress { + total + exercises + } + created_at + updated_at +} diff --git a/frontend/graphql/fragments/userCourseProgress.ts b/frontend/graphql/fragments/userCourseProgress.ts deleted file mode 100644 index 4f20f661d..000000000 --- a/frontend/graphql/fragments/userCourseProgress.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { gql } from "@apollo/client" - -export const UserCourseProgressFragment = gql` - fragment UserCourseProgressFragment on UserCourseProgress { - id - course_id - max_points - n_points - progress - extra - exercise_progress { - total - exercises - } - } -` - -export const ProgressUserCourseProgressFragment = gql` - fragment ProgressUserCourseProgressFragment on Progress { - user_course_progress { - ...UserCourseProgressFragment - } - } - ${UserCourseProgressFragment} -` - -export const UserCourseSummaryUserCourseProgressFragment = gql` - fragment UserCourseSummaryUserCourseProgressFragment on UserCourseSummary { - user_course_progress { - ...UserCourseProgressFragment - } - } - ${UserCourseProgressFragment} -` diff --git a/frontend/graphql/fragments/userCourseServiceProgress.fragments.graphql b/frontend/graphql/fragments/userCourseServiceProgress.fragments.graphql new file mode 100644 index 000000000..ea87939b5 --- /dev/null +++ b/frontend/graphql/fragments/userCourseServiceProgress.fragments.graphql @@ -0,0 +1,13 @@ +fragment UserCourseServiceProgressCoreFields on UserCourseServiceProgress { + id + course_id + service_id + user_id + progress + service { + name + id + } + created_at + updated_at +} diff --git a/frontend/graphql/fragments/userCourseServiceProgress.ts b/frontend/graphql/fragments/userCourseServiceProgress.ts deleted file mode 100644 index c167b65b5..000000000 --- a/frontend/graphql/fragments/userCourseServiceProgress.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { gql } from "@apollo/client" - -export const UserCourseServiceProgressFragment = gql` - fragment UserCourseServiceProgressFragment on UserCourseServiceProgress { - progress - service { - name - id - } - } -` -export const ProgressUserCourseServiceProgressFragment = gql` - fragment ProgressUserCourseServiceProgressFragment on Progress { - user_course_service_progresses { - ...UserCourseServiceProgressFragment - } - } - ${UserCourseServiceProgressFragment} -` - -export const UserCourseSummaryUserCourseServiceProgressFragment = gql` - fragment UserCourseSummaryUserCourseServiceProgressFragment on UserCourseSummary { - user_course_service_progresses { - ...UserCourseServiceProgressFragment - } - } - ${UserCourseServiceProgressFragment} -` diff --git a/frontend/graphql/fragments/userCourseSetting.fragments.graphql b/frontend/graphql/fragments/userCourseSetting.fragments.graphql new file mode 100644 index 000000000..d5f5d3ee8 --- /dev/null +++ b/frontend/graphql/fragments/userCourseSetting.fragments.graphql @@ -0,0 +1,34 @@ +fragment UserCourseSettingCoreFields on UserCourseSetting { + id + user_id + course_id + created_at + updated_at +} + +fragment UserCourseSettingDetailedFields on UserCourseSetting { + ...UserCourseSettingCoreFields + language + country + research + marketing + course_variant + other +} + +fragment StudentProgressesQueryNodeFields on UserCourseSetting { + ...UserCourseSettingCoreFields + user { + ...UserCoreFields + progress(course_id: $course_id) { + ...ProgressCoreFields + } + } +} + +fragment UserProfileUserCourseSettingsQueryNodeFields on UserCourseSetting { + ...UserCourseSettingDetailedFields + course { + ...CourseCoreFields + } +} diff --git a/frontend/graphql/fragments/userCourseSummary.fragments.graphql b/frontend/graphql/fragments/userCourseSummary.fragments.graphql new file mode 100644 index 000000000..c1dcfc77b --- /dev/null +++ b/frontend/graphql/fragments/userCourseSummary.fragments.graphql @@ -0,0 +1,25 @@ +fragment UserCourseSummaryCourseFields on Course { + ...CourseWithPhotoCoreFields + has_certificate + exercises { + ...ExerciseCoreFields + } +} + +fragment UserCourseSummaryCoreFields on UserCourseSummary { + course { + ...UserCourseSummaryCourseFields + } + exercise_completions { + ...ExerciseCompletionCoreFields + } + user_course_progress { + ...UserCourseProgressCoreFields + } + user_course_service_progresses { + ...UserCourseServiceProgressCoreFields + } + completion { + ...CompletionDetailedFields + } +} diff --git a/frontend/graphql/fragments/userOrganization.fragments.graphql b/frontend/graphql/fragments/userOrganization.fragments.graphql new file mode 100644 index 000000000..5364a3a5f --- /dev/null +++ b/frontend/graphql/fragments/userOrganization.fragments.graphql @@ -0,0 +1,32 @@ +fragment UserOrganizationCoreFields on UserOrganization { + id + user_id + organization_id + # confirmed + # consented + organization { + ...OrganizationCoreFields + } + created_at + updated_at +} + +# fragment UserOrganizationJoinConfirmationData on UserOrganizationJoinConfirmation { +# id +# email +# confirmed +# confirmed_at +# created_at +# updated_at +# expired +# expires_at +# redirect +# language +# email_delivery { +# id +# email +# sent +# error +# updated_at +# } +# } diff --git a/frontend/graphql/generated/index.ts b/frontend/graphql/generated/index.ts new file mode 100644 index 000000000..cb3a32c61 --- /dev/null +++ b/frontend/graphql/generated/index.ts @@ -0,0 +1,10795 @@ +/** + * This is an automatically generated file. + * Run `npm run graphql-codegen` to regenerate. + **/ + +import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core" + +export type Maybe = T | null +export type InputMaybe = Maybe +export type Exact = { + [K in keyof T]: T[K] +} +export type MakeOptional = Omit & { + [SubKey in K]?: Maybe +} +export type MakeMaybe = Omit & { + [SubKey in K]: Maybe +} +// Generated on 2022-08-12T14:26:07+03:00 + +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string + String: string + Boolean: boolean + Int: number + Float: number + /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ + DateTime: any + /** The `JSON` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ + Json: any + /** The `Upload` scalar type represents a file upload. */ + Upload: any +} + +export type AbEnrollment = { + __typename?: "AbEnrollment" + ab_study: AbStudy + ab_study_id: Scalars["String"] + created_at: Maybe + group: Maybe + id: Scalars["String"] + updated_at: Maybe + user: Maybe + user_id: Maybe +} + +export type AbEnrollmentCreateOrUpsertInput = { + ab_study_id: Scalars["ID"] + group: Scalars["Int"] + user_id: Scalars["ID"] +} + +export type AbEnrollmentUser_idAb_study_idCompoundUniqueInput = { + ab_study_id: Scalars["String"] + user_id: Scalars["String"] +} + +export type AbEnrollmentWhereUniqueInput = { + id?: InputMaybe + user_id_ab_study_id?: InputMaybe +} + +export type AbStudy = { + __typename?: "AbStudy" + ab_enrollments: Array + created_at: Maybe + group_count: Scalars["Int"] + id: Scalars["String"] + name: Scalars["String"] + updated_at: Maybe +} + +export type AbStudyab_enrollmentsArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type AbStudyCreateInput = { + group_count: Scalars["Int"] + name: Scalars["String"] +} + +export type AbStudyUpsertInput = { + group_count: Scalars["Int"] + id: Scalars["ID"] + name: Scalars["String"] +} + +export type Completion = { + __typename?: "Completion" + certificate_id: Maybe + completion_date: Maybe + completion_language: Maybe + completion_link: Maybe + completion_registration_attempt_date: Maybe + completions_registered: Array + course: Maybe + course_id: Maybe + created_at: Maybe + eligible_for_ects: Maybe + email: Scalars["String"] + grade: Maybe + id: Scalars["String"] + project_completion: Maybe + registered: Maybe + student_number: Maybe + tier: Maybe + updated_at: Maybe + user: Maybe + user_id: Maybe + user_upstream_id: Maybe +} + +export type Completioncompletions_registeredArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type CompletionArg = { + completion_id: Scalars["String"] + eligible_for_ects?: InputMaybe + student_number: Scalars["String"] + tier?: InputMaybe +} + +export type CompletionEdge = { + __typename?: "CompletionEdge" + /** https://facebook.github.io/relay/graphql/connections.htm#sec-Cursor */ + cursor: Scalars["String"] + /** https://facebook.github.io/relay/graphql/connections.htm#sec-Node */ + node: Maybe +} + +export type CompletionRegistered = { + __typename?: "CompletionRegistered" + completion: Maybe + completion_id: Maybe + course: Maybe + course_id: Maybe + created_at: Maybe + id: Scalars["String"] + organization: Maybe + organization_id: Maybe + real_student_number: Scalars["String"] + registration_date: Maybe + updated_at: Maybe + user: Maybe + user_id: Maybe +} + +export type CompletionRegisteredWhereUniqueInput = { + id?: InputMaybe +} + +export type Course = { + __typename?: "Course" + automatic_completions: Maybe + automatic_completions_eligible_for_ects: Maybe + completion_email: Maybe + completion_email_id: Maybe + completions: Maybe>> + completions_handled_by: Maybe + completions_handled_by_id: Maybe + course_aliases: Array + course_organizations: Array + course_stats_email: Maybe + course_stats_email_id: Maybe + course_translations: Array + course_variants: Array + created_at: Maybe + description: Maybe + ects: Maybe + end_date: Maybe + exercise_completions_needed: Maybe + exercises: Maybe>> + handles_completions_for: Array + has_certificate: Maybe + hidden: Maybe + id: Scalars["String"] + inherit_settings_from: Maybe + inherit_settings_from_id: Maybe + instructions: Maybe + link: Maybe + name: Scalars["String"] + open_university_registration_links: Array + order: Maybe + owner_organization: Maybe + owner_organization_id: Maybe + photo: Maybe + photo_id: Maybe + points_needed: Maybe + promote: Maybe + services: Array + slug: Scalars["String"] + start_date: Scalars["String"] + start_point: Maybe + status: Maybe + study_module_order: Maybe + study_module_start_point: Maybe + study_modules: Array + support_email: Maybe + teacher_in_charge_email: Scalars["String"] + teacher_in_charge_name: Scalars["String"] + tier: Maybe + upcoming_active_link: Maybe + updated_at: Maybe + user_course_settings_visibilities: Array +} + +export type CoursecompletionsArgs = { + user_id?: InputMaybe + user_upstream_id?: InputMaybe +} + +export type Coursecourse_aliasesArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type Coursecourse_organizationsArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type Coursecourse_translationsArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type Coursecourse_variantsArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type CourseexercisesArgs = { + includeDeleted?: InputMaybe +} + +export type Coursehandles_completions_forArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type Courseopen_university_registration_linksArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type CourseservicesArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type Coursestudy_modulesArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type Courseuser_course_settings_visibilitiesArgs = { + cursor?: InputMaybe + skip?: InputMaybe + take?: InputMaybe +} + +export type CourseAlias = { + __typename?: "CourseAlias" + course: Maybe + course_code: Scalars["String"] + course_id: Maybe + created_at: Maybe + id: Scalars["String"] + updated_at: Maybe +} + +export type CourseAliasCreateInput = { + course?: InputMaybe + course_code: Scalars["String"] +} + +export type CourseAliasUpsertInput = { + course?: InputMaybe + course_code: Scalars["String"] + id?: InputMaybe +} + +export type CourseAliasWhereUniqueInput = { + course_code?: InputMaybe + id?: InputMaybe +} + +export type CourseCreateArg = { + automatic_completions?: InputMaybe + automatic_completions_eligible_for_ects?: InputMaybe + base64?: InputMaybe + completion_email_id?: InputMaybe + completions_handled_by?: InputMaybe + course_aliases?: InputMaybe>> + course_stats_email_id?: InputMaybe + course_translations?: InputMaybe< + Array> + > + course_variants?: InputMaybe>> + ects?: InputMaybe + end_date?: InputMaybe + exercise_completions_needed?: InputMaybe + has_certificate?: InputMaybe + hidden?: InputMaybe + inherit_settings_from?: InputMaybe + name?: InputMaybe + new_photo?: InputMaybe + open_university_registration_links?: InputMaybe< + Array> + > + order?: InputMaybe + photo?: InputMaybe + points_needed?: InputMaybe + promote?: InputMaybe + slug: Scalars["String"] + start_date: Scalars["String"] + start_point?: InputMaybe + status?: InputMaybe + study_module_order?: InputMaybe + study_module_start_point?: InputMaybe + study_modules?: InputMaybe>> + support_email?: InputMaybe + teacher_in_charge_email: Scalars["String"] + teacher_in_charge_name: Scalars["String"] + tier?: InputMaybe + upcoming_active_link?: InputMaybe + user_course_settings_visibilities?: InputMaybe< + Array> + > +} + +export type CourseOrderByInput = { + automatic_completions?: InputMaybe + automatic_completions_eligible_for_ects?: InputMaybe + completion_email_id?: InputMaybe + completions_handled_by_id?: InputMaybe + course_stats_email_id?: InputMaybe + created_at?: InputMaybe + ects?: InputMaybe + end_date?: InputMaybe + exercise_completions_needed?: InputMaybe + has_certificate?: InputMaybe + hidden?: InputMaybe + id?: InputMaybe + inherit_settings_from_id?: InputMaybe + name?: InputMaybe + order?: InputMaybe + owner_organization_id?: InputMaybe + photo_id?: InputMaybe + points_needed?: InputMaybe
librdkafka
Loading...
Error: {error.message}
Hello, {data.staffMember?.user.name ?? "stranger"}