[FIX] form basic models

This commit is contained in:
Edouard DUPIN 2025-02-11 21:35:42 +01:00
parent c65e7d5e25
commit 4ebfa4e2ca
17 changed files with 1382 additions and 1667 deletions

View File

@ -22,9 +22,9 @@ public class WebLauncherLocal extends WebLauncher {
Initialization.generateObjects();
final WebLauncherLocal launcher = new WebLauncherLocal();
launcher.process();
launcher.LOGGER.info("end-configure the server & wait finish process:");
LOGGER.info("end-configure the server & wait finish process:");
Thread.currentThread().join();
launcher.LOGGER.info("STOP the REST server:");
LOGGER.info("STOP the REST server:");
}
@Override

View File

@ -38,8 +38,8 @@
"dayjs": "1.11.13",
"history": "5.3.0",
"next-themes": "^0.4.4",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"react-error-boundary": "5.0.0",
"react-icons": "5.4.0",
"react-router-dom": "7.1.5",
@ -51,13 +51,13 @@
"devDependencies": {
"@chakra-ui/styled-system": "^2.12.0",
"@playwright/test": "1.50.1",
"@storybook/addon-actions": "8.5.3",
"@storybook/addon-essentials": "8.5.3",
"@storybook/addon-links": "8.5.3",
"@storybook/addon-mdx-gfm": "8.5.3",
"@storybook/react": "8.5.3",
"@storybook/react-vite": "8.5.3",
"@storybook/theming": "8.5.3",
"@storybook/addon-actions": "8.5.4",
"@storybook/addon-essentials": "8.5.4",
"@storybook/addon-links": "8.5.4",
"@storybook/addon-mdx-gfm": "8.5.4",
"@storybook/react": "8.5.4",
"@storybook/react-vite": "8.5.4",
"@storybook/theming": "8.5.4",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
@ -66,24 +66,23 @@
"@types/node": "22.13.1",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"@typescript-eslint/eslint-plugin": "8.23.0",
"@typescript-eslint/parser": "8.23.0",
"@typescript-eslint/eslint-plugin": "8.24.0",
"@typescript-eslint/parser": "8.24.0",
"@vitejs/plugin-react": "4.3.4",
"eslint": "9.20.0",
"eslint-plugin-codeceptjs": "1.3.0",
"eslint": "9.20.1",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "5.1.0",
"eslint-plugin-storybook": "0.11.2",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"knip": "5.43.6",
"knip": "5.44.0",
"lint-staged": "15.4.3",
"npm-check-updates": "^17.1.14",
"prettier": "3.4.2",
"prettier": "3.5.0",
"puppeteer": "24.2.0",
"react-is": "19.0.0",
"storybook": "8.5.3",
"storybook": "8.5.4",
"ts-node": "10.9.2",
"typescript": "5.7.3",
"vite": "6.1.0",

2765
front/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +1,26 @@
import { ReactNode } from 'react';
import {
Box,
Button,
ConditionalValue,
Flex,
HStack,
IconButton,
Span,
Text,
useDisclosure,
} from '@chakra-ui/react';
import {
LuAlignJustify,
LuArrowBigLeft,
LuCircleUserRound,
LuKeySquare,
LuLogIn,
LuLogOut,
LuMoon,
LuSettings,
LuSun,
} from 'react-icons/lu';
import {
MdHelp,
MdHome,
MdMore,
MdOutlinePlaylistPlay,
MdOutlineUploadFile,
MdSupervisedUserCircle,
} from 'react-icons/md';
import { Box, Button, ConditionalValue, Flex, HStack, IconButton, Span, Text, useDisclosure } from '@chakra-ui/react';
import { LuAlignJustify, LuArrowBigLeft, LuCircleUserRound, LuKeySquare, LuLogIn, LuLogOut, LuMoon, LuSettings, LuSun } from 'react-icons/lu';
import { MdHelp, MdHome, MdMore, MdOutlinePlaylistPlay, MdOutlineUploadFile, MdSupervisedUserCircle } from 'react-icons/md';
import { useNavigate } from 'react-router-dom';
import { useColorMode, useColorModeValue } from '@/components/ui/color-mode';
import {
DrawerBody,
DrawerContent,
DrawerHeader,
DrawerRoot,
} from '@/components/ui/drawer';
import {
MenuContent,
MenuItem,
MenuRoot,
MenuTrigger,
} from '@/components/ui/menu';
import { DrawerBody, DrawerContent, DrawerHeader, DrawerRoot } from '@/components/ui/drawer';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '@/components/ui/menu';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionState } from '@/service/SessionState';
import { useSessionService } from '@/service/session';
import { colors } from '@/theme/colors';
import {
requestOpenSite,
requestSignIn,
requestSignOut,
requestSignUp,
} from '@/utils/sso';
import { requestOpenSite, requestSignIn, requestSignOut, requestSignUp } from '@/utils/sso';
export const TOP_BAR_HEIGHT = '50px';
@ -109,16 +75,15 @@ const ButtonMenuLeft = ({
);
};
export const TopBar = ({ title, children }: TopBarProps) => {
const navigate = useNavigate();
const { colorMode, toggleColorMode } = useColorMode();
const { clearToken } = useSessionService();
const { session } = useServiceContext();
const { clearToken } = useSessionService();
const backColor = useColorModeValue('back.100', 'back.800');
const drawerDisclose = useDisclosure();
const onChangeTheme = () => {
drawerDisclose.onOpen();
};
const navigate = useNavigate();
const onSignIn = (): void => {
clearToken();
requestSignIn();
@ -299,4 +264,4 @@ export const TopBar = ({ title, children }: TopBarProps) => {
</DrawerRoot>
</Flex>
);
};
};

