Skip to content

Commit

Permalink
form add pet to owner setup
Browse files Browse the repository at this point in the history
  • Loading branch information
firasrg committed Aug 26, 2024
1 parent 904ea7a commit ed7d474
Show file tree
Hide file tree
Showing 26 changed files with 307 additions and 85 deletions.
2 changes: 2 additions & 0 deletions spring-petclinic-reactjs-client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ dist-ssr
*.sw?

node/

tsconfig.tsbuildinfo
3 changes: 2 additions & 1 deletion spring-petclinic-reactjs-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"http-method-enum": "^1.0.0",
"ra-data-json-server": "^5.1.0",
"react": "^18.3.0",
"react-admin": "^5.1.0",
"react-dom": "^18.3.0",
"react-flatpickr": "^3.10.13",
"react-hook-form": "^7.52.2",
"react-router-dom": "^6.26.1",
"yup": "^1.4.0"
Expand All @@ -26,6 +26,7 @@
"@types/node": "^20.10.7",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-flatpickr": "^3.8.11",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@vitejs/plugin-react": "^4.0.1",
Expand Down
2 changes: 1 addition & 1 deletion spring-petclinic-reactjs-client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import PetForm from "@pages/forms/PetForm";
import VisitsPage from "@pages/owners/Visits";
import VeterinariansPage from "@pages/Veterinarians";
import ErrorPage from "@pages/Error";
import dataProvider from "./providers/dataProvider";
import dataProvider from "./data-providers";
import { Layout } from "./Layout";

export const App = () => (
Expand Down
2 changes: 1 addition & 1 deletion spring-petclinic-reactjs-client/src/NavigationBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Link } from "react-router-dom";
import { useState } from "react";
import { Link } from "react-router-dom";
import { DASHBOARD, OWNERS, VETERINARIANS } from "@constants/routes";

export enum ENavBar {
Expand Down
2 changes: 2 additions & 0 deletions spring-petclinic-reactjs-client/src/constants/resources.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const OWNERS = "owners";
export const PETS = "pets";
export const PET_TYPES = "pettypes";
2 changes: 1 addition & 1 deletion spring-petclinic-reactjs-client/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const GET_OWNER = `${OWNERS}/:id`;
export const EDIT_OWNER = `${GET_OWNER}/edit`;

export const PET_NEW_FORM = `${GET_OWNER}/pets/new`;
export const GET_PET = `${GET_OWNER}/pets/:id`;
export const GET_PET = `${GET_OWNER}/pets/:petId`;
export const PET_EDIT_FORM = `${GET_PET}/edit`;

export const PET_VISITS = `${GET_PET}/visits/new`;
Expand Down
17 changes: 17 additions & 0 deletions spring-petclinic-reactjs-client/src/data-providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { combineDataProviders } from "react-admin";
import { OWNERS, PET_TYPES } from "@constants/resources";
import ownersDataProvider from "./ownersDataProvider";
import petTypesDataProvider from "./petTypesDataProvider";

const dataProviders = combineDataProviders((resource) => {
switch (resource) {
case OWNERS:
return ownersDataProvider;
case PET_TYPES:
return petTypesDataProvider;
default:
throw new Error(`Unknown resource: ${resource}`);
}
});

export default dataProviders;
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import { combineDataProviders, DataProvider, fetchUtils, RaRecord, UpdateResult } from "react-admin";
import HTTPMethod from "http-method-enum";
import { stringify } from "query-string";
import { OWNERS } from "@constants/resources";
import { fetchUtils, UpdateResult } from "react-admin";
import { GetOwnersListParams } from "@models/api/GetOwnersListParams";
import { LAST_NAME } from "@constants/searchParams";
import { GetListParams } from "@models/api/GetListParams";
import { stringify } from "query-string";
import HTTPMethod from "http-method-enum";
import { OwnersDataProvider } from "@models/api/OwnersDataProvider";
import { PETS } from "@constants/resources";

const apiUrl = import.meta.env.VITE_SPRING_PETCLINIC_REST_API_URL;

const httpClient = fetchUtils.fetchJson;

/**
* The following data was made to manage api calls dedicated to owners path ("/api/owner").
* For other endpoints, it would be ideal to manage them in separate data providers.
* @author Firas Regaieg
*/
const ownersDataProvider: DataProvider = {
getList: async (resource, { filter, signal }: GetListParams) => {
export default {
getList: async (resource, { filter, signal }: GetOwnersListParams) => {
const url = new URL(`${apiUrl}/${resource}`);

const searchParams = new URLSearchParams();
Expand Down Expand Up @@ -72,7 +68,6 @@ const ownersDataProvider: DataProvider = {
});
return { data: json };
},

update: async (resource, params) => {
const url = `${apiUrl}/${resource}/${params.id}`;
await httpClient(url, {
Expand Down Expand Up @@ -112,16 +107,28 @@ const ownersDataProvider: DataProvider = {
method: HTTPMethod.DELETE
});
return { data: json };
}
};
},
createPet: async (resource: string, { meta: { ownerId }, data }) => {
const { json } = await httpClient(`${apiUrl}/${resource}/${ownerId}/${PETS}`, {
method: HTTPMethod.POST,
body: JSON.stringify(data)
});
return { data: json };
},
getPet: async (resource: string, { id, meta: { petId } }) => {
const { json } = await httpClient(`${apiUrl}/${resource}/${id}/${PETS}/${petId}`, {
method: HTTPMethod.GET
});

const dataProviders = combineDataProviders((resource) => {
switch (resource) {
case OWNERS:
return ownersDataProvider;
default:
throw new Error(`Unknown resource: ${resource}`);
}
});
return { data: json };
},
// TODO: create endpoint handler to edit pet owner
editPet: async (resource: string, { meta: { ownerId }, data: { petId, ...body } }) => {
const { json } = await httpClient(`${apiUrl}/${resource}/${ownerId}/${PETS}/${petId}`, {
method: HTTPMethod.PUT,
body: JSON.stringify(body)
});

export default dataProviders;
return { data: json };
}
} as OwnersDataProvider;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CreateResult, DataProvider, fetchUtils, UpdateResult, DeleteResult, GetOneResult } from "react-admin";

