diff --git a/front2/src/components/form/FormGroup.tsx b/front2/src/components/form/FormGroup.tsx index c4d631d..d0bdcb5 100644 --- a/front2/src/components/form/FormGroup.tsx +++ b/front2/src/components/form/FormGroup.tsx @@ -1,18 +1,21 @@ -import { ReactNode } from 'react'; +import { ReactElement, ReactNode } from 'react'; +import { Button, Flex, IconButton, Text, Tooltip } from '@chakra-ui/react'; import { - FormControl, - FormControlProps, - FormErrorMessage, - FormHelperText, - FormLabel, -} from '@chakra-ui/react'; -import { MdLabelImportant } from 'react-icons/md'; + MdErrorOutline, + MdHelpOutline, + MdLabelImportant, + MdRefresh, +} from 'react-icons/md'; -export type FormGroupProps = FormControlProps & { +export type FormGroupProps = { error?: ReactNode; help?: ReactNode; label?: ReactNode; + isModify?: boolean; + onRestore?: () => void; + isRequired?: boolean; + children: ReactNode; }; export const FormGroup = ({ @@ -20,16 +23,45 @@ export const FormGroup = ({ error, help, label, - ...props + isModify = false, + isRequired = false, + onRestore, }: FormGroupProps) => ( - - {!!label && {label}} + + + {!!label && ( + + {label}{' '} + {isRequired && ( + + * + + )} + + )} + {!!onRestore && isModify && ( + + )} + {children} - {!!help && {help}} + {!!help && ( + + + {help} + + )} - - - {error} - - + {!!error && ( + + + {error} + + )} + ); diff --git a/front2/src/components/form/FormSelectList.tsx b/front2/src/components/form/FormSelectList.tsx index f890e1b..379c548 100644 --- a/front2/src/components/form/FormSelectList.tsx +++ b/front2/src/components/form/FormSelectList.tsx @@ -1,3 +1,5 @@ +import { useEffect, useRef } from 'react'; + import { Box, Button, Flex, Text } from '@chakra-ui/react'; import { isNullOrUndefined } from '@/utils/validator'; @@ -45,7 +47,15 @@ export const FormSelectList = ({ search, }: FormSelectListProps) => { const displayedValue = optionToOptionDisplay(options, selected, search); - + const scrollToRef = useRef(null); + useEffect(() => { + if (scrollToRef?.current) { + scrollToRef?.current?.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + } + }, []); return ( onSelectValue(data)} + ref={data.isSelected ? scrollToRef : undefined} > {data.name} diff --git a/front2/src/components/form/Formidable.tsx b/front2/src/components/form/Formidable.tsx index 33cdc5f..965525a 100644 --- a/front2/src/components/form/Formidable.tsx +++ b/front2/src/components/form/Formidable.tsx @@ -1,14 +1,6 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; -const getDifferences = (obj1: any, obj2: any): { [key: string]: boolean } => { - const result: { [key: string]: boolean } = {}; - for (const key in obj1) { - if (obj1.hasOwnProperty(key)) { - result[key] = obj1[key] !== obj2[key]; - } - } - return result; -}; +import { isArray, isNullOrUndefined, isObject } from '@/utils/validator'; const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => { for (const key in obj) { @@ -19,55 +11,153 @@ const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => { return false; }; -// const onChangeValue = (key: string, data) => { -// console.log(`[${key}] data: ${data}`); -// setNewData((previous) => { -// if (previous === undefined) { -// return previous; -// } -// if (previous[key] === data) { -// return previous; -// } -// const tmp = { ...previous }; -// tmp[key] = data; -// console.log(`data = ${JSON.stringify(tmp, null, 2)}`); -// return tmp; -// }); -// }; +function getDifferences( + obj1: object, + obj2: object +): { [key: string]: boolean } { + // Create an empty object to store the differences + const result: { [key: string]: boolean } = {}; + // Recursive function to compare values + function compareValues(value1: any, value2: any): boolean { + // If both values are objects, compare their properties recursively + if (isObject(value1) && isObject(value2)) { + return hasAnyTrue(getDifferences(value1, value2)); + } + // If both values are arrays, compare their elements + if (isArray(value1) && isArray(value2)) { + //console.log(`Check is array: ${JSON.stringify(value1)} =?= ${JSON.stringify(value2)}`); + if (value1.length !== value2.length) { + return true; + } + for (let i = 0; i < value1.length; i++) { + if (compareValues(value1[i], value2[i])) { + return true; + } + } + return false; + } + // Otherwise, compare the values directly + //console.log(`compare : ${value1} =?= ${value2}`); + return value1 !== value2; + } -export const useFormidable = ({ + // Get all keys from both objects + const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]); + + // Iterate over all keys + for (const key of allKeys) { + if (compareValues(obj1[key], obj2[key])) { + result[key] = true; + } else { + result[key] = false; + } + } + return result; +} + +export const useFormidable = ({ initialValues = {} as TYPE, }: { initialValues?: TYPE; }) => { const [values, setValues] = useState({ ...initialValues } as TYPE); - const initialData = useMemo(() => { - setValues({ ...initialValues } as TYPE); - return { ...initialValues } as TYPE; - }, [initialValues, setValues]); + const [initialData, setInitialData] = useState(initialValues); const [valueChange, setValueChange] = useState<{ [key: string]: boolean }>( {} ); const [isFormModified, setIsFormModified] = useState(false); + useEffect(() => { + setInitialData((previous) => { + //console.log(`FORMIDABLE: useMemo initial Values(${JSON.stringify(initialValues)})`); + const previousJson = JSON.stringify(previous); + const newJson = JSON.stringify(initialValues); + if (previousJson === newJson) { + return previous; + } + //console.log(`FORMIDABLE: ==> update new values`); + setValues({ ...initialValues }); + const ret = getDifferences(initialValues, initialValues); + setValueChange(ret); + setIsFormModified(hasAnyTrue(ret)); + return initialValues; + }); + }, [ + initialValues, + setInitialData, + setValues, + setValueChange, + setIsFormModified, + ]); const setValuesExternal = useCallback( (data: object) => { + //console.log(`FORMIDABLE: setValuesExternal(${JSON.stringify(data)}) ==> keys=${Object.keys(data)}`); setValues((previous) => { const newValues = { ...previous, ...data }; - const ret = getDifferences(initialData, values); + const ret = getDifferences(initialData, newValues); setValueChange(ret); setIsFormModified(hasAnyTrue(ret)); - console.log( - ` ppppppppppppppppppp ${JSON.stringify(ret, null, 2)} ==> ${hasAnyTrue(ret)}` - ); return newValues; }); }, [setValues, initialData] ); + const restoreValue = useCallback( + (data: object) => { + setValues((previous) => { + const keysInPrevious = Object.keys(previous); + const newValue = { ...previous }; + let countModify = 0; + //console.log(`restore value ${JSON.stringify(data, null, 2)}`); + for (const key of Object.keys(data)) { + if (!keysInPrevious.includes(key)) { + continue; + } + if (data[key] === false) { + continue; + } + newValue[key] = initialValues[key]; + countModify++; + } + if (countModify === 0) { + return previous; + } + //console.log(`initialData data ${JSON.stringify(initialData, null, 2)}`); + //console.log(`New data ${JSON.stringify(newValue, null, 2)}`); + const ret = getDifferences(initialData, newValue); + setValueChange(ret); + setIsFormModified(hasAnyTrue(ret)); + return newValue; + }); + }, + [setValues, initialData, setIsFormModified, setValueChange] + ); + const getDeltaData = useCallback( + ({ omit = [], only }: { omit?: string[]; only?: string[] }) => { + const out = {}; + Object.keys(valueChange).forEach((key) => { + if (omit.includes(key) || (only && !only.includes(key))) { + return; + } + if (!valueChange[key]) { + return; + } + const tmpValue = values[key]; + if (isNullOrUndefined(tmpValue)) { + out[key] = null; + } else { + out[key] = tmpValue; + } + }); + return out; + }, + [valueChange, values] + ); return { values, valueChange, isFormModified, setValues: setValuesExternal, + getDeltaData, + restoreValue, }; }; diff --git a/front2/src/components/form/SelectMultiple.tsx b/front2/src/components/form/SelectMultiple.tsx index 9d028ff..b38897a 100644 --- a/front2/src/components/form/SelectMultiple.tsx +++ b/front2/src/components/form/SelectMultiple.tsx @@ -60,7 +60,7 @@ export const SelectMultiple = ({ const selectValue = (data: SelectMultipleValueDisplayType) => { const newValues = values?.includes(data.id) - ? values.filter((elem) => data.id === elem) + ? values.filter((elem) => data.id !== elem) : [...(values ?? []), data.id]; setShowList(false); if (onChange) { diff --git a/front2/src/components/popup/ConfirmPopUp.tsx b/front2/src/components/popup/ConfirmPopUp.tsx new file mode 100644 index 0000000..7277390 --- /dev/null +++ b/front2/src/components/popup/ConfirmPopUp.tsx @@ -0,0 +1,61 @@ +import { useRef } from 'react'; + +import { + AlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Button, + UseDisclosureReturn, + useDisclosure, +} from '@chakra-ui/react'; + +export type ConfirmPopUpProps = { + title: string; + body: string; + confirmTitle: string; + onConfirm: () => void; + disclosure: UseDisclosureReturn; +}; + +export const ConfirmPopUp = ({ + title, + body, + confirmTitle, + onConfirm, + disclosure, +}: ConfirmPopUpProps) => { + const onClickConfirm = () => { + onConfirm(); + disclosure.onClose(); + }; + const cancelRef = useRef(null); + return ( + + + + + {title} + + + {body} + + + + + + + + + ); +}; diff --git a/front2/src/components/popup/TrackEditPopUp.tsx b/front2/src/components/popup/TrackEditPopUp.tsx index 6be79e2..5e788cc 100644 --- a/front2/src/components/popup/TrackEditPopUp.tsx +++ b/front2/src/components/popup/TrackEditPopUp.tsx @@ -2,8 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { Button, - FormControl, - FormLabel, + IconButton, Input, Modal, ModalBody, @@ -12,205 +11,246 @@ import { ModalFooter, ModalHeader, ModalOverlay, + NumberDecrementStepper, + NumberIncrementStepper, NumberInput, + NumberInputField, + NumberInputStepper, + Text, Textarea, + useDisclosure, } from '@chakra-ui/react'; -import { Formiz, useForm, useFormFields } from '@formiz/core'; +import { + MdAdminPanelSettings, + MdDeleteForever, + MdEdit, + MdRemove, +} from 'react-icons/md'; import { useNavigate, useParams } from 'react-router-dom'; -import { Album, Track } from '@/back-api'; +import { Track, TrackResource } from '@/back-api'; import { FormGroup } from '@/components/form/FormGroup'; +import { useFormidable } from '@/components/form/Formidable'; import { SelectMultiple } from '@/components/form/SelectMultiple'; import { SelectSingle } from '@/components/form/SelectSingle'; +import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp'; import { useOrderedAlbums } from '@/service/Album'; import { useOrderedArtists } from '@/service/Artist'; import { useOrderedGenders } from '@/service/Gender'; -import { useSpecificTrack } from '@/service/Track'; +import { useServiceContext } from '@/service/ServiceContext'; +import { useSpecificTrack, useTrackService } from '@/service/Track'; import { isNullOrUndefined } from '@/utils/validator'; export type TrackEditPopUpProps = {}; -const getDifferences = (obj1: any, obj2: any): { [key: string]: boolean } => { - const result: { [key: string]: boolean } = {}; - for (const key in obj1) { - if (obj1.hasOwnProperty(key)) { - result[key] = obj1[key] !== obj2[key]; - } - } - return result; -}; - -const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => { - for (const key in obj) { - if (obj.hasOwnProperty(key) && obj[key] === true) { - return true; - } - } - return false; -}; - export const TrackEditPopUp = ({}: TrackEditPopUpProps) => { const { trackId } = useParams(); + const trackIdInt = isNullOrUndefined(trackId) + ? undefined + : parseInt(trackId, 10); + const { session } = useServiceContext(); const { dataGenders } = useOrderedGenders(undefined); const { dataArtist } = useOrderedArtists(undefined); const { dataAlbums } = useOrderedAlbums(undefined); - const { dataTrack } = useSpecificTrack( - isNullOrUndefined(trackId) ? undefined : parseInt(trackId, 10) - ); + const { store } = useTrackService(); + const { dataTrack } = useSpecificTrack(trackIdInt); + const [admin, setAdmin] = useState(false); const navigate = useNavigate(); + const disclosure = useDisclosure(); const onClose = () => { navigate('../../', { relative: 'path' }); }; - const onSubmit = (values) => { - console.log(`onSubmit = ${values}`); - }; - const onValuesChange = (values, form) => { - console.log(`onValuesChange = ${values}`); + const onRemove = () => { + if (isNullOrUndefined(trackIdInt)) { + return; + } + store.remove( + trackIdInt, + TrackResource.remove({ + restConfig: session.getRestConfig(), + params: { + id: trackIdInt, + }, + }) + ); + onClose(); }; const initialRef = useRef(null); const finalRef = useRef(null); - const form = useForm({ - onSubmit, - onValuesChange, - initialValues: { ...dataTrack }, - onValid: () => console.log('onValid'), - onInvalid: () => console.log('onInvalid'), + const form = useFormidable({ + //onSubmit, + //onValuesChange, + initialValues: dataTrack, + //onValid: () => console.log('onValid'), + //onInvalid: () => console.log('onInvalid'), }); - const formValues = useFormFields({ - connect: form, - selector: (field) => field.value, - }); - /* - const formValues = useFormFields({ - connect: form, - fields: ['genderId'] as const, - selector: 'value', - }); - */ - console.log(`dataTrack : ${JSON.stringify(dataTrack, null, 2)}`); - console.log(`Redraw : ${JSON.stringify(formValues, null, 2)}`); - const [newData, setNewData] = useState(); - const [changeData, setChangeData] = useState<{ [key: string]: boolean }>(); - const [changeOneData, setChangeOneData] = useState(true); - // initialize value when ready - useEffect(() => { - setNewData(dataTrack); - }, [dataTrack, setNewData, setChangeData]); - - useEffect(() => { - if (!newData) { + const onSave = async () => { + if (isNullOrUndefined(trackIdInt)) { return; } - const ret = getDifferences(dataTrack, newData); - setChangeData(ret); - ///////////////////////setChangeOneData(hasAnyTrue(ret)); - console.log( - `ChangeData=${JSON.stringify(ret, null, 2)} ChangeOneData=${hasAnyTrue(ret)}` + const dataThatNeedToBeUpdated = form.getDeltaData({ omit: ['covers'] }); + console.log(`onSave = ${JSON.stringify(dataThatNeedToBeUpdated, null, 2)}`); + store.update( + TrackResource.patch({ + restConfig: session.getRestConfig(), + data: dataThatNeedToBeUpdated, + params: { + id: trackIdInt, + }, + }) ); - }, [dataTrack, newData, setChangeData, setChangeOneData]); - - // const onChangeValue = (key: string, data) => { - // console.log(`[${key}] data: ${data}`); - // setNewData((previous) => { - // if (previous === undefined) { - // return previous; - // } - // if (previous[key] === data) { - // return previous; - // } - // const tmp = { ...previous }; - // tmp[key] = data; - // console.log(`data = ${JSON.stringify(tmp, null, 2)}`); - // return tmp; - // }); - // }; - + }; return ( - -
- - - - Edit Track - + + + + Edit Track + - - - Title + + {admin && ( + <> + + {dataTrack?.id} + + + {dataTrack?.dataId} + + + + + + + )} + {!admin && ( + <> + form.restoreValue({ name: true })} + label="Title" + > form.setValues({ name: e.target.value })} /> - - + form.restoreValue({ description: true })} + label="Description" + >