Skip to content

Commit

Permalink
Don't allow empty strings when creating items on the glossary (#1335)
Browse files Browse the repository at this point in the history
* Don't allow empty strings on the glossary

* Update system tests
  • Loading branch information
nygrenh authored Nov 15, 2024
1 parent 334d62a commit bec3610
Show file tree
Hide file tree
Showing 16 changed files with 253 additions and 124 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE glossary DROP CONSTRAINT term_not_empty,
DROP CONSTRAINT definition_not_empty;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE glossary
ADD CONSTRAINT term_not_empty CHECK (trim(term) <> ''),
ADD CONSTRAINT definition_not_empty CHECK (trim(definition) <> '');
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,31 @@ import React, { useState } from "react"
import { useTranslation } from "react-i18next"

import { CourseManagementPagesProps } from "../../../../../../pages/manage/courses/[id]/[...path]"
import { fetchGlossary, postNewTerm } from "../../../../../../services/backend/courses"
import { deleteTerm, updateTerm } from "../../../../../../services/backend/glossary"
import { fetchGlossary } from "../../../../../../services/backend/courses"

import CreateTermForm from "./CreateTermForm"
import TermItem from "./TermItem"

import Button from "@/shared-module/common/components/Button"
import ErrorBanner from "@/shared-module/common/components/ErrorBanner"
import TextAreaField from "@/shared-module/common/components/InputFields/TextAreaField"
import TextField from "@/shared-module/common/components/InputFields/TextField"
import Spinner from "@/shared-module/common/components/Spinner"
import useToastMutation from "@/shared-module/common/hooks/useToastMutation"
import { baseTheme, headingFont } from "@/shared-module/common/styles"

interface GlossaryTerm {
id: string
term: string
definition: string
}

const CourseGlossary: React.FC<React.PropsWithChildren<CourseManagementPagesProps>> = ({
courseId,
}) => {
const { t } = useTranslation()

const [newTerm, setNewTerm] = useState("")
const [newDefinition, setNewDefinition] = useState("")
const [updatedTerm, setUpdatedTerm] = useState("")
const [updatedDefinition, setUpdatedDefinition] = useState("")
const [editingTerm, setEditingTerm] = useState<string | null>(null)
const glossary = useQuery({
queryKey: [`glossary-${courseId}`],
queryFn: () => fetchGlossary(courseId),
})
const createMutation = useToastMutation(
() => postNewTerm(courseId, newTerm, newDefinition),
{
notify: true,
method: "POST",
},
{
onSuccess: () => {
setNewTerm("")
setNewDefinition("")
glossary.refetch()
},
},
)
const updateMutation = useToastMutation(
(termId: string) => updateTerm(termId, updatedTerm, updatedDefinition),
{
notify: true,
method: "PUT",
},
{
onSuccess: () => {
setEditingTerm(null)
glossary.refetch()
},
},
)
const deleteMutation = useToastMutation(
(termId: string) => deleteTerm(termId),
{
notify: true,
method: "DELETE",
},
{ onSuccess: () => glossary.refetch() },
)

return (
<>
Expand All @@ -79,83 +44,22 @@ const CourseGlossary: React.FC<React.PropsWithChildren<CourseManagementPagesProp
</h1>
{glossary.isError && <ErrorBanner variant={"readOnly"} error={glossary.error} />}
{glossary.isPending && <Spinner variant={"medium"} />}
<div>
<TextField
label={t("new-term")}
placeholder={t("new-term")}
value={newTerm}
onChangeByValue={setNewTerm}
/>
<TextAreaField
name={t("new-definition")}
placeholder={t("new-definition")}
label={t("new-definition")}
value={newDefinition}
onChangeByValue={setNewDefinition}
disabled={false}
/>
<Button variant="primary" size="medium" onClick={() => createMutation.mutate()}>
{t("button-text-save")}
</Button>
</div>
<CreateTermForm refetch={glossary.refetch} courseId={courseId} />
{glossary.isSuccess &&
glossary.data
.sort((a, b) => a.term.localeCompare(b.term))
.map((term) => {
return editingTerm === term.id ? (
<div key={term.id}>
<hr />
<TextField
placeholder={t("updated-term")}
label={t("updated-term")}
value={updatedTerm}
onChangeByValue={setUpdatedTerm}
/>
<TextAreaField
name={t("updated-definition")}
label={t("updated-definition")}
placeholder={t("updated-definition")}
value={updatedDefinition}
onChangeByValue={setUpdatedDefinition}
disabled={false}
/>
<Button
variant="primary"
size="medium"
onClick={() => updateMutation.mutate(term.id)}
>
{t("button-text-save")}
</Button>
<Button variant="tertiary" size="medium" onClick={() => setEditingTerm(null)}>
{t("button-text-cancel")}
</Button>
</div>
) : (
<div key={term.id}>
<hr />
<div>{term.term}</div>
<div>{term.definition}</div>
<Button
variant="primary"
size="medium"
onClick={() => {
setUpdatedTerm(term.term)
setUpdatedDefinition(term.definition)
setEditingTerm(term.id)
}}
>
{t("edit")}
</Button>
<Button
variant="tertiary"
size="medium"
onClick={() => deleteMutation.mutate(term.id)}
>
{t("button-text-delete")}
</Button>
</div>
)
})}
.sort((a: GlossaryTerm, b: GlossaryTerm) => a.term.localeCompare(b.term))
.map((term: GlossaryTerm) => (
<TermItem
key={term.id}
term={term}
isEditing={editingTerm === term.id}
onEdit={() => {
setEditingTerm(term.id)
}}
onCancel={() => setEditingTerm(null)}
refetch={glossary.refetch}
/>
))}
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"

import { postNewTerm } from "@/services/backend/courses"
import Button from "@/shared-module/common/components/Button"
import TextAreaField from "@/shared-module/common/components/InputFields/TextAreaField"
import TextField from "@/shared-module/common/components/InputFields/TextField"
import useToastMutation from "@/shared-module/common/hooks/useToastMutation"

interface NewTermForm {
newTerm: string
newDefinition: string
}

interface CreateTermFormProps {
refetch: () => void
courseId: string
}

const CreateTermForm: React.FC<CreateTermFormProps> = ({ refetch, courseId }) => {
const { t } = useTranslation()
const {
register,
handleSubmit,
formState: { errors, isValid },
reset,
// eslint-disable-next-line i18next/no-literal-string
} = useForm<NewTermForm>({ mode: "onChange" })

const createMutation = useToastMutation(
(data: NewTermForm) => postNewTerm(courseId, data.newTerm, data.newDefinition),
{
notify: true,
method: "POST",
},
{
onSuccess: () => {
reset()
refetch()
},
},
)

const onCreate = (data: NewTermForm) => {
createMutation.mutate(data)
}

return (
<form onSubmit={handleSubmit(onCreate)}>
<TextField
label={t("new-term")}
placeholder={t("new-term")}
{...register("newTerm", {
required: true,
pattern: {
value: /\S+/,
message: t("required"),
},
})}
error={errors.newTerm && t("required")}
/>
<TextAreaField
placeholder={t("new-definition")}
label={t("new-definition")}
{...register("newDefinition", {
required: true,
pattern: {
value: /\S+/,
message: t("required"),
},
})}
error={errors.newDefinition && t("required")}
/>
<Button variant="primary" size="medium" type="submit" disabled={!isValid}>
{t("button-text-save")}
</Button>
</form>
)
}

export default CreateTermForm
Loading

0 comments on commit bec3610

Please sign in to comment.