Skip to content

Commit

Permalink
form works
Browse files Browse the repository at this point in the history
  • Loading branch information
assafnoahkoren committed Feb 13, 2024
1 parent 7a71a2d commit cd91e37
Show file tree
Hide file tree
Showing 14 changed files with 259 additions and 18 deletions.
52 changes: 50 additions & 2 deletions services/client/src/core/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { DefaultError, QueryClient, UseMutationOptions, useMutation, useQuery } from '@tanstack/react-query';
import { chain, scalars } from '../../generated/zeus/chain';
import { ModelTypes, ValueTypes, order_by } from '../../generated/zeus';
import { useAuth, useAuthId } from '../firebase/firebase';
import toast from 'react-hot-toast';
import toast, { Renderable, Toast, ValueOrFunction } from 'react-hot-toast';
import { useRef } from 'react';
import { queryClient } from './query-client';
import { useAppMutation } from './hooks';


// ---- Profile --------------------------------------------------------------------------
Expand Down Expand Up @@ -93,6 +94,53 @@ export const useMutation_UpdateProfile = () => {
});
};


// ---- Type ----------------------------------------------------------------------------
export type DeviceType = DeepPartial<ModelTypes['DeviceType']>;

export const useQuery_AllDeviceTypes = () => {
return useQuery({
queryKey: ['allDeviceTypes'],
queryFn: () =>
chain('query', { scalars })({
DeviceType: [{

}, {
id: true,
name: true,
created_at: true,
updated_at: true
}]
})
});
}



export const useMutation_CreateDeviceType = () => {
return useAppMutation({
mutationFn: (deviceType: DeviceType) => {
return chain('mutation', { scalars })({
insert_DeviceType_one: [{
object: toInput(deviceType)
}, {
id: true
}]
});
},
onSettled: (data, error) => {
queryClient.invalidateQueries({ queryKey: ['allDeviceTypes'] });
},
toast: {
loading: 'יוצר סוג...',
success: 'סוג נוצר בהצלחה',
error: 'שגיאה ביצירת סוג'
}
}, queryClient);

}


