Skip to content

Commit

Permalink
✨(frontend) add a button to export orders
Browse files Browse the repository at this point in the history
We want to export the order list using the current filters.
  • Loading branch information
kernicPanel committed Dec 11, 2024
1 parent d08d562 commit 7501b40
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to
to `Product`model
- Add admin api endpoints to CRUD `Teacher` and `Skill` resources.
- Add certification section in back office product detail view
- Add order export to CSV in back office

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import CircularProgress from "@mui/material/CircularProgress";
import SearchOutlined from "@mui/icons-material/SearchOutlined";
import TextField from "@mui/material/TextField";
import { useDebouncedCallback } from "use-debounce";
import { FilterList } from "@mui/icons-material";
import { FileDownload, FilterList } from "@mui/icons-material";
import Chip from "@mui/material/Chip";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
Expand All @@ -32,6 +32,11 @@ const messages = defineMessages({
defaultMessage: "Filters",
description: "Label for the filters button",
},
exportLabelButton: {
id: "components.presentational.filters.searchFilters.exportLabelButton",
defaultMessage: "Export",
description: "Label for the export button",
},
clear: {
id: "components.presentational.filters.searchFilters.clear",
defaultMessage: "Clear",
Expand Down Expand Up @@ -74,6 +79,7 @@ export type SearchFilterProps = MandatorySearchFilterProps & {
addChip: (chip: FilterChip) => void,
removeChip: (chipName: string) => void,
) => ReactNode;
export?: () => void;
};

export function SearchFilters(props: PropsWithChildren<SearchFilterProps>) {
Expand Down Expand Up @@ -181,6 +187,19 @@ export function SearchFilters(props: PropsWithChildren<SearchFilterProps>) {
<FormattedMessage {...messages.filtersLabelButton} />
</Button>
)}
{props.export && (
<Button
sx={{ ml: 2 }}
startIcon={<FileDownload />}
onClick={() => {
if (props.export) {
props.export();
}
}}
>
<FormattedMessage {...messages.exportLabelButton} />
</Button>
)}
</Box>
{props.renderContent && (
<Box display="flex" alignItems="center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { OrganizationSearch } from "@/components/templates/organizations/inputs/
import { UserSearch } from "@/components/templates/users/inputs/search/UserSearch";
import { RHFOrderState } from "@/components/templates/orders/inputs/RHFOrderState";
import { entitiesInputLabel } from "@/translations/common/entitiesInputLabel";
import { OrderListQuery } from "@/hooks/useOrders/useOrders";
import { OrderListQuery, useOrders } from "@/hooks/useOrders/useOrders";

const messages = defineMessages({
searchPlaceholder: {
Expand Down Expand Up @@ -49,6 +49,7 @@ type Props = MandatorySearchFilterProps & {

export function OrderFilters({ onFilter, ...searchFilterProps }: Props) {
const intl = useIntl();
const ordersQuery = useOrders({}, { enabled: false });

const getDefaultValues = () => {
return {
Expand Down Expand Up @@ -162,6 +163,11 @@ export function OrderFilters({ onFilter, ...searchFilterProps }: Props) {
</RHFValuesChange>
</RHFProvider>
)}
export={() => {
ordersQuery.methods.export({
currentFilters: formValuesToFilterValues(methods.getValues()),
});
}}
/>
);
}
21 changes: 21 additions & 0 deletions src/frontend/admin/src/hooks/useOrders/useOrders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ export const useOrdersMessages = defineMessages({
description: "Error message shown to the user when no order matches.",
defaultMessage: "Cannot find the order",
},
errorExport: {
id: "hooks.useOrders.errorExport",
description:
"Error message shown to the user when order export request fails.",
defaultMessage:
"An error occurred while exporting orders. Please retry later.",
},
});

export type OrderListQuery = ResourcesQuery & {
Expand Down Expand Up @@ -97,6 +104,9 @@ const orderProps: UseResourcesProps<Order, OrderQuery> = {
refund: async (id: string) => {
return OrderRepository.refund(id);
},
export: async (filters) => {
return OrderRepository.export(filters);
},
}),
session: true,
messages: useOrdersMessages,
Expand Down Expand Up @@ -148,6 +158,17 @@ export const useOrders = (
);
},
}).mutate,
export: mutation({
mutationFn: async (data: { currentFilters: OrderListQuery }) => {
return OrderRepository.export(data.currentFilters);
},
onError: (error: HttpError) => {
custom.methods.setError(
error.data?.details ??
intl.formatMessage(useOrdersMessages.errorExport),
);
},
}).mutate,
},
};
};
Expand Down
1 change: 1 addition & 0 deletions src/frontend/admin/src/hooks/useResources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ApiResourceInterface<
create?: (payload: any) => Promise<TData>;
update?: (payload: any) => Promise<TData>;
delete?: (id: TData["id"]) => Promise<void>;
export?: (filters?: TResourceQuery) => Promise<void>;
}

