mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 21:38:48 +00:00
PUI general improvements (#5947)
* First draft for refactoring the api forms including modals * Fix merging errors * Fix deepsource * Fix jsdoc * trigger: deepsource * Try to improve performance by not passing the whole definition down * First draft for switching to react-hook-form * Fix warning log in console with i18n when locale is not loaded * Fix: deepsource * Fixed RelatedModelField initial value loading and disable submit if form is not 'dirty' * Make field state hookable to state * Added nested object field to PUI form framework * Fix ts errors while integrating the new forms api into a few places * Fix: deepsource * Fix some values were not present in the submit data if the field is hidden * Handle error while loading locales * Fix: deepsource * Added few general improvements * Fix missig key prop * Fix storage deprecation warnings
This commit is contained in:
parent
333e2ce993
commit
264dc9d27a
@ -15,8 +15,6 @@ export function ButtonMenu({
|
|||||||
label?: string;
|
label?: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
}) {
|
}) {
|
||||||
let idx = 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu shadow="xs">
|
<Menu shadow="xs">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
@ -26,8 +24,8 @@ export function ButtonMenu({
|
|||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{label && <Menu.Label>{label}</Menu.Label>}
|
{label && <Menu.Label>{label}</Menu.Label>}
|
||||||
{actions.map((action) => (
|
{actions.map((action, i) => (
|
||||||
<Menu.Item key={idx++}>{action}</Menu.Item>
|
<Menu.Item key={i}>{action}</Menu.Item>
|
||||||
))}
|
))}
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
mapFields
|
mapFields
|
||||||
} from '../../functions/forms';
|
} from '../../functions/forms';
|
||||||
import { invalidResponse } from '../../functions/notifications';
|
import { invalidResponse } from '../../functions/notifications';
|
||||||
|
import { PathParams } from '../../states/ApiState';
|
||||||
import {
|
import {
|
||||||
ApiFormField,
|
ApiFormField,
|
||||||
ApiFormFieldSet,
|
ApiFormFieldSet,
|
||||||
@ -46,6 +47,7 @@ export interface ApiFormAction {
|
|||||||
* Properties for the ApiForm component
|
* Properties for the ApiForm component
|
||||||
* @param url : The API endpoint to fetch the form data from
|
* @param url : The API endpoint to fetch the form data from
|
||||||
* @param pk : Optional primary-key value when editing an existing object
|
* @param pk : Optional primary-key value when editing an existing object
|
||||||
|
* @param pathParams : Optional path params for the url
|
||||||
* @param method : Optional HTTP method to use when submitting the form (default: GET)
|
* @param method : Optional HTTP method to use when submitting the form (default: GET)
|
||||||
* @param fields : The fields to render in the form
|
* @param fields : The fields to render in the form
|
||||||
* @param submitText : Optional custom text to display on the submit button (default: Submit)4
|
* @param submitText : Optional custom text to display on the submit button (default: Submit)4
|
||||||
@ -60,6 +62,7 @@ export interface ApiFormAction {
|
|||||||
export interface ApiFormProps {
|
export interface ApiFormProps {
|
||||||
url: ApiPaths;
|
url: ApiPaths;
|
||||||
pk?: number | string | undefined;
|
pk?: number | string | undefined;
|
||||||
|
pathParams?: PathParams;
|
||||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||||
fields?: ApiFormFieldSet;
|
fields?: ApiFormFieldSet;
|
||||||
submitText?: string;
|
submitText?: string;
|
||||||
@ -92,13 +95,20 @@ export function OptionsApiForm({
|
|||||||
const id = useId(pId);
|
const id = useId(pId);
|
||||||
|
|
||||||
const url = useMemo(
|
const url = useMemo(
|
||||||
() => constructFormUrl(props.url, props.pk),
|
() => constructFormUrl(props.url, props.pk, props.pathParams),
|
||||||
[props.url, props.pk]
|
[props.url, props.pk, props.pathParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
queryKey: ['form-options-data', id, props.method, props.url, props.pk],
|
queryKey: [
|
||||||
|
'form-options-data',
|
||||||
|
id,
|
||||||
|
props.method,
|
||||||
|
props.url,
|
||||||
|
props.pk,
|
||||||
|
props.pathParams
|
||||||
|
],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
api.options(url).then((res) => {
|
api.options(url).then((res) => {
|
||||||
let fields: Record<string, ApiFormFieldType> | null = {};
|
let fields: Record<string, ApiFormFieldType> | null = {};
|
||||||
@ -171,14 +181,21 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
|
|||||||
|
|
||||||
// Cache URL
|
// Cache URL
|
||||||
const url = useMemo(
|
const url = useMemo(
|
||||||
() => constructFormUrl(props.url, props.pk),
|
() => constructFormUrl(props.url, props.pk, props.pathParams),
|
||||||
[props.url, props.pk]
|
[props.url, props.pk, props.pathParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Query manager for retrieving initial data from the server
|
// Query manager for retrieving initial data from the server
|
||||||
const initialDataQuery = useQuery({
|
const initialDataQuery = useQuery({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
queryKey: ['form-initial-data', id, props.method, props.url, props.pk],
|
queryKey: [
|
||||||
|
'form-initial-data',
|
||||||
|
id,
|
||||||
|
props.method,
|
||||||
|
props.url,
|
||||||
|
props.pk,
|
||||||
|
props.pathParams
|
||||||
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return api
|
return api
|
||||||
.get(url)
|
.get(url)
|
||||||
@ -223,7 +240,14 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
|
|||||||
// Fetch initial data if the fetchInitialData property is set
|
// Fetch initial data if the fetchInitialData property is set
|
||||||
if (props.fetchInitialData) {
|
if (props.fetchInitialData) {
|
||||||
queryClient.removeQueries({
|
queryClient.removeQueries({
|
||||||
queryKey: ['form-initial-data', id, props.method, props.url, props.pk]
|
queryKey: [
|
||||||
|
'form-initial-data',
|
||||||
|
id,
|
||||||
|
props.method,
|
||||||
|
props.url,
|
||||||
|
props.pk,
|
||||||
|
props.pathParams
|
||||||
|
]
|
||||||
});
|
});
|
||||||
initialDataQuery.refetch();
|
initialDataQuery.refetch();
|
||||||
}
|
}
|
||||||
|
@ -50,10 +50,9 @@ export function ActionDropdown({
|
|||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{actions.map((action) =>
|
{actions.map((action) =>
|
||||||
action.disabled ? null : (
|
action.disabled ? null : (
|
||||||
<Tooltip label={action.tooltip} key={`tooltip-${action.name}`}>
|
<Tooltip label={action.tooltip} key={action.name}>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={action.icon}
|
icon={action.icon}
|
||||||
key={action.name}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (action.onClick != undefined) {
|
if (action.onClick != undefined) {
|
||||||
action.onClick();
|
action.onClick();
|
||||||
|
@ -38,7 +38,7 @@ export function BreadcrumbList({
|
|||||||
{breadcrumbs.map((breadcrumb, index) => {
|
{breadcrumbs.map((breadcrumb, index) => {
|
||||||
return (
|
return (
|
||||||
<Anchor
|
<Anchor
|
||||||
key={`breadcrumb-${index}`}
|
key={index}
|
||||||
onClick={() => breadcrumb.url && navigate(breadcrumb.url)}
|
onClick={() => breadcrumb.url && navigate(breadcrumb.url)}
|
||||||
>
|
>
|
||||||
<Text size="sm">{breadcrumb.name}</Text>
|
<Text size="sm">{breadcrumb.name}</Text>
|
||||||
|
@ -88,7 +88,7 @@ export function NotificationDrawer({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{notificationQuery.data?.results?.map((notification: any) => (
|
{notificationQuery.data?.results?.map((notification: any) => (
|
||||||
<Group position="apart">
|
<Group position="apart" key={notification.pk}>
|
||||||
<Stack spacing="3">
|
<Stack spacing="3">
|
||||||
<Text size="sm">{notification.target?.name ?? 'target'}</Text>
|
<Text size="sm">{notification.target?.name ?? 'target'}</Text>
|
||||||
<Text size="xs">{notification.age_human ?? 'name'}</Text>
|
<Text size="xs">{notification.age_human ?? 'name'}</Text>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Group, Paper, Space, Stack, Text } from '@mantine/core';
|
import { Group, Paper, Space, Stack, Text } from '@mantine/core';
|
||||||
import { ReactNode } from 'react';
|
import { Fragment, ReactNode } from 'react';
|
||||||
|
|
||||||
import { ApiImage } from '../images/ApiImage';
|
import { ApiImage } from '../images/ApiImage';
|
||||||
import { StylishText } from '../items/StylishText';
|
import { StylishText } from '../items/StylishText';
|
||||||
@ -58,8 +58,10 @@ export function PageDetail({
|
|||||||
{detail}
|
{detail}
|
||||||
<Space />
|
<Space />
|
||||||
{actions && (
|
{actions && (
|
||||||
<Group key="page-actions" spacing={5} position="right">
|
<Group spacing={5} position="right">
|
||||||
{actions}
|
{actions.map((action, idx) => (
|
||||||
|
<Fragment key={idx}>{action}</Fragment>
|
||||||
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
@ -90,15 +90,14 @@ export function PanelGroup({
|
|||||||
>
|
>
|
||||||
<Tabs.List position="left">
|
<Tabs.List position="left">
|
||||||
{panels.map(
|
{panels.map(
|
||||||
(panel, idx) =>
|
(panel) =>
|
||||||
!panel.hidden && (
|
!panel.hidden && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={panel.label}
|
label={panel.label}
|
||||||
key={`panel-tab-tooltip-${panel.name}`}
|
key={panel.name}
|
||||||
disabled={expanded}
|
disabled={expanded}
|
||||||
>
|
>
|
||||||
<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}
|
||||||
@ -125,10 +124,10 @@ export function PanelGroup({
|
|||||||
)}
|
)}
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
{panels.map(
|
{panels.map(
|
||||||
(panel, idx) =>
|
(panel) =>
|
||||||
!panel.hidden && (
|
!panel.hidden && (
|
||||||
<Tabs.Panel
|
<Tabs.Panel
|
||||||
key={idx}
|
key={panel.name}
|
||||||
value={panel.name}
|
value={panel.name}
|
||||||
p="sm"
|
p="sm"
|
||||||
style={{
|
style={{
|
||||||
|
@ -90,12 +90,11 @@ function QueryResultGroup({
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack>
|
<Stack>
|
||||||
{query.results.results.map((result: any) => (
|
{query.results.results.map((result: any) => (
|
||||||
<Anchor onClick={() => onResultClick(query.model, result.pk)}>
|
<Anchor
|
||||||
<RenderInstance
|
onClick={() => onResultClick(query.model, result.pk)}
|
||||||
key={`${query.model}-${result.pk}`}
|
key={result.pk}
|
||||||
instance={result}
|
>
|
||||||
model={query.model}
|
<RenderInstance instance={result} model={query.model} />
|
||||||
/>
|
|
||||||
</Anchor>
|
</Anchor>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
@ -395,8 +394,9 @@ export function SearchDrawer({
|
|||||||
)}
|
)}
|
||||||
{!searchQuery.isFetching && !searchQuery.isError && (
|
{!searchQuery.isFetching && !searchQuery.isError && (
|
||||||
<Stack spacing="md">
|
<Stack spacing="md">
|
||||||
{queryResults.map((query) => (
|
{queryResults.map((query, idx) => (
|
||||||
<QueryResultGroup
|
<QueryResultGroup
|
||||||
|
key={idx}
|
||||||
query={query}
|
query={query}
|
||||||
onRemove={(query) => removeResults(query)}
|
onRemove={(query) => removeResults(query)}
|
||||||
onResultClick={(query, pk) => onResultClick(query, pk)}
|
onResultClick={(query, pk) => onResultClick(query, pk)}
|
||||||
|
@ -23,7 +23,10 @@ function SettingValue({
|
|||||||
// Callback function when a boolean value is changed
|
// Callback function when a boolean value is changed
|
||||||
function onToggle(value: boolean) {
|
function onToggle(value: boolean) {
|
||||||
api
|
api
|
||||||
.patch(apiUrl(settingsState.endpoint, setting.key), { value: value })
|
.patch(
|
||||||
|
apiUrl(settingsState.endpoint, setting.key, settingsState.pathParams),
|
||||||
|
{ value: value }
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
showNotification({
|
showNotification({
|
||||||
title: t`Setting updated`,
|
title: t`Setting updated`,
|
||||||
@ -53,6 +56,7 @@ function SettingValue({
|
|||||||
openModalApiForm({
|
openModalApiForm({
|
||||||
url: settingsState.endpoint,
|
url: settingsState.endpoint,
|
||||||
pk: setting.key,
|
pk: setting.key,
|
||||||
|
pathParams: settingsState.pathParams,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
title: t`Edit Setting`,
|
title: t`Edit Setting`,
|
||||||
ignorePermissionCheck: true,
|
ignorePermissionCheck: true,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Stack, Text } from '@mantine/core';
|
import { Stack, Text, useMantineTheme } from '@mantine/core';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SettingsStateProps,
|
SettingsStateProps,
|
||||||
@ -16,21 +16,36 @@ export function SettingList({
|
|||||||
keys
|
keys
|
||||||
}: {
|
}: {
|
||||||
settingsState: SettingsStateProps;
|
settingsState: SettingsStateProps;
|
||||||
keys: string[];
|
keys?: string[];
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
settingsState.fetchSettings();
|
settingsState.fetchSettings();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const allKeys = useMemo(
|
||||||
|
() => settingsState?.settings?.map((s) => s.key),
|
||||||
|
[settingsState?.settings]
|
||||||
|
);
|
||||||
|
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
{keys.map((key) => {
|
{(keys || allKeys).map((key, i) => {
|
||||||
const setting = settingsState?.settings?.find(
|
const setting = settingsState?.settings?.find(
|
||||||
(s: any) => s.key === key
|
(s: any) => s.key === key
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const style: Record<string, string> = { paddingLeft: '8px' };
|
||||||
|
if (i % 2 === 0)
|
||||||
|
style['backgroundColor'] =
|
||||||
|
theme.colorScheme === 'light'
|
||||||
|
? theme.colors.gray[1]
|
||||||
|
: theme.colors.gray[9];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key}>
|
<div key={key} style={style}>
|
||||||
{setting ? (
|
{setting ? (
|
||||||
<SettingItem settingsState={settingsState} setting={setting} />
|
<SettingItem settingsState={settingsState} setting={setting} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* Interface for the table column definition
|
* Interface for the table column definition
|
||||||
*/
|
*/
|
||||||
export type TableColumn = {
|
export type TableColumn<T = any> = {
|
||||||
accessor: string; // The key in the record to access
|
accessor: string; // The key in the record to access
|
||||||
ordering?: string; // The key in the record to sort by (defaults to accessor)
|
ordering?: string; // The key in the record to sort by (defaults to accessor)
|
||||||
title: string; // The title of the column
|
title: string; // The title of the column
|
||||||
sortable?: boolean; // Whether the column is sortable
|
sortable?: boolean; // Whether the column is sortable
|
||||||
switchable?: boolean; // Whether the column is switchable
|
switchable?: boolean; // Whether the column is switchable
|
||||||
hidden?: boolean; // Whether the column is hidden
|
hidden?: boolean; // Whether the column is hidden
|
||||||
render?: (record: any) => any; // A custom render function
|
render?: (record: T) => any; // A custom render function
|
||||||
filter?: any; // A custom filter function
|
filter?: any; // A custom filter function
|
||||||
filtering?: boolean; // Whether the column is filterable
|
filtering?: boolean; // Whether the column is filterable
|
||||||
width?: number; // The width of the column
|
width?: number; // The width of the column
|
||||||
|
@ -6,7 +6,7 @@ import { IconFilter, IconRefresh } from '@tabler/icons-react';
|
|||||||
import { IconBarcode, IconPrinter } from '@tabler/icons-react';
|
import { IconBarcode, IconPrinter } from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import { ButtonMenu } from '../buttons/ButtonMenu';
|
import { ButtonMenu } from '../buttons/ButtonMenu';
|
||||||
@ -44,7 +44,7 @@ const defaultPageSize: number = 25;
|
|||||||
* @param rowActions : (record: any) => RowAction[] - Callback function to generate row actions
|
* @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
|
* @param onRowClick : (record: any, index: number, event: any) => void - Callback function when a row is clicked
|
||||||
*/
|
*/
|
||||||
export type InvenTreeTableProps = {
|
export type InvenTreeTableProps<T = any> = {
|
||||||
params?: any;
|
params?: any;
|
||||||
defaultSortColumn?: string;
|
defaultSortColumn?: string;
|
||||||
noRecordsText?: string;
|
noRecordsText?: string;
|
||||||
@ -57,12 +57,12 @@ export type InvenTreeTableProps = {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
barcodeActions?: any[];
|
barcodeActions?: any[];
|
||||||
customFilters?: TableFilter[];
|
customFilters?: TableFilter[];
|
||||||
customActionGroups?: any[];
|
customActionGroups?: React.ReactNode[];
|
||||||
printingActions?: any[];
|
printingActions?: any[];
|
||||||
idAccessor?: string;
|
idAccessor?: string;
|
||||||
dataFormatter?: (data: any) => any;
|
dataFormatter?: (data: T) => any;
|
||||||
rowActions?: (record: any) => RowAction[];
|
rowActions?: (record: T) => RowAction[];
|
||||||
onRowClick?: (record: any, index: number, event: any) => void;
|
onRowClick?: (record: T, index: number, event: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,7 +90,7 @@ const defaultInvenTreeTableProps: InvenTreeTableProps = {
|
|||||||
/**
|
/**
|
||||||
* Table Component which extends DataTable with custom InvenTree functionality
|
* Table Component which extends DataTable with custom InvenTree functionality
|
||||||
*/
|
*/
|
||||||
export function InvenTreeTable({
|
export function InvenTreeTable<T = any>({
|
||||||
url,
|
url,
|
||||||
tableKey,
|
tableKey,
|
||||||
columns,
|
columns,
|
||||||
@ -98,8 +98,8 @@ export function InvenTreeTable({
|
|||||||
}: {
|
}: {
|
||||||
url: string;
|
url: string;
|
||||||
tableKey: string;
|
tableKey: string;
|
||||||
columns: TableColumn[];
|
columns: TableColumn<T>[];
|
||||||
props: InvenTreeTableProps;
|
props: InvenTreeTableProps<T>;
|
||||||
}) {
|
}) {
|
||||||
// Use the first part of the table key as the table name
|
// Use the first part of the table key as the table name
|
||||||
const tableName: string = useMemo(() => {
|
const tableName: string = useMemo(() => {
|
||||||
@ -107,7 +107,7 @@ export function InvenTreeTable({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Build table properties based on provided props (and default props)
|
// Build table properties based on provided props (and default props)
|
||||||
const tableProps: InvenTreeTableProps = useMemo(() => {
|
const tableProps: InvenTreeTableProps<T> = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
...defaultInvenTreeTableProps,
|
...defaultInvenTreeTableProps,
|
||||||
...props
|
...props
|
||||||
@ -432,9 +432,9 @@ export function InvenTreeTable({
|
|||||||
<Stack spacing="sm">
|
<Stack spacing="sm">
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Group position="left" key="custom-actions" spacing={5}>
|
<Group position="left" key="custom-actions" spacing={5}>
|
||||||
{tableProps.customActionGroups?.map(
|
{tableProps.customActionGroups?.map((group, idx) => (
|
||||||
(group: any, idx: number) => group
|
<Fragment key={idx}>{group}</Fragment>
|
||||||
)}
|
))}
|
||||||
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
|
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
|
||||||
<ButtonMenu
|
<ButtonMenu
|
||||||
key="barcode-actions"
|
key="barcode-actions"
|
||||||
|
@ -150,8 +150,8 @@ export function RowActions({
|
|||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Group position="right" spacing="xs" p={8}>
|
<Group position="right" spacing="xs" p={8}>
|
||||||
{visibleActions.map((action, _idx) => (
|
{visibleActions.map((action) => (
|
||||||
<RowActionIcon {...action} />
|
<RowActionIcon key={action.title} {...action} />
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
|
@ -11,15 +11,19 @@ import {
|
|||||||
} from '../components/forms/fields/ApiFormField';
|
} from '../components/forms/fields/ApiFormField';
|
||||||
import { StylishText } from '../components/items/StylishText';
|
import { StylishText } from '../components/items/StylishText';
|
||||||
import { ApiPaths } from '../enums/ApiEndpoints';
|
import { ApiPaths } from '../enums/ApiEndpoints';
|
||||||
import { apiUrl } from '../states/ApiState';
|
import { PathParams, apiUrl } from '../states/ApiState';
|
||||||
import { invalidResponse, permissionDenied } from './notifications';
|
import { invalidResponse, permissionDenied } from './notifications';
|
||||||
import { generateUniqueId } from './uid';
|
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(url: ApiPaths, pk?: string | number): string {
|
export function constructFormUrl(
|
||||||
return apiUrl(url, pk);
|
url: ApiPaths,
|
||||||
|
pk?: string | number,
|
||||||
|
pathParams?: PathParams
|
||||||
|
): string {
|
||||||
|
return apiUrl(url, pk, pathParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -208,7 +212,7 @@ export function openModalApiForm(props: OpenApiFormProps) {
|
|||||||
modals.close(modalId);
|
modals.close(modalId);
|
||||||
};
|
};
|
||||||
|
|
||||||
let url = constructFormUrl(props.url, props.pk);
|
let url = constructFormUrl(props.url, props.pk, props.pathParams);
|
||||||
|
|
||||||
// Make OPTIONS request first
|
// Make OPTIONS request first
|
||||||
api
|
api
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||||
|
|
||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
import { StatusCodeListInterface } from '../components/render/StatusRenderer';
|
import { StatusCodeListInterface } from '../components/render/StatusRenderer';
|
||||||
@ -15,7 +15,7 @@ interface ServerApiStateProps {
|
|||||||
server: ServerAPIProps;
|
server: ServerAPIProps;
|
||||||
setServer: (newServer: ServerAPIProps) => void;
|
setServer: (newServer: ServerAPIProps) => void;
|
||||||
fetchServerApiState: () => void;
|
fetchServerApiState: () => void;
|
||||||
status: StatusLookup | undefined;
|
status?: StatusLookup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useServerApiState = create<ServerApiStateProps>()(
|
export const useServerApiState = create<ServerApiStateProps>()(
|
||||||
@ -44,7 +44,7 @@ export const useServerApiState = create<ServerApiStateProps>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'server-api-state',
|
name: 'server-api-state',
|
||||||
getStorage: () => sessionStorage
|
storage: createJSONStorage(() => sessionStorage)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -189,13 +189,15 @@ export function apiEndpoint(path: ApiPaths): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PathParams = Record<string, string | number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct an API URL with an endpoint and (optional) pk value
|
* Construct an API URL with an endpoint and (optional) pk value
|
||||||
*/
|
*/
|
||||||
export function apiUrl(
|
export function apiUrl(
|
||||||
path: ApiPaths,
|
path: ApiPaths,
|
||||||
pk?: any,
|
pk?: any,
|
||||||
data?: Record<string, string | number>
|
pathParams?: PathParams
|
||||||
): string {
|
): string {
|
||||||
let _url = apiEndpoint(path);
|
let _url = apiEndpoint(path);
|
||||||
|
|
||||||
@ -208,9 +210,9 @@ export function apiUrl(
|
|||||||
_url += `${pk}/`;
|
_url += `${pk}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_url && data) {
|
if (_url && pathParams) {
|
||||||
for (const key in data) {
|
for (const key in pathParams) {
|
||||||
_url = _url.replace(`:${key}`, `${data[key]}`);
|
_url = _url.replace(`:${key}`, `${pathParams[key]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||||
|
|
||||||
import { setApiDefaults } from '../App';
|
import { setApiDefaults } from '../App';
|
||||||
|
|
||||||
interface SessionStateProps {
|
interface SessionStateProps {
|
||||||
token: string | undefined;
|
token?: string;
|
||||||
setToken: (newToken: string | undefined) => void;
|
setToken: (newToken?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSessionState = create<SessionStateProps>()(
|
export const useSessionState = create<SessionStateProps>()(
|
||||||
@ -19,7 +19,7 @@ export const useSessionState = create<SessionStateProps>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'session-state',
|
name: 'session-state',
|
||||||
getStorage: () => sessionStorage
|
storage: createJSONStorage(() => sessionStorage)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ import { create } from 'zustand';
|
|||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
import { ApiPaths } from '../enums/ApiEndpoints';
|
import { ApiPaths } from '../enums/ApiEndpoints';
|
||||||
import { isTrue } from '../functions/conversion';
|
import { isTrue } from '../functions/conversion';
|
||||||
import { apiUrl } from './ApiState';
|
import { PathParams, apiUrl } from './ApiState';
|
||||||
import { Setting, SettingsLookup } from './states';
|
import { Setting, SettingsLookup } from './states';
|
||||||
|
|
||||||
export interface SettingsStateProps {
|
export interface SettingsStateProps {
|
||||||
@ -14,6 +14,7 @@ export interface SettingsStateProps {
|
|||||||
lookup: SettingsLookup;
|
lookup: SettingsLookup;
|
||||||
fetchSettings: () => void;
|
fetchSettings: () => void;
|
||||||
endpoint: ApiPaths;
|
endpoint: ApiPaths;
|
||||||
|
pathParams?: PathParams;
|
||||||
getSetting: (key: string, default_value?: string) => string; // Return a raw setting value
|
getSetting: (key: string, default_value?: string) => string; // Return a raw setting value
|
||||||
isSet: (key: string, default_value?: boolean) => boolean; // Check a "boolean" setting
|
isSet: (key: string, default_value?: boolean) => boolean; // Check a "boolean" setting
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user