const apiUrl = import.meta.env.VITE_SPRING_PETCLINIC_REST_API_URL;

const httpClient = fetchUtils.fetchJson;

export default {
create: async () => Promise.resolve<CreateResult>({ data: null }),
delete: () => Promise.resolve<DeleteResult>({ data: null }),
deleteMany: () => Promise.resolve({ data: [] }),
getList: async (resource, { signal }) => {
const url = new URL(`${apiUrl}/${resource}`);

const { json } = await httpClient(url, { signal });
return {
data: json,
total: json ? json.length : 0
};
},
getMany: () => Promise.resolve({ data: [] }),
getManyReference: () => Promise.resolve({ data: [], total: 0 }),
getOne: () => Promise.resolve<GetOneResult>({ data: null }),
update: () => Promise.resolve<UpdateResult>({ data: null }),
updateMany: () => Promise.resolve({ data: [] })
} as DataProvider;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IApiPetType } from "@models/api/IApiPetType";
import { PetFormSchema } from "@models/form/PetFormSchema";
import { EPetForm } from "@models/enums/EPetForm";
import { CreateParams } from "react-admin";

export interface CreateOrEditOwnerPetData extends CreateParams {
meta: { ownerId: number };
data: Omit<PetFormSchema, EPetForm.PET_TYPE> & { petId?: number; type: IApiPetType };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GetOneParams } from "react-admin";