export const useLocalizedQueryKey = (queryKey: QueryKey) => queryKey;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import queryString from "query-string";
import { Order, OrderQuery } from "@/services/api/models/Order";
import { Maybe } from "@/types/utils";
import { ResourcesQuery } from "@/hooks/useResources/types";
import { checkStatus, fetchApi } from "@/services/http/HttpService";
import {
buildApiUrl,
checkStatus,
fetchApi,
} from "@/services/http/HttpService";
import { PaginatedResponse } from "@/types/api";

export const orderRoutes = {
Expand All @@ -11,6 +15,7 @@ export const orderRoutes = {
delete: (id: string) => `/orders/${id}/`,
generateCertificate: (id: string) => `/orders/${id}/generate_certificate/`,
refund: (id: string) => `/orders/${id}/refund/`,
export: (params: string = "") => `/orders/export/${params}`,
};

export class OrderRepository {
Expand Down Expand Up @@ -45,4 +50,11 @@ export class OrderRepository {
const url = orderRoutes.refund(id);
return fetchApi(url, { method: "POST" }).then(checkStatus);
}

static export(filters: Maybe<ResourcesQuery>): void {
const url = orderRoutes.export(
filters ? `?${queryString.stringify(filters)}` : "",
);
window.open(buildApiUrl(url));
}
}
34 changes: 34 additions & 0 deletions src/frontend/admin/src/tests/orders/orders-filters.test.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ test.describe("Order filters", () => {
}
});

const context = page.context();
const exportUrl = "http://localhost:8071/api/v1.0/admin/orders/export/";
const exportQueryParamsRegex = getUrlCatchSearchParamsRegex(exportUrl);
await context.unroute(exportQueryParamsRegex);
await context.route(exportQueryParamsRegex, async (route, request) => {
if (request.method() === "GET") {
await route.fulfill({
contentType: "application/csv",
body: "data",
});
}
});

await mockPlaywrightCrud<ProductSimple, DTOProduct>({
data: store.products,
routeUrl: "http://localhost:8071/api/v1.0/admin/products/",
Expand Down Expand Up @@ -124,4 +137,25 @@ test.describe("Order filters", () => {
page.getByRole("button", { name: `Owner: ${store.users[0].username}` }),
).toBeVisible();
});

test("Test export with filters", async ({ page }) => {
await page.goto(PATH_ADMIN.orders.list);

await page.getByRole("button", { name: "Filters" }).click();
await page
.getByTestId("select-order-state-filter")
.getByLabel("State")
.click();
await page.getByRole("option", { name: "Completed" }).click();
await page.getByLabel("close").click();

await page.getByRole("button", { name: "Export" }).click();

page.on("popup", async (popup) => {
await popup.waitForLoadState();
expect(popup.url()).toBe(
"http://localhost:8071/api/v1.0/admin/orders/export/?state=completed",
);
});
});
});
6 changes: 3 additions & 3 deletions src/frontend/admin/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
},
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"exclude": ["node_modules"]
}

0 comments on commit 7501b40

Please sign in to comment.