diff --git a/src/frontend/src/components/nav/PanelGroup.tsx b/src/frontend/src/components/nav/PanelGroup.tsx
index 058bf87763..1e939e2050 100644
--- a/src/frontend/src/components/nav/PanelGroup.tsx
+++ b/src/frontend/src/components/nav/PanelGroup.tsx
@@ -131,7 +131,7 @@ export function PanelGroup({
}}
>
- {panel.label}
+ {panel.label}
{panel.content ?? }
diff --git a/src/frontend/src/components/tables/InvenTreeTable.tsx b/src/frontend/src/components/tables/InvenTreeTable.tsx
index 2e54456872..ff16019ada 100644
--- a/src/frontend/src/components/tables/InvenTreeTable.tsx
+++ b/src/frontend/src/components/tables/InvenTreeTable.tsx
@@ -40,6 +40,7 @@ const defaultPageSize: number = 25;
* @param customFilters : TableFilter[] - List of custom filters
* @param customActionGroups : any[] - List of custom action groups
* @param printingActions : any[] - List of printing actions
+ * @param dataFormatter : (data: any) => any - Callback function to reformat data returned by server (if not in default format)
* @param rowActions : (record: any) => RowAction[] - Callback function to generate row actions
* @param onRowClick : (record: any, index: number, event: any) => void - Callback function when a row is clicked
*/
@@ -59,6 +60,7 @@ export type InvenTreeTableProps = {
customActionGroups?: any[];
printingActions?: any[];
idAccessor?: string;
+ dataFormatter?: (data: any) => any;
rowActions?: (record: any) => RowAction[];
onRowClick?: (record: any, index: number, event: any) => void;
};
@@ -356,8 +358,15 @@ export function InvenTreeTable({
tableProps.noRecordsText ?? t`No records found`
);
- // Extract returned data (accounting for pagination) and ensure it is a list
- let results = response.data?.results ?? response.data ?? [];
+ let results = [];
+
+ if (props.dataFormatter) {
+ // Custom data formatter provided
+ results = props.dataFormatter(response.data);
+ } else {
+ // Extract returned data (accounting for pagination) and ensure it is a list
+ results = response.data?.results ?? response.data ?? [];
+ }
if (!Array.isArray(results)) {
setMissingRecordsText(t`Server returned incorrect data type`);
diff --git a/src/frontend/src/components/tables/settings/CurrencyTable.tsx b/src/frontend/src/components/tables/settings/CurrencyTable.tsx
new file mode 100644
index 0000000000..eb4c205ddd
--- /dev/null
+++ b/src/frontend/src/components/tables/settings/CurrencyTable.tsx
@@ -0,0 +1,82 @@
+import { t } from '@lingui/macro';
+import { showNotification } from '@mantine/notifications';
+import { IconReload } from '@tabler/icons-react';
+import { useCallback, useMemo } from 'react';
+
+import { api } from '../../../App';
+import { useTableRefresh } from '../../../hooks/TableRefresh';
+import { ApiPaths, apiUrl } from '../../../states/ApiState';
+import { ActionButton } from '../../buttons/ActionButton';
+import { InvenTreeTable } from '../InvenTreeTable';
+
+/*
+ * Table for displaying available currencies
+ */
+export function CurrencyTable() {
+ const { tableKey, refreshTable } = useTableRefresh('currency');
+
+ const columns = useMemo(() => {
+ return [
+ {
+ accessor: 'currency',
+ title: t`Currency`,
+ switchable: false
+ },
+ {
+ accessor: 'rate',
+ title: t`Rate`,
+ switchable: false
+ }
+ ];
+ }, []);
+
+ const refreshCurrencies = useCallback(() => {
+ api
+ .post(apiUrl(ApiPaths.currency_refresh), {})
+ .then(() => {
+ refreshTable();
+ showNotification({
+ message: t`Exchange rates updated`,
+ color: 'green'
+ });
+ })
+ .catch((error) => {
+ showNotification({
+ title: t`Exchange rate update error`,
+ message: error,
+ color: 'red'
+ });
+ });
+ }, []);
+
+ const tableActions = useMemo(() => {
+ return [
+ }
+ />
+ ];
+ }, []);
+
+ return (
+ {
+ let rates = data?.exchange_rates ?? {};
+
+ return Object.entries(rates).map(([currency, rate]) => {
+ return {
+ currency: currency,
+ rate: rate
+ };
+ });
+ }
+ }}
+ />
+ );
+}
diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
index 32c3c21795..fbeaa428f1 100644
--- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
+++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
@@ -21,10 +21,11 @@ import {
} from '@tabler/icons-react';
import { useMemo } from 'react';
-import { PlaceholderPill } from '../../../components/items/Placeholder';
+import { StylishText } from '../../../components/items/StylishText';
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
import { GlobalSettingList } from '../../../components/settings/SettingList';
+import { CurrencyTable } from '../../../components/tables/settings/CurrencyTable';
import { CustomUnitsTable } from '../../../components/tables/settings/CustomUnitsTable';
import { ProjectCodeTable } from '../../../components/tables/settings/ProjectCodeTable';
import { useServerApiState } from '../../../states/ApiState';
@@ -152,7 +153,9 @@ export default function SystemSettings() {
-
+ {t`Exchange Rates`}
+
+
>
)
},
diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx
index 2aff116728..2bbac38c8d 100644
--- a/src/frontend/src/states/ApiState.tsx
+++ b/src/frontend/src/states/ApiState.tsx
@@ -71,6 +71,9 @@ export enum ApiPaths {
settings_user_list = 'api-settings-user-list',
notifications_list = 'api-notifications-list',
+ currency_list = 'api-currency-list',
+ currency_refresh = 'api-currency-refresh',
+
barcode = 'api-barcode',
news = 'news',
global_status = 'api-global-status',
@@ -165,6 +168,10 @@ export function apiEndpoint(path: ApiPaths): string {
return 'auth/emails/$id/verify/';
case ApiPaths.user_email_primary:
return 'auth/emails/$id/primary/';
+ case ApiPaths.currency_list:
+ return 'currency/exchange/';
+ case ApiPaths.currency_refresh:
+ return 'currency/refresh/';
case ApiPaths.api_search:
return 'search/';
case ApiPaths.settings_global_list: