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

feat(ui): enhance and refactor validation across UI components #1956

Merged
merged 6 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
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",
hdinia marked this conversation as resolved.
Show resolved Hide resolved
"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),
hdinia marked this conversation as resolved.
Show resolved Hide resolved
[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) }}
skamril marked this conversation as resolved.
Show resolved Hide resolved
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) =>
hdinia marked this conversation as resolved.
Show resolved Hide resolved
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
Loading