Api tweaks (#5690)

* Improvements to API handling on react UI

- Do not force "/api/" prefix to the base URL of the server
- We will need to fetch media files from the server (at /media/)
- Extend API URL helper functions

* Update some more hard-coded URLs

* Fix search API endpoint

* Fix div for panel tab

* Fix debug msg
This commit is contained in:
Oliver 2023-10-12 23:25:17 +11:00 committed by GitHub
parent 0c519c6b98
commit 814322e512
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 114 additions and 70 deletions

View File

@ -26,7 +26,7 @@ export function InstanceOptions({
]); ]);
const hostListData = Object.keys(hostList).map((key) => ({ const hostListData = Object.keys(hostList).map((key) => ({
value: key, value: key,
label: hostList[key].name label: hostList[key]?.name
})); }));
function SaveOptions(newHostList: HostList): void { function SaveOptions(newHostList: HostList): void {
@ -93,7 +93,7 @@ function ServerInfo({
return ( return (
<Text> <Text>
{hostList[hostKey].host} {hostList[hostKey]?.host}
<br /> <br />
<Trans>Version: {server.version}</Trans> <Trans>Version: {server.version}</Trans>
<br /> <br />

View File

@ -23,6 +23,7 @@ import { Html5QrcodeResult } from 'html5-qrcode/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { ApiPaths, apiUrl } from '../../states/ApiState';
export function QrCodeModal({ export function QrCodeModal({
context, context,
@ -63,16 +64,18 @@ export function QrCodeModal({
qrCodeScanner?.pause(); qrCodeScanner?.pause();
handlers.append(decodedText); handlers.append(decodedText);
api.post('/barcode/', { barcode: decodedText }).then((response) => { api
showNotification({ .post(apiUrl(ApiPaths.barcode), { barcode: decodedText })
title: response.data?.success || t`Unknown response`, .then((response) => {
message: JSON.stringify(response.data), showNotification({
color: response.data?.success ? 'teal' : 'red' title: response.data?.success || t`Unknown response`,
message: JSON.stringify(response.data),
color: response.data?.success ? 'teal' : 'red'
});
if (response.data?.url) {
window.location.href = response.data.url;
}
}); });
if (response.data?.url) {
window.location.href = response.data.url;
}
});
qrCodeScanner?.resume(); qrCodeScanner?.resume();
} }

View File

@ -17,7 +17,10 @@ export function BreadcrumbList({ breadcrumbs }: { breadcrumbs: Breadcrumb[] }) {
<Breadcrumbs> <Breadcrumbs>
{breadcrumbs.map((breadcrumb, index) => { {breadcrumbs.map((breadcrumb, index) => {
return ( return (
<Anchor onClick={() => breadcrumb.url && navigate(breadcrumb.url)}> <Anchor
key={`breadcrumb-${index}`}
onClick={() => breadcrumb.url && navigate(breadcrumb.url)}
>
<Text size="sm">{breadcrumb.name}</Text> <Text size="sm">{breadcrumb.name}</Text>
</Anchor> </Anchor>
); );

View File

@ -8,6 +8,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { navTabs as mainNavTabs } from '../../defaults/links'; import { navTabs as mainNavTabs } from '../../defaults/links';
import { InvenTreeStyle } from '../../globalStyle'; import { InvenTreeStyle } from '../../globalStyle';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { ScanButton } from '../items/ScanButton'; import { ScanButton } from '../items/ScanButton';
import { MainMenu } from './MainMenu'; import { MainMenu } from './MainMenu';
import { NavHoverMenu } from './NavHoverMenu'; import { NavHoverMenu } from './NavHoverMenu';
@ -36,7 +37,7 @@ export function Header() {
queryKey: ['notification-count'], queryKey: ['notification-count'],
queryFn: async () => { queryFn: async () => {
return api return api
.get('/notifications/', { .get(apiUrl(ApiPaths.notifications_list), {
params: { params: {
read: false, read: false,
limit: 1 limit: 1

View File

@ -34,7 +34,7 @@ export function NavHoverMenu({
useEffect(() => { useEffect(() => {
if (hostKey && hostList[hostKey]) { if (hostKey && hostList[hostKey]) {
setInstanceName(hostList[hostKey].name); setInstanceName(hostList[hostKey]?.name);
} }
}, [hostKey]); }, [hostKey]);

View File

@ -15,6 +15,7 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ApiPaths, apiUrl } from '../../states/ApiState';
/** /**
* Construct a notification drawer. * Construct a notification drawer.
@ -33,7 +34,7 @@ export function NotificationDrawer({
queryKey: ['notifications', opened], queryKey: ['notifications', opened],
queryFn: async () => queryFn: async () =>
api api
.get('/notifications/', { .get(apiUrl(ApiPaths.notifications_list), {
params: { params: {
read: false, read: false,
limit: 10 limit: 10

View File

@ -68,6 +68,7 @@ export function PanelGroup({
(panel, idx) => (panel, idx) =>
!panel.hidden && ( !panel.hidden && (
<Tabs.Tab <Tabs.Tab
key={`panel-tab-${panel.name}`}
p="xs" p="xs"
value={panel.name} value={panel.name}
icon={panel.icon} icon={panel.icon}

View File

@ -30,6 +30,7 @@ import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { RenderInstance } from '../render/Instance'; import { RenderInstance } from '../render/Instance';
import { ModelInformationDict, ModelType } from '../render/ModelType'; import { ModelInformationDict, ModelType } from '../render/ModelType';
@ -264,7 +265,7 @@ export function SearchDrawer({
}); });
return api return api
.post(`/search/`, params) .post(apiUrl(ApiPaths.api_search), params)
.then(function (response) { .then(function (response) {
return response.data; return response.data;
}) })

View File

@ -2,7 +2,7 @@ import { Anchor, Loader } from '@mantine/core';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '../../App'; import { api } from '../../App';
import { ApiPaths, url } from '../../states/ApiState'; import { ApiPaths, apiUrl } from '../../states/ApiState';
import { ThumbnailHoverCard } from '../items/Thumbnail'; import { ThumbnailHoverCard } from '../items/Thumbnail';
export function GeneralRenderer({ export function GeneralRenderer({
@ -33,7 +33,7 @@ export function GeneralRenderer({
queryKey: [ref, pk], queryKey: [ref, pk],
queryFn: () => { queryFn: () => {
return api return api
.get(url(api_key, pk)) .get(apiUrl(api_key, pk))
.then((res) => res.data) .then((res) => res.data)
.catch(() => { .catch(() => {
{ {

View File

@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, url } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { TableFilter } from '../Filter'; import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
@ -142,7 +142,7 @@ export function BuildOrderTable({ params = {} }: { params?: any }) {
return ( return (
<InvenTreeTable <InvenTreeTable
url={url(ApiPaths.build_order_list)} url={apiUrl(ApiPaths.build_order_list)}
tableKey={tableKey} tableKey={tableKey}
columns={tableColumns} columns={tableColumns}
props={{ props={{

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ApiPaths, url } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions'; import { RowAction } from '../RowActions';
@ -40,7 +40,7 @@ export function NotificationTable({
return ( return (
<InvenTreeTable <InvenTreeTable
url={url(ApiPaths.notifications_list)} url={apiUrl(ApiPaths.notifications_list)}
tableKey={tableKey} tableKey={tableKey}
columns={columns} columns={columns}
props={{ props={{

View File

@ -3,7 +3,7 @@ import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, url } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
@ -46,7 +46,7 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
return ( return (
<InvenTreeTable <InvenTreeTable
url={url(ApiPaths.category_list)} url={apiUrl(ApiPaths.category_list)}
tableKey={tableKey} tableKey={tableKey}
columns={tableColumns} columns={tableColumns}
props={{ props={{

View File

@ -7,7 +7,7 @@ import { editPart } from '../../../functions/forms/PartForms';
import { notYetImplemented } from '../../../functions/notifications'; import { notYetImplemented } from '../../../functions/notifications';
import { shortenString } from '../../../functions/tables'; import { shortenString } from '../../../functions/tables';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, url } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { TableFilter } from '../Filter'; import { TableFilter } from '../Filter';
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable'; import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
@ -221,7 +221,7 @@ export function PartListTable({ props }: { props: InvenTreeTableProps }) {
return ( return (
<InvenTreeTable <InvenTreeTable
url={url(ApiPaths.part_list)} url={apiUrl(ApiPaths.part_list)}
tableKey={tableKey} tableKey={tableKey}
columns={tableColumns} columns={tableColumns}
props={{ props={{

View File

@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom';
import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms'; import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, url } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../items/Thumbnail'; import { Thumbnail } from '../../items/Thumbnail';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
@ -116,7 +116,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
return ( return (
<InvenTreeTable <InvenTreeTable
url={url(ApiPaths.related_part_list)} url={apiUrl(ApiPaths.related_part_list)}
tableKey={tableKey} tableKey={tableKey}
columns={tableColumns} columns={tableColumns}
props={{ props={{

View File

@ -5,8 +5,7 @@ import { useNavigate } from 'react-router-dom';
import { notYetImplemented } from '../../../functions/notifications'; import { notYetImplemented } from '../../../functions/notifications';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, url } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { ThumbnailHoverCard } from '../../items/Thumbnail';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { TableFilter } from '../Filter'; import { TableFilter } from '../Filter';
import { RowAction } from '../RowActions'; import { RowAction } from '../RowActions';
@ -126,7 +125,7 @@ export function StockItemTable({ params = {} }: { params?: any }) {
return ( return (
<InvenTreeTable <InvenTreeTable
url={url(ApiPaths.stock_item_list)} url={apiUrl(ApiPaths.stock_item_list)}
tableKey={tableKey} tableKey={tableKey}
columns={tableColumns} columns={tableColumns}
props={{ props={{

View File

@ -3,7 +3,7 @@ import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, url } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
@ -67,7 +67,7 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
return ( return (
<InvenTreeTable <InvenTreeTable
url={url(ApiPaths.stock_location_list)} url={apiUrl(ApiPaths.stock_location_list)}
tableKey={tableKey} tableKey={tableKey}
columns={tableColumns} columns={tableColumns}
props={{ props={{

View File

@ -1,10 +1,10 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { notifications } from '@mantine/notifications'; import { notifications, showNotification } from '@mantine/notifications';
import { IconCheck } from '@tabler/icons-react'; import { IconCheck } from '@tabler/icons-react';
import axios from 'axios'; import axios from 'axios';
import { api } from '../App'; import { api } from '../App';
import { ApiPaths, url, useServerApiState } from '../states/ApiState'; import { ApiPaths, apiUrl, useServerApiState } from '../states/ApiState';
import { useLocalState } from '../states/LocalState'; import { useLocalState } from '../states/LocalState';
import { useSessionState } from '../states/SessionState'; import { useSessionState } from '../states/SessionState';
import { import {
@ -18,9 +18,17 @@ export const doClassicLogin = async (username: string, password: string) => {
// Get token from server // Get token from server
const token = await axios const token = await axios
.get(`${host}${url(ApiPaths.user_token)}`, { auth: { username, password } }) .get(apiUrl(ApiPaths.user_token), {
auth: { username, password },
baseURL: host.toString()
})
.then((response) => response.data.token) .then((response) => response.data.token)
.catch((error) => { .catch((error) => {
showNotification({
title: t`Login failed`,
message: t`Error fetching token from server.`,
color: 'red'
});
return false; return false;
}); });
@ -50,7 +58,7 @@ export const doClassicLogout = async () => {
export const doSimpleLogin = async (email: string) => { export const doSimpleLogin = async (email: string) => {
const { host } = useLocalState.getState(); const { host } = useLocalState.getState();
const mail = await axios const mail = await axios
.post(`${host}${url(ApiPaths.user_simple_login)}`, { .post(apiUrl(ApiPaths.user_simple_login), {
email: email email: email
}) })
.then((response) => response.data) .then((response) => response.data)
@ -77,7 +85,7 @@ export const doTokenLogin = (token: string) => {
export function handleReset(navigate: any, values: { email: string }) { export function handleReset(navigate: any, values: { email: string }) {
api api
.post(url(ApiPaths.user_reset), values, { .post(apiUrl(ApiPaths.user_reset), values, {
headers: { Authorization: '' } headers: { Authorization: '' }
}) })
.then((val) => { .then((val) => {
@ -101,7 +109,7 @@ export function handleReset(navigate: any, values: { email: string }) {
export function checkLoginState(navigate: any) { export function checkLoginState(navigate: any) {
api api
.get(url(ApiPaths.user_token)) .get(apiUrl(ApiPaths.user_token))
.then((val) => { .then((val) => {
if (val.status === 200 && val.data.token) { if (val.status === 200 && val.data.token) {
doTokenLogin(val.data.token); doTokenLogin(val.data.token);

View File

@ -6,7 +6,7 @@ import { AxiosResponse } from 'axios';
import { api } from '../App'; import { api } from '../App';
import { ApiForm, ApiFormProps } from '../components/forms/ApiForm'; import { ApiForm, ApiFormProps } from '../components/forms/ApiForm';
import { ApiFormFieldType } from '../components/forms/fields/ApiFormField'; import { ApiFormFieldType } from '../components/forms/fields/ApiFormField';
import { url } from '../states/ApiState'; import { apiUrl } from '../states/ApiState';
import { invalidResponse, permissionDenied } from './notifications'; import { invalidResponse, permissionDenied } from './notifications';
import { generateUniqueId } from './uid'; import { generateUniqueId } from './uid';
@ -14,7 +14,7 @@ import { generateUniqueId } from './uid';
* Construct an API url from the provided ApiFormProps object * Construct an API url from the provided ApiFormProps object
*/ */
export function constructFormUrl(props: ApiFormProps): string { export function constructFormUrl(props: ApiFormProps): string {
return url(props.url, props.pk); return apiUrl(props.url, props.pk);
} }
/** /**

View File

@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { api } from '../App'; import { api } from '../App';
import { ApiPaths, url } from '../states/ApiState'; import { ApiPaths, apiUrl } from '../states/ApiState';
/** /**
* Custom hook for loading a single instance of an instance from the API * Custom hook for loading a single instance of an instance from the API
@ -32,8 +32,10 @@ export function useInstance({
return null; return null;
} }
let url = apiUrl(endpoint, pk);
return api return api
.get(url(endpoint, pk), { .get(url, {
params: params params: params
}) })
.then((response) => { .then((response) => {
@ -48,7 +50,7 @@ export function useInstance({
}) })
.catch((error) => { .catch((error) => {
setInstance({}); setInstance({});
console.error(`Error fetching instance ${url}${pk}:`, error); console.error(`Error fetching instance ${url}:`, error);
return null; return null;
}); });
}, },

View File

@ -28,13 +28,13 @@ export const IS_DEV_OR_DEMO = IS_DEV || IS_DEMO;
window.INVENTREE_SETTINGS = { window.INVENTREE_SETTINGS = {
server_list: { server_list: {
'mantine-cqj63coxn': { 'mantine-cqj63coxn': {
host: `${window.location.origin}/api/`, host: `${window.location.origin}/`,
name: 'Current Server' name: 'Current Server'
}, },
...(IS_DEV_OR_DEMO ...(IS_DEV_OR_DEMO
? { ? {
'mantine-u56l5jt85': { 'mantine-u56l5jt85': {
host: 'https://demo.inventree.org/api/', host: 'https://demo.inventree.org/',
name: 'InvenTree Demo' name: 'InvenTree Demo'
} }
} }

View File

@ -22,12 +22,12 @@ export default function Login() {
state.fetchServerApiState state.fetchServerApiState
]); ]);
const hostname = const hostname =
hostList[hostKey] === undefined ? t`No selection` : hostList[hostKey].name; hostList[hostKey] === undefined ? t`No selection` : hostList[hostKey]?.name;
const [hostEdit, setHostEdit] = useToggle([false, true] as const); const [hostEdit, setHostEdit] = useToggle([false, true] as const);
// Data manipulation functions // Data manipulation functions
function ChangeHost(newHost: string): void { function ChangeHost(newHost: string): void {
setHost(hostList[newHost].host, newHost); setHost(hostList[newHost]?.host, newHost);
setApiDefaults(); setApiDefaults();
fetchServerApiState(); fetchServerApiState();
} }

View File

@ -14,7 +14,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { LanguageContext } from '../../contexts/LanguageContext'; import { LanguageContext } from '../../contexts/LanguageContext';
import { ApiPaths, url } from '../../states/ApiState'; import { ApiPaths, apiUrl } from '../../states/ApiState';
export default function Set_Password() { export default function Set_Password() {
const simpleForm = useForm({ initialValues: { password: '' } }); const simpleForm = useForm({ initialValues: { password: '' } });
@ -57,7 +57,7 @@ export default function Set_Password() {
// Set password with call to backend // Set password with call to backend
api api
.post( .post(
url(ApiPaths.user_reset_set), apiUrl(ApiPaths.user_reset_set),
{ {
uid: uid, uid: uid,
token: token, token: token,

View File

@ -19,6 +19,7 @@ import { api, queryClient } from '../../../App';
import { ColorToggle } from '../../../components/items/ColorToggle'; import { ColorToggle } from '../../../components/items/ColorToggle';
import { EditButton } from '../../../components/items/EditButton'; import { EditButton } from '../../../components/items/EditButton';
import { LanguageSelect } from '../../../components/items/LanguageSelect'; import { LanguageSelect } from '../../../components/items/LanguageSelect';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { useLocalState } from '../../../states/LocalState'; import { useLocalState } from '../../../states/LocalState';
import { UserTheme } from './UserTheme'; import { UserTheme } from './UserTheme';
@ -29,7 +30,8 @@ export function UserPanel() {
// data // data
function fetchData() { function fetchData() {
return api.get('user/me/').then((res) => res.data); // TODO: Replace this call with the global user state, perhaps?
return api.get(apiUrl(ApiPaths.user_me)).then((res) => res.data);
} }
const { isLoading, data } = useQuery({ const { isLoading, data } = useQuery({
queryKey: ['user-me'], queryKey: ['user-me'],
@ -68,7 +70,7 @@ export function UserInfo({ data }: { data: any }) {
const form = useForm({ initialValues: data }); const form = useForm({ initialValues: data });
const [editing, setEditing] = useToggle([false, true] as const); const [editing, setEditing] = useToggle([false, true] as const);
function SaveData(values: any) { function SaveData(values: any) {
api.put('user/me/', values).then((res) => { api.put(apiUrl(ApiPaths.user_me)).then((res) => {
if (res.status === 200) { if (res.status === 200) {
setEditing(); setEditing();
queryClient.invalidateQueries(['user-me']); queryClient.invalidateQueries(['user-me']);

View File

@ -50,7 +50,7 @@ import { TitleWithDoc } from '../../components/items/TitleWithDoc';
import { Render, RenderTypes } from '../../components/renderers'; import { Render, RenderTypes } from '../../components/renderers';
import { notYetImplemented } from '../../functions/notifications'; import { notYetImplemented } from '../../functions/notifications';
import { IS_DEV_OR_DEMO } from '../../main'; import { IS_DEV_OR_DEMO } from '../../main';
import { ApiPaths, url } from '../../states/ApiState'; import { ApiPaths, apiUrl } from '../../states/ApiState';
interface ScanItem { interface ScanItem {
id: string; id: string;
@ -136,7 +136,7 @@ export default function Scan() {
function runBarcode(value: string, id?: string) { function runBarcode(value: string, id?: string) {
api api
.post(url(ApiPaths.barcode), { barcode: value }) .post(apiUrl(ApiPaths.barcode), { barcode: value })
.then((response) => { .then((response) => {
// update item in history // update item in history
if (!id) return; if (!id) return;

View File

@ -24,7 +24,7 @@ import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, url } from '../../states/ApiState'; import { ApiPaths, apiUrl } from '../../states/ApiState';
/** /**
* Detail page for a single Build Order * Detail page for a single Build Order
@ -121,7 +121,7 @@ export default function BuildDetail() {
icon: <IconNotes size="18" />, icon: <IconNotes size="18" />,
content: ( content: (
<NotesEditor <NotesEditor
url={url(ApiPaths.build_order_list, build.pk)} url={apiUrl(ApiPaths.build_order_list, build.pk)}
data={build.notes ?? ''} data={build.notes ?? ''}
allowEdit={true} allowEdit={true}
/> />

View File

@ -29,7 +29,7 @@ import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { editPart } from '../../functions/forms/PartForms'; import { editPart } from '../../functions/forms/PartForms';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, url } from '../../states/ApiState'; import { ApiPaths, apiUrl } from '../../states/ApiState';
/** /**
* Detail view for a single Part instance * Detail view for a single Part instance
@ -157,7 +157,7 @@ export default function PartDetail() {
// TODO: Set edit permission based on user permissions // TODO: Set edit permission based on user permissions
return ( return (
<NotesEditor <NotesEditor
url={url(ApiPaths.part_list, part.pk)} url={apiUrl(ApiPaths.part_list, part.pk)}
data={part.notes ?? ''} data={part.notes ?? ''}
allowEdit={true} allowEdit={true}
/> />

View File

@ -18,7 +18,7 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/AttachmentTable'; import { AttachmentTable } from '../../components/tables/AttachmentTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, url } from '../../states/ApiState'; import { ApiPaths, apiUrl } from '../../states/ApiState';
export default function StockDetail() { export default function StockDetail() {
const { id } = useParams(); const { id } = useParams();
@ -87,7 +87,7 @@ export default function StockDetail() {
icon: <IconNotes size="18" />, icon: <IconNotes size="18" />,
content: ( content: (
<NotesEditor <NotesEditor
url={url(ApiPaths.stock_item_list, stockitem.pk)} url={apiUrl(ApiPaths.stock_item_list, stockitem.pk)}
data={stockitem.notes ?? ''} data={stockitem.notes ?? ''}
allowEdit={true} allowEdit={true}
/> />

View File

@ -15,13 +15,17 @@ export const useServerApiState = create<ServerApiStateProps>((set, get) => ({
setServer: (newServer: ServerAPIProps) => set({ server: newServer }), setServer: (newServer: ServerAPIProps) => set({ server: newServer }),
fetchServerApiState: async () => { fetchServerApiState: async () => {
// Fetch server data // Fetch server data
await api.get('/').then((response) => { await api.get(apiUrl(ApiPaths.api_server_info)).then((response) => {
set({ server: response.data }); set({ server: response.data });
}); });
} }
})); }));
export enum ApiPaths { export enum ApiPaths {
api_server_info = 'api-server-info',
api_search = 'api-search',
// User information // User information
user_me = 'api-user-me', user_me = 'api-user-me',
user_roles = 'api-user-roles', user_roles = 'api-user-roles',
@ -62,11 +66,21 @@ export enum ApiPaths {
sales_order_list = 'api-sales-order-list' sales_order_list = 'api-sales-order-list'
} }
/**
* Function to return the API prefix.
* For now it is fixed, but may be configurable in the future.
*/
export function apiPrefix(): string {
return '/api/';
}
/** /**
* Return the endpoint associated with a given API path * Return the endpoint associated with a given API path
*/ */
export function endpoint(path: ApiPaths): string { export function apiEndpoint(path: ApiPaths): string {
switch (path) { switch (path) {
case ApiPaths.api_server_info:
return '';
case ApiPaths.user_me: case ApiPaths.user_me:
return 'user/me/'; return 'user/me/';
case ApiPaths.user_roles: case ApiPaths.user_roles:
@ -76,9 +90,13 @@ export function endpoint(path: ApiPaths): string {
case ApiPaths.user_simple_login: case ApiPaths.user_simple_login:
return 'email/generate/'; return 'email/generate/';
case ApiPaths.user_reset: case ApiPaths.user_reset:
// Note leading prefix here
return '/auth/password/reset/'; return '/auth/password/reset/';
case ApiPaths.user_reset_set: case ApiPaths.user_reset_set:
// Note leading prefix here
return '/auth/password/reset/confirm/'; return '/auth/password/reset/confirm/';
case ApiPaths.api_search:
return 'search/';
case ApiPaths.settings_global_list: case ApiPaths.settings_global_list:
return 'settings/global/'; return 'settings/global/';
case ApiPaths.settings_user_list: case ApiPaths.settings_user_list:
@ -122,8 +140,13 @@ export function endpoint(path: ApiPaths): string {
/** /**
* Construct an API URL with an endpoint and (optional) pk value * Construct an API URL with an endpoint and (optional) pk value
*/ */
export function url(path: ApiPaths, pk?: any): string { export function apiUrl(path: ApiPaths, pk?: any): string {
let _url = endpoint(path); let _url = apiEndpoint(path);
// If the URL does not start with a '/', add the API prefix
if (!_url.startsWith('/')) {
_url = apiPrefix() + _url;
}
if (_url && pk) { if (_url && pk) {
_url += `${pk}/`; _url += `${pk}/`;

View File

@ -4,7 +4,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { api } from '../App'; import { api } from '../App';
import { ApiPaths, url } from './ApiState'; import { ApiPaths, apiUrl } from './ApiState';
import { Setting } from './states'; import { Setting } from './states';
interface SettingsStateProps { interface SettingsStateProps {
@ -20,7 +20,7 @@ export const useGlobalSettingsState = create<SettingsStateProps>(
settings: [], settings: [],
fetchSettings: async () => { fetchSettings: async () => {
await api await api
.get(url(ApiPaths.settings_global_list)) .get(apiUrl(ApiPaths.settings_global_list))
.then((response) => { .then((response) => {
set({ settings: response.data }); set({ settings: response.data });
}) })
@ -38,7 +38,7 @@ export const useUserSettingsState = create<SettingsStateProps>((set, get) => ({
settings: [], settings: [],
fetchSettings: async () => { fetchSettings: async () => {
await api await api
.get(url(ApiPaths.settings_user_list)) .get(apiUrl(ApiPaths.settings_user_list))
.then((response) => { .then((response) => {
set({ settings: response.data }); set({ settings: response.data });
}) })

View File

@ -1,7 +1,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { api } from '../App'; import { api } from '../App';
import { ApiPaths, url } from './ApiState'; import { ApiPaths, apiUrl } from './ApiState';
import { UserProps } from './states'; import { UserProps } from './states';
interface UserStateProps { interface UserStateProps {
@ -19,7 +19,7 @@ export const useUserState = create<UserStateProps>((set, get) => ({
fetchUserState: async () => { fetchUserState: async () => {
// Fetch user data // Fetch user data
await api await api
.get(url(ApiPaths.user_me)) .get(apiUrl(ApiPaths.user_me))
.then((response) => { .then((response) => {
const user: UserProps = { const user: UserProps = {
name: `${response.data.first_name} ${response.data.last_name}`, name: `${response.data.first_name} ${response.data.last_name}`,
@ -34,7 +34,7 @@ export const useUserState = create<UserStateProps>((set, get) => ({
// Fetch role data // Fetch role data
await api await api
.get(url(ApiPaths.user_roles)) .get(apiUrl(ApiPaths.user_roles))
.then((response) => { .then((response) => {
const user: UserProps = get().user as UserProps; const user: UserProps = get().user as UserProps;