Skip to content

Commit

Permalink
Merge pull request #142 from irontec/CDL-110-flag-edit-double-check
Browse files Browse the repository at this point in the history
Flag edit double check
  • Loading branch information
rbatistadev authored Oct 21, 2024
2 parents 477438f + f63f453 commit 650d434
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 7 deletions.
2 changes: 1 addition & 1 deletion library/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
54 changes: 53 additions & 1 deletion library/src/components/form.helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EntityItem } from '../router/routeMapParser';
import { PropertySpec } from 'services/api';

type GetMarshallerWhiteListPropsType = Pick<
EntityItem,
Expand Down Expand Up @@ -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 };
103 changes: 103 additions & 0 deletions library/src/components/shared/ConfirmEditDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<any> = forwardRef(function (
props: SlideProps,
ref: React.Ref<unknown>
) {
return <Slide {...props} direction='up' ref={ref} />;
});
Transition.displayName = 'ConfirmDialogTransition';

interface ConfirmEditDialogProps {
text: React.ReactNode;
open: boolean;
formEvent?: FormEvent<HTMLFormElement>;
handleClose: () => void;
handleSave: (e: FormEvent<HTMLFormElement>) => 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 (
<Dialog
open={open}
TransitionComponent={Transition}
keepMounted
onClose={handleClose}
aria-labelledby='alert-dialog-slide-title'
aria-describedby='alert-dialog-slide-description'
>
<CloseRoundedIcon className='close-icon' onClick={handleClose} />
<img src='assets/img/warning-dialog.svg' className='modal-icon' />
<DialogTitle id='alert-dialog-slide-title'>
{_('Save element')}
</DialogTitle>
<DialogContent>
<DialogContentText id='alert-dialog-slide-description'>
{text}
</DialogContentText>
<LinearProgress variant='determinate' value={progress} />
</DialogContent>
<DialogActions>
<Button
onClick={() => {
handleClose();
}}
>
Cancel
</Button>
<Button
hidden={progress !== 0}
onClick={() => {
if (formEvent) {
handleSave(formEvent);
}
}}
disabled={progress !== 0}
>
Apply
</Button>
</DialogActions>
</Dialog>
);
};
69 changes: 64 additions & 5 deletions library/src/entities/DefaultEntityBehavior/Form/Form.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -74,6 +77,7 @@ export type EntityFormProps = FormProps &
| 'unmarshaller'
>;
export type EntityFormType = (props: EntityFormProps) => JSX.Element | null;

const Form: EntityFormType = (props) => {
const {
entityService,
Expand All @@ -84,10 +88,17 @@ const Form: EntityFormType = (props) => {
foreignKeyGetter: foreignKeyGetterLoader,
row,
match,
edit,
} = props;

const { fkChoices } = props;

const [showConfirm, setShowConfirm] = useState<boolean>(false);
const editDoubleCheck = !edit
? false
: props.entityService.getEntity().editDoubleCheck;
const [formEvent, setFormEvent] = useState<
FormEvent<HTMLFormElement> | undefined
>(undefined);
const [foreignKeyGetter, setForeignKeyGetter] = useState<
ForeignKeyGetterType | undefined
>();
Expand Down Expand Up @@ -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<FieldsetGroups> = [];
if (props.groups) {
groups = filterFieldsetGroups(props.groups);
Expand Down Expand Up @@ -183,6 +206,9 @@ const Form: EntityFormType = (props) => {
const errorList = validationErrosToJsxErrorList(formik, allProperties);
const divRef = useRef<HTMLDivElement>(null);

const entity = entityService.getEntity();
const iden = row ? entity.toStr(row) : '';

const focusOnDiv = () => {
const node = divRef.current;
node?.focus();
Expand All @@ -196,14 +222,40 @@ const Form: EntityFormType = (props) => {
divRef
);

const confirmEditionText = (): JSX.Element => {
if (totalEntitiesUsed) {
return (
<span>
{_(`You are about to update`)} <strong>{iden}</strong>
<br />
{_(`This change will affect`)} <strong>{totalEntitiesUsed}</strong>{' '}
{_(`entities`)}
</span>
);
}

return (
<span>
{_(`You are about to update`)} <strong>{iden}</strong>
</span>
);
};

return (
<div>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();

if (formik.isSubmitting) {
e.preventDefault();
e.stopPropagation();
return false;
return;
}

if (editDoubleCheck) {
setFormEvent(e);
setShowConfirm(true);
return;
}

formik.handleSubmit(e);
Expand Down Expand Up @@ -277,6 +329,13 @@ const Form: EntityFormType = (props) => {
<SaveButton />
{reqError && <ErrorMessage message={reqError} />}
</form>
<ConfirmEditionDialog
text={confirmEditionText()}
open={showConfirm}
handleClose={() => setShowConfirm(false)}
formEvent={formEvent}
handleSave={(e) => formik.handleSubmit(e)}
/>
</div>
);
};
Expand Down
1 change: 1 addition & 0 deletions library/src/entities/EntityInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,6 @@ export default interface EntityInterface {
icon: React.FunctionComponent;
link?: string;
deleteDoubleCheck?: boolean;
editDoubleCheck?: boolean;
disableMultiDelete?: boolean;
}
Original file line number Diff line number Diff line change
@@ -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);
});
}
6 changes: 6 additions & 0 deletions library/src/services/form/InverseRealtions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {
collectReferences,
findMatchingColumns,
} from './InverseRelationHelper';

export { collectReferences, findMatchingColumns };
1 change: 1 addition & 0 deletions library/src/services/form/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './Field';
export * from './InverseRealtions';
export * from './FormFieldFactory/FormFieldFactory.styles';
export * from './FormFieldFactory';
export * from './types';
Loading

0 comments on commit 650d434

Please sign in to comment.