export interface GetOwnerPetByIdParams extends GetOneParams {
meta: {
petId: number;
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface GetListParams {
export interface GetOwnersListParams {
filter?: {
lastName: string;
};
Expand Down
16 changes: 3 additions & 13 deletions spring-petclinic-reactjs-client/src/models/api/IApiOwner.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import { IApiPet } from "@models/api/IApiPet";

export interface IApiOwner {
id: number;
firstName: string;
lastName: string;
address: string;
city: string;
telephone: string;
pets: [
{
id: number;
ownerId: number;
name: string;
birthDate: string;
visits: [];
type: {
id: number;
name: string;
};
}
];
pets: IApiPet[];
}
10 changes: 10 additions & 0 deletions spring-petclinic-reactjs-client/src/models/api/IApiPet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IApiPetType } from "@models/api/IApiPetType";

export interface IApiPet {
id: number;
ownerId: number;
name: string;
birthDate: string;
visits: [];
type: IApiPetType;
}
4 changes: 4 additions & 0 deletions spring-petclinic-reactjs-client/src/models/api/IApiPetType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IApiPetType {
id: number;
name: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DataProvider, GetOneResult } from "react-admin";
import { CreateOrEditOwnerPetData } from "@models/api/CreateOrEditOwnerPetData.ts";
import { GetOwnerPetByIdParams } from "@models/api/GetOwnerPetByIdParams.ts";
import { IApiPet } from "@models/api/IApiPet";

export interface OwnersDataProvider extends DataProvider {
createPet: (resource: string, params: CreateOrEditOwnerPetData) => Promise<GetOneResult<IApiPet>>;
getPet: (resource: string, params: GetOwnerPetByIdParams) => Promise<GetOneResult<IApiPet>>;
editPet: (resource: string, params: CreateOrEditOwnerPetData) => Promise<GetOneResult<IApiPet>>;
}
5 changes: 5 additions & 0 deletions spring-petclinic-reactjs-client/src/models/enums/EPetForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum EPetForm {
NAME = "name",
BIRTH_DATE = "birthDate",
PET_TYPE = "type"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EOwnerForm } from "@models/form/EOwnerForm";
import { EOwnerForm } from "@models/enums/EOwnerForm";

export type OwnerFormSchema = {
[EOwnerForm.FIRST_NAME]: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { EPetForm } from "@models/enums/EPetForm";

export type PetFormSchema = {
[EPetForm.NAME]: string;
[EPetForm.BIRTH_DATE]: Date;
[EPetForm.PET_TYPE]: number;
};
28 changes: 18 additions & 10 deletions spring-petclinic-reactjs-client/src/pages/forms/OwnerForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import { Navigate, useParams } from "react-router-dom";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { OwnerFormSchema } from "@models/form/OwnerFormSchema";
import { EOwnerForm } from "@models/form/EOwnerForm";
import { EOwnerForm } from "@models/enums/EOwnerForm";
import { PHONE_NUMBER } from "@constants/regexp";
import { REQUIRED_INPUT } from "@constants/messages";
import { FormError } from "@components/FormError";
import { useCreate, useGetOne, useUpdate } from "react-admin";
import { Loading, useCreate, useGetOne, useUpdate } from "react-admin";
import { OWNERS } from "@constants/resources";
import * as Routes from "@constants/routes";
import { IApiOwner } from "@models/api/IApiOwner";
import { useEffect } from "react";

const yupResolverSchema = yup
const yupSchema = yup
.object()
.shape({
[EOwnerForm.FIRST_NAME]: yup.string().required(REQUIRED_INPUT),
Expand All @@ -24,20 +24,24 @@ const yupResolverSchema = yup
})
.required();

/**
* This component represents 2 scenarios: "Add new owner" and "Edit existing owner".
* @constructor
*/
export default function OwnerForm() {
const { id } = useParams();
const ownerId = id ? Number(id) : undefined;

const {
handleSubmit,
formState: { errors },
register,
reset
} = useForm<OwnerFormSchema>({
resolver: yupResolver(yupResolverSchema),
resolver: yupResolver(yupSchema),
mode: "onSubmit"
});

const { id } = useParams();
const ownerId = id ? Number(id) : undefined;

const { data: ownerData } = useGetOne<IApiOwner>(OWNERS, { id: ownerId });

useEffect(() => {
Expand All @@ -53,12 +57,12 @@ export default function OwnerForm() {
}
}, [ownerData]);

const [create, { isSuccess: addSuccess }] = useCreate<IApiOwner>(OWNERS);
const [edit, { isSuccess: editSuccess }] = useUpdate(OWNERS);
const [create, { isSuccess: addSuccess, isPending: addPending }] = useCreate<IApiOwner>();
const [edit, { isSuccess: editSuccess, isPending: editPending }] = useUpdate();

const isEdit = !!ownerId;

const onSubmit: SubmitHandler<OwnerFormSchema> = async (data: OwnerFormSchema, e) => {
const onSubmit: SubmitHandler<OwnerFormSchema> = async (data, e) => {
e?.preventDefault();
if (!isEdit) {
await create(OWNERS, { data });
Expand All @@ -68,6 +72,10 @@ export default function OwnerForm() {
await edit(OWNERS, { id: ownerId, data });
};

if (addPending || editPending) {
return <Loading />;
}

if (addSuccess) {
return <Navigate to={Routes.OWNERS_FIND} />;
} else if (editSuccess) {
Expand Down
Loading

0 comments on commit ed7d474

Please sign in to comment.