Skip to content

Commit

Permalink
feat(ui): enhance and refactor validation across UI components (#1956)
Browse files Browse the repository at this point in the history
  • Loading branch information
hdinia authored and skamril committed Apr 19, 2024
1 parent 8804875 commit 0d59d5b
Show file tree
Hide file tree
Showing 18 changed files with 346 additions and 122 deletions.
8 changes: 8 additions & 0 deletions webapp/public/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@
"form.field.maxValue": "The maximum value is {{0}}",
"form.field.notAllowedValue": "Not allowed value",
"form.field.specialChars": "Special characters allowed: {{0}}",
"form.field.specialCharsNotAllowed": "Special characters are not allowed",
"form.field.spacesNotAllowed": "Spaces are not allowed",
"form.field.requireLowercase": "Must contain at least one lowercase letter.",
"form.field.requireUppercase": "Must contain at least one uppercase letter.",
"form.field.requireDigit": "Must contain at least one digit.",
"form.field.requireSpecialChars": "Must contain at least one special character.",
"matrix.graphSelector": "Columns",
"matrix.message.importHint": "Click or drag and drop a matrix here",
"matrix.importNewMatrix": "Import a new matrix",
Expand Down Expand Up @@ -186,6 +192,8 @@
"settings.error.groupRolesSave": "Role(s) for group '{{0}}' not saved",
"settings.error.tokenSave": "'{{0}}' token not saved",
"settings.error.updateMaintenance": "Maintenance mode not updated",
"settings.user.form.confirmPassword":"Confirm password",
"settings.user.form.error.passwordMismatch": "Passwords do not match",
"launcher.additionalModes": "Additional modes",
"launcher.autoUnzip": "Automatically unzip",
"launcher.xpress": "Xpress (>= 8.3)",
Expand Down
8 changes: 8 additions & 0 deletions webapp/public/locales/fr/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@
"form.field.maxValue": "La valeur maximum est {{0}}",
"form.field.notAllowedValue": "Valeur non autorisée",
"form.field.specialChars": "Caractères spéciaux autorisés: {{0}}",
"form.field.specialCharsNotAllowed": "Les caractères spéciaux ne sont pas autorisés",
"form.field.spacesNotAllowed": "Les espaces ne sont pas autorisés",
"form.field.requireLowercase": "Doit contenir au moins une lettre minuscule.",
"form.field.requireUppercase": "Doit contenir au moins une lettre majuscule.",
"form.field.requireDigit": "Doit contenir au moins un chiffre.",
"form.field.requireSpecialChars": "Doit contenir au moins un caractère spécial.",
"matrix.graphSelector": "Colonnes",
"matrix.message.importHint": "Cliquer ou glisser une matrice ici",
"matrix.importNewMatrix": "Import d'une nouvelle matrice",
Expand Down Expand Up @@ -186,6 +192,8 @@
"settings.error.groupRolesSave": "Role(s) pour le groupe '{{0}}' non sauvegardé",
"settings.error.tokenSave": "Token '{{0}}' non sauvegardé",
"settings.error.updateMaintenance": "Erreur lors du changement du status de maintenance",
"settings.user.form.confirmPassword": "Confirmer le mot de passe",
"settings.user.form.error.passwordMismatch": "Les mots de passe ne correspondent pas",
"launcher.additionalModes": "Mode additionnels",
"launcher.autoUnzip": "Dézippage automatique",
"launcher.xpress": "Xpress (>= 8.3)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ import {
import { RoleType, UserDTO } from "../../../../../../common/types";
import { roleToString, sortByName } from "../../../../../../services/utils";
import usePromise from "../../../../../../hooks/usePromise";
import { getUsers } from "../../../../../../services/api/user";
import { getGroups, getUsers } from "../../../../../../services/api/user";
import { getAuthUser } from "../../../../../../redux/selectors";
import useAppSelector from "../../../../../../redux/hooks/useAppSelector";
import { UseFormReturnPlus } from "../../../../../common/Form/types";
import { validateString } from "../../../../../../utils/validationUtils";

function GroupForm(props: UseFormReturnPlus) {
const {
Expand All @@ -44,15 +45,23 @@ function GroupForm(props: UseFormReturnPlus) {
formState: { errors, defaultValues },
} = props;

const { t } = useTranslation();
const authUser = useAppSelector(getAuthUser);
const userLabelId = useRef(uuidv4()).current;
const [selectedUser, setSelectedUser] = useState<UserDTO>();
const { data: users, isLoading: isUsersLoading } = usePromise(getUsers);
const { data: groups } = usePromise(getGroups);

const existingGroups = useMemo(
() => groups?.map((group) => group.name),
[groups],
);

const { fields, append, remove } = useFieldArray({
control,
name: "permissions",
});
const [selectedUser, setSelectedUser] = useState<UserDTO>();
const { data: users, isLoading: isUsersLoading } = usePromise(getUsers);
const { t } = useTranslation();
const authUser = useAppSelector(getAuthUser);

const allowToAddPermission =
selectedUser &&
!getValues("permissions").some(
Expand All @@ -63,6 +72,7 @@ function GroupForm(props: UseFormReturnPlus) {
if (!users) {
return [];
}

return sortByName(
users.filter(
(user) =>
Expand Down Expand Up @@ -101,12 +111,11 @@ function GroupForm(props: UseFormReturnPlus) {
}
fullWidth
{...register("name", {
required: t("form.field.required") as string,
validate: (value) => {
if (RESERVED_GROUP_NAMES.includes(value)) {
return t("form.field.notAllowedValue") as string;
}
},
validate: (v) =>
validateString(v, {
existingValues: existingGroups,
excludedValues: RESERVED_GROUP_NAMES,
}),
})}
/>
{/* Permissions */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,18 @@ import {
import { GroupDTO, RoleType } from "../../../../../../common/types";
import { roleToString, sortByName } from "../../../../../../services/utils";
import usePromise from "../../../../../../hooks/usePromise";
import { getGroups } from "../../../../../../services/api/user";
import { getGroups, getUsers } from "../../../../../../services/api/user";
import { UserFormDialogProps } from ".";
import { UseFormReturnPlus } from "../../../../../common/Form/types";
import {
validatePassword,
validateString,
} from "../../../../../../utils/validationUtils";

interface Props extends UseFormReturnPlus {
onlyPermissions?: UserFormDialogProps["onlyPermissions"];
}

const PASSWORD_MIN_LENGTH = 8;

function UserForm(props: Props) {
const {
control,
Expand All @@ -50,14 +52,19 @@ function UserForm(props: Props) {
onlyPermissions,
} = props;

const { t } = useTranslation();
const groupLabelId = useRef(uuidv4()).current;
const [selectedGroup, setSelectedGroup] = useState<GroupDTO>();
const { data: groups, isLoading: isGroupsLoading } = usePromise(getGroups);
const { data: users } = usePromise(getUsers);

const existingUsers = useMemo(() => users?.map(({ name }) => name), [users]);

const { fields, append, remove } = useFieldArray({
control,
name: "permissions",
});
const [selectedGroup, setSelectedGroup] = useState<GroupDTO>();
const { data: groups, isLoading: isGroupsLoading } = usePromise(getGroups);
const { t } = useTranslation();

const commonTextFieldProps = {
required: true,
sx: { mx: 0 },
Expand Down Expand Up @@ -104,12 +111,11 @@ function UserForm(props: Props) {
helperText={errors.username?.message?.toString()}
{...commonTextFieldProps}
{...register("username", {
required: t("form.field.required") as string,
validate: (value) => {
if (RESERVED_USER_NAMES.includes(value)) {
return t("form.field.notAllowedValue") as string;
}
},
validate: (v) =>
validateString(v, {
existingValues: existingUsers,
excludedValues: RESERVED_USER_NAMES,
}),
})}
/>
<TextField
Expand All @@ -119,11 +125,20 @@ function UserForm(props: Props) {
helperText={errors.password?.message?.toString()}
{...commonTextFieldProps}
{...register("password", {
required: t("form.field.required") as string,
minLength: {
value: PASSWORD_MIN_LENGTH,
message: t("form.field.minLength", { 0: PASSWORD_MIN_LENGTH }),
},
validate: (v) => validatePassword(v),
})}
/>
<TextField
label={t("settings.user.form.confirmPassword")}
type="password"
spellCheck
error={!!errors.confirmPassword}
helperText={errors.confirmPassword?.message?.toString()}
{...commonTextFieldProps}
{...register("confirmPassword", {
validate: (v) =>
v === getValues("password") ||
t("settings.user.form.error.passwordMismatch"),
})}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import AddCircleIcon from "@mui/icons-material/AddCircle";
Expand All @@ -10,6 +10,7 @@ import StringFE from "../../../../common/fieldEditors/StringFE";
import Fieldset from "../../../../common/Fieldset";
import SelectFE from "../../../../common/fieldEditors/SelectFE";
import { SubmitHandlerPlus } from "../../../../common/Form/types";
import { validateString } from "../../../../../utils/validationUtils";

interface Props {
parentId: string;
Expand All @@ -25,6 +26,11 @@ function CreateVariantDialog(props: Props) {
const [sourceList, setSourceList] = useState<GenericInfo[]>([]);
const defaultValues = { name: "", sourceId: parentId };

const existingVariants = useMemo(
() => sourceList.map((variant) => variant.name),
[sourceList],
);

useEffect(() => {
setSourceList(createListFromTree(tree));
}, [tree]);
Expand Down Expand Up @@ -67,8 +73,8 @@ function CreateVariantDialog(props: Props) {
name="name"
control={control}
rules={{
required: true,
validate: (val) => val.trim().length > 0,
validate: (v) =>
validateString(v, { existingValues: existingVariants }),
}}
/>
<SelectFE
Expand Down
3 changes: 2 additions & 1 deletion webapp/src/components/App/Singlestudy/PropertiesDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Fieldset from "../../common/Fieldset";
import { SubmitHandlerPlus } from "../../common/Form/types";
import useAppDispatch from "../../../redux/hooks/useAppDispatch";
import { updateStudy } from "../../../redux/ducks/studies";
import { validateString } from "../../../utils/validationUtils";

const logErr = debug("antares:createstudyform:error");

Expand Down Expand Up @@ -137,7 +138,7 @@ function PropertiesDialog(props: Props) {
label={t("studies.studyName")}
name="name"
control={control}
rules={{ required: true, validate: (val) => val.trim().length > 0 }}
rules={{ validate: (v) => validateString(v) }}
sx={{ mx: 0 }}
fullWidth
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import SelectFE from "../../../../../common/fieldEditors/SelectFE";
import StringFE from "../../../../../common/fieldEditors/StringFE";
import SwitchFE from "../../../../../common/fieldEditors/SwitchFE";
import { StudyMetadata } from "../../../../../../common/types";
import { validateString } from "../../../../../../utils/validationUtils";

interface Props {
studyId: StudyMetadata["id"];
Expand Down Expand Up @@ -102,14 +103,8 @@ function AddDialog({ studyId, existingConstraints, open, onClose }: Props) {
label={t("global.name")}
control={control}
rules={{
validate: (v) => {
if (v.trim().length <= 0) {
return t("form.field.required");
}
if (existingConstraints.includes(v.trim().toLowerCase())) {
return t("form.field.duplicate", { 0: v });
}
},
validate: (v) =>
validateString(v, { existingValues: existingConstraints }),
}}
/>
<StringFE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import AddCircleIcon from "@mui/icons-material/AddCircle";
import FormDialog from "../../../../../common/dialogs/FormDialog";
import StringFE from "../../../../../common/fieldEditors/StringFE";
import { SubmitHandlerPlus } from "../../../../../common/Form/types";
import useAppSelector from "../../../../../../redux/hooks/useAppSelector";
import { getAreas } from "../../../../../../redux/selectors";
import { validateString } from "../../../../../../utils/validationUtils";

interface Props {
studyId: string;
open: boolean;
onClose: () => void;
createArea: (name: string) => void;
}

function CreateAreaDialog(props: Props) {
const { open, onClose, createArea } = props;
const { studyId, open, onClose, createArea } = props;
const [t] = useTranslation();
const existingAreas = useAppSelector((state) =>
getAreas(state, studyId).map((area) => area.name),
);

const defaultValues = {
name: "",
Expand All @@ -23,7 +30,7 @@ function CreateAreaDialog(props: Props) {
////////////////////////////////////////////////////////////////

const handleSubmit = (data: SubmitHandlerPlus<typeof defaultValues>) => {
createArea(data.values.name);
return createArea(data.values.name.trim());
};

////////////////////////////////////////////////////////////////
Expand All @@ -48,8 +55,8 @@ function CreateAreaDialog(props: Props) {
control={control}
fullWidth
rules={{
required: true,
validate: (val) => val.trim().length > 0,
validate: (v) =>
validateString(v, { existingValues: existingAreas }),
}}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useAppDispatch from "../../../../../../../../redux/hooks/useAppDispatch";
import { createStudyMapDistrict } from "../../../../../../../../redux/ducks/studyMaps";
import useAppSelector from "../../../../../../../../redux/hooks/useAppSelector";
import { getStudyMapDistrictsById } from "../../../../../../../../redux/selectors";
import { validateString } from "../../../../../../../../utils/validationUtils";

interface Props {
open: boolean;
Expand All @@ -32,10 +33,7 @@ function CreateDistrictDialog(props: Props) {
const districtsById = useAppSelector(getStudyMapDistrictsById);

const existingDistricts = useMemo(
() =>
Object.values(districtsById).map((district) =>
district.name.toLowerCase(),
),
() => Object.values(districtsById).map(({ name }) => name),
[districtsById],
);

Expand Down Expand Up @@ -81,15 +79,8 @@ function CreateDistrictDialog(props: Props) {
control={control}
fullWidth
rules={{
required: { value: true, message: t("form.field.required") },
validate: (v) => {
if (v.trim().length <= 0) {
return false;
}
if (existingDistricts.includes(v.toLowerCase())) {
return `The District "${v}" already exists`;
}
},
validate: (v) =>
validateString(v, { existingValues: existingDistricts }),
}}
sx={{ m: 0 }}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useAppDispatch from "../../../../../../../../redux/hooks/useAppDispatch";
import useEnqueueErrorSnackbar from "../../../../../../../../hooks/useEnqueueErrorSnackbar";
import useAppSelector from "../../../../../../../../redux/hooks/useAppSelector";
import { getStudyMapLayersById } from "../../../../../../../../redux/selectors";
import { validateString } from "../../../../../../../../utils/validationUtils";

interface Props {
open: boolean;
Expand All @@ -31,7 +32,7 @@ function CreateLayerDialog(props: Props) {
const layersById = useAppSelector(getStudyMapLayersById);

const existingLayers = useMemo(
() => Object.values(layersById).map((layer) => layer.name.toLowerCase()),
() => Object.values(layersById).map(({ name }) => name),
[layersById],
);

Expand Down Expand Up @@ -73,15 +74,8 @@ function CreateLayerDialog(props: Props) {
control={control}
fullWidth
rules={{
required: { value: true, message: t("form.field.required") },
validate: (v) => {
if (v.trim().length <= 0) {
return false;
}
if (existingLayers.includes(v.toLowerCase())) {
return `The layer "${v}" already exists`;
}
},
validate: (v) =>
validateString(v, { existingValues: existingLayers }),
}}
/>
)}
Expand Down
Loading

0 comments on commit 0d59d5b

Please sign in to comment.