Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: redesign quality evaluation #2478

Merged
merged 2 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const AverageQualityEvaluation = ({ gradeAverage, nodeType }: Props) => {
<QualityEvaluationGrade
grade={gradeAverage.averageValue}
averageGrade={gradeAverage.averageValue.toFixed(1)}
ariaLabel={t("taxonomy.qualityDescription", {
tooltip={t("taxonomy.qualityDescription", {
nodeType: t(`taxonomy.${nodeType}`),
count: gradeAverage.count,
})}
Expand Down
27 changes: 1 addition & 26 deletions src/components/QualityEvaluation/QualityEvaluation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@
*/

import { FieldHelperProps, FieldInputProps } from "formik";
import { CSSProperties } from "react";
import { useTranslation } from "react-i18next";
import styled from "@emotion/styled";
import { colors, spacing } from "@ndla/core";
import { IArticle, IUpdatedArticle } from "@ndla/types-backend/draft-api";
import { Node } from "@ndla/types-taxonomy";
import { Text } from "@ndla/typography";
import { gradeItemStyles, qualityEvaluationOptions } from "./QualityEvaluationForm";
import QualityEvaluationModal from "./QualityEvaluationModal";
import { ArticleFormType } from "../../containers/FormikForm/articleFormHooks";
import SmallQualityEvaluationGrade from "../../containers/StructurePage/resourceComponents/QualityEvaluationGrade";
Expand All @@ -25,11 +23,6 @@ const FlexWrapper = styled.div`
gap: ${spacing.xsmall};
`;

const LargeGradeItem = styled.div`
${gradeItemStyles}
cursor: default;
`;

const StyledNoEvaluation = styled(Text)`
color: ${colors.brand.greyMedium};
font-style: italic;
Expand All @@ -42,7 +35,6 @@ interface Props {
iconButtonColor?: "light" | "primary";
revisionMetaField?: FieldInputProps<ArticleFormType["revisionMeta"]>;
revisionMetaHelpers?: FieldHelperProps<ArticleFormType["revisionMeta"]>;
gradeVariant?: "small" | "large";
updateNotes?: (art: IUpdatedArticle) => Promise<IArticle>;
}

Expand All @@ -53,7 +45,6 @@ const QualityEvaluation = ({
iconButtonColor,
revisionMetaField,
revisionMetaHelpers,
gradeVariant = "large",
updateNotes,
}: Props) => {
const { t } = useTranslation();
Expand All @@ -67,23 +58,7 @@ const QualityEvaluation = ({
</Text>
{qualityEvaluation?.grade ? (
<>
{gradeVariant === "large" && (
<LargeGradeItem
title={qualityEvaluation?.note}
aria-label={qualityEvaluation?.note}
style={
{
"--item-color": qualityEvaluationOptions[qualityEvaluation.grade],
} as CSSProperties
}
data-border={qualityEvaluation.grade === 1 || qualityEvaluation.grade === 5}
>
{qualityEvaluation?.grade}
</LargeGradeItem>
)}
{gradeVariant === "small" && (
<SmallQualityEvaluationGrade grade={qualityEvaluation.grade} ariaLabel={qualityEvaluation?.note} />
)}
<SmallQualityEvaluationGrade grade={qualityEvaluation.grade} tooltip={qualityEvaluation?.note} />
</>
) : (
<StyledNoEvaluation margin="none" textStyle="button">
Expand Down
260 changes: 132 additions & 128 deletions src/components/QualityEvaluation/QualityEvaluationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,26 @@
*
*/

import { FieldHelperProps, FieldInputProps, Form, Formik } from "formik";
import { CSSProperties, useMemo, useState } from "react";
import { FieldHelperProps, FieldInputProps, Formik } from "formik";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { Item, Indicator } from "@radix-ui/react-radio-group";
import { useQueryClient } from "@tanstack/react-query";
import { ButtonV2 } from "@ndla/button";
import { colors, spacing, fonts, misc } from "@ndla/core";
import { FieldErrorMessage, Fieldset, InputV3, Label, Legend, RadioButtonGroup } from "@ndla/forms";
import {
Button,
FieldErrorMessage,
FieldHelper,
FieldInput,
FieldLabel,
FieldRoot,
RadioGroupItem,
RadioGroupItemControl,
RadioGroupItemHiddenInput,
RadioGroupItemText,
RadioGroupLabel,
RadioGroupRoot,
Text,
} from "@ndla/primitives";
import { styled } from "@ndla/styled-system/jsx";
import { IArticle, IUpdatedArticle } from "@ndla/types-backend/draft-api";
import { Grade, Node } from "@ndla/types-taxonomy";
import { ArticleFormType } from "../../containers/FormikForm/articleFormHooks";
Expand All @@ -25,78 +35,91 @@ import { usePutNodeMutation } from "../../modules/nodes/nodeMutations";
import { nodeQueryKeys } from "../../modules/nodes/nodeQueries";
import { formatDateForBackend } from "../../util/formatDate";
import handleError from "../../util/handleError";
import { FieldWarning, FormControl, FormField } from "../FormField";
import { FormField } from "../FormField";
import { FormActionsContainer, FormikForm } from "../FormikForm";
import validateFormik, { RulesType } from "../formikValidationSchema";
import Spinner from "../Spinner";

export const qualityEvaluationOptions: { [key: number]: string } = {
1: colors.support.green,
2: "#90C670",
3: "#C3D060",
4: colors.support.yellow,
5: colors.support.red,
};

const StyledFieldset = styled(Fieldset)`
display: flex;
gap: ${spacing.xsmall};
align-items: center;
`;

// Color needed in order for wcag contrast reqirements to be met
export const blackContrastColor = "#000";
export type QualityEvaluationValue = "1" | "2" | "3" | "4" | "5";

export const gradeItemStyles = css`
padding: 0px ${spacing.nsmall};
font-weight: ${fonts.weight.semibold};
border-radius: ${misc.borderRadius};
color: ${blackContrastColor};
${fonts.size.text.content};
&[data-border="false"] {
background-color: var(--item-color);
}
&[data-border="true"] {
box-shadow: inset 0px 0px 0px 2px var(--item-color);
}
`;

const StyledItem = styled(Item)`
all: unset;
${gradeItemStyles};

&:hover {
cursor: pointer;
border-radius: ${misc.borderRadius};
outline: 2px solid ${colors.brand.primary};
}
&[data-state="checked"] {
outline: 2px solid ${blackContrastColor};
}
`;
// TODO: We should change these colors
const qualityEvaluationOptions: Record<QualityEvaluationValue, string> = {
"1": "#5cbc80",
"2": "#90C670",
"3": "#C3D060",
"4": "#ead854",
"5": "#d1372e",
};

const ButtonContainer = styled.div`
margin-top: ${spacing.small};
display: flex;
justify-content: space-between;
`;
const ButtonContainer = styled("div", {
base: {
display: "flex",
justifyContent: "space-between",
},
});

const RightButtonsWrapper = styled.div`
display: flex;
gap: ${spacing.xsmall};
`;
const StyledRadioGroupRoot = styled(RadioGroupRoot, {
base: {
_horizontal: {
flexDirection: "column",
},
},
});

const StyledForm = styled(Form)`
display: flex;
flex-direction: column;
gap: ${spacing.small};
`;
const MutationErrorMessage = styled(FieldErrorMessage)`
margin-left: auto;
`;
const StyledRadioGroupItem = styled(RadioGroupItem, {
base: {
padding: "xxsmall",
borderRadius: "xsmall",
outlineOffset: "-5xsmall",
"&:has(input:focus-visible)": {
outlineOffset: "0",
},
},
variants: {
quality: {
"1": {
borderRadius: "xsmall",
outline: "2px solid",
outlineOffset: "-5xsmall",
outlineColor: qualityEvaluationOptions["1"],
"&:has(input:focus-visible)": {
outlineColor: qualityEvaluationOptions["1"],
outlineOffset: "-5xsmall",
boxShadow: "0 0 0 2px var(--shadow-color)",
boxShadowColor: "stroke.default",
},
},
"2": {
background: qualityEvaluationOptions["2"],
},
"3": {
background: qualityEvaluationOptions["3"],
},
"4": {
background: qualityEvaluationOptions["4"],
},
"5": {
borderRadius: "xsmall",
outline: "2px solid",
outlineOffset: "-5xsmall",
outlineColor: qualityEvaluationOptions["5"],
"&:has(input:focus-visible)": {
outlineColor: qualityEvaluationOptions["5"],
outlineOffset: "-5xsmall",
boxShadow: "0 0 0 2px var(--shadow-color)",
boxShadowColor: "stroke.default",
},
},
},
},
});

const StyledFieldWarning = styled(FieldWarning)`
margin-left: auto;
`;
const ItemsWrapper = styled("div", {
base: {
display: "flex",
gap: "3xsmall",
flexWrap: "wrap",
},
});

interface Props {
setOpen: (open: boolean) => void;
Expand Down Expand Up @@ -237,76 +260,57 @@ const QualityEvaluationForm = ({
onSubmit={onSubmit}
onReset={onDelete}
>
{({ dirty, isValid, isSubmitting, values }) => (
<StyledForm>
{({ dirty, isValid, isSubmitting }) => (
<FormikForm>
<FormField name="grade">
{({ field, meta, helpers }) => (
<FormControl isInvalid={!!meta.error} isRequired>
<RadioButtonGroup
<FieldRoot invalid={!!meta.error} required>
<StyledRadioGroupRoot
orientation="horizontal"
value={field.value?.toString()}
onValueChange={(v) => helpers.setValue(Number(v))}
asChild
onValueChange={(details) => helpers.setValue(Number(details.value))}
>
<StyledFieldset>
<Legend margin="none" textStyle="label-small">
{t("qualityEvaluationForm.title")}
</Legend>
{Object.entries(qualityEvaluationOptions).map(([value, color]) => (
<div key={value}>
<StyledItem
id={`quality-${value}`}
value={value.toString()}
data-color-value={value}
style={{ "--item-color": color } as CSSProperties}
data-border={value === "1" || value === "5"}
>
<Indicator forceMount>{value}</Indicator>
</StyledItem>
<Label htmlFor={`quality-${value}`} visuallyHidden>
{value}
</Label>
</div>
<RadioGroupLabel>{t("qualityEvaluationForm.title")}</RadioGroupLabel>
<FieldErrorMessage>{meta.error}</FieldErrorMessage>
{field.value === 5 && <FieldHelper>{t("qualityEvaluationForm.warning")}</FieldHelper>}
<ItemsWrapper>
{Object.entries(qualityEvaluationOptions).map(([value, _]) => (
<StyledRadioGroupItem key={value} value={value} quality={value as QualityEvaluationValue}>
<RadioGroupItemControl />
<RadioGroupItemText>{value}</RadioGroupItemText>
<RadioGroupItemHiddenInput />
</StyledRadioGroupItem>
))}
</StyledFieldset>
</RadioButtonGroup>
<FieldErrorMessage>{meta.error}</FieldErrorMessage>
</FormControl>
</ItemsWrapper>
</StyledRadioGroupRoot>
</FieldRoot>
)}
</FormField>
<FormField name="note">
{({ field }) => (
<FormControl>
<Label margin="none" textStyle="label-small">
{t("qualityEvaluationForm.note")}
</Label>
<InputV3 {...field} />
</FormControl>
<FieldRoot>
<FieldLabel>{t("qualityEvaluationForm.note")}</FieldLabel>
<FieldInput {...field} />
</FieldRoot>
)}
</FormField>
<ButtonContainer>
<div>
{node.qualityEvaluation?.grade && (
<ButtonV2 variant="outline" colorTheme="danger" type="reset">
{loading.delete && <Spinner appearance="small" />}
{t("qualityEvaluationForm.delete")}
</ButtonV2>
)}
</div>
<RightButtonsWrapper>
<ButtonV2 variant="outline" onClick={() => setOpen(false)}>
{node.qualityEvaluation?.grade && (
<Button variant="danger" type="reset" loading={loading.delete}>
{t("qualityEvaluationForm.delete")}
</Button>
)}
<FormActionsContainer>
<Button variant="secondary" onClick={() => setOpen(false)}>
{t("form.abort")}
</ButtonV2>
<ButtonV2 disabled={!dirty || !isValid || isSubmitting} type="submit">
{loading.save && <Spinner appearance="small" />} {t("form.save")}
</ButtonV2>
</RightButtonsWrapper>
</Button>
<Button disabled={!dirty || !isValid || isSubmitting} loading={loading.save} type="submit">
{t("form.save")}
</Button>
</FormActionsContainer>
</ButtonContainer>
{updateTaxMutation.isError && <MutationErrorMessage>{t("qualityEvaluationForm.error")}</MutationErrorMessage>}
{isResource && values.grade === 5 && (
<StyledFieldWarning>{t("qualityEvaluationForm.warning")}</StyledFieldWarning>
)}
</StyledForm>
{updateTaxMutation.isError && <Text color="text.error">{t("qualityEvaluationForm.error")}</Text>}
</FormikForm>
)}
</Formik>
);
Expand Down
Loading
Loading