diff --git a/services/client/src/core/api/api.ts b/services/client/src/core/api/api.ts index f2a7093..a0f2d68 100644 --- a/services/client/src/core/api/api.ts +++ b/services/client/src/core/api/api.ts @@ -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 -------------------------------------------------------------------------- @@ -93,6 +94,53 @@ export const useMutation_UpdateProfile = () => { }); }; + +// ---- Type ---------------------------------------------------------------------------- +export type DeviceType = DeepPartial; + +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) => { diff --git a/services/client/src/core/api/hooks.ts b/services/client/src/core/api/hooks.ts new file mode 100644 index 0000000..c9e7da7 --- /dev/null +++ b/services/client/src/core/api/hooks.ts @@ -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; + success: ValueOrFunction; + error: ValueOrFunction; + }; +} +type useMutationOptions = UseMutationOptions & AppMutationOptions; + +export const useAppMutation = (options: useMutationOptions , queryClient: QueryClient) => { + const toastIdRef = useRef(); + + 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); +}; \ No newline at end of file diff --git a/services/client/src/core/form/AppForm.tsx b/services/client/src/core/form/AppForm.tsx index 8b478c5..2596673 100644 --- a/services/client/src/core/form/AppForm.tsx +++ b/services/client/src/core/form/AppForm.tsx @@ -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; }; diff --git a/services/client/src/core/form/custom-fields/CustomFieldTemplate.tsx b/services/client/src/core/form/custom-fields/CustomFieldTemplate.tsx new file mode 100644 index 0000000..ae5eb24 --- /dev/null +++ b/services/client/src/core/form/custom-fields/CustomFieldTemplate.tsx @@ -0,0 +1,24 @@ +import React, { FC, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { AppFieldProps } from './Field.type'; + +type RegisterProps = ReturnType['register']>; +type FieldProps = RegisterProps & AppFieldProps; + +export const CustomField: FC = React.memo(props => { + + const [value, setValue] = React.useState<{ label: string; value: number } | null>(null); + + useEffect(() => { + setValue(props.value); + }, [props.value]); + + return ( +
+ +
+ {/* Put here your custom field */} +
+
+ ); +}); diff --git a/services/client/src/core/form/custom-fields/StatusFieldsInput.tsx b/services/client/src/core/form/custom-fields/StatusFieldsInput.tsx new file mode 100644 index 0000000..6ad3ccf --- /dev/null +++ b/services/client/src/core/form/custom-fields/StatusFieldsInput.tsx @@ -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['register']>; +type FieldProps = RegisterProps & AppFieldProps; + +export const StatusFieldsInput: FC = React.memo(props => { + + + const [newStatusKey, setNewStatusKey] = React.useState(''); + 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) => props.setValue!({...props.value, [key]: { ...props.value[key], [field]: e.target.value }}), + value: props.value[key][field], + } + } + + return ( +
+ + {props.value && Object.keys(props.value).length > 0 && Object.keys(props.value).map((key, index) => ( +
+
+ + +
+ + + +
+ ))} +
+ setNewStatusKey(e.target.value)} /> + +
+
+ ); +}); diff --git a/services/client/src/core/form/custom-fields/field-map.tsx b/services/client/src/core/form/custom-fields/field-map.tsx index fc04cd6..27d9e9f 100644 --- a/services/client/src/core/form/custom-fields/field-map.tsx +++ b/services/client/src/core/form/custom-fields/field-map.tsx @@ -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) => , - number: ({ name, register, ...props }: any) => , - boolean: ({ name, register, ...props }: any) => , - date: ({ name, register, ...props }: any) => , - country: (props: any) => + text: ({ name, register, setValue, ...props }: any) => , + number: ({ name, register, setValue, ...props }: any) => , + boolean: ({ name, register, setValue, ...props }: any) => , + date: ({ name, register, setValue, ...props }: any) => , + country: (props: any) => , + status_fields: (props: any) => } as const; diff --git a/services/client/src/core/routing/ProtectedRoutes.tsx b/services/client/src/core/routing/ProtectedRoutes.tsx index c6f98b9..b8e2768 100644 --- a/services/client/src/core/routing/ProtectedRoutes.tsx +++ b/services/client/src/core/routing/ProtectedRoutes.tsx @@ -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(); @@ -47,5 +48,6 @@ export const ProtectedRoutes: RouteObject = { { path: 'elevators', element: <>elevators }, { path: 'add-device', element: <>add-device }, { path: 'device/:id', element: }, + { path: 'types', element: } ] }; diff --git a/services/client/src/generated/zeus/chain.ts b/services/client/src/generated/zeus/chain.ts index 3d16dbb..ca8e327 100644 --- a/services/client/src/generated/zeus/chain.ts +++ b/services/client/src/generated/zeus/chain.ts @@ -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); diff --git a/services/client/src/generated/zeus/index.ts b/services/client/src/generated/zeus/index.ts index 10d20cb..7c6cd4a 100644 --- a/services/client/src/generated/zeus/index.ts +++ b/services/client/src/generated/zeus/index.ts @@ -69,6 +69,7 @@ export const apiFetch = return response.data; }); } + return fetch(`${options[0]}`, { body: JSON.stringify({ query, variables }), method: 'POST', diff --git a/services/client/src/main.tsx b/services/client/src/main.tsx index 654e4ae..bd3cf04 100644 --- a/services/client/src/main.tsx +++ b/services/client/src/main.tsx @@ -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( diff --git a/services/client/src/view/layout/SideMenu.tsx b/services/client/src/view/layout/SideMenu.tsx index 419f55e..f1c4fa3 100644 --- a/services/client/src/view/layout/SideMenu.tsx +++ b/services/client/src/view/layout/SideMenu.tsx @@ -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 ( {