From 3a88e7d91a959ee68a142977fd856747d21695dd Mon Sep 17 00:00:00 2001 From: Roger Batista Date: Sun, 20 Oct 2024 13:13:59 +0200 Subject: [PATCH 1/3] added form logic for inverse relations search into double check edition --- library/src/components/form.helper.ts | 54 ++++++++- .../components/shared/ConfirmEditDialog.tsx | 103 ++++++++++++++++++ .../DefaultEntityBehavior/Form/Form.tsx | 69 +++++++++++- library/src/entities/EntityInterface.ts | 1 + .../InverseRealtions/InverseRelationHelper.ts | 53 +++++++++ .../services/form/InverseRealtions/index.ts | 6 + library/src/services/form/index.ts | 1 + 7 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 library/src/components/shared/ConfirmEditDialog.tsx create mode 100644 library/src/services/form/InverseRealtions/InverseRelationHelper.ts create mode 100644 library/src/services/form/InverseRealtions/index.ts diff --git a/library/src/components/form.helper.ts b/library/src/components/form.helper.ts index 8629504e..73acf326 100644 --- a/library/src/components/form.helper.ts +++ b/library/src/components/form.helper.ts @@ -1,4 +1,5 @@ import { EntityItem } from '../router/routeMapParser'; +import { PropertySpec } from 'services/api'; type GetMarshallerWhiteListPropsType = Pick< EntityItem, @@ -29,4 +30,55 @@ const getMarshallerWhiteList = ( return whitelist; }; -export { getMarshallerWhiteList }; +const collectReferences = ( + obj: any, + references: PropertySpec[] = [] +): PropertySpec[] => { + if (isReferenceObject(obj)) { + references.push(obj); + } + + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (isObject(value)) { + collectReferences(value, references); + } + }); + + return references; +}; + +const isObject = (value: any): boolean => { + return value && typeof value === 'object'; +}; + +const isReferenceObject = (obj: any): boolean => { + return isObject(obj) && obj.hasOwnProperty('$ref'); +}; + +const findMatchingColumns = ( + columnNames: string[], + inverseRelations: PropertySpec[] +): string[] => { + return columnNames.filter((column) => { + const singularColumn = getSingularForm(column); + + return inverseRelations.some((relation) => { + return objectHasMatchingValue(relation, singularColumn); + }); + }); +}; + +const getSingularForm = (word: string): string => { + return word.endsWith('s') + ? word.slice(0, -1).toLowerCase() + : word.toLowerCase(); +}; + +const objectHasMatchingValue = (obj: any, target: string): boolean => { + return Object.values(obj).some((value) => { + return typeof value === 'string' && value.toLowerCase().includes(target); + }); +}; + +export { getMarshallerWhiteList, collectReferences, findMatchingColumns }; diff --git a/library/src/components/shared/ConfirmEditDialog.tsx b/library/src/components/shared/ConfirmEditDialog.tsx new file mode 100644 index 00000000..493c7fac --- /dev/null +++ b/library/src/components/shared/ConfirmEditDialog.tsx @@ -0,0 +1,103 @@ +import React, { + useState, + useEffect, + forwardRef, + ComponentType, + FormEvent, +} from 'react'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; +import LinearProgress from '@mui/material/LinearProgress'; +import { DialogContentText, Slide, SlideProps } from '@mui/material'; +import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import _ from '../../services/translations/translate'; + +const Transition: ComponentType = forwardRef(function ( + props: SlideProps, + ref: React.Ref +) { + return ; +}); +Transition.displayName = 'ConfirmDialogTransition'; + +interface ConfirmEditDialogProps { + text: React.ReactNode; + open: boolean; + formEvent?: FormEvent; + handleClose: () => void; + handleSave: (e: FormEvent) => void; +} + +export const ConfirmEditionDialog = (props: ConfirmEditDialogProps) => { + const { open, handleClose, text, handleSave, formEvent } = props; + const TOTAL_TIME = 100; + const [progress, setProgress] = useState(TOTAL_TIME); + + useEffect(() => { + let timer: NodeJS.Timeout | null = null; + if (open) { + timer = setInterval(() => { + setProgress((oldProgress) => { + if (oldProgress === 0) { + return 0; + } + return oldProgress - 5; + }); + }, 250); + } + + setProgress(TOTAL_TIME); + + return () => { + if (timer) { + clearInterval(timer); + } + }; + }, [open]); + + return ( + + + + + {_('Save element')} + + + + {text} + + + + + + + + + ); +}; diff --git a/library/src/entities/DefaultEntityBehavior/Form/Form.tsx b/library/src/entities/DefaultEntityBehavior/Form/Form.tsx index f1171c65..223fbb8d 100644 --- a/library/src/entities/DefaultEntityBehavior/Form/Form.tsx +++ b/library/src/entities/DefaultEntityBehavior/Form/Form.tsx @@ -1,6 +1,6 @@ import { Alert, AlertTitle } from '@mui/material'; import { FormikHelpers } from 'formik'; -import { useEffect, useRef, useState } from 'react'; +import { FormEvent, useEffect, useRef, useState } from 'react'; import { PathMatch } from 'react-router-dom'; import { useStoreState } from 'store'; import ErrorBoundary from '../../../components/ErrorBoundary'; @@ -9,8 +9,10 @@ import SaveButton from '../../../components/shared/Button/SaveButton'; import ErrorMessage from '../../../components/shared/ErrorMessage'; import { FilterValuesType } from '../../../router/routeMapParser'; import { + collectReferences, DropdownChoices, EmbeddableProperty, + findMatchingColumns, ScalarEntityValue, useFormikType, } from '../../../services'; @@ -33,6 +35,7 @@ import filterFieldsetGroups, { import FormFieldMemo from './FormField'; import { useFormHandler } from './useFormHandler'; import { validationErrosToJsxErrorList } from './validationErrosToJsxErrorList'; +import { ConfirmEditionDialog } from '../../../components/shared/ConfirmEditDialog'; export type FormOnChangeEvent = React.ChangeEvent<{ name: string; value: any }>; export type PropertyFkChoices = DropdownChoices; @@ -74,6 +77,7 @@ export type EntityFormProps = FormProps & | 'unmarshaller' >; export type EntityFormType = (props: EntityFormProps) => JSX.Element | null; + const Form: EntityFormType = (props) => { const { entityService, @@ -84,10 +88,17 @@ const Form: EntityFormType = (props) => { foreignKeyGetter: foreignKeyGetterLoader, row, match, + edit, } = props; const { fkChoices } = props; - + const [showConfirm, setShowConfirm] = useState(false); + const editDoubleCheck = !edit + ? false + : props.entityService.getEntity().editDoubleCheck; + const [formEvent, setFormEvent] = useState< + FormEvent | undefined + >(undefined); const [foreignKeyGetter, setForeignKeyGetter] = useState< ForeignKeyGetterType | undefined >(); @@ -121,6 +132,18 @@ const Form: EntityFormType = (props) => { const columns = entityService.getProperties(); const columnNames = Object.keys(columns); + const inverseRelations = collectReferences(columns); + const inverseRelationsMatch = findMatchingColumns( + columnNames, + inverseRelations + ); + let totalEntitiesUsed = 0; + inverseRelationsMatch.forEach((value) => { + if (Array.isArray(row?.[value])) { + totalEntitiesUsed = (row?.[value] as string[]).length + totalEntitiesUsed; + } + }); + let groups: Array = []; if (props.groups) { groups = filterFieldsetGroups(props.groups); @@ -183,6 +206,9 @@ const Form: EntityFormType = (props) => { const errorList = validationErrosToJsxErrorList(formik, allProperties); const divRef = useRef(null); + const entity = entityService.getEntity(); + const iden = row ? entity.toStr(row) : ''; + const focusOnDiv = () => { const node = divRef.current; node?.focus(); @@ -196,14 +222,40 @@ const Form: EntityFormType = (props) => { divRef ); + const confirmEditionText = (): JSX.Element => { + if (totalEntitiesUsed) { + return ( + + {_(`You are about to update`)} {iden} +
+ {_(`This change will affect`)} {totalEntitiesUsed}{' '} + {_(`entities`)} +
+ ); + } + + return ( + + {_(`You are about to update`)} {iden} + + ); + }; + return (
{ + e.preventDefault(); + e.stopPropagation(); + if (formik.isSubmitting) { - e.preventDefault(); - e.stopPropagation(); - return false; + return; + } + + if (editDoubleCheck) { + setFormEvent(e); + setShowConfirm(true); + return; } formik.handleSubmit(e); @@ -277,6 +329,13 @@ const Form: EntityFormType = (props) => { {reqError && } + setShowConfirm(false)} + formEvent={formEvent} + handleSave={(e) => formik.handleSubmit(e)} + />
); }; diff --git a/library/src/entities/EntityInterface.ts b/library/src/entities/EntityInterface.ts index ab85db5d..7a8dcc39 100644 --- a/library/src/entities/EntityInterface.ts +++ b/library/src/entities/EntityInterface.ts @@ -166,5 +166,6 @@ export default interface EntityInterface { icon: React.FunctionComponent; link?: string; deleteDoubleCheck?: boolean; + editDoubleCheck?: boolean; disableMultiDelete?: boolean; } diff --git a/library/src/services/form/InverseRealtions/InverseRelationHelper.ts b/library/src/services/form/InverseRealtions/InverseRelationHelper.ts new file mode 100644 index 00000000..073b0069 --- /dev/null +++ b/library/src/services/form/InverseRealtions/InverseRelationHelper.ts @@ -0,0 +1,53 @@ +import { PropertySpec } from 'services/api'; + +export const collectReferences = ( + obj: any, + references: PropertySpec[] = [] +): PropertySpec[] => { + if (isReferenceObject(obj)) { + references.push(obj); + } + + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (isObject(value)) { + collectReferences(value, references); + } + }); + + return references; +}; + +function isObject(value: any): boolean { + return value && typeof value === 'object'; +} + +function isReferenceObject(obj: any): boolean { + return isObject(obj) && obj.hasOwnProperty('$ref'); +} + +export const findMatchingColumns = ( + columnNames: string[], + inverseRelations: PropertySpec[] +): string[] => { + return columnNames.filter((column) => { + const singularColumn = getSingularForm(column); + + return inverseRelations.some((relation) => { + return objectHasMatchingValue(relation, singularColumn); + }); + }); +}; + +function getSingularForm(word: string): string { + return word.endsWith('s') + ? word.slice(0, -1).toLowerCase() + : word.toLowerCase(); +} + +// Helper function to check if an object contains a value that includes a target string (case-insensitive) +function objectHasMatchingValue(obj: any, target: string): boolean { + return Object.values(obj).some((value) => { + return typeof value === 'string' && value.toLowerCase().includes(target); + }); +} diff --git a/library/src/services/form/InverseRealtions/index.ts b/library/src/services/form/InverseRealtions/index.ts new file mode 100644 index 00000000..041ba424 --- /dev/null +++ b/library/src/services/form/InverseRealtions/index.ts @@ -0,0 +1,6 @@ +import { + collectReferences, + findMatchingColumns, +} from './InverseRelationHelper'; + +export { collectReferences, findMatchingColumns }; diff --git a/library/src/services/form/index.ts b/library/src/services/form/index.ts index d1212492..91140d08 100644 --- a/library/src/services/form/index.ts +++ b/library/src/services/form/index.ts @@ -1,4 +1,5 @@ export * from './Field'; +export * from './InverseRealtions'; export * from './FormFieldFactory/FormFieldFactory.styles'; export * from './FormFieldFactory'; export * from './types'; From 5d7e17ab81bcb203dd8cab673844287b44fe70f9 Mon Sep 17 00:00:00 2001 From: Roger Batista Date: Sun, 20 Oct 2024 13:14:27 +0200 Subject: [PATCH 2/3] updated translations --- library/src/translations/ca.json | 4 ++++ library/src/translations/en.json | 4 ++++ library/src/translations/es.json | 4 ++++ library/src/translations/it.json | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/library/src/translations/ca.json b/library/src/translations/ca.json index 56624b7f..f29f3f65 100644 --- a/library/src/translations/ca.json +++ b/library/src/translations/ca.json @@ -30,10 +30,12 @@ "Please type the item name, as shown in bold font above, to continue": "Escriba el nombre del elemento, tal y como se muestra arriba en negrita, para continuar", "Remove element": "Eliminar elemento", "Save": "Guardar", + "Save element": "Desa element", "Select Fields": "Seleccionar campos", "Select all": "Seleccionar todo", "Sign In": "Entrar", "Starts with": "empieza por", + "This change will affect": "Aquest canvi afectarà", "True": "Verdadero", "Upload image": "Subir imagen", "Username": "Usuari", @@ -42,7 +44,9 @@ "Welcome back!": "Bienvenido de nuevo!", "Yes, delete it": "Si, eliminar", "You are about to remove": "Esteu a punt d'eliminar-lo", + "You are about to update": "Estàs a punt d'actualitzar", "You haven’t created any {{entity}} yet.": "Aún no has creado ningún {{entity}}", + "entities": "entitats", "invalid pattern": "Patrón invalido", "required value": "valor requerido", "settings": "Configuracions", diff --git a/library/src/translations/en.json b/library/src/translations/en.json index bdf7007f..d657fd2e 100644 --- a/library/src/translations/en.json +++ b/library/src/translations/en.json @@ -30,10 +30,12 @@ "Please type the item name, as shown in bold font above, to continue": "Please type the item name, as shown in bold font above, to continue", "Remove element": "Remove element", "Save": "Save", + "Save element": "Save element", "Select Fields": "Select Fields", "Select all": "Select all", "Sign In": "Sign In", "Starts with": "Starts with", + "This change will affect": "This change will affect", "True": "True", "Upload image": "Upload image", "Username": "Username", @@ -42,7 +44,9 @@ "Welcome back!": "Welcome back!", "Yes, delete it": "Yes, delete it", "You are about to remove": "You are about to remove", + "You are about to update": "You are about to update", "You haven’t created any {{entity}} yet.": "You haven’t created any {{entity}} yet.", + "entities": "entities", "invalid pattern": "invalid pattern", "required value": "required value", "settings": "Settings", diff --git a/library/src/translations/es.json b/library/src/translations/es.json index 002025f7..26e813ec 100644 --- a/library/src/translations/es.json +++ b/library/src/translations/es.json @@ -30,10 +30,12 @@ "Please type the item name, as shown in bold font above, to continue": "Escriba el nombre del elemento, tal y como se muestra arriba en negrita, para continuar", "Remove element": "Eliminar elemento", "Save": "Guardar", + "Save element": "Guardar elemento", "Select Fields": "Seleccionar campos", "Select all": "Seleccionar todo", "Sign In": "Entrar", "Starts with": "empieza por", + "This change will affect": "Este cambio afectará", "True": "Verdadero", "Upload image": "Subir imagen", "Username": "Usuario", @@ -42,7 +44,9 @@ "Welcome back!": "Bienvenido de nuevo!", "Yes, delete it": "Si, eliminar", "You are about to remove": "Estás a punto de eliminar", + "You are about to update": "Estás a punto de actualizar", "You haven’t created any {{entity}} yet.": "Aún no has creado ningún {{entity}}", + "entities": "entidades", "invalid pattern": "Patrón invalido", "required value": "valor requerido", "settings": "Configuración", diff --git a/library/src/translations/it.json b/library/src/translations/it.json index 1666c1ab..e40df2a8 100644 --- a/library/src/translations/it.json +++ b/library/src/translations/it.json @@ -30,10 +30,12 @@ "Please type the item name, as shown in bold font above, to continue": "Please type the item name, as shown in bold font above, to continue", "Remove element": "Remove element", "Save": "Save", + "Save element": "Salva elemento", "Select Fields": "Select Fields", "Select all": "Select all", "Sign In": "Sign In", "Starts with": "Starts with", + "This change will affect": "Questa modifica influirà", "True": "True", "Upload image": "Upload image", "Username": "Username", @@ -42,7 +44,9 @@ "Welcome back!": "Welcome back!", "Yes, delete it": "Yes, delete it", "You are about to remove": "Stai per rimuovere", + "You are about to update": "Stai per aggiornare ", "You haven’t created any {{entity}} yet.": "You haven’t created any {{entity}} yet.", + "entities": "entità", "invalid pattern": "invalid pattern", "required value": "required value", "settings": "Impostazioni", From f63f4537eaba6cb6db1f989c9ddb8b9ef526c7b4 Mon Sep 17 00:00:00 2001 From: Roger Batista Date: Sun, 20 Oct 2024 13:14:36 +0200 Subject: [PATCH 3/3] updated package version --- library/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/package.json b/library/package.json index 6e27a8e8..f0baedc9 100644 --- a/library/package.json +++ b/library/package.json @@ -1,6 +1,6 @@ { "name": "@irontec/ivoz-ui", - "version": "1.6.2", + "version": "1.7.0", "description": "UI library used in ivozprovider", "license": "GPL-3.0", "main": "index.js",