// ---- Utils ----------------------------------------------------------------------------
// A function that loop over keys of object and if the key's first latter is uppercase then nest the value in {data: value}
export const toInput = (obj: any) => {
Expand Down
37 changes: 37 additions & 0 deletions services/client/src/core/api/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { DefaultError, UseMutationOptions, QueryClient, useMutation } from "@tanstack/react-query";
import { useRef } from "react";
import toast, { ValueOrFunction, Renderable, Toast } from "react-hot-toast";

type AppMutationOptions = {
toast?: {
loading: ValueOrFunction<Renderable, Toast>;
success: ValueOrFunction<Renderable, Toast>;
error: ValueOrFunction<Renderable, Toast>;
};
}
type useMutationOptions<T, TError = DefaultError, TVariables = void, TContext = unknown> = UseMutationOptions<T, TError, TVariables, TContext> & AppMutationOptions;

export const useAppMutation = <T, TError = DefaultError, TVariables = void, TContext = unknown>(options: useMutationOptions<T, TError, TVariables, TContext> , queryClient: QueryClient) => {
const toastIdRef = useRef<string>();

const originMutationFn = options.mutationFn!;
options.mutationFn = (variables) => {
if (toastIdRef.current) return new Promise((res, rej) => {rej('Another mutation is in progress')});
if (options.toast) toastIdRef.current = toast.loading(options.toast.loading);
return originMutationFn(variables);
}

const originOnSettled = options.onSettled!;
options.onSettled = (data: any, error: any, variables, context) => {
error && console.error(error);
if (error == 'Another mutation is in progress') return new Promise(() => {});
if (options.toast) {
if (error) toast.error(options.toast.error, { id: toastIdRef.current });
else toast.success(options.toast.success, { id: toastIdRef.current });
toastIdRef.current = undefined;
}
return originOnSettled(data, error, variables, context);
}

return useMutation(options, queryClient);
};
2 changes: 1 addition & 1 deletion services/client/src/core/form/AppForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const rowColToCssGrid = (grid?: { row?: number; col?: number; rowSpan?: number;
if (grid.colSpan) {
className = className.concat(`col-span-${grid.colSpan} `);
} else {
className = className.concat(`col-span-2 `);
className = className.concat(`lg:col-span-2 col-span-12`);
}
return className;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { FC, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { AppFieldProps } from './Field.type';

type RegisterProps = ReturnType<ReturnType<typeof useForm>['register']>;
type FieldProps = RegisterProps & AppFieldProps;

export const CustomField: FC<FieldProps> = React.memo(props => {

const [value, setValue] = React.useState<{ label: string; value: number } | null>(null);

useEffect(() => {
setValue(props.value);
}, [props.value]);

return (
<div className="relative ">
<input className="hidden" {...props.register!(props.name)} />
<div className="h-[43px]">
{/* Put here your custom field */}
</div>
</div>
);
});
59 changes: 59 additions & 0 deletions services/client/src/core/form/custom-fields/StatusFieldsInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { FC, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { AppFieldProps } from './Field.type';
import { Button, TextField } from '@mui/material';

type RegisterProps = ReturnType<ReturnType<typeof useForm>['register']>;
type FieldProps = RegisterProps & AppFieldProps;

export const StatusFieldsInput: FC<FieldProps> = React.memo(props => {


const [newStatusKey, setNewStatusKey] = React.useState<string>('');
const addNewStatus = () => {
if (newStatusKey) {
props.setValue!({...props.value, [newStatusKey]: { label: '', correctState: '', helperText: '' }});
setNewStatusKey('');
}
}


const deleteKey = (key: string) => {
const newValues = { ...props.value };
delete newValues[key];
props.setValue!(newValues);
}

const registerStatusKeyField = (key: string, field: 'label' | 'correctState' | 'helperText') => {
return {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => props.setValue!({...props.value, [key]: { ...props.value[key], [field]: e.target.value }}),
value: props.value[key][field],
}
}

return (
<div className="relative ">
<input className="hidden" {...props.register!(props.name)} />
{props.value && Object.keys(props.value).length > 0 && Object.keys(props.value).map((key, index) => (
<div className='w-full mb-4 flex-wrap flex gap-[3%]'>
<div className='flex gap-2 mb-4 w-full'>
<TextField className='w-full' disabled value={key}/>
<Button onClick={() => deleteKey(key)} color='error' variant='text' className='min-w-0' >
<i className="fas fa-trash"></i>
</Button>
</div>
<TextField helperText="שם סנסור" className='w-[31.333%] mb-4' {...registerStatusKeyField(key, 'label')}/>
<TextField helperText="מצב תקין" className='w-[31.333%] mb-4' {...registerStatusKeyField(key, 'correctState')}/>
<TextField helperText="טקסט עזר" className='w-[31.333%] mb-4' {...registerStatusKeyField(key, 'helperText')}/>
</div>
))}
<div className='flex gap-4'>
<TextField helperText='' className='flex-1' value={newStatusKey} onChange={e => setNewStatusKey(e.target.value)} />
<Button variant='outlined' onClick={addNewStatus}>
<i className="fas fa-plus me-2"></i>
הוסף שדה
</Button>
</div>
</div>
);
});
12 changes: 7 additions & 5 deletions services/client/src/core/form/custom-fields/field-map.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Checkbox, TextField } from '@mui/material';
import { CountryField } from './CountryField';
import { StatusFieldsInput } from './StatusFieldsInput';

export const FieldMap = {
text: ({ name, register, ...props }: any) => <TextField autoComplete="off" {...props} className='rounded overflow-hidden' {...register(name)} />,
number: ({ name, register, ...props }: any) => <TextField {...props} {...register(name)} type="number" />,
boolean: ({ name, register, ...props }: any) => <Checkbox {...props} {...register(name)} />,
date: ({ name, register, ...props }: any) => <TextField {...props} {...register(name)} />,
country: (props: any) => <CountryField {...props} />
text: ({ name, register, setValue, ...props }: any) => <TextField autoComplete="off" {...props} className='rounded overflow-hidden' {...register(name)} />,
number: ({ name, register, setValue, ...props }: any) => <TextField {...props} {...register(name)} type="number" />,
boolean: ({ name, register, setValue, ...props }: any) => <Checkbox {...props} {...register(name)} />,
date: ({ name, register, setValue, ...props }: any) => <TextField {...props} {...register(name)} />,
country: (props: any) => <CountryField {...props} />,
status_fields: (props: any) => <StatusFieldsInput {...props} />
} as const;
2 changes: 2 additions & 0 deletions services/client/src/core/routing/ProtectedRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { checkAuthStatus } from '../firebase/firebase';
import { GlobalJobs } from '../global-jobs/GlobalJobs';
import { OnboardingPage } from '../../view/pages/onboarding/OnboardingPage';
import { DevicePage } from '../../view/pages/device/DevicePage';
import { TypesPage } from '../../view/pages/types/TypesPage';

const authGuard = async () => {
const isAuthenticated = await checkAuth();
Expand Down Expand Up @@ -47,5 +48,6 @@ export const ProtectedRoutes: RouteObject = {
{ path: 'elevators', element: <>elevators</> },
{ path: 'add-device', element: <>add-device</> },
{ path: 'device/:id', element: <DevicePage/> },
{ path: 'types', element: <TypesPage/>}
]
};
6 changes: 6 additions & 0 deletions services/client/src/generated/zeus/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ export const scalars: any = ZeusScalars({
float8: {
decode: (e: unknown) => parseFloat(e as string),
encode: (e: unknown) => e as string
},
jsonb: {
decode: (e: unknown) => JSON.parse(e as string),
encode: (e: unknown) => removeQuotesFromKeys(JSON.stringify(e))
}
});

const removeQuotesFromKeys = (stringifiedJson: string) => stringifiedJson.replace(/"([^"]+)":/g, '$1:');

export const chain = Chain(import.meta.env.VITE_HASURA_GQL_ENDPOINT);
1 change: 1 addition & 0 deletions services/client/src/generated/zeus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const apiFetch =
return response.data;
});
}

