Skip to content

Commit

Permalink
Merge pull request #3122 from LiteFarmOrg/LF-4082-rtk-query-setup-and…
Browse files Browse the repository at this point in the history
…-fetching-inventory

LF-4082 RTK Query setup and fetching inventory
  • Loading branch information
antsgar authored Feb 14, 2024
2 parents 1cf8d99 + 0cbcc0f commit 8252a38
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 60 deletions.
8 changes: 7 additions & 1 deletion packages/webapp/src/apiConfig.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2022 LiteFarm.org
* Copyright 2019-2024 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -74,6 +74,9 @@ export const alertsUrl = `${URI}/notification_user/subscribe`;
export const notificationsUrl = `${URI}/notification_user`;
export const clearAlertsUrl = `${URI}/notification_user/clear_alerts`;
export const sensorUrl = `${URI}/sensor`;
export const animalsUrl = `${URI}/animals`;
export const animalBatchesUrl = `${URI}/animal_batches`;
export const animalGroupsUrl = `${URI}/animal_groups`;
export const url = URI;

export default {
Expand Down Expand Up @@ -117,5 +120,8 @@ export default {
alertsUrl,
notificationsUrl,
sensorUrl,
animalsUrl,
animalBatchesUrl,
animalGroupsUrl,
url,
};
20 changes: 0 additions & 20 deletions packages/webapp/src/containers/Animals/Inventory/index.js

This file was deleted.

46 changes: 46 additions & 0 deletions packages/webapp/src/containers/Animals/Inventory/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2024 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LiteFarm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import {
useGetAnimalsQuery,
useGetAnimalBatchesQuery,
useGetAnimalGroupsQuery,
} from '../../../store/api/apiSlice';
import Layout from '../../../components/Layout';
import { Title } from '../../../components/Typography';

function AnimalInventory() {
const { data: animals } = useGetAnimalsQuery();
const { data: animalBatches } = useGetAnimalBatchesQuery();
const { data: animalGroups } = useGetAnimalGroupsQuery();

return (
<Layout>
<Title>Animals</Title>
{animals &&
animals.map((animal, index) => <pre key={index}>{JSON.stringify(animal, null, 2)}</pre>)}
<Title>Animal Batches</Title>
{animalBatches &&
animalBatches.map((batch, index) => (
<pre key={index}>{JSON.stringify(batch, null, 2)}</pre>
))}
<Title>Animal Groups</Title>
{animalGroups &&
animalGroups.map((group, index) => <pre key={index}>{JSON.stringify(group, null, 2)}</pre>)}
</Layout>
);
}

export default AnimalInventory;
2 changes: 1 addition & 1 deletion packages/webapp/src/containers/hooks/useCurrencySymbol.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useSelector } from 'react-redux';
import { currencySelector } from '../userFarmSlice.js';
import { currencySelector } from '../userFarmSlice.ts';
import commonCurrency from '../AddFarm/currency/commonCurrency.json';

// TODO: Should not import entire commonCurrency to find one symbol
Expand Down
2 changes: 1 addition & 1 deletion packages/webapp/src/containers/saga.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { all, call, delay, put, select, takeLatest, takeLeading } from 'redux-sa
import apiConfig, { url } from '../apiConfig';
import history from '../history';
import i18n from '../locales/i18n';
import { store } from '../store/store.js';
import { store } from '../store/store.ts';
import { APP_VERSION } from '../util/constants';
import { logout } from '../util/jwt';
import { handle403 } from './ErrorHandler/saga.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,75 @@
import { createSlice } from '@reduxjs/toolkit';
// https://redux-toolkit.js.org/tutorials/typescript#define-slice-state-and-action-types
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createSelector } from 'reselect';
import type { RootState } from '../store/store';
import { AxiosError } from 'axios';