View File

@ -1,8 +1,8 @@
import { ReactNode } from 'react';
import { Box } from '@chakra-ui/react';
import { LuMenu } from 'react-icons/lu';
import { Button } from '../ui/button';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '../ui/menu';
export type MenuElement = {
@ -27,10 +27,9 @@ export const ContextMenu = ({ elements }: ContextMenuProps) => {
marginRight="4px"
data-testid="context-menu_trigger"
>
{/* This is very stupid, we need to set as span to prevent a button in button... WTF */}
<Button variant="ghost" color="brand.500">
<Box asChild color="brand.500" cursor="pointer">
<LuMenu />
</Button>
</Box>
</MenuTrigger>
<MenuContent data-testid="context-menu_content">
{elements?.map((data) => (

View File

@ -1,8 +1,9 @@
import { ReactNode } from 'react';
import { Box, Flex, Text } from '@chakra-ui/react';
import { Flex, Text } from '@chakra-ui/react';
import { MdErrorOutline, MdHelpOutline, MdRefresh } from 'react-icons/md';
import { Icon } from '../Icon';
import { useFormidableContextElement } from '../formidable';
const DisplayLabel = ({
@ -16,7 +17,7 @@ const DisplayLabel = ({
return <></>;
}
return (
<Text marginRight="auto" fontWeight="bold">
<Text marginRight="auto" paddingY="5px" fontWeight="bold">
{label}{' '}
{isRequired && (
<Text as="span" color="red.600">
@ -76,6 +77,48 @@ export const FormGroup = ({
const singleLine = disableSingleLine
? false
: form.configuration.singleLineForm;
return (
<FormGroupShow
help={help}
label={label}
isRequired={isRequired}
error={error}
isModify={isModify}
enableModifyNotification={enableModifyNotification}
enableReset={enableReset}
singleLine={singleLine}
onRestore={onRestore}
>
{children}
</FormGroupShow>
);
};
export type FormGroupShowProps = {
children: ReactNode;
help?: ReactNode;
label?: ReactNode;
isRequired?: boolean;
error?: ReactNode;
isModify?: boolean;
enableModifyNotification?: boolean;
enableReset?: boolean;
singleLine?: boolean;
onRestore?: () => void;
};
export const FormGroupShow = ({
children,
help,
label,
isRequired = false,
error,
isModify = false,
enableModifyNotification = true,
enableReset = true,
singleLine = false,
onRestore,
}: FormGroupShowProps) => {
return (
<Flex
borderLeftWidth="3px"
@ -94,12 +137,14 @@ export const FormGroup = ({
{singleLine && (
<>
<Flex direction="row" width="full" gap="52px">
<Box width="10%">
<Flex width="10%">
<DisplayLabel label={label} isRequired={isRequired} />
{!!onRestore && isModify && enableReset && (
<MdRefresh size="15px" onClick={onRestore} cursor="pointer" />
<Icon sizeIcon="150px">
<MdRefresh onClick={onRestore} cursor="pointer" />
</Icon>
)}
</Box>
</Flex>
<Flex direction="column" width={'90%'} gap="5px">
{children}
<DisplayHelp help={help} />
@ -111,12 +156,14 @@ export const FormGroup = ({
{!singleLine && (
<>
<Flex direction="row" width="full" gap="52px">
<Box width="full">
<Flex width="full">
<DisplayLabel label={label} isRequired={isRequired} />
{!!onRestore && isModify && enableReset && (
<MdRefresh size="15px" onClick={onRestore} cursor="pointer" />
<Icon sizeIcon="30px" onClick={onRestore} cursor="pointer">
<MdRefresh />
</Icon>
)}
</Box>
</Flex>
</Flex>
{children}
<DisplayHelp help={help} />

View File

@ -20,8 +20,7 @@ export const FormTextarea = ({
placeholder,
...rest
}: FormTextareaProps) => {
const { form, value, isModify, onChange, onRestore } =
useFormidableContextElement(name);
const { value, onChange } = useFormidableContextElement(name);
return (
<FormGroup name={name} {...rest}>
<Textarea

View File

@ -1,11 +1,14 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { z as zod } from 'zod';
import { isArray, isNullOrUndefined, isObject } from '@/utils/validator';
import { isNullOrUndefined } from '@/utils/validator';
import { getDifferences, hasAnyTrue } from './utils';
export type FormidableDeltaConfig<TYPE> = {
omit?: string[]; // (keyof TYPE)[];
only?: string[]; // (keyof TYPE)[];
};
export type FormidableConfig = {
enableReset?: boolean;
enableModifyNotification?: boolean;
@ -20,12 +23,14 @@ const initialFormConfig: Required<FormidableConfig> = {
export const useFormidable = <TYPE extends object = object>({
initialValues = {} as TYPE,
configuration: inputConfiguration = initialFormConfig,
deltaConfig,
resolver = (_data: TYPE) => {
return {};
},
}: {
initialValues?: TYPE;
configuration?: FormidableConfig;
deltaConfig?: FormidableDeltaConfig<TYPE>;
resolver?: (data: any) => Record<string, string>;
}) => {
const configuration: Required<FormidableConfig> = {
@ -107,7 +112,7 @@ export const useFormidable = <TYPE extends object = object>({
[setValues, initialData, setIsFormModified, setIsModify]
);
const getDeltaData = useCallback(
({ omit = [], only }: { omit?: string[]; only?: string[] } = {}) => {
({ omit = [], only }: FormidableDeltaConfig<TYPE> = {}) => {
const out = {};
Object.keys(isModify).forEach((key) => {
if (omit.includes(key) || (only && !only.includes(key))) {
@ -137,6 +142,7 @@ export const useFormidable = <TYPE extends object = object>({
values,
errors,
configuration,
deltaConfig,
};
};

View File

@ -29,6 +29,7 @@ export const formContext = createContext<FromContextProps>({
enableModifyNotification: false,
singleLineForm: false,
},
deltaConfig: {},
},
});
@ -57,11 +58,12 @@ export const useFormidableContextElement = (name: string) => {
[name, form, form.setValues]
);
const onRestore = useCallback(() => {
console.log('Restore value : ' + name);
form.restoreValue({ [name]: true });
}, [name, form, form.restoreValue]);
return {
form,
value: form.values[name],
value: form.values[name] || '',
error: form.errors[name],
isModify: form.isModify[name],
onChange,

View File

@ -29,7 +29,7 @@ export const FormidableForm = <TYPE extends object = object>({
onSubmit(form.values);
}
if (onSubmitDelta) {
onSubmitDelta(form.getDeltaData());
onSubmitDelta(form.getDeltaData(form.deltaConfig));
}
}
};

View File

@ -11,7 +11,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { AlbumResource, AlbumWrite } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers';
import { FormGroup } from '@/components/form/FormGroup';
import { FormGroupShow } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput';
import { FormTextarea } from '@/components/form/FormTextarea';
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
@ -66,17 +66,17 @@ export const AlbumEditPopUp = ({}: AlbumEditPopUpProps) => {
const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<AlbumWrite>({
initialValues: dataAlbum,
deltaConfig: { omit: ['covers'] },
});
const onSave = async (deltaData: AlbumWrite) => {
if (isNullOrUndefined(albumIdInt)) {
return;
}
const dataThatNeedToBeUpdated = form.getDeltaData({ omit: ['covers'] });
console.log(`onSave = ${JSON.stringify(dataThatNeedToBeUpdated, null, 2)}`);
console.log(`onSave = ${JSON.stringify(deltaData, null, 2)}`);
store.update(
AlbumResource.patch({
restConfig: session.getRestConfig(),
data: dataThatNeedToBeUpdated,
data: deltaData,
params: {
id: albumIdInt,
},
@ -146,8 +146,6 @@ export const AlbumEditPopUp = ({}: AlbumEditPopUpProps) => {
open={true}
data-testid="album-edit-pop-up"
>
{/* <DialogOverlay /> */}
{/* <DialogCloseTrigger /> */}
<DialogContent>
<Formidable.From form={form} onSubmitDelta={onSave}>
<DialogHeader>Edit Album</DialogHeader>
@ -156,9 +154,9 @@ export const AlbumEditPopUp = ({}: AlbumEditPopUpProps) => {
<DialogBody pb={6} gap="0px" paddingLeft="18px">
{admin && (
<>
<FormGroup name="" isRequired label="Id">
<FormGroupShow isRequired label="Id">
<Text>{dataAlbum?.id}</Text>
</FormGroup>
</FormGroupShow>
{countTracksOfAnAlbum !== 0 && (
<Flex paddingLeft="14px">
<MdWarning color="red.600" />
@ -168,7 +166,7 @@ export const AlbumEditPopUp = ({}: AlbumEditPopUpProps) => {
</Text>
</Flex>
)}
<FormGroup name="" label="Action(s):">
<FormGroupShow label="Action(s):">
<Button
onClick={disclosure.onOpen}
marginRight="auto"
@ -177,7 +175,7 @@ export const AlbumEditPopUp = ({}: AlbumEditPopUpProps) => {
>
<MdDeleteForever /> Remove Media
</Button>
</FormGroup>
</FormGroupShow>
<ConfirmPopUp
disclosure={disclosure}
title="Remove album"

View File

@ -11,7 +11,6 @@ import { useNavigate, useParams } from 'react-router-dom';
import { ArtistResource, ArtistWrite } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers';
import { FormGroup } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput';
import { FormTextarea } from '@/components/form/FormTextarea';
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
@ -27,6 +26,7 @@ import { useServiceContext } from '@/service/ServiceContext';
import { useCountTracksOfAnArtist } from '@/service/Track';
import { isNullOrUndefined } from '@/utils/validator';
import { FormGroupShow } from '../form/FormGroup';
import { Formidable, useFormidable } from '../formidable';
export type ArtistEditPopUpProps = {};
@ -65,17 +65,17 @@ export const ArtistEditPopUp = ({}: ArtistEditPopUpProps) => {
const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<ArtistWrite>({
initialValues: dataArtist,
deltaConfig: { omit: ['covers'] },
});
const onSave = async (data: ArtistWrite) => {
const onSave = async (dataDelta: ArtistWrite) => {
if (isNullOrUndefined(artistIdInt)) {
return;
}
const dataThatNeedToBeUpdated = form.getDeltaData({ omit: ['covers'] });
console.log(`onSave = ${JSON.stringify(dataThatNeedToBeUpdated, null, 2)}`);
console.log(`onSave = ${JSON.stringify(dataDelta, null, 2)}`);
store.update(
ArtistResource.patch({
restConfig: session.getRestConfig(),
data: dataThatNeedToBeUpdated,
data: dataDelta,
params: {
id: artistIdInt,
},
@ -153,9 +153,9 @@ export const ArtistEditPopUp = ({}: ArtistEditPopUpProps) => {
<DialogBody pb={6} gap="0px" paddingLeft="18px">
{admin && (
<>
<FormGroup name="" isRequired label="Id">
<FormGroupShow isRequired label="Id">
<Text>{dataArtist?.id}</Text>
</FormGroup>
</FormGroupShow>
{countTracksOnAnArtist !== 0 && (
<Flex paddingLeft="14px">
<MdWarning color="red.600" />
@ -165,7 +165,7 @@ export const ArtistEditPopUp = ({}: ArtistEditPopUpProps) => {
</Text>
</Flex>
)}
<FormGroup name="" label="Action(s):">
<FormGroupShow label="Action(s):">
<Button
onClick={disclosure.onOpen}
marginRight="auto"
@ -174,7 +174,7 @@ export const ArtistEditPopUp = ({}: ArtistEditPopUpProps) => {
>
<MdDeleteForever /> Remove Media
</Button>
</FormGroup>
</FormGroupShow>
<ConfirmPopUp
disclosure={disclosure}
title="Remove artist"

View File

@ -11,7 +11,6 @@ import { useNavigate, useParams } from 'react-router-dom';
import { GenderResource, GenderWrite } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers';
import { FormGroup } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput';
import { FormTextarea } from '@/components/form/FormTextarea';
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
@ -65,17 +64,17 @@ export const GenderEditPopUp = ({}: GenderEditPopUpProps) => {
const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<GenderWrite>({
initialValues: dataGender,
deltaConfig: { omit: ['covers'] },
});
const onSave = async (data: GenderWrite) => {
const onSave = async (dataDelta: GenderWrite) => {
if (isNullOrUndefined(genderIdInt)) {
return;
}
const dataThatNeedToBeUpdated = form.getDeltaData({ omit: ['covers'] });
console.log(`onSave = ${JSON.stringify(dataThatNeedToBeUpdated, null, 2)}`);
console.log(`onSave = ${JSON.stringify(dataDelta, null, 2)}`);
store.update(
GenderResource.patch({
restConfig: session.getRestConfig(),
data: dataThatNeedToBeUpdated,
data: dataDelta,
params: {
id: genderIdInt,
},
@ -152,9 +151,9 @@ export const GenderEditPopUp = ({}: GenderEditPopUpProps) => {
<DialogBody pb={6} gap="0px" paddingLeft="18px">
{admin && (
<>
<FormGroup name="" isRequired label="Id">
<FormGroupShow isRequired label="Id">
<Text>{dataGender?.id}</Text>
</FormGroup>
</FormGroupShow>
{countTracksOnAGender !== 0 && (
<Flex paddingLeft="14px">
<MdWarning color="red.600" />
@ -164,7 +163,7 @@ export const GenderEditPopUp = ({}: GenderEditPopUpProps) => {
</Text>
</Flex>
)}
<FormGroup name="" label="Action(s):">
<FormGroupShow label="Action(s):">
<Button
onClick={disclosure.onOpen}
marginRight="auto"
@ -173,7 +172,7 @@ export const GenderEditPopUp = ({}: GenderEditPopUpProps) => {
>
<MdDeleteForever /> Remove gender
</Button>
</FormGroup>
</FormGroupShow>
<ConfirmPopUp
disclosure={disclosure}
title="Remove gender"

View File

@ -5,7 +5,7 @@ import { MdAdminPanelSettings, MdDeleteForever, MdEdit } from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom';
import { TrackResource, TrackWrite } from '@/back-api';
import { FormGroup } from '@/components/form/FormGroup';
import { FormGroupShow } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput';
import { FormNumber } from '@/components/form/FormNumber';
import { FormSelect } from '@/components/form/FormSelect';
@ -65,22 +65,18 @@ export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<TrackWrite>({
//onSubmit,
//onValuesChange,
initialValues: dataTrack,
//onValid: () => console.log('onValid'),
//onInvalid: () => console.log('onInvalid'),
deltaConfig: { omit: ['covers'] },
});
const onSave = async (data: TrackWrite) => {
const onSave = async (dataDelta: TrackWrite) => {
if (isNullOrUndefined(trackIdInt)) {
return;
}
const dataThatNeedToBeUpdated = form.getDeltaData({ omit: ['covers'] });
console.log(`onSave = ${JSON.stringify(dataThatNeedToBeUpdated, null, 2)}`);
console.log(`onSave = ${JSON.stringify(dataDelta, null, 2)}`);
store.update(
TrackResource.patch({
restConfig: session.getRestConfig(),
data: dataThatNeedToBeUpdated,
data: dataDelta,
params: {
id: trackIdInt,
},
@ -105,13 +101,13 @@ export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
<DialogBody pb={6} gap="0px" paddingLeft="18px">
{admin && (
<>
<FormGroup name="" isRequired label="Id">
<FormGroupShow isRequired label="Id">
<Text>{dataTrack?.id}</Text>
</FormGroup>
<FormGroup name="" label="Data Id">
</FormGroupShow>
<FormGroupShow label="Data Id">
<Text>{dataTrack?.dataId}</Text>
</FormGroup>
<FormGroup name="" label="Action(s):">
</FormGroupShow>
<FormGroupShow label="Action(s):">
<Button
onClick={disclosure.onOpen}
marginRight="auto"
@ -119,7 +115,7 @@ export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
>
<MdDeleteForever /> Remove Media
</Button>
</FormGroup>
</FormGroupShow>
<ConfirmPopUp
disclosure={disclosure}
title="Remove track"

View File

@ -10,7 +10,7 @@ import { Toaster } from './components/ui/toaster';
import { systemTheme } from './theme/theme';
// Render the app
const rootElement = document.getElementById('root');
const rootElement = document.getElementById('root') as HTMLElement;
if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,9 @@ import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
define: {
'process.env.NODE_ENV': '"development"', // Forcer le mode dev pour éviter l'erreur
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),