Skip to content

Commit

Permalink
refactor(ui): replace existing validation logic with validateString
Browse files Browse the repository at this point in the history
… utility
  • Loading branch information
hdinia committed Mar 12, 2024
1 parent d50ca47 commit 6a8e418
Show file tree
Hide file tree
Showing 17 changed files with 300 additions and 157 deletions.
11 changes: 10 additions & 1 deletion webapp/public/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"global.group": "Group",
"global.variants": "Variants management",
"global.password": "Password",
"global.confirmPassword": "Confirm password",
"global.create": "Create",
"global.open": "Open",
"global.name": "Name",
Expand Down Expand Up @@ -117,7 +118,14 @@
"form.field.minValue": "The minimum value is {{0}}",
"form.field.maxValue": "The maximum value is {{0}}",
"form.field.notAllowedValue": "Not allowed value",
"form.field.specialChars": "Special characters allowed: {{0}}",
"form.field.allowedChars": "Special characters allowed: {{0}}",
"form.field.specialCharsNotAllowed": "Special characters are not allowed",
"form.field.spacesNotAllowed": "Spaces are not allowed",
"form.field.requireLowercase": "Password must contain at least one lowercase letter.",
"form.field.requireUppercase": "Password must contain at least one uppercase letter.",
"form.field.requireDigit": "Password must contain at least one digit.",
"form.field.requireSpecialChars": "Password must contain at least one special character.",
"form.field.requireMinimumLength": "Password must be at least 8 characters long.",
"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 +194,7 @@
"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.error.passwordMismatch": "Passwords do not match",
"launcher.additionalModes": "Additional modes",
"launcher.autoUnzip": "Automatically unzip",
"launcher.xpress": "Xpress (>= 8.3)",
Expand Down
11 changes: 10 additions & 1 deletion webapp/public/locales/fr/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"global.group": "Groupe",
"global.variants": "Gestion des variantes",
"global.password": "Mot de passe",
"global.confirmPassword": "Confirmer le mot de passe",
"global.create": "Créer",
"global.open": "Ouvrir",
"global.name": "Nom",
Expand Down Expand Up @@ -117,7 +118,14 @@
"form.field.minValue": "La valeur minimum est {{0}}",
"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.allowedChars": "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": "Le mot de passe doit contenir au moins une lettre minuscule.",
"form.field.requireUppercase": "Le mot de passe doit contenir au moins une lettre majuscule.",
"form.field.requireDigit": "Le mot de passe doit contenir au moins un chiffre.",
"form.field.requireSpecialChars": "Le mot de passe doit contenir au moins un caractère spécial.",
"form.field.requireMinimumLength": "Le mot de passe doit comporter au moins 8 caractères.",
"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 +194,7 @@
"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.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,
}) || undefined,
})}
/>
{/* 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,
}) || undefined,
})}
/>
<TextField
Expand All @@ -119,11 +125,18 @@ 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) || undefined,
})}
/>
<TextField
label={t("global.confirmPassword")}
type="password"
spellCheck
error={!!errors.confirmPassword}
helperText={errors.confirmPassword?.message?.toString()}
{...commonTextFieldProps}
{...register("confirmPassword", {
validate: (v) => validatePassword(v, getValues("password")),
})}
/>
</>
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 @@ -56,7 +56,7 @@ function CreateAreaDialog(props: Props) {
fullWidth
rules={{
validate: (v) =>
validateString(v, { existingEntries: existingAreas }),
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 6a8e418

Please sign in to comment.