From 7918c00970e5e5f2883b51b865d4257039adf412 Mon Sep 17 00:00:00 2001 From: Katrine Wist Date: Fri, 20 Dec 2024 16:08:42 +0100 Subject: [PATCH] Improve learningpath form typing --- .../EditLearningpathStepsPage.tsx | 15 +++--- .../Learningpath/components/ExternalForm.tsx | 19 +++----- .../components/LearningpathStepForm.tsx | 31 ++++--------- .../components/LearningpathStepListItem.tsx | 5 +- .../Learningpath/components/ResourceForm.tsx | 9 +--- .../Learningpath/components/TextForm.tsx | 16 ++----- src/containers/MyNdla/Learningpath/types.ts | 18 ++++++++ src/containers/MyNdla/Learningpath/utils.ts | 46 ++++++++++++------- 8 files changed, 81 insertions(+), 78 deletions(-) create mode 100644 src/containers/MyNdla/Learningpath/types.ts diff --git a/src/containers/MyNdla/Learningpath/EditLearningpathStepsPage.tsx b/src/containers/MyNdla/Learningpath/EditLearningpathStepsPage.tsx index 90dcf4bf3..8e762037a 100644 --- a/src/containers/MyNdla/Learningpath/EditLearningpathStepsPage.tsx +++ b/src/containers/MyNdla/Learningpath/EditLearningpathStepsPage.tsx @@ -14,7 +14,7 @@ import { Button, Heading, Spinner, Text } from "@ndla/primitives"; import { SafeLinkButton } from "@ndla/safelink"; import { Stack, styled } from "@ndla/styled-system/jsx"; import { HelmetWithTracker, useTracker } from "@ndla/tracker"; -import { FormValues, LearningpathStepForm } from "./components/LearningpathStepForm"; +import { LearningpathStepForm } from "./components/LearningpathStepForm"; import { useCreateLearningpathStep } from "./learningpathMutations"; import { learningpathQuery, useFetchLearningpath } from "./learningpathQueries"; import { formValuesToGQLInput } from "./utils"; @@ -26,6 +26,7 @@ import MyNdlaBreadcrumb from "../components/MyNdlaBreadcrumb"; import MyNdlaPageWrapper from "../components/MyNdlaPageWrapper"; import { LearningpathStepListItem } from "./components/LearningpathStepListItem"; import { LearningpathStepper } from "./components/LearningpathStepper"; +import { FormValues } from "./types"; const StyledOl = styled("ol", { base: { @@ -68,7 +69,11 @@ export const EditLearningpathStepsPage = () => { await createStep({ variables: { learningpathId: data.myNdlaLearningpath.id, - params: { ...transformedData, language: i18n.language, showTitle: false }, + params: { + ...transformedData, + language: i18n.language, + showTitle: false, + }, }, refetchQueries: [{ query: learningpathQuery, variables: { pathId: data.myNdlaLearningpath.id.toString() } }], }); @@ -111,11 +116,7 @@ export const EditLearningpathStepsPage = () => { {t("myNdla.learningpath.form.steps.add")} ) : ( - setIsCreating(false)} - onSave={onSaveStep} - /> + setIsCreating(false)} onSave={onSaveStep} /> )} diff --git a/src/containers/MyNdla/Learningpath/components/ExternalForm.tsx b/src/containers/MyNdla/Learningpath/components/ExternalForm.tsx index 6eef21865..807ad9768 100644 --- a/src/containers/MyNdla/Learningpath/components/ExternalForm.tsx +++ b/src/containers/MyNdla/Learningpath/components/ExternalForm.tsx @@ -14,7 +14,6 @@ import { FieldLabel, FieldHelper, FieldRoot, - Input, Text, CheckboxRoot, CheckboxLabel, @@ -22,12 +21,14 @@ import { CheckboxIndicator, CheckboxHiddenInput, FieldErrorMessage, + FieldInput, } from "@ndla/primitives"; import { SafeLink } from "@ndla/safelink"; import { Stack, styled } from "@ndla/styled-system/jsx"; import useValidationTranslation from "../../../../util/useValidationTranslation"; import FieldLength from "../../components/FieldLength"; import { useFetchOpengraph } from "../learningpathQueries"; +import { FormValuesMap } from "../types"; const StyledCheckboxRoot = styled(CheckboxRoot, { base: { @@ -44,17 +45,9 @@ const CopyrightText = styled(Text, { const TITLE_MAX_LENGTH = 64; const INTRODUCTION_MAX_LENGTH = 250; -export interface ExternalFormValues { - type: "external"; - title: string; - introduction: string; - url: string; - shareable: boolean; -} - export const ExternalForm = () => { const { t } = useTranslation(); - const { control, setValue, watch } = useFormContext(); + const { control, setValue, watch } = useFormContext(); const { refetch } = useFetchOpengraph({ skip: true }); const { validationT } = useValidationTranslation(); @@ -98,7 +91,7 @@ export const ExternalForm = () => { {t("myNdla.learningpath.form.content.external.title.label")} {t("myNdla.learningpath.form.content.external.title.labelHelper")} {fieldState.error?.message} - + )} @@ -125,7 +118,7 @@ export const ExternalForm = () => { {t("myNdla.learningpath.form.content.external.introduction.label")} {t("myNdla.learningpath.form.content.external.introduction.labelHelper")} {fieldState.error?.message} - + )} @@ -144,7 +137,7 @@ export const ExternalForm = () => { {t("myNdla.learningpath.form.content.external.content.label")} {t("myNdla.learningpath.form.content.external.content.labelHelper")} {fieldState.error?.message} - + )} /> diff --git a/src/containers/MyNdla/Learningpath/components/LearningpathStepForm.tsx b/src/containers/MyNdla/Learningpath/components/LearningpathStepForm.tsx index 5f6c73237..e0aa0ac03 100644 --- a/src/containers/MyNdla/Learningpath/components/LearningpathStepForm.tsx +++ b/src/containers/MyNdla/Learningpath/components/LearningpathStepForm.tsx @@ -21,11 +21,12 @@ import { RadioGroupRoot, } from "@ndla/primitives"; import { HStack, styled } from "@ndla/styled-system/jsx"; -import { LearningpathStepDeleteDialog } from "./LearningpathStepDeleteDialog"; -import { GQLMyNdlaLearningpathStepFragment } from "../../../../graphqlTypes"; -import { formValues, getFormTypeFromStep, getValuesFromStep } from "../utils"; import { ExternalForm } from "./ExternalForm"; +import { LearningpathStepDeleteDialog } from "./LearningpathStepDeleteDialog"; import { ResourceForm } from "./ResourceForm"; +import { GQLMyNdlaLearningpathStepFragment } from "../../../../graphqlTypes"; +import { FormKeys, FormValues } from "../types"; +import { toFormValues } from "../utils"; import { TextForm } from "./TextForm"; const ContentForm = styled("form", { @@ -41,33 +42,21 @@ const ContentForm = styled("form", { }, }); -const RADIO_GROUP_OPTIONS = ["text", "resource", "external", "folder"] as const; -export type FormType = (typeof RADIO_GROUP_OPTIONS)[number]; - -export type FormValues = { - type: string; - title: string; - introduction: string; - description: string; - embedUrl: string; - url: string; - shareable: boolean; -}; +const LEARNINGPATH_FORM_VARIANTS: FormKeys[] = ["text", "resource", "external", "folder"]; interface Props { - learningpathId: number; step?: GQLMyNdlaLearningpathStepFragment; + defaultStepType: FormKeys; onClose?: VoidFunction; onDelete?: (close: VoidFunction) => Promise; onSave: (data: FormValues) => Promise; } -export const LearningpathStepForm = ({ step, onClose, onSave, onDelete }: Props) => { +export const LearningpathStepForm = ({ step, defaultStepType, onClose, onSave, onDelete }: Props) => { const { t } = useTranslation(); - const stepType = getFormTypeFromStep(step); const methods = useForm({ - defaultValues: stepType ? getValuesFromStep(stepType, step) : formValues(), + defaultValues: toFormValues(defaultStepType, step), }); const { handleSubmit, control, watch, reset, formState } = methods; @@ -83,11 +72,11 @@ export const LearningpathStepForm = ({ step, onClose, onSave, onDelete }: Props) {t("myNdla.learningpath.form.content.subTitle")} {fieldState.error?.message} reset(getValuesFromStep(details.value as FormType, step))} + onValueChange={(details) => reset(toFormValues(details.value as FormKeys, step))} orientation="vertical" {...field} > - {RADIO_GROUP_OPTIONS.map((val) => ( + {LEARNINGPATH_FORM_VARIANTS.map((val) => ( {t(`myNdla.learningpath.form.options.${val}`)} diff --git a/src/containers/MyNdla/Learningpath/components/LearningpathStepListItem.tsx b/src/containers/MyNdla/Learningpath/components/LearningpathStepListItem.tsx index e1f28e9b7..ad0730586 100644 --- a/src/containers/MyNdla/Learningpath/components/LearningpathStepListItem.tsx +++ b/src/containers/MyNdla/Learningpath/components/LearningpathStepListItem.tsx @@ -14,8 +14,9 @@ import { Stack, styled } from "@ndla/styled-system/jsx"; import { GQLMyNdlaLearningpathStepFragment } from "../../../../graphqlTypes"; import { useUpdateLearningpathStep, useDeleteLearningpathStep } from "../learningpathMutations"; import { formValuesToGQLInput, getFormTypeFromStep } from "../utils"; -import { FormValues, LearningpathStepForm } from "./LearningpathStepForm"; +import { LearningpathStepForm } from "./LearningpathStepForm"; import { learningpathQuery } from "../learningpathQueries"; +import { FormValues } from "../types"; const ContentWrapper = styled("div", { base: { @@ -101,7 +102,7 @@ export const LearningpathStepListItem = ({ step, learningpathId }: LearningpathS )} {isEditing ? ( - + ) : null} ); diff --git a/src/containers/MyNdla/Learningpath/components/ResourceForm.tsx b/src/containers/MyNdla/Learningpath/components/ResourceForm.tsx index d4081f67e..2704aa80c 100644 --- a/src/containers/MyNdla/Learningpath/components/ResourceForm.tsx +++ b/src/containers/MyNdla/Learningpath/components/ResourceForm.tsx @@ -17,12 +17,7 @@ import { ContentTypeBadge } from "@ndla/ui"; import { ResourcePicker } from "./ResourcePicker"; import { GQLResourceType } from "../../../../graphqlTypes"; import { contentTypeMapping } from "../../../../util/getContentType"; - -export interface ResourceFormValues { - type: "resource"; - embedUrl: string; - title: string; -} +import { FormValuesMap } from "../types"; interface ResourceFormProps { resource?: ResourceData; @@ -31,7 +26,7 @@ interface ResourceFormProps { export const ResourceForm = ({ resource }: ResourceFormProps) => { const { t } = useTranslation(); const [selectedResource, setSelectedResource] = useState(resource); - const { setValue } = useFormContext(); + const { setValue } = useFormContext(); const onSelectResource = (resource?: ResourceData) => { setSelectedResource(resource); diff --git a/src/containers/MyNdla/Learningpath/components/TextForm.tsx b/src/containers/MyNdla/Learningpath/components/TextForm.tsx index 3dc47b267..b8d096e76 100644 --- a/src/containers/MyNdla/Learningpath/components/TextForm.tsx +++ b/src/containers/MyNdla/Learningpath/components/TextForm.tsx @@ -12,22 +12,16 @@ import { Controller, useFormContext } from "react-hook-form"; import { FieldErrorMessage, FieldHelper, FieldInput, FieldLabel, FieldRoot, Spinner } from "@ndla/primitives"; import useValidationTranslation from "../../../../util/useValidationTranslation"; import FieldLength from "../../components/FieldLength"; +import { FormValuesMap } from "../types"; const MarkdownEditor = lazy(() => import("../../../../components/MarkdownEditor/MarkdownEditor")); const TITLE_MAX_LENGTH = 64; const INTRODUCTION_MAX_LENGTH = 250; -export interface TextFormValues { - type: "text"; - title: string; - introduction: string; - description: string; -} - export const TextForm = () => { const { validationT } = useValidationTranslation(); - const { setValue, control } = useFormContext(); + const { setValue, control } = useFormContext(); return ( <> @@ -53,7 +47,7 @@ export const TextForm = () => { {t("myNdla.learningpath.form.content.text.title.label")} {t("myNdla.learningpath.form.content.text.title.labelHelper")} {fieldState.error?.message} - + )} @@ -80,7 +74,7 @@ export const TextForm = () => { {t("myNdla.learningpath.form.content.text.introduction.label")} {t("myNdla.learningpath.form.content.text.introduction.labelHelper")} {fieldState.error?.message} - + )} @@ -108,7 +102,7 @@ export const TextForm = () => { shouldDirty: true, }); }} - initialValue={field.value} + initialValue={field.value ?? "

"} {...field} /> diff --git a/src/containers/MyNdla/Learningpath/types.ts b/src/containers/MyNdla/Learningpath/types.ts new file mode 100644 index 000000000..acfb1cd44 --- /dev/null +++ b/src/containers/MyNdla/Learningpath/types.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2024-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface FormValuesMap { + text: { type: "text"; title: string; introduction: string; description: string }; + resource: { type: "resource"; title: string; embedUrl: string }; + external: { type: "external"; title: string; url: string; introduction: string; shareable: boolean }; + folder: { type: "folder"; title: string }; +} + +export type FormKeys = keyof FormValuesMap; + +export type FormValues = FormValuesMap[keyof FormValuesMap]; diff --git a/src/containers/MyNdla/Learningpath/utils.ts b/src/containers/MyNdla/Learningpath/utils.ts index 79617ff78..0638fd33a 100644 --- a/src/containers/MyNdla/Learningpath/utils.ts +++ b/src/containers/MyNdla/Learningpath/utils.ts @@ -7,8 +7,8 @@ */ import config from "../../../config"; -import { FormType, FormValues } from "./components/LearningpathStepForm"; import { GQLMyNdlaLearningpathStepFragment } from "../../../graphqlTypes"; +import { FormValuesMap, FormValues, FormKeys } from "./types"; export const sharedLearningpathLink = (id: number) => `${config.ndlaFrontendDomain}/learningpath/${id}`; @@ -19,7 +19,7 @@ export const LEARNINGPATH_SHARED = "UNLISTED"; export const LEARNINGPATH_PRIVATE = "PRIVATE"; export const LEARNINGPATH_READY_FOR_SHARING = "READY_FOR_SHARING"; -export const getFormTypeFromStep = (step?: GQLMyNdlaLearningpathStepFragment) => { +export const getFormTypeFromStep = (step?: GQLMyNdlaLearningpathStepFragment): FormKeys | undefined => { if (!step?.resource && !step?.oembed && !step?.embedUrl) { return "text"; } @@ -34,22 +34,30 @@ export const getFormTypeFromStep = (step?: GQLMyNdlaLearningpathStepFragment) => return undefined; }; -export const formValues: (type?: FormType, step?: GQLMyNdlaLearningpathStepFragment) => Partial = ( - type?: FormType, +export const toFormValues = ( + type: T, step?: GQLMyNdlaLearningpathStepFragment, -) => ({ - type: type ?? "", - title: step?.title ?? "", - introduction: step?.introduction ?? "", - embedUrl: step?.embedUrl?.url ?? "", - shareable: !!step?.embedUrl?.url, - description: step?.description ?? "

", -}); - -export const getValuesFromStep = (type: FormType, step?: GQLMyNdlaLearningpathStepFragment) => { - const formType = getFormTypeFromStep(step); - const isInitialType = formType === type; - return formValues(type, isInitialType ? step : undefined); +): FormValuesMap[T] => { + const baseValues = { title: step?.title ?? "" }; + return ( + { + text: { + ...baseValues, + type: "text", + introduction: step?.introduction ?? "", + description: step?.description ?? "", + }, + resource: { ...baseValues, type: "resource", embedUrl: step?.embedUrl?.url ?? "" }, + external: { + ...baseValues, + type: "external", + url: step?.embedUrl?.url ?? "", + introduction: step?.introduction ?? "", + shareable: !!step?.embedUrl?.url, + }, + folder: { ...baseValues, type: "folder" }, + } as const + )[type]; }; export const formValuesToGQLInput = (values: FormValues) => { @@ -73,6 +81,10 @@ export const formValuesToGQLInput = (values: FormValues) => { }, }; } + if (values.type === "folder") { + // TODO: Implement once folder form is added + return { type: "TEXT", title: values.title }; + } return { type: "TEXT",