export function onLoadingStart(state) {
export interface Units {
currency: string;
measurement: 'metric' | 'imperial';
}

interface UserFarm {
farm_id: string;
user_id: string;
units: Units;

// https://www.typescriptlang.org/glossary#index-signatures
[key: string]: any;
}

type SagaError = Error & Partial<AxiosError>;

interface UserFarmState {
farmIdUserIdTuple: Array<{ farm_id: string; user_id: string }>;
byFarmIdUserId: {
[farmId: string]: {
[userId: string]: UserFarm;
};
};
loading: boolean;
error?: SagaError | null;
loaded: boolean;
farm_id?: string;
user_id?: string;
}

interface InvitePseudoUserSuccessPayload {
newUserFarm: UserFarm;
pseudoUserFarm: UserFarm;
}

export function onLoadingStart(state: UserFarmState) {
state.loading = true;
}

export function onLoadingFail(state, { payload: error }) {
export function onLoadingFail(state: UserFarmState, action: PayloadAction<SagaError>) {
const { payload: error } = action;
state.loading = false;
state.error = error;
state.loaded = true;
}

export function onLoadingSuccess(state) {
export function onLoadingSuccess(state: UserFarmState) {
state.loading = false;
state.error = null;
state.loaded = true;
}

const adminRoles = [1, 2, 5];

export const initialState = {
farmIdUserIdTuple: [
// {farm_id, user_id}
],
byFarmIdUserId: {
// farm_id1:{
// user_id1:{...userFarm},
// user_id2:{...userFarm},
// },
// farm_id2:{
// user_id1:{...userFarm},
// user_id2:{...userFarm},
// }
},
export const initialState: UserFarmState = {
farmIdUserIdTuple: [],
byFarmIdUserId: {},
loading: false,
error: undefined,
loaded: false,
farm_id: undefined,
user_id: undefined,
};

const addUserFarm = (state, { payload: userFarm }) => {
const addUserFarm = (state: UserFarmState, action: PayloadAction<UserFarm>) => {
const { payload: userFarm } = action;
state.loading = false;
state.error = null;
const { farm_id, user_id } = userFarm;
Expand All @@ -52,7 +81,8 @@ const addUserFarm = (state, { payload: userFarm }) => {
delete state.byFarmIdUserId[farm_id][user_id].role;
};

const removeUserFarm = (state, { payload: userFarm }) => {
const removeUserFarm = (state: UserFarmState, action: PayloadAction<UserFarm>) => {
const { payload: userFarm } = action;
const { farm_id, user_id } = userFarm;
if (state.byFarmIdUserId[farm_id]?.[user_id]) {
delete state.byFarmIdUserId[farm_id]?.[user_id];
Expand Down Expand Up @@ -84,7 +114,7 @@ const userFarmSlice = createSlice({
state.loading = false;
state.error = null;
state.loaded = true;
userFarms.forEach((userFarm) => {
userFarms.forEach((userFarm: UserFarm) => {
const { farm_id, user_id } = userFarm;
if (!(state.byFarmIdUserId[farm_id] && state.byFarmIdUserId[farm_id][user_id])) {
state.farmIdUserIdTuple.push({ farm_id, user_id });
Expand Down Expand Up @@ -150,14 +180,18 @@ const userFarmSlice = createSlice({
const { farm_id, user_id } = userFarm;
Object.assign(state.byFarmIdUserId[farm_id][user_id], userFarm);
},
acceptInvitationSuccess: (state, { payload: userFarm }) => {
addUserFarm(state, { payload: userFarm });
acceptInvitationSuccess: (state: UserFarmState, action: PayloadAction<UserFarm>) => {
const { payload: userFarm } = action;
addUserFarm(state, { payload: userFarm } as PayloadAction<UserFarm>);
state.user_id = userFarm.user_id;
state.farm_id = userFarm.farm_id;
},
invitePseudoUserSuccess: (state, { payload: { newUserFarm, pseudoUserFarm } }) => {
removeUserFarm(state, { payload: pseudoUserFarm });
addUserFarm(state, { payload: newUserFarm });
invitePseudoUserSuccess: (state, action: PayloadAction<InvitePseudoUserSuccessPayload>) => {
const {
payload: { newUserFarm, pseudoUserFarm },
} = action;
removeUserFarm(state, { payload: pseudoUserFarm } as PayloadAction<UserFarm>);
addUserFarm(state, { payload: newUserFarm } as PayloadAction<UserFarm>);
},
setLoadingStart: (state) => {
state.loading = true;
Expand Down Expand Up @@ -193,7 +227,8 @@ export const {
} = userFarmSlice.actions;
export default userFarmSlice.reducer;

export const userFarmReducerSelector = (state) => state.entitiesReducer[userFarmSlice.name];
export const userFarmReducerSelector = (state: RootState) =>
state.entitiesReducer[userFarmSlice.name];
export const loginSelector = createSelector(userFarmReducerSelector, ({ farm_id, user_id }) => ({
farm_id,
user_id,
Expand Down Expand Up @@ -251,15 +286,15 @@ export const userFarmEntitiesSelector = createSelector(

export const measurementSelector = createSelector(
userFarmSelector,
(userFarm) => userFarm.units.measurement,
(userFarm) => (userFarm as UserFarm).units.measurement,
);

export const currencySelector = createSelector(
userFarmSelector,
(userFarm) => userFarm.units.currency || 'USD',
(userFarm) => (userFarm as UserFarm).units.currency || 'USD',
);

const getUserFarmsByUser = (byFarmIdUserId, user_id) => {
const getUserFarmsByUser = (byFarmIdUserId: UserFarmState['byFarmIdUserId'], user_id: string) => {
let userFarms = [];
for (let by_user of Object.values(byFarmIdUserId)) {
by_user[user_id] && userFarms.push(by_user[user_id]);
Expand All @@ -269,7 +304,7 @@ const getUserFarmsByUser = (byFarmIdUserId, user_id) => {
);
};

export const getUserFarmSelector = (farmId, userId) => {
export const getUserFarmSelector = (farmId: string, userId: string) => {
return createSelector(userFarmReducerSelector, ({ byFarmIdUserId }) =>
byFarmIdUserId[farmId] && byFarmIdUserId[farmId][userId] ? byFarmIdUserId[farmId][userId] : {},
);
Expand Down
55 changes: 55 additions & 0 deletions packages/webapp/src/store/api/apiSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2024 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LiteFarm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { RootState } from '../store';
import { animalsUrl, animalBatchesUrl, animalGroupsUrl, url } from '../../apiConfig';
import type { Animal, AnimalBatch, AnimalGroup } from './types';

export const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: url,
prepareHeaders: (headers, { getState }) => {
const state = getState() as RootState;

headers.set('Content-Type', 'application/json');
headers.set('Authorization', `Bearer ${localStorage.getItem('id_token')}`);
headers.set('user_id', state.entitiesReducer.userFarmReducer.user_id || '');
headers.set('farm_id', state.entitiesReducer.userFarmReducer.farm_id || '');

return headers;
},
responseHandler: 'content-type',
}),
tagTypes: ['Animals', 'AnimalBatches', 'AnimalGroups'],
endpoints: (build) => ({
// redux-toolkit.js.org/rtk-query/usage-with-typescript#typing-query-and-mutation-endpoints
// <ResultType, QueryArg>
getAnimals: build.query<Animal[], void>({
query: () => `${animalsUrl}`,
providesTags: ['Animals'],
}),
getAnimalBatches: build.query<AnimalBatch[], void>({
query: () => `${animalBatchesUrl}`,
providesTags: ['AnimalBatches'],
}),
getAnimalGroups: build.query<AnimalGroup[], void>({
query: () => `${animalGroupsUrl}`,
providesTags: ['AnimalGroups'],
}),
}),
});

export const { useGetAnimalsQuery, useGetAnimalBatchesQuery, useGetAnimalGroupsQuery } = api;
Loading

0 comments on commit 8252a38

Please sign in to comment.