Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('CalendarComponent', () => {
1,
"Body Fat",
"%",
"NONE",
[new MeasurementEntry(1, 1, new Date(currentYear, currentMonth, 1, 12, 0), 20, "Normal")]
),
]));
Expand Down
24 changes: 13 additions & 11 deletions src/components/Measurements/api/measurements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@ describe('measurement service tests', () => {
{
"id": 1,
"name": "Weight",
"unit": "kg"
"unit": "kg",
"dynamic_type": "NONE"
}
]
};

const measurementDetailResponse = {
"id": 1,
"name": "Weight",
"unit": "kg"
"unit": "kg",
"dynamic_type": "NONE"
};


Expand Down Expand Up @@ -88,7 +90,7 @@ describe('measurement service tests', () => {
expect(axios.get).toHaveBeenCalledTimes(2);

expect(result).toStrictEqual([
new MeasurementCategory(1, "Weight", "kg", [
new MeasurementCategory(1, "Weight", "kg", "NONE", [
new MeasurementEntry(1, 1, new Date("2021-01-01"), 80, "")
])
]);
Expand All @@ -108,38 +110,38 @@ describe('measurement service tests', () => {
expect(axios.get).toHaveBeenCalledTimes(2);

expect(result).toStrictEqual(
new MeasurementCategory(1, "Weight", "kg", [
new MeasurementCategory(1, "Weight", "kg", "NONE", [
new MeasurementEntry(1, 1, new Date("2021-01-01"), 80, "")
])
);
});

test('addMeasurementCategory POSTs name + unit and returns the parsed category', async () => {
test('addMeasurementCategory POSTs name + unit + dynamic_type and returns the parsed category', async () => {
(axios.post as Mock).mockResolvedValue({
data: { id: 9, name: "Body fat", unit: "%" },
data: { id: 9, name: "Body fat", unit: "%", dynamic_type: "NONE" },
});

const result = await addMeasurementCategory({ name: "Body fat", unit: "%" });
const result = await addMeasurementCategory({ name: "Body fat", unit: "%", dynamic_type: "NONE" });

expect(axios.post).toHaveBeenCalledTimes(1);
const [url, body] = (axios.post as Mock).mock.calls[0];
expect(url).toMatch(/\/api\/v2\/measurement-category\/$/);
expect(body).toEqual({ name: "Body fat", unit: "%" });
expect(body).toEqual({ name: "Body fat", unit: "%", dynamic_type: "NONE" });
expect(result).toBeInstanceOf(MeasurementCategory);
expect(result.id).toBe(9);
});

test('editMeasurementCategory PATCHes /measurement-category/<id>/', async () => {
(axios.patch as Mock).mockResolvedValue({
data: { id: 9, name: "Renamed", unit: "%" },
data: { id: 9, name: "Renamed", unit: "%", dynamic_type: "NONE" },
});

const result = await editMeasurementCategory({ id: 9, name: "Renamed", unit: "%" });
const result = await editMeasurementCategory({ id: 9, name: "Renamed", unit: "%", dynamic_type: "NONE" });

expect(axios.patch).toHaveBeenCalledTimes(1);
const [url, body] = (axios.patch as Mock).mock.calls[0];
expect(url).toMatch(/\/api\/v2\/measurement-category\/9\/$/);
expect(body).toEqual({ name: "Renamed", unit: "%" });
expect(body).toEqual({ name: "Renamed", unit: "%", dynamic_type: "NONE" });
expect(result.name).toBe("Renamed");
});

Expand Down
41 changes: 33 additions & 8 deletions src/components/Measurements/api/measurements.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios';
import { MeasurementCategory } from "@/components/Measurements/models/Category";
import { useQuery } from '@tanstack/react-query';
import { MeasurementCategory, DynamicMeasurementType } from "@/components/Measurements/models/Category";
import { MeasurementEntry } from "@/components/Measurements/models/Entry";
import { ApiMeasurementCategoryType } from '@/types';
import { API_MAX_PAGE_SIZE } from "@/core/lib/consts";
Expand All @@ -15,6 +16,26 @@ export type MeasurementQueryOptions = {
filtersetQueryEntries?: object,
}

export interface DynamicCategory {
id: number;
name: string;
unit: string;
dynamic_type: DynamicMeasurementType;
}

export const getDynamicCategories = async (): Promise<DynamicCategory[]> => {
const url = makeUrl(`${API_MEASUREMENTS_CATEGORY_PATH}/dynamic-types`);
const response = await axios.get(url, { headers: makeHeader() });
return response.data;
};

export const useDynamicCategoriesQuery = () => {
return useQuery({
queryKey: [API_MEASUREMENTS_CATEGORY_PATH, 'dynamic'],
queryFn: getDynamicCategories
});
};

export const getMeasurementCategories = async (options?: MeasurementQueryOptions): Promise<MeasurementCategory[]> => {
const { filtersetQueryCategories = {}, filtersetQueryEntries = {} } = options || {};

Expand Down Expand Up @@ -44,10 +65,10 @@ export const getMeasurementCategories = async (options?: MeasurementQueryOptions
});

// Collect all pages of entries
for await (const page of fetchPaginated(url, makeHeader())) {
for (const entries of page) {
out.push(MeasurementEntry.fromJson(entries));
}
for await (const page of fetchPaginated(url, makeHeader())) {
for (const entries of page) {
out.push(MeasurementEntry.fromJson(entries));
}
}
return out;
});
Expand Down Expand Up @@ -90,14 +111,16 @@ export const getMeasurementCategory = async (id: number): Promise<MeasurementCat
export interface AddMeasurementCategoryParams {
name: string;
unit: string;
dynamic_type: DynamicMeasurementType;
}

export const addMeasurementCategory = async (data: AddMeasurementCategoryParams): Promise<MeasurementCategory> => {
const response = await axios.post(
makeUrl(API_MEASUREMENTS_CATEGORY_PATH,),
makeUrl(API_MEASUREMENTS_CATEGORY_PATH),
{
name: data.name,
unit: data.unit
unit: data.unit,
dynamic_type: data.dynamic_type // eslint-disable-line camelcase
},
{ headers: makeHeader() }
);
Expand All @@ -109,14 +132,16 @@ export interface editMeasurementCategoryParams {
id: number,
name: string;
unit: string;
dynamic_type: DynamicMeasurementType;
}

export const editMeasurementCategory = async (data: editMeasurementCategoryParams): Promise<MeasurementCategory> => {
const response = await axios.patch(
makeUrl(API_MEASUREMENTS_CATEGORY_PATH, { id: data.id }),
{
name: data.name,
unit: data.unit
unit: data.unit,
dynamic_type: data.dynamic_type // eslint-disable-line camelcase
},
{ headers: makeHeader() }
);
Expand Down
13 changes: 11 additions & 2 deletions src/components/Measurements/models/Category.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { MeasurementEntry } from "@/components/Measurements/models/Entry";
import { Adapter } from "@/core/lib/Adapter";

export type DynamicMeasurementType = 'NONE' | 'BMI'; // add future types when more dynamic types added to Measurement category in Django

export const DYNAMIC_TYPE_DEFAULTS: Record<string, { name: string, unit: string }> = {
'BMI': { name: 'BMI', unit: 'kg/m²' },
// add future types here
};

export class MeasurementCategory {

entries: MeasurementEntry[] = [];
Expand All @@ -9,6 +16,7 @@ export class MeasurementCategory {
public id: number,
public name: string,
public unit: string,
public dynamic_type: DynamicMeasurementType = 'NONE',
entries?: MeasurementEntry[]
) {
if (entries) {
Expand All @@ -26,14 +34,14 @@ export class MeasurementCategory {
}
}


class MeasurementCategoryAdapter implements Adapter<MeasurementCategory> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fromJson(item: any) {
return new MeasurementCategory(
item.id,
item.name,
item.unit
item.unit,
item.dynamic_type
);
}

Expand All @@ -42,6 +50,7 @@ class MeasurementCategoryAdapter implements Adapter<MeasurementCategory> {
id: item.id,
name: item.name,
unit: item.unit,
dynamic_type: item.dynamic_type
};
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/components/Measurements/queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
editMeasurementCategoryParams,
editMeasurementEntry,
editMeasurementParams,
getDynamicCategories,
getMeasurementCategories,
getMeasurementCategory,
MeasurementQueryOptions
Expand All @@ -24,6 +25,13 @@ export function useMeasurementsCategoryQuery(options?: MeasurementQueryOptions)
});
}

export function useDynamicCategoriesQuery() {
return useQuery({
queryKey: [QueryKey.MEASUREMENTS_CATEGORIES, 'dynamic'],
queryFn: getDynamicCategories
});
}

export const useAddMeasurementCategoryQuery = () => {
const queryClient = useQueryClient();

Expand Down
16 changes: 11 additions & 5 deletions src/components/Measurements/screens/MeasurementCategoryDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,21 @@ export const MeasurementCategoryDetail = () => {
return <LoadingPlaceholder />;
}

// Safety check: if the query failed or returned nothing
if (categoryQuery.isError || !categoryQuery.data) {
return <p>Error loading category data. Check if category {categoryId} exists.</p>;
}

// Now it is safe to access .name
return <WgerContainerRightSidebar
title={categoryQuery.data!.name}
optionsMenu={<CategoryDetailDropdown category={categoryQuery.data!} />}
title={categoryQuery.data.name}
optionsMenu={<CategoryDetailDropdown category={categoryQuery.data} />}
mainContent={
<Stack spacing={2}>
<MeasurementChart category={categoryQuery.data!} />
<CategoryDetailDataGrid category={categoryQuery.data!} />
<MeasurementChart category={categoryQuery.data} />
<CategoryDetailDataGrid category={categoryQuery.data} />
</Stack>
}
fab={<AddMeasurementEntryFab />}
fab={categoryQuery.data.dynamic_type.includes('NONE') ? <AddMeasurementEntryFab /> : undefined}
/>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ export const CategoryList = (props: { category: MeasurementCategory }) => {
{t("seeDetails")}
</Link>
</Button>

<IconButton onClick={handleOpenModal}>
<AddIcon />
</IconButton>
{props.category.dynamic_type === 'NONE' && (
<IconButton onClick={handleOpenModal}>
<AddIcon />
</IconButton>
)}
</CardActions>
</Card>
<WgerModal title={t('add')} isOpen={openModal} closeFn={handleCloseModal}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const CategoryDetailDropdown = (props: { category: MeasurementCategory })
},
}}
>
<MenuItem onClick={handleEdit}>{t("edit")}</MenuItem>
{props.category.dynamic_type === 'NONE' && <MenuItem onClick={handleEdit}>{t("edit")}</MenuItem>}
<MenuItem onClick={handleDelete}>{t("delete")}</MenuItem>
</Menu>

Expand Down
Loading
Loading