return fetch(`${options[0]}`, {
body: JSON.stringify({ query, variables }),
method: 'POST',
Expand Down
17 changes: 17 additions & 0 deletions services/client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ import './core/translations/i18n';
import 'swiper/css';
import 'swiper/css/pagination';

const originalFetch = window.fetch;

window.fetch = (...args) => {
console.log('fetch args = ', args);
const url = args[0];
const request = args[1];
const isGraphql = request?.method === 'POST' && url.toString().includes('graphql');
if (isGraphql && request.body?.toString()) {
// remove backslashes from the body
const body = JSON.parse(request.body?.toString());
const queryName = body.query.split('{')[1].split('(')[0].trim();
args[0] = url + `?${queryName}`
}

return originalFetch(...args);
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
Expand Down
20 changes: 11 additions & 9 deletions services/client/src/view/layout/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const SideMenu: FC = React.memo(() => {
}
}

const isActive = (path: string) => location.pathname === path ? 'bg-gray-300' : '';

if (location.pathname === '/s/onboarding') return null;
return (
<SwipeableDrawer
Expand All @@ -48,23 +50,23 @@ export const SideMenu: FC = React.memo(() => {
<div className='w-[48px]'></div>
</ListItem>
<nav className='flex-1'>
<List>
<ListItem>
<List className='p-0'>
<ListItem className={isActive('/s/home')}>
<ListItemButton onClick={goto('/s/home')}>
<i className='fa-solid w-5 fa-home me-4'></i>
<i className='fa-solid w-5 fa-home me-2 text-primary-color opacity-75'></i>
<ListItemText primary="בית" />
</ListItemButton>
</ListItem>
<ListItem>
<ListItem className={isActive('/s/elevators')}>
<ListItemButton onClick={goto('/s/elevators')}>
<i className='fa-solid w-5 fa-elevator me-4'></i>
<i className='fa-solid w-5 fa-elevator me-2 text-primary-color opacity-75'></i>
<ListItemText primary="מעליות" />
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton onClick={goto('/s/add-device')}>
<i className='fa-solid w-5 fa-plus me-4'></i>
<ListItemText primary="הוסף מכשיר"/>
<ListItem className={isActive('/s/types')}>
<ListItemButton onClick={goto('/s/types')}>
<i className='fa-solid w-5 fa-grid me-2 text-primary-color opacity-75'></i>
<ListItemText primary="סוגים"/>
</ListItemButton>
</ListItem>
</List>
Expand Down
32 changes: 32 additions & 0 deletions services/client/src/view/pages/types/TypeForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Button } from '@mui/material';
import React, { FC } from 'react';
import { useMutation_CreateDeviceType } from '../../../core/api/api';
import { useForm } from 'react-hook-form';
import { AppForm } from '../../../core/form/AppForm';

interface TypeFormProps extends React.PropsWithChildren {}

export const TypeForm: FC<TypeFormProps> = React.memo(props => {
const mutation_createType = useMutation_CreateDeviceType()
const form = useForm();
const onSubmit = form.handleSubmit((data) => {

mutation_createType.mutate({...data, status_fields: {x: '2'}})
});
return (
<>
<AppForm
form={form}
onSubmit={onSubmit}
submitText='שמור'
fields={[{
name: 'name',
helperText: 'שם הסוג',
type: 'text',
}, {
name: 'status_fields',
type: 'status_fields',
}]}/>
</>
);
});
11 changes: 11 additions & 0 deletions services/client/src/view/pages/types/TypesPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { FC } from 'react';
import { TypeForm } from './TypeForm';

export const TypesPage: FC = React.memo(() => {
return (
<div className='p-4'>
<TypeForm />
</div>
);
});
2 changes: 1 addition & 1 deletion services/client/src/view/theme/AppTheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const AppTheme: FC<AppThemeProps> = props => {
<ThemeProvider theme={theme}>
<Toaster
toastOptions={{
duration: 5000
duration: 3000
}}
/>
{props.children}
Expand Down

0 comments on commit cd91e37

Please sign in to comment.