Forms initial data (#6365)

* Use PATCH for edit form

* Add "localhost:8000" server option

* Add initialData property for forms

- Allows user to specify an initial dataset to override default values

* Override field values when constructing form props

* Remove debug messages

* Wrap ApiForm in FormProvider

- Allows lower elements to access all form data without rebuild
- Pass all form data through to adjustFilters routine

* Fixes for RelatedModelField

- Ensure that the saved data are cleared when filters change

* Fix debug message for token creation

* Fix address rendering for modals

* Refactor "address" forms

- Use new "hook" form structure

* Update Contacts table

* Prevent related model fields from fetching on initial form open

- Only fetch when the drop-down is actually opened
- Significantly reduces the number of API calls

* Fix for ChoiceField

- Display label / description / placeholder text

* Fix for DateInput

- Correct conversion of datatype

* Implement "new purchase order" form

- Uses modal form hook
- Supply initial data
- Adjust filters according to selected company

* Add new company from company table

* Edit company from detail page

* More table updates

- StockLocation
- PartCategory

* Update more tables / forms:

- PartParameter table
- PartParameterTemplate table
- Cleanup unused imports

* Update ProjectCode table

* CustomUnits table

* Update RelatedPart table

* Update PartTestTemplate table

* Cleanup PartParameterTable

* Add "IPN" column to PartParameterTable

* Update BuildOrder table

* Update BuildDetail page

* PurchaseOrderLineItem table

* Simplify

- Move fields which are only used in one location

* Create new salesorder with context

- Also consolidate translated strings
- Also improve consistency of inline rendering (with missing image)

* Revert change to RenderInlineModel

* Fix for build table

- Use apiUrl wrapper around ApiEndpoint

* Fix parameter for PurchaseOrderTable

* Adjust server selector

- Only show localhost:8000 if in dev mode

* Tweak URL

* Add extra test to playground

- Check initial value works for nested field

* Cleanup playground

* Cleanup unused vars

* memoize fields

* Fix typo

host -> host

* Fix part editing

* Cleanup unused

* update group table
This commit is contained in:
Oliver 2024-02-01 00:38:59 +11:00 committed by GitHub
parent 2557383892
commit 7fe8207463
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1418 additions and 1060 deletions

View File

@ -253,6 +253,12 @@ class GetAuthToken(APIView):
# User is authenticated, and requesting a token against the provided name. # User is authenticated, and requesting a token against the provided name.
token = ApiToken.objects.create(user=request.user, name=name) token = ApiToken.objects.create(user=request.user, name=name)
logger.info(
"Created new API token for user '%s' (name='%s')",
user.username,
name,
)
# Add some metadata about the request # Add some metadata about the request
token.set_metadata('user_agent', request.META.get('HTTP_USER_AGENT', '')) token.set_metadata('user_agent', request.META.get('HTTP_USER_AGENT', ''))
token.set_metadata('remote_addr', request.META.get('REMOTE_ADDR', '')) token.set_metadata('remote_addr', request.META.get('REMOTE_ADDR', ''))
@ -263,10 +269,6 @@ class GetAuthToken(APIView):
data = {'token': token.key, 'name': token.name, 'expiry': token.expiry} data = {'token': token.key, 'name': token.name, 'expiry': token.expiry}
logger.info(
"Created new API token for user '%s' (name='%s')", user.username, name
)
# Ensure that the users session is logged in (PUI -> CUI login) # Ensure that the users session is logged in (PUI -> CUI login)
if not get_user(request).is_authenticated: if not get_user(request).is_authenticated:
login(request, user) login(request, user)

View File

@ -14,6 +14,7 @@ import { useCallback, useEffect, useMemo } from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { import {
FieldValues, FieldValues,
FormProvider,
SubmitErrorHandler, SubmitErrorHandler,
SubmitHandler, SubmitHandler,
useForm useForm
@ -65,6 +66,7 @@ export interface ApiFormProps {
pathParams?: PathParams; pathParams?: PathParams;
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
fields?: ApiFormFieldSet; fields?: ApiFormFieldSet;
initialData?: FieldValues;
submitText?: string; submitText?: string;
submitColor?: string; submitColor?: string;
fetchInitialData?: boolean; fetchInitialData?: boolean;
@ -146,6 +148,13 @@ export function OptionsApiForm({
field: v, field: v,
definition: data?.[k] definition: data?.[k]
}); });
// If the user has specified initial data, use that value here
let value = _props?.initialData?.[k];
if (value) {
_props.fields[k].value = value;
}
} }
return _props; return _props;
@ -163,13 +172,23 @@ export function OptionsApiForm({
* based on an API endpoint. * based on an API endpoint.
*/ */
export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) { export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
const defaultValues: FieldValues = useMemo( const defaultValues: FieldValues = useMemo(() => {
() => let defaultValuesMap = mapFields(props.fields ?? {}, (_path, field) => {
mapFields(props.fields ?? {}, (_path, field) => { return field.value ?? field.default ?? undefined;
return field.default ?? undefined; });
}),
[props.fields] // If the user has specified initial data, use that instead
); if (props.initialData) {
defaultValuesMap = {
...defaultValuesMap,
...props.initialData
};
}
// Update the form values, but only for the fields specified for this form
return defaultValuesMap;
}, [props.fields, props.initialData]);
// Form errors which are not associated with a specific field // Form errors which are not associated with a specific field
const [nonFieldErrors, setNonFieldErrors] = useState<string[]>([]); const [nonFieldErrors, setNonFieldErrors] = useState<string[]>([]);
@ -179,6 +198,7 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
criteriaMode: 'all', criteriaMode: 'all',
defaultValues defaultValues
}); });
const { const {
isValid, isValid,
isDirty, isDirty,
@ -390,16 +410,18 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
{props.preFormWarning} {props.preFormWarning}
</Alert> </Alert>
)} )}
<Stack spacing="xs"> <FormProvider {...form}>
{Object.entries(props.fields ?? {}).map(([fieldName, field]) => ( <Stack spacing="xs">
<ApiFormField {Object.entries(props.fields ?? {}).map(([fieldName, field]) => (
key={fieldName} <ApiFormField
fieldName={fieldName} key={fieldName}
definition={field} fieldName={fieldName}
control={form.control} definition={field}
/> control={form.control}
))} />
</Stack> ))}
</Stack>
</FormProvider>
{props.postFormContent} {props.postFormContent}
</Stack> </Stack>
<Divider /> <Divider />

View File

@ -7,7 +7,6 @@ import {
Switch, Switch,
TextInput TextInput
} from '@mantine/core'; } from '@mantine/core';
import { DateInput } from '@mantine/dates';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { useId } from '@mantine/hooks'; import { useId } from '@mantine/hooks';
import { IconX } from '@tabler/icons-react'; import { IconX } from '@tabler/icons-react';
@ -17,11 +16,17 @@ import { Control, FieldValues, useController } from 'react-hook-form';
import { ModelType } from '../../../enums/ModelType'; import { ModelType } from '../../../enums/ModelType';
import { ChoiceField } from './ChoiceField'; import { ChoiceField } from './ChoiceField';
import DateField from './DateField';
import { NestedObjectField } from './NestedObjectField'; import { NestedObjectField } from './NestedObjectField';
import { RelatedModelField } from './RelatedModelField'; import { RelatedModelField } from './RelatedModelField';
export type ApiFormData = UseFormReturnType<Record<string, unknown>>; export type ApiFormData = UseFormReturnType<Record<string, unknown>>;
export type ApiFormAdjustFilterType = {
filters: any;
data: FieldValues;
};
/** Definition of the ApiForm field component. /** Definition of the ApiForm field component.
* - The 'name' attribute *must* be provided * - The 'name' attribute *must* be provided
* - All other attributes are optional, and may be provided by the API * - All other attributes are optional, and may be provided by the API
@ -80,7 +85,7 @@ export type ApiFormFieldType = {
preFieldContent?: JSX.Element; preFieldContent?: JSX.Element;
postFieldContent?: JSX.Element; postFieldContent?: JSX.Element;
onValueChange?: (value: any) => void; onValueChange?: (value: any) => void;
adjustFilters?: (filters: any) => any; adjustFilters?: (value: ApiFormAdjustFilterType) => any;
}; };
/** /**
@ -207,20 +212,7 @@ export function ApiFormField({
/> />
); );
case 'date': case 'date':
return ( return <DateField controller={controller} definition={definition} />;
<DateInput
{...reducedDefinition}
ref={ref}
id={fieldId}
radius="sm"
type={undefined}
error={error?.message}
value={value}
clearable={!definition.required}
onChange={(value) => onChange(value)}
valueFormat="YYYY-MM-DD"
/>
);
case 'integer': case 'integer':
case 'decimal': case 'decimal':
case 'float': case 'float':

View File

@ -58,6 +58,10 @@ export function ChoiceField({
onChange={onChange} onChange={onChange}
data={choices} data={choices}
value={field.value} value={field.value}
label={definition.label}
description={definition.description}
placeholder={definition.placeholder}
icon={definition.icon}
withinPortal={true} withinPortal={true}
/> />
); );

View File

@ -0,0 +1,60 @@
import { DateInput } from '@mantine/dates';
import { useCallback, useId, useMemo } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form';
import { ApiFormFieldType } from './ApiFormField';
export default function DateField({
controller,
definition
}: {
controller: UseControllerReturn<FieldValues, any>;
definition: ApiFormFieldType;
}) {
const fieldId = useId();
const {
field,
fieldState: { error }
} = controller;
const onChange = useCallback(
(value: any) => {
// Convert the returned date object to a string
if (value) {
value = value.toString();
let date = new Date(value);
value = date.toISOString().split('T')[0];
}
field.onChange(value);
definition.onValueChange?.(value);
},
[field.onChange, definition]
);
const dateValue = useMemo(() => {
if (field.value) {
return new Date(field.value);
} else {
return undefined;
}
}, [field.value]);
return (
<DateInput
id={fieldId}
radius="sm"
type={undefined}
error={error?.message}
value={dateValue}
clearable={!definition.required}
onChange={onChange}
valueFormat="YYYY-MM-DD"
label={definition.label}
description={definition.description}
placeholder={definition.placeholder}
icon={definition.icon}
/>
);
}

View File

@ -4,7 +4,11 @@ import { useDebouncedValue } from '@mantine/hooks';
import { useId } from '@mantine/hooks'; import { useId } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form'; import {
FieldValues,
UseControllerReturn,
useFormContext
} from 'react-hook-form';
import Select from 'react-select'; import Select from 'react-select';
import { api } from '../../../App'; import { api } from '../../../App';
@ -32,6 +36,8 @@ export function RelatedModelField({
fieldState: { error } fieldState: { error }
} = controller; } = controller;
const form = useFormContext();
// Keep track of the primary key value for this field // Keep track of the primary key value for this field
const [pk, setPk] = useState<number | null>(null); const [pk, setPk] = useState<number | null>(null);
@ -40,6 +46,8 @@ export function RelatedModelField({
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const dataRef = useRef<any[]>([]); const dataRef = useRef<any[]>([]);
const [isOpen, setIsOpen] = useState<boolean>(false);
// If an initial value is provided, load from the API // If an initial value is provided, load from the API
useEffect(() => { useEffect(() => {
// If the value is unchanged, do nothing // If the value is unchanged, do nothing
@ -71,28 +79,49 @@ export function RelatedModelField({
const [value, setValue] = useState<string>(''); const [value, setValue] = useState<string>('');
const [searchText, cancelSearchText] = useDebouncedValue(value, 250); const [searchText, cancelSearchText] = useDebouncedValue(value, 250);
const [filters, setFilters] = useState<any>({});
const resetSearch = useCallback(() => {
setOffset(0);
setData([]);
dataRef.current = [];
}, []);
// reset current data on search value change // reset current data on search value change
useEffect(() => { useEffect(() => {
dataRef.current = []; resetSearch();
setData([]); }, [searchText, filters]);
}, [searchText]);
const selectQuery = useQuery({ const selectQuery = useQuery({
enabled: !definition.disabled && !!definition.api_url && !definition.hidden, enabled:
isOpen &&
!definition.disabled &&
!!definition.api_url &&
!definition.hidden,
queryKey: [`related-field-${fieldName}`, fieldId, offset, searchText], queryKey: [`related-field-${fieldName}`, fieldId, offset, searchText],
queryFn: async () => { queryFn: async () => {
if (!definition.api_url) { if (!definition.api_url) {
return null; return null;
} }
let filters = definition.filters ?? {}; let _filters = definition.filters ?? {};
if (definition.adjustFilters) { if (definition.adjustFilters) {
filters = definition.adjustFilters(filters); _filters =
definition.adjustFilters({
filters: _filters,
data: form.getValues()
}) ?? _filters;
}
// If the filters have changed, clear the data
if (_filters != filters) {
resetSearch();
setFilters(_filters);
} }
let params = { let params = {
...filters, ..._filters,
search: searchText, search: searchText,
offset: offset, offset: offset,
limit: limit limit: limit
@ -189,16 +218,19 @@ export function RelatedModelField({
filterOption={null} filterOption={null}
onInputChange={(value: any) => { onInputChange={(value: any) => {
setValue(value); setValue(value);
setOffset(0); resetSearch();
setData([]);
}} }}
onChange={onChange} onChange={onChange}
onMenuScrollToBottom={() => setOffset(offset + limit)} onMenuScrollToBottom={() => setOffset(offset + limit)}
onMenuOpen={() => { onMenuOpen={() => {
setIsOpen(true);
setValue(''); setValue('');
setOffset(0); resetSearch();
selectQuery.refetch(); selectQuery.refetch();
}} }}
onMenuClose={() => {
setIsOpen(false);
}}
isLoading={ isLoading={
selectQuery.isFetching || selectQuery.isFetching ||
selectQuery.isLoading || selectQuery.isLoading ||

View File

@ -7,7 +7,6 @@ import { RenderInlineModel } from './Instance';
*/ */
export function RenderAddress({ instance }: { instance: any }): ReactNode { export function RenderAddress({ instance }: { instance: any }): ReactNode {
let text = [ let text = [
instance.title,
instance.country, instance.country,
instance.postal_code, instance.postal_code,
instance.postal_city, instance.postal_city,
@ -18,12 +17,7 @@ export function RenderAddress({ instance }: { instance: any }): ReactNode {
.filter(Boolean) .filter(Boolean)
.join(', '); .join(', ');
return ( return <RenderInlineModel primary={instance.title} secondary={text} />;
<RenderInlineModel
primary={instance.description}
secondary={instance.address}
/>
);
} }
/** /**

View File

@ -5,9 +5,14 @@ import { useNavigate } from 'react-router-dom';
import { renderDate } from '../../../defaults/formatters'; import { renderDate } from '../../../defaults/formatters';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType'; import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles';
import { buildOrderFields } from '../../../forms/BuildForms';
import { getDetailUrl } from '../../../functions/urls'; import { getDetailUrl } from '../../../functions/urls';
import { useCreateApiFormModal } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { PartHoverCard } from '../../images/Thumbnail'; import { PartHoverCard } from '../../images/Thumbnail';
import { ProgressBar } from '../../items/ProgressBar'; import { ProgressBar } from '../../items/ProgressBar';
import { RenderUser } from '../../render/User'; import { RenderUser } from '../../render/User';
@ -88,7 +93,15 @@ function buildOrderTableColumns(): TableColumn[] {
/* /*
* Construct a table of build orders, according to the provided parameters * Construct a table of build orders, according to the provided parameters
*/ */
export function BuildOrderTable({ params = {} }: { params?: any }) { export function BuildOrderTable({
partId,
parentBuildId,
salesOrderId
}: {
partId?: number;
parentBuildId?: number;
salesOrderId?: number;
}) {
const tableColumns = useMemo(() => buildOrderTableColumns(), []); const tableColumns = useMemo(() => buildOrderTableColumns(), []);
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
@ -130,23 +143,56 @@ export function BuildOrderTable({ params = {} }: { params?: any }) {
}, []); }, []);
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserState();
const table = useTable('buildorder'); const table = useTable('buildorder');
const newBuild = useCreateApiFormModal({
url: ApiEndpoints.build_order_list,
title: t`Add Build Order`,
fields: buildOrderFields(),
initialData: {
part: partId,
sales_order: salesOrderId,
parent: parentBuildId
},
onFormSuccess: (data: any) => {
if (data.pk) {
navigate(getDetailUrl(ModelType.build, data.pk));
}
}
});
const tableActions = useMemo(() => {
return [
<AddItemButton
hidden={!user.hasAddRole(UserRoles.build)}
tooltip={t`Add Build Order`}
onClick={() => newBuild.open()}
/>
];
}, [user]);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.build_order_list)} {newBuild.modal}
tableState={table} <InvenTreeTable
columns={tableColumns} url={apiUrl(ApiEndpoints.build_order_list)}
props={{ tableState={table}
enableDownload: true, columns={tableColumns}
params: { props={{
...params, enableDownload: true,
part_detail: true params: {
}, part: partId,
tableFilters: tableFilters, sales_order: salesOrderId,
onRowClick: (row) => navigate(getDetailUrl(ModelType.build, row.pk)) parent: parentBuildId,
}} part_detail: true
/> },
tableActions: tableActions,
tableFilters: tableFilters,
onRowClick: (row) => navigate(getDetailUrl(ModelType.build, row.pk))
}}
/>
</>
); );
} }

View File

@ -1,18 +1,18 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { addressFields } from '../../../forms/CompanyForms';
import { import {
openCreateApiForm, useCreateApiFormModal,
openDeleteApiForm, useDeleteApiFormModal,
openEditApiForm useEditApiFormModal
} from '../../../functions/forms'; } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { ApiFormFieldSet } from '../../forms/fields/ApiFormField';
import { YesNoButton } from '../../items/YesNoButton'; import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
@ -108,6 +108,54 @@ export function AddressTable({
]; ];
}, []); }, []);
const addressFields: ApiFormFieldSet = useMemo(() => {
return {
company: {},
title: {},
primary: {},
line1: {},
line2: {},
postal_code: {},
postal_city: {},
province: {},
country: {},
shipping_notes: {},
internal_shipping_notes: {},
link: {}
};
}, []);
const newAddress = useCreateApiFormModal({
url: ApiEndpoints.address_list,
title: t`Add Address`,
fields: addressFields,
initialData: {
company: companyId
},
successMessage: t`Address created`,
onFormSuccess: table.refreshTable
});
const [selectedAddress, setSelectedAddress] = useState<number | undefined>(
undefined
);
const editAddress = useEditApiFormModal({
url: ApiEndpoints.address_list,
pk: selectedAddress,
title: t`Edit Address`,
fields: addressFields,
onFormSuccess: table.refreshTable
});
const deleteAddress = useDeleteApiFormModal({
url: ApiEndpoints.address_list,
pk: selectedAddress,
title: t`Delete Address`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to delete this address?`
});
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
let can_edit = let can_edit =
@ -122,27 +170,15 @@ export function AddressTable({
RowEditAction({ RowEditAction({
hidden: !can_edit, hidden: !can_edit,
onClick: () => { onClick: () => {
openEditApiForm({ setSelectedAddress(record.pk);
url: ApiEndpoints.address_list, editAddress.open();
pk: record.pk,
title: t`Edit Address`,
fields: addressFields(),
successMessage: t`Address updated`,
onFormSuccess: table.refreshTable
});
} }
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !can_delete, hidden: !can_delete,
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedAddress(record.pk);
url: ApiEndpoints.address_list, deleteAddress.open();
pk: record.pk,
title: t`Delete Address`,
successMessage: t`Address deleted`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to delete this address?`
});
} }
}) })
]; ];
@ -150,20 +186,6 @@ export function AddressTable({
[user] [user]
); );
const addAddress = useCallback(() => {
let fields = addressFields();
fields['company'].value = companyId;
openCreateApiForm({
url: ApiEndpoints.address_list,
title: t`Add Address`,
fields: fields,
successMessage: t`Address created`,
onFormSuccess: table.refreshTable
});
}, [companyId]);
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
let can_add = let can_add =
user.hasChangeRole(UserRoles.purchase_order) || user.hasChangeRole(UserRoles.purchase_order) ||
@ -172,25 +194,30 @@ export function AddressTable({
return [ return [
<AddItemButton <AddItemButton
tooltip={t`Add Address`} tooltip={t`Add Address`}
onClick={addAddress} onClick={() => newAddress.open()}
disabled={!can_add} disabled={!can_add}
/> />
]; ];
}, [user]); }, [user]);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.address_list)} {newAddress.modal}
tableState={table} {editAddress.modal}
columns={columns} {deleteAddress.modal}
props={{ <InvenTreeTable
rowActions: rowActions, url={apiUrl(ApiEndpoints.address_list)}
tableActions: tableActions, tableState={table}
params: { columns={columns}
...params, props={{
company: companyId rowActions: rowActions,
} tableActions: tableActions,
}} params: {
/> ...params,
company: companyId
}
}}
/>
</>
); );
} }

View File

@ -4,8 +4,13 @@ import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { companyFields } from '../../../forms/CompanyForms';
import { useCreateApiFormModal } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { DescriptionColumn } from '../ColumnRenderers'; import { DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
@ -24,6 +29,7 @@ export function CompanyTable({
const table = useTable('company'); const table = useTable('company');
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserState();
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
@ -53,22 +59,56 @@ export function CompanyTable({
]; ];
}, []); }, []);
const newCompany = useCreateApiFormModal({
url: ApiEndpoints.company_list,
title: t`New Company`,
fields: companyFields(),
initialData: params,
onFormSuccess: (response) => {
console.log('onFormSuccess:', response);
if (response.pk) {
let base = path ?? 'company';
navigate(`/${base}/${response.pk}`);
} else {
table.refreshTable();
}
}
});
const tableActions = useMemo(() => {
const can_add =
user.hasAddRole(UserRoles.purchase_order) ||
user.hasAddRole(UserRoles.sales_order);
return [
<AddItemButton
tooltip={t`Add Company`}
onClick={() => newCompany.open()}
hidden={!can_add}
/>
];
}, [user]);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.company_list)} {newCompany.modal}
tableState={table} <InvenTreeTable
columns={columns} url={apiUrl(ApiEndpoints.company_list)}
props={{ tableState={table}
params: { columns={columns}
...params props={{
}, params: {
onRowClick: (row: any) => { ...params
if (row.pk) { },
let base = path ?? 'company'; tableActions: tableActions,
navigate(`/${base}/${row.pk}`); onRowClick: (row: any) => {
if (row.pk) {
let base = path ?? 'company';
navigate(`/${base}/${row.pk}`);
}
} }
} }}
}} />
/> </>
); );
} }

View File

@ -1,18 +1,18 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { contactFields } from '../../../forms/CompanyForms';
import { import {
openCreateApiForm, useCreateApiFormModal,
openDeleteApiForm, useDeleteApiFormModal,
openEditApiForm useEditApiFormModal
} from '../../../functions/forms'; } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { ApiFormFieldSet } from '../../forms/fields/ApiFormField';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
import { RowDeleteAction, RowEditAction } from '../RowActions'; import { RowDeleteAction, RowEditAction } from '../RowActions';
@ -57,6 +57,45 @@ export function ContactTable({
]; ];
}, []); }, []);
const contactFields: ApiFormFieldSet = useMemo(() => {
return {
company: {},
name: {},
phone: {},
email: {},
role: {}
};
}, []);
const [selectedContact, setSelectedContact] = useState<number | undefined>(
undefined
);
const editContact = useEditApiFormModal({
url: ApiEndpoints.contact_list,
pk: selectedContact,
title: t`Edit Contact`,
fields: contactFields,
onFormSuccess: table.refreshTable
});
const newContact = useCreateApiFormModal({
url: ApiEndpoints.contact_list,
title: t`Add Contact`,
initialData: {
company: companyId
},
fields: contactFields,
onFormSuccess: table.refreshTable
});
const deleteContact = useDeleteApiFormModal({
url: ApiEndpoints.contact_list,
pk: selectedContact,
title: t`Delete Contact`,
onFormSuccess: table.refreshTable
});
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
let can_edit = let can_edit =
@ -70,27 +109,15 @@ export function ContactTable({
RowEditAction({ RowEditAction({
hidden: !can_edit, hidden: !can_edit,
onClick: () => { onClick: () => {
openEditApiForm({ setSelectedContact(record.pk);
url: ApiEndpoints.contact_list, editContact.open();
pk: record.pk,
title: t`Edit Contact`,
fields: contactFields(),
successMessage: t`Contact updated`,
onFormSuccess: table.refreshTable
});
} }
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !can_delete, hidden: !can_delete,
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedContact(record.pk);
url: ApiEndpoints.contact_list, deleteContact.open();
pk: record.pk,
title: t`Delete Contact`,
successMessage: t`Contact deleted`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to delete this contact?`
});
} }
}) })
]; ];
@ -98,20 +125,6 @@ export function ContactTable({
[user] [user]
); );
const addContact = useCallback(() => {
var fields = contactFields();
fields['company'].value = companyId;
openCreateApiForm({
url: ApiEndpoints.contact_list,
title: t`Create Contact`,
fields: fields,
successMessage: t`Contact created`,
onFormSuccess: table.refreshTable
});
}, [companyId]);
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
let can_add = let can_add =
user.hasAddRole(UserRoles.purchase_order) || user.hasAddRole(UserRoles.purchase_order) ||
@ -120,25 +133,30 @@ export function ContactTable({
return [ return [
<AddItemButton <AddItemButton
tooltip={t`Add contact`} tooltip={t`Add contact`}
onClick={addContact} onClick={() => newContact.open()}
disabled={!can_add} disabled={!can_add}
/> />
]; ];
}, [user]); }, [user]);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.contact_list)} {newContact.modal}
tableState={table} {editContact.modal}
columns={columns} {deleteContact.modal}
props={{ <InvenTreeTable
rowActions: rowActions, url={apiUrl(ApiEndpoints.contact_list)}
tableActions: tableActions, tableState={table}
params: { columns={columns}
...params, props={{
company: companyId rowActions: rowActions,
} tableActions: tableActions,
}} params: {
/> ...params,
company: companyId
}
}}
/>
</>
); );
} }

View File

@ -1,13 +1,16 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType'; import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { partCategoryFields } from '../../../forms/PartForms'; import { partCategoryFields } from '../../../forms/PartForms';
import { openCreateApiForm, openEditApiForm } from '../../../functions/forms';
import { getDetailUrl } from '../../../functions/urls'; import { getDetailUrl } from '../../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
@ -73,26 +76,33 @@ export function PartCategoryTable({ parentId }: { parentId?: any }) {
]; ];
}, []); }, []);
const addCategory = useCallback(() => { const newCategory = useCreateApiFormModal({
let fields = partCategoryFields({}); url: ApiEndpoints.category_list,
title: t`New Part Category`,
if (parentId) { fields: partCategoryFields({}),
fields['parent'].value = parentId; initialData: {
} parent: parentId
},
openCreateApiForm({ onFormSuccess(data: any) {
url: apiUrl(ApiEndpoints.category_list), if (data.pk) {
title: t`Add Part Category`, navigate(getDetailUrl(ModelType.partcategory, data.pk));
fields: fields, } else {
onFormSuccess(data: any) { table.refreshTable();
if (data.pk) {
navigate(`/part/category/${data.pk}`);
} else {
table.refreshTable();
}
} }
}); }
}, [parentId]); });
const [selectedCategory, setSelectedCategory] = useState<number | undefined>(
undefined
);
const editCategory = useEditApiFormModal({
url: ApiEndpoints.category_list,
pk: selectedCategory,
title: t`Edit Part Category`,
fields: partCategoryFields({}),
onFormSuccess: table.refreshTable
});
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
let can_add = user.hasAddRole(UserRoles.part_category); let can_add = user.hasAddRole(UserRoles.part_category);
@ -100,7 +110,7 @@ export function PartCategoryTable({ parentId }: { parentId?: any }) {
return [ return [
<AddItemButton <AddItemButton
tooltip={t`Add Part Category`} tooltip={t`Add Part Category`}
onClick={addCategory} onClick={() => newCategory.open()}
disabled={!can_add} disabled={!can_add}
/> />
]; ];
@ -114,14 +124,8 @@ export function PartCategoryTable({ parentId }: { parentId?: any }) {
RowEditAction({ RowEditAction({
hidden: !can_edit, hidden: !can_edit,
onClick: () => { onClick: () => {
openEditApiForm({ setSelectedCategory(record.pk);
url: ApiEndpoints.category_list, editCategory.open();
pk: record.pk,
title: t`Edit Part Category`,
fields: partCategoryFields({}),
successMessage: t`Part category updated`,
onFormSuccess: table.refreshTable
});
} }
}) })
]; ];
@ -130,21 +134,25 @@ export function PartCategoryTable({ parentId }: { parentId?: any }) {
); );
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.category_list)} {newCategory.modal}
tableState={table} {editCategory.modal}
columns={tableColumns} <InvenTreeTable
props={{ url={apiUrl(ApiEndpoints.category_list)}
enableDownload: true, tableState={table}
params: { columns={tableColumns}
parent: parentId ?? 'null' props={{
}, enableDownload: true,
tableFilters: tableFilters, params: {
tableActions: tableActions, parent: parentId ?? 'null'
rowActions: rowActions, },
onRowClick: (record, index, event) => tableFilters: tableFilters,
navigate(getDetailUrl(ModelType.partcategory, record.pk)) tableActions: tableActions,
}} rowActions: rowActions,
/> onRowClick: (record) =>
navigate(getDetailUrl(ModelType.partcategory, record.pk))
}}
/>
</>
); );
} }

View File

@ -1,18 +1,19 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { import {
openCreateApiForm, useCreateApiFormModal,
openDeleteApiForm, useDeleteApiFormModal,
openEditApiForm useEditApiFormModal
} from '../../../functions/forms'; } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { ApiFormFieldSet } from '../../forms/fields/ApiFormField';
import { YesNoButton } from '../../items/YesNoButton'; import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { PartColumn } from '../ColumnRenderers'; import { PartColumn } from '../ColumnRenderers';
@ -36,6 +37,12 @@ export function PartParameterTable({ partId }: { partId: any }) {
sortable: true, sortable: true,
render: (record: any) => PartColumn(record?.part_detail) render: (record: any) => PartColumn(record?.part_detail)
}, },
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: false,
switchable: true
},
{ {
accessor: 'name', accessor: 'name',
title: t`Parameter`, title: t`Parameter`,
@ -85,6 +92,43 @@ export function PartParameterTable({ partId }: { partId: any }) {
]; ];
}, [partId]); }, [partId]);
const partParameterFields: ApiFormFieldSet = useMemo(() => {
return {
part: {},
template: {},
data: {}
};
}, []);
const newParameter = useCreateApiFormModal({
url: ApiEndpoints.part_parameter_list,
title: t`New Part Parameter`,
fields: partParameterFields,
initialData: {
part: partId
},
onFormSuccess: table.refreshTable
});
const [selectedParameter, setSelectedParameter] = useState<
number | undefined
>(undefined);
const editParameter = useEditApiFormModal({
url: ApiEndpoints.part_parameter_list,
pk: selectedParameter,
title: t`Edit Part Parameter`,
fields: partParameterFields,
onFormSuccess: table.refreshTable
});
const deleteParameter = useDeleteApiFormModal({
url: ApiEndpoints.part_parameter_list,
pk: selectedParameter,
title: t`Delete Part Parameter`,
onFormSuccess: table.refreshTable
});
// Callback for row actions // Callback for row actions
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
@ -93,107 +137,65 @@ export function PartParameterTable({ partId }: { partId: any }) {
return []; return [];
} }
let actions = []; return [
actions.push(
RowEditAction({ RowEditAction({
tooltip: t`Edit Part Parameter`, tooltip: t`Edit Part Parameter`,
hidden: !user.hasChangeRole(UserRoles.part), hidden: !user.hasChangeRole(UserRoles.part),
onClick: () => { onClick: () => {
openEditApiForm({ setSelectedParameter(record.pk);
url: ApiEndpoints.part_parameter_list, editParameter.open();
pk: record.pk,
title: t`Edit Part Parameter`,
fields: {
part: {
hidden: true
},
template: {},
data: {}
},
successMessage: t`Part parameter updated`,
onFormSuccess: table.refreshTable
});
} }
}) }),
);
actions.push(
RowDeleteAction({ RowDeleteAction({
tooltip: t`Delete Part Parameter`, tooltip: t`Delete Part Parameter`,
hidden: !user.hasDeleteRole(UserRoles.part), hidden: !user.hasDeleteRole(UserRoles.part),
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedParameter(record.pk);
url: ApiEndpoints.part_parameter_list, deleteParameter.open();
pk: record.pk,
title: t`Delete Part Parameter`,
successMessage: t`Part parameter deleted`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to remove this parameter?`
});
} }
}) })
); ];
return actions;
}, },
[partId, user] [partId, user]
); );
const addParameter = useCallback(() => {
if (!partId) {
return;
}
openCreateApiForm({
url: ApiEndpoints.part_parameter_list,
title: t`Add Part Parameter`,
fields: {
part: {
hidden: true,
value: partId
},
template: {},
data: {}
},
successMessage: t`Part parameter added`,
onFormSuccess: table.refreshTable
});
}, [partId]);
// Custom table actions // Custom table actions
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
let actions = []; return [
<AddItemButton
// TODO: Hide if user does not have permission to edit parts hidden={!user.hasAddRole(UserRoles.part)}
actions.push( tooltip={t`Add parameter`}
<AddItemButton tooltip={t`Add parameter`} onClick={addParameter} /> onClick={() => newParameter.open()}
); />
];
return actions; }, [user]);
}, []);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.part_parameter_list)} {newParameter.modal}
tableState={table} {editParameter.modal}
columns={tableColumns} {deleteParameter.modal}
props={{ <InvenTreeTable
rowActions: rowActions, url={apiUrl(ApiEndpoints.part_parameter_list)}
tableActions: tableActions, tableState={table}
tableFilters: [ columns={tableColumns}
{ props={{
name: 'include_variants', rowActions: rowActions,
label: t`Include Variants`, tableActions: tableActions,
type: 'boolean' tableFilters: [
{
name: 'include_variants',
label: t`Include Variants`,
type: 'boolean'
}
],
params: {
part: partId,
template_detail: true,
part_detail: true
} }
], }}
params: { />
part: partId, </>
template_detail: true,
part_detail: true
}
}}
/>
); );
} }

View File

@ -1,18 +1,18 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { partParameterTemplateFields } from '../../../forms/PartForms';
import { import {
openCreateApiForm, useCreateApiFormModal,
openDeleteApiForm, useDeleteApiFormModal,
openEditApiForm useEditApiFormModal
} from '../../../functions/forms'; } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { ApiFormFieldSet } from '../../forms/fields/ApiFormField';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers'; import { DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter'; import { TableFilter } from '../Filter';
@ -69,6 +69,42 @@ export default function PartParameterTemplateTable() {
]; ];
}, []); }, []);
const partParameterTemplateFields: ApiFormFieldSet = useMemo(() => {
return {
name: {},
description: {},
units: {},
choices: {},
checkbox: {}
};
}, []);
const newTemplate = useCreateApiFormModal({
url: ApiEndpoints.part_parameter_template_list,
title: t`Add Parameter Template`,
fields: partParameterTemplateFields,
onFormSuccess: table.refreshTable
});
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
undefined
);
const editTemplate = useEditApiFormModal({
url: ApiEndpoints.part_parameter_template_list,
pk: selectedTemplate,
title: t`Edit Parameter Template`,
fields: partParameterTemplateFields,
onFormSuccess: table.refreshTable
});
const deleteTemplate = useDeleteApiFormModal({
url: ApiEndpoints.part_parameter_template_list,
pk: selectedTemplate,
title: t`Delete Parameter Template`,
onFormSuccess: table.refreshTable
});
// Callback for row actions // Callback for row actions
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
@ -76,27 +112,15 @@ export default function PartParameterTemplateTable() {
RowEditAction({ RowEditAction({
hidden: !user.hasChangeRole(UserRoles.part), hidden: !user.hasChangeRole(UserRoles.part),
onClick: () => { onClick: () => {
openEditApiForm({ setSelectedTemplate(record.pk);
url: ApiEndpoints.part_parameter_template_list, editTemplate.open();
pk: record.pk,
title: t`Edit Parameter Template`,
fields: partParameterTemplateFields(),
successMessage: t`Parameter template updated`,
onFormSuccess: table.refreshTable
});
} }
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.part), hidden: !user.hasDeleteRole(UserRoles.part),
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedTemplate(record.pk);
url: ApiEndpoints.part_parameter_template_list, deleteTemplate.open();
pk: record.pk,
title: t`Delete Parameter Template`,
successMessage: t`Parameter template deleted`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to remove this parameter template?`
});
} }
}) })
]; ];
@ -104,36 +128,31 @@ export default function PartParameterTemplateTable() {
[user] [user]
); );
const addParameterTemplate = useCallback(() => {
openCreateApiForm({
url: ApiEndpoints.part_parameter_template_list,
title: t`Create Parameter Template`,
fields: partParameterTemplateFields(),
successMessage: t`Parameter template created`,
onFormSuccess: table.refreshTable
});
}, []);
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
return [ return [
<AddItemButton <AddItemButton
tooltip={t`Add parameter template`} tooltip={t`Add parameter template`}
onClick={addParameterTemplate} onClick={() => newTemplate.open()}
disabled={!user.hasAddRole(UserRoles.part)} disabled={!user.hasAddRole(UserRoles.part)}
/> />
]; ];
}, [user]); }, [user]);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.part_parameter_template_list)} {newTemplate.modal}
tableState={table} {editTemplate.modal}
columns={tableColumns} {deleteTemplate.modal}
props={{ <InvenTreeTable
rowActions: rowActions, url={apiUrl(ApiEndpoints.part_parameter_template_list)}
tableFilters: tableFilters, tableState={table}
tableActions: tableActions columns={tableColumns}
}} props={{
/> rowActions: rowActions,
tableFilters: tableFilters,
tableActions: tableActions
}}
/>
</>
); );
} }

View File

@ -1,18 +1,18 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { partTestTemplateFields } from '../../../forms/PartForms';
import { import {
openCreateApiForm, useCreateApiFormModal,
openDeleteApiForm, useDeleteApiFormModal,
openEditApiForm useEditApiFormModal
} from '../../../functions/forms'; } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { ApiFormFieldSet } from '../../forms/fields/ApiFormField';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers'; import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter'; import { TableFilter } from '../Filter';
@ -69,6 +69,48 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
]; ];
}, []); }, []);
const partTestTemplateFields: ApiFormFieldSet = useMemo(() => {
return {
part: {
hidden: true
},
test_name: {},
description: {},
required: {},
requires_value: {},
requires_attachment: {}
};
}, []);
const newTestTemplate = useCreateApiFormModal({
url: ApiEndpoints.part_test_template_list,
title: t`Add Test Template`,
fields: partTestTemplateFields,
initialData: {
part: partId
},
onFormSuccess: table.refreshTable
});
const [selectedTest, setSelectedTest] = useState<number | undefined>(
undefined
);
const editTestTemplate = useEditApiFormModal({
url: ApiEndpoints.part_test_template_list,
pk: selectedTest,
title: t`Edit Test Template`,
fields: partTestTemplateFields,
onFormSuccess: table.refreshTable
});
const deleteTestTemplate = useDeleteApiFormModal({
url: ApiEndpoints.part_test_template_list,
pk: selectedTest,
title: t`Delete Test Template`,
onFormSuccess: table.refreshTable
});
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
let can_edit = user.hasChangeRole(UserRoles.part); let can_edit = user.hasChangeRole(UserRoles.part);
@ -78,26 +120,15 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
RowEditAction({ RowEditAction({
hidden: !can_edit, hidden: !can_edit,
onClick: () => { onClick: () => {
openEditApiForm({ setSelectedTest(record.pk);
url: ApiEndpoints.part_test_template_list, editTestTemplate.open();
pk: record.pk,
title: t`Edit Test Template`,
fields: partTestTemplateFields(),
successMessage: t`Template updated`,
onFormSuccess: table.refreshTable
});
} }
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !can_delete, hidden: !can_delete,
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedTest(record.pk);
url: ApiEndpoints.part_test_template_list, deleteTestTemplate.open();
pk: record.pk,
title: t`Delete Test Template`,
successMessage: t`Test Template deleted`,
onFormSuccess: table.refreshTable
});
} }
}) })
]; ];
@ -105,45 +136,36 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
[user] [user]
); );
const addTestTemplate = useCallback(() => {
let fields = partTestTemplateFields();
fields['part'].value = partId;
openCreateApiForm({
url: ApiEndpoints.part_test_template_list,
title: t`Create Test Template`,
fields: fields,
successMessage: t`Template created`,
onFormSuccess: table.refreshTable
});
}, [partId]);
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
let can_add = user.hasAddRole(UserRoles.part); let can_add = user.hasAddRole(UserRoles.part);
return [ return [
<AddItemButton <AddItemButton
tooltip={t`Add Test Template`} tooltip={t`Add Test Template`}
onClick={addTestTemplate} onClick={() => newTestTemplate.open()}
disabled={!can_add} disabled={!can_add}
/> />
]; ];
}, [user]); }, [user]);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.part_test_template_list)} {newTestTemplate.modal}
tableState={table} {editTestTemplate.modal}
columns={tableColumns} {deleteTestTemplate.modal}
props={{ <InvenTreeTable
params: { url={apiUrl(ApiEndpoints.part_test_template_list)}
part: partId tableState={table}
}, columns={tableColumns}
tableFilters: tableFilters, props={{
tableActions: tableActions, params: {
rowActions: rowActions part: partId
}} },
/> tableFilters: tableFilters,
tableActions: tableActions,
rowActions: rowActions
}}
/>
</>
); );
} }

View File

@ -1,15 +1,19 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ActionIcon, Group, Text, Tooltip } from '@mantine/core'; import { Group, Text } from '@mantine/core';
import { IconLayersLinked } from '@tabler/icons-react'; import { ReactNode, useCallback, useMemo, useState } from 'react';
import { ReactNode, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms'; import {
useCreateApiFormModal,
useDeleteApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { ApiFormFieldSet } from '../../forms/fields/ApiFormField';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
@ -66,55 +70,54 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
]; ];
}, [partId]); }, [partId]);
const addRelatedPart = useCallback(() => { const relatedPartFields: ApiFormFieldSet = useMemo(() => {
openCreateApiForm({ return {
title: t`Add Related Part`, part_1: {
url: ApiEndpoints.related_part_list, hidden: true
fields: {
part_1: {
hidden: true,
value: partId
},
part_2: {
label: t`Related Part`
}
}, },
successMessage: t`Related part added`, part_2: {}
onFormSuccess: table.refreshTable };
});
}, [partId]);
const customActions: ReactNode[] = useMemo(() => {
// TODO: Hide if user does not have permission to edit parts
let actions = [];
actions.push(
<Tooltip label={t`Add related part`}>
<ActionIcon radius="sm" onClick={addRelatedPart}>
<IconLayersLinked />
</ActionIcon>
</Tooltip>
);
return actions;
}, []); }, []);
// Generate row actions const newRelatedPart = useCreateApiFormModal({
// TODO: Hide if user does not have permission to edit parts url: ApiEndpoints.related_part_list,
title: t`Add Related Part`,
fields: relatedPartFields,
initialData: {
part_1: partId
},
onFormSuccess: table.refreshTable
});
const [selectedRelatedPart, setSelectedRelatedPart] = useState<
number | undefined
>(undefined);
const deleteRelatedPart = useDeleteApiFormModal({
url: ApiEndpoints.related_part_list,
pk: selectedRelatedPart,
title: t`Delete Related Part`,
onFormSuccess: table.refreshTable
});
const tableActions: ReactNode[] = useMemo(() => {
return [
<AddItemButton
tooltip={t`Add related part`}
hidden={!user.hasAddRole(UserRoles.part)}
onClick={() => newRelatedPart.open()}
/>
];
}, [user]);
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
return [ return [
RowDeleteAction({ RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.part), hidden: !user.hasDeleteRole(UserRoles.part),
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedRelatedPart(record.pk);
url: ApiEndpoints.related_part_list, deleteRelatedPart.open();
pk: record.pk,
title: t`Delete Related Part`,
successMessage: t`Related part deleted`,
preFormWarning: t`Are you sure you want to remove this relationship?`,
onFormSuccess: table.refreshTable
});
} }
}) })
]; ];
@ -123,18 +126,22 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
); );
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.related_part_list)} {newRelatedPart.modal}
tableState={table} {deleteRelatedPart.modal}
columns={tableColumns} <InvenTreeTable
props={{ url={apiUrl(ApiEndpoints.related_part_list)}
params: { tableState={table}
part: partId, columns={tableColumns}
category_detail: true props={{
}, params: {
rowActions: rowActions, part: partId,
tableActions: customActions category_detail: true
}} },
/> rowActions: rowActions,
tableActions: tableActions
}}
/>
</>
); );
} }

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
import { IconSquareArrowRight } from '@tabler/icons-react'; import { IconSquareArrowRight } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ProgressBar } from '../../../components/items/ProgressBar'; import { ProgressBar } from '../../../components/items/ProgressBar';
@ -9,8 +9,12 @@ import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType'; import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { purchaseOrderLineItemFields } from '../../../forms/PurchaseOrderForms'; import { purchaseOrderLineItemFields } from '../../../forms/PurchaseOrderForms';
import { openCreateApiForm, openEditApiForm } from '../../../functions/forms';
import { getDetailUrl } from '../../../functions/urls'; import { getDetailUrl } from '../../../functions/urls';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
@ -47,52 +51,6 @@ export function PurchaseOrderLineItemTable({
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserState(); const user = useUserState();
const rowActions = useCallback(
(record: any) => {
let received = (record?.received ?? 0) >= (record?.quantity ?? 0);
return [
{
hidden: received,
title: t`Receive line item`,
icon: <IconSquareArrowRight />,
color: 'green'
},
RowEditAction({
hidden: !user.hasAddRole(UserRoles.purchase_order),
onClick: () => {
let supplier = record?.supplier_part_detail?.supplier;
if (!supplier) {
return;
}
let fields = purchaseOrderLineItemFields({
supplierId: supplier,
create: false
});
openEditApiForm({
url: ApiEndpoints.purchase_order_line_list,
pk: record.pk,
title: t`Edit Line Item`,
fields: fields,
onFormSuccess: table.refreshTable,
successMessage: t`Line item updated`
});
}
}),
RowDuplicateAction({
hidden: !user.hasAddRole(UserRoles.purchase_order)
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order)
})
];
},
[orderId, user]
);
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
return [ return [
{ {
@ -220,18 +178,67 @@ export function PurchaseOrderLineItemTable({
]; ];
}, [orderId, user]); }, [orderId, user]);
const addLine = useCallback(() => { const newLine = useCreateApiFormModal({
openCreateApiForm({ url: ApiEndpoints.purchase_order_line_list,
url: ApiEndpoints.purchase_order_line_list, title: t`Add Line Item`,
title: t`Add Line Item`, fields: purchaseOrderLineItemFields(),
fields: purchaseOrderLineItemFields({ initialData: {
create: true, order: orderId
orderId: orderId },
}), onFormSuccess: table.refreshTable
onFormSuccess: table.refreshTable, });
successMessage: t`Line item added`
}); const [selectedLine, setSelectedLine] = useState<number | undefined>(
}, [orderId]); undefined
);
const editLine = useEditApiFormModal({
url: ApiEndpoints.purchase_order_line_list,
pk: selectedLine,
title: t`Edit Line Item`,
fields: purchaseOrderLineItemFields(),
onFormSuccess: table.refreshTable
});
const deleteLine = useDeleteApiFormModal({
url: ApiEndpoints.purchase_order_line_list,
pk: selectedLine,
title: t`Delete Line Item`,
onFormSuccess: table.refreshTable
});
const rowActions = useCallback(
(record: any) => {
let received = (record?.received ?? 0) >= (record?.quantity ?? 0);
return [
{
hidden: received,
title: t`Receive line item`,
icon: <IconSquareArrowRight />,
color: 'green'
},
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order),
onClick: () => {
setSelectedLine(record.pk);
editLine.open();
}
}),
RowDuplicateAction({
hidden: !user.hasAddRole(UserRoles.purchase_order)
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order),
onClick: () => {
setSelectedLine(record.pk);
deleteLine.open();
}
})
];
},
[orderId, user]
);
// Custom table actions // Custom table actions
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
@ -239,7 +246,7 @@ export function PurchaseOrderLineItemTable({
<AddItemButton <AddItemButton
key="add-line-item" key="add-line-item"
tooltip={t`Add line item`} tooltip={t`Add line item`}
onClick={addLine} onClick={() => newLine.open()}
hidden={!user?.hasAddRole(UserRoles.purchase_order)} hidden={!user?.hasAddRole(UserRoles.purchase_order)}
/>, />,
<ActionButton <ActionButton
@ -251,26 +258,31 @@ export function PurchaseOrderLineItemTable({
}, [orderId, user]); }, [orderId, user]);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.purchase_order_line_list)} {newLine.modal}
tableState={table} {editLine.modal}
columns={tableColumns} {deleteLine.modal}
props={{ <InvenTreeTable
enableSelection: true, url={apiUrl(ApiEndpoints.purchase_order_line_list)}
enableDownload: true, tableState={table}
params: { columns={tableColumns}
...params, props={{
order: orderId, enableSelection: true,
part_detail: true enableDownload: true,
}, params: {
rowActions: rowActions, ...params,
tableActions: tableActions, order: orderId,
onRowClick: (row: any) => { part_detail: true
if (row.part) { },
navigate(getDetailUrl(ModelType.supplierpart, row.part)); rowActions: rowActions,
tableActions: tableActions,
onRowClick: (row: any) => {
if (row.part) {
navigate(getDetailUrl(ModelType.supplierpart, row.part));
}
} }
} }}
}} />
/> </>
); );
} }

View File

@ -1,12 +1,13 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType'; import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { notYetImplemented } from '../../../functions/notifications'; import { purchaseOrderFields } from '../../../forms/PurchaseOrderForms';
import { getDetailUrl } from '../../../functions/urls'; import { getDetailUrl } from '../../../functions/urls';
import { useCreateApiFormModal } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
@ -34,7 +35,13 @@ import { InvenTreeTable } from '../InvenTreeTable';
/** /**
* Display a table of purchase orders * Display a table of purchase orders
*/ */
export function PurchaseOrderTable({ params }: { params?: any }) { export function PurchaseOrderTable({
supplierId,
supplierPartId
}: {
supplierId?: number;
supplierPartId?: number;
}) {
const navigate = useNavigate(); const navigate = useNavigate();
const table = useTable('purchase-order'); const table = useTable('purchase-order');
@ -56,10 +63,6 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
]; ];
}, []); }, []);
// TODO: Row actions
// TODO: Table actions (e.g. create new purchase order)
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
return [ return [
{ {
@ -100,38 +103,54 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
]; ];
}, []); }, []);
const addPurchaseOrder = useCallback(() => { const newPurchaseOrder = useCreateApiFormModal({
notYetImplemented(); url: ApiEndpoints.purchase_order_list,
}, []); title: t`Add Purchase Order`,
fields: purchaseOrderFields(),
initialData: {
supplier: supplierId
},
onFormSuccess: (response) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.purchaseorder, response.pk));
} else {
table.refreshTable();
}
}
});
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
return [ return [
<AddItemButton <AddItemButton
tooltip={t`Add Purchase Order`} tooltip={t`Add Purchase Order`}
onClick={addPurchaseOrder} onClick={() => newPurchaseOrder.open()}
hidden={!user.hasAddRole(UserRoles.purchase_order)} hidden={!user.hasAddRole(UserRoles.purchase_order)}
/> />
]; ];
}, [user]); }, [user]);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.purchase_order_list)} {newPurchaseOrder.modal}
tableState={table} <InvenTreeTable
columns={tableColumns} url={apiUrl(ApiEndpoints.purchase_order_list)}
props={{ tableState={table}
params: { columns={tableColumns}
...params, props={{
supplier_detail: true params: {
}, supplier_detail: true,
tableFilters: tableFilters, supplier: supplierId,
tableActions: tableActions, supplier_part: supplierPartId
onRowClick: (row: any) => { },
if (row.pk) { tableFilters: tableFilters,
navigate(getDetailUrl(ModelType.purchaseorder, row.pk)); tableActions: tableActions,
onRowClick: (row: any) => {
if (row.pk) {
navigate(getDetailUrl(ModelType.purchaseorder, row.pk));
}
} }
} }}
}} />
/> </>
); );
} }

View File

@ -1,12 +1,13 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType'; import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { notYetImplemented } from '../../../functions/notifications'; import { salesOrderFields } from '../../../forms/SalesOrderForms';
import { getDetailUrl } from '../../../functions/urls'; import { getDetailUrl } from '../../../functions/urls';
import { useCreateApiFormModal } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
@ -31,7 +32,13 @@ import {
} from '../Filter'; } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
export function SalesOrderTable({ params }: { params?: any }) { export function SalesOrderTable({
partId,
customerId
}: {
partId?: number;
customerId?: number;
}) {
const table = useTable('sales-order'); const table = useTable('sales-order');
const user = useUserState(); const user = useUserState();
@ -53,9 +60,31 @@ export function SalesOrderTable({ params }: { params?: any }) {
]; ];
}, []); }, []);
// TODO: Row actions const newSalesOrder = useCreateApiFormModal({
url: ApiEndpoints.sales_order_list,
title: t`Add Sales Order`,
fields: salesOrderFields(),
initialData: {
customer: customerId
},
onFormSuccess: (response) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.salesorder, response.pk));
} else {
table.refreshTable();
}
}
});
// TODO: Table actions (e.g. create new sales order) const tableActions = useMemo(() => {
return [
<AddItemButton
tooltip={t`Add Sales Order`}
onClick={() => newSalesOrder.open()}
hidden={!user.hasAddRole(UserRoles.sales_order)}
/>
];
}, [user]);
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
return [ return [
@ -97,38 +126,28 @@ export function SalesOrderTable({ params }: { params?: any }) {
]; ];
}, []); }, []);
const addSalesOrder = useCallback(() => {
notYetImplemented();
}, []);
const tableActions = useMemo(() => {
return [
<AddItemButton
tooltip={t`Add Sales Order`}
onClick={addSalesOrder}
hidden={!user.hasAddRole(UserRoles.sales_order)}
/>
];
}, [user]);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.sales_order_list)} {newSalesOrder.modal}
tableState={table} <InvenTreeTable
columns={tableColumns} url={apiUrl(ApiEndpoints.sales_order_list)}
props={{ tableState={table}
params: { columns={tableColumns}
...params, props={{
customer_detail: true params: {
}, customer_detail: true,
tableFilters: tableFilters, part: partId,
tableActions: tableActions, customer: customerId
onRowClick: (row: any) => { },
if (row.pk) { tableFilters: tableFilters,
navigate(getDetailUrl(ModelType.salesorder, row.pk)); tableActions: tableActions,
onRowClick: (row: any) => {
if (row.pk) {
navigate(getDetailUrl(ModelType.salesorder, row.pk));
}
} }
} }}
}} />
/> </>
); );
} }

View File

@ -1,13 +1,14 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { customUnitsFields } from '../../../forms/CommonForms';
import { import {
openCreateApiForm, useCreateApiFormModal,
openDeleteApiForm, useDeleteApiFormModal,
openEditApiForm useEditApiFormModal
} from '../../../functions/forms'; } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
@ -47,37 +48,47 @@ export default function CustomUnitsTable() {
]; ];
}, []); }, []);
const newUnit = useCreateApiFormModal({
url: ApiEndpoints.custom_unit_list,
title: t`Add Custom Unit`,
fields: customUnitsFields(),
onFormSuccess: table.refreshTable
});
const [selectedUnit, setSelectedUnit] = useState<number | undefined>(
undefined
);
const editUnit = useEditApiFormModal({
url: ApiEndpoints.custom_unit_list,
pk: selectedUnit,
title: t`Edit Custom Unit`,
fields: customUnitsFields(),
onFormSuccess: table.refreshTable
});
const deleteUnit = useDeleteApiFormModal({
url: ApiEndpoints.custom_unit_list,
pk: selectedUnit,
title: t`Delete Custom Unit`,
onFormSuccess: table.refreshTable
});
const rowActions = useCallback( const rowActions = useCallback(
(record: any): RowAction[] => { (record: any): RowAction[] => {
return [ return [
RowEditAction({ RowEditAction({
hidden: !user.hasChangeRole(UserRoles.admin), hidden: !user.hasChangeRole(UserRoles.admin),
onClick: () => { onClick: () => {
openEditApiForm({ setSelectedUnit(record.pk);
url: ApiEndpoints.custom_unit_list, editUnit.open();
pk: record.pk,
title: t`Edit custom unit`,
fields: {
name: {},
definition: {},
symbol: {}
},
onFormSuccess: table.refreshTable,
successMessage: t`Custom unit updated`
});
} }
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.admin), hidden: !user.hasDeleteRole(UserRoles.admin),
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedUnit(record.pk);
url: ApiEndpoints.custom_unit_list, deleteUnit.open();
pk: record.pk,
title: t`Delete custom unit`,
successMessage: t`Custom unit deleted`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to remove this custom unit?`
});
} }
}) })
]; ];
@ -85,40 +96,34 @@ export default function CustomUnitsTable() {
[user] [user]
); );
const addCustomUnit = useCallback(() => {
openCreateApiForm({
url: ApiEndpoints.custom_unit_list,
title: t`Add custom unit`,
fields: {
name: {},
definition: {},
symbol: {}
},
successMessage: t`Custom unit created`,
onFormSuccess: table.refreshTable
});
}, []);
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
let actions = []; let actions = [];
actions.push( actions.push(
// TODO: Adjust actions based on user permissions // TODO: Adjust actions based on user permissions
<AddItemButton tooltip={t`Add custom unit`} onClick={addCustomUnit} /> <AddItemButton
tooltip={t`Add custom unit`}
onClick={() => newUnit.open()}
/>
); );
return actions; return actions;
}, []); }, []);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.custom_unit_list)} {newUnit.modal}
tableState={table} {editUnit.modal}
columns={columns} {deleteUnit.modal}
props={{ <InvenTreeTable
rowActions: rowActions, url={apiUrl(ApiEndpoints.custom_unit_list)}
tableActions: tableActions tableState={table}
}} columns={columns}
/> props={{
rowActions: rowActions,
tableActions: tableActions
}}
/>
</>
); );
} }

View File

@ -1,10 +1,13 @@
import { Trans, t } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { Group, LoadingOverlay, Stack, Text, Title } from '@mantine/core'; import { Group, LoadingOverlay, Stack, Text, Title } from '@mantine/core';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms'; import {
useCreateApiFormModal,
useDeleteApiFormModal
} from '../../../hooks/UseForm';
import { useInstance } from '../../../hooks/UseInstance'; import { useInstance } from '../../../hooks/UseInstance';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
@ -109,28 +112,31 @@ export function GroupTable() {
}), }),
RowDeleteAction({ RowDeleteAction({
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedGroup(record.pk), deleteGroup.open();
url: ApiEndpoints.group_list,
pk: record.pk,
title: t`Delete group`,
successMessage: t`Group deleted`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to delete this group?`
});
} }
}) })
]; ];
}, []); }, []);
const addGroup = useCallback(() => { const [selectedGroup, setSelectedGroup] = useState<number | undefined>(
openCreateApiForm({ undefined
url: ApiEndpoints.group_list, );
title: t`Add group`,
fields: { name: {} }, const deleteGroup = useDeleteApiFormModal({
onFormSuccess: table.refreshTable, url: ApiEndpoints.group_list,
successMessage: t`Added group` pk: selectedGroup,
}); title: t`Delete group`,
}, []); successMessage: t`Group deleted`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to delete this group?`
});
const newGroup = useCreateApiFormModal({
url: ApiEndpoints.group_list,
title: t`Add group`,
fields: { name: {} },
onFormSuccess: table.refreshTable
});
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
let actions = []; let actions = [];
@ -138,7 +144,7 @@ export function GroupTable() {
actions.push( actions.push(
<AddItemButton <AddItemButton
key={'add-group'} key={'add-group'}
onClick={addGroup} onClick={() => newGroup.open()}
tooltip={t`Add group`} tooltip={t`Add group`}
/> />
); );
@ -148,6 +154,8 @@ export function GroupTable() {
return ( return (
<> <>
{newGroup.modal}
{deleteGroup.modal}
<DetailDrawer <DetailDrawer
title={t`Edit group`} title={t`Edit group`}
renderContent={(id) => { renderContent={(id) => {

View File

@ -1,13 +1,14 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { projectCodeFields } from '../../../forms/CommonForms';
import { import {
openCreateApiForm, useCreateApiFormModal,
openDeleteApiForm, useDeleteApiFormModal,
openEditApiForm useEditApiFormModal
} from '../../../functions/forms'; } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
@ -37,37 +38,47 @@ export default function ProjectCodeTable() {
]; ];
}, []); }, []);
const newProjectCode = useCreateApiFormModal({
url: ApiEndpoints.project_code_list,
title: t`Add Project Code`,
fields: projectCodeFields(),
onFormSuccess: table.refreshTable
});
const [selectedProjectCode, setSelectedProjectCode] = useState<
number | undefined
>(undefined);
const editProjectCode = useEditApiFormModal({
url: ApiEndpoints.project_code_list,
pk: selectedProjectCode,
title: t`Edit Project Code`,
fields: projectCodeFields(),
onFormSuccess: table.refreshTable
});
const deleteProjectCode = useDeleteApiFormModal({
url: ApiEndpoints.project_code_list,
pk: selectedProjectCode,
title: t`Delete Project Code`,
onFormSuccess: table.refreshTable
});
const rowActions = useCallback( const rowActions = useCallback(
(record: any): RowAction[] => { (record: any): RowAction[] => {
return [ return [
RowEditAction({ RowEditAction({
hidden: !user.hasChangeRole(UserRoles.admin), hidden: !user.hasChangeRole(UserRoles.admin),
onClick: () => { onClick: () => {
openEditApiForm({ setSelectedProjectCode(record.pk);
url: ApiEndpoints.project_code_list, editProjectCode.open();
pk: record.pk,
title: t`Edit project code`,
fields: {
code: {},
description: {},
responsible: {}
},
onFormSuccess: table.refreshTable,
successMessage: t`Project code updated`
});
} }
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.admin), hidden: !user.hasDeleteRole(UserRoles.admin),
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedProjectCode(record.pk);
url: ApiEndpoints.project_code_list, deleteProjectCode.open();
pk: record.pk,
title: t`Delete project code`,
successMessage: t`Project code deleted`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to remove this project code?`
});
} }
}) })
]; ];
@ -75,39 +86,33 @@ export default function ProjectCodeTable() {
[user] [user]
); );
const addProjectCode = useCallback(() => {
openCreateApiForm({
url: ApiEndpoints.project_code_list,
title: t`Add project code`,
fields: {
code: {},
description: {},
responsible: {}
},
onFormSuccess: table.refreshTable,
successMessage: t`Added project code`
});
}, []);
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
let actions = []; let actions = [];
actions.push( actions.push(
<AddItemButton onClick={addProjectCode} tooltip={t`Add project code`} /> <AddItemButton
onClick={() => newProjectCode.open()}
tooltip={t`Add project code`}
/>
); );
return actions; return actions;
}, []); }, []);
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.project_code_list)} {newProjectCode.modal}
tableState={table} {editProjectCode.modal}
columns={columns} {deleteProjectCode.modal}
props={{ <InvenTreeTable
rowActions: rowActions, url={apiUrl(ApiEndpoints.project_code_list)}
tableActions: tableActions tableState={table}
}} columns={columns}
/> props={{
rowActions: rowActions,
tableActions: tableActions
}}
/>
</>
); );
} }

View File

@ -1,13 +1,16 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType'; import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles'; import { UserRoles } from '../../../enums/Roles';
import { stockLocationFields } from '../../../forms/StockForms'; import { stockLocationFields } from '../../../forms/StockForms';
import { openCreateApiForm, openEditApiForm } from '../../../functions/forms';
import { getDetailUrl } from '../../../functions/urls'; import { getDetailUrl } from '../../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable'; import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
@ -96,26 +99,33 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
]; ];
}, []); }, []);
const addLocation = useCallback(() => { const newLocation = useCreateApiFormModal({
let fields = stockLocationFields({}); url: ApiEndpoints.stock_location_list,
title: t`Add Stock Location`,
if (parentId) { fields: stockLocationFields({}),
fields['parent'].value = parentId; initialData: {
} parent: parentId
},
openCreateApiForm({ onFormSuccess(data: any) {
url: apiUrl(ApiEndpoints.stock_location_list), if (data.pk) {
title: t`Add Stock Location`, navigate(getDetailUrl(ModelType.stocklocation, data.pk));
fields: fields, } else {
onFormSuccess(data: any) { table.refreshTable();
if (data.pk) {
navigate(`/stock/location/${data.pk}`);
} else {
table.refreshTable();
}
} }
}); }
}, [parentId]); });
const [selectedLocation, setSelectedLocation] = useState<number | undefined>(
undefined
);
const editLocation = useEditApiFormModal({
url: ApiEndpoints.stock_location_list,
pk: selectedLocation,
title: t`Edit Stock Location`,
fields: stockLocationFields({}),
onFormSuccess: table.refreshTable
});
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
let can_add = user.hasAddRole(UserRoles.stock_location); let can_add = user.hasAddRole(UserRoles.stock_location);
@ -123,7 +133,7 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
return [ return [
<AddItemButton <AddItemButton
tooltip={t`Add Stock Location`} tooltip={t`Add Stock Location`}
onClick={addLocation} onClick={() => newLocation.open()}
disabled={!can_add} disabled={!can_add}
/> />
]; ];
@ -137,14 +147,8 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
RowEditAction({ RowEditAction({
hidden: !can_edit, hidden: !can_edit,
onClick: () => { onClick: () => {
openEditApiForm({ setSelectedLocation(record.pk);
url: ApiEndpoints.stock_location_list, editLocation.open();
pk: record.pk,
title: t`Edit Stock Location`,
fields: stockLocationFields({}),
successMessage: t`Stock location updated`,
onFormSuccess: table.refreshTable
});
} }
}) })
]; ];
@ -153,23 +157,26 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
); );
return ( return (
<InvenTreeTable <>
url={apiUrl(ApiEndpoints.stock_location_list)} {newLocation.modal}
tableState={table} {editLocation.modal}
columns={tableColumns} <InvenTreeTable
props={{ url={apiUrl(ApiEndpoints.stock_location_list)}
enableDownload: true, tableState={table}
params: { columns={tableColumns}
parent: parentId ?? 'null' props={{
}, enableDownload: true,
tableFilters: tableFilters, params: {
tableActions: tableActions, parent: parentId ?? 'null'
rowActions: rowActions, },
onRowClick: (record) => { tableFilters: tableFilters,
navigate(getDetailUrl(ModelType.stocklocation, record.pk)); tableActions: tableActions,
} rowActions: rowActions,
// TODO: allow for "tree view" with cascade onRowClick: (record) => {
}} navigate(getDetailUrl(ModelType.stocklocation, record.pk));
/> }
}}
/>
</>
); );
} }

View File

@ -0,0 +1,21 @@
import { IconUsers } from '@tabler/icons-react';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
export function projectCodeFields(): ApiFormFieldSet {
return {
code: {},
description: {},
responsible: {
icon: <IconUsers />
}
};
}
export function customUnitsFields(): ApiFormFieldSet {
return {
name: {},
definition: {},
symbol: {}
};
}

View File

@ -1,4 +1,3 @@
import { t } from '@lingui/macro';
import { import {
IconAt, IconAt,
IconCurrencyDollar, IconCurrencyDollar,
@ -12,8 +11,6 @@ import {
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { openEditApiForm } from '../functions/forms';
/** /**
* Field set for SupplierPart instance * Field set for SupplierPart instance
@ -131,54 +128,3 @@ export function companyFields(): ApiFormFieldSet {
is_customer: {} is_customer: {}
}; };
} }
/**
* Edit a company instance
*/
export function editCompany({
pk,
callback
}: {
pk: number;
callback?: () => void;
}) {
openEditApiForm({
title: t`Edit Company`,
url: ApiEndpoints.company_list,
pk: pk,
fields: companyFields(),
successMessage: t`Company updated`,
onFormSuccess: callback
});
}
export function contactFields(): ApiFormFieldSet {
return {
company: {
hidden: true
},
name: {},
phone: {},
email: {},
role: {}
};
}
export function addressFields(): ApiFormFieldSet {
return {
company: {
hidden: true
},
title: {},
primary: {},
line1: {},
line2: {},
postal_code: {},
postal_city: {},
province: {},
country: {},
shipping_notes: {},
internal_shipping_notes: {},
link: {}
};
}

View File

@ -2,8 +2,6 @@ import { t } from '@lingui/macro';
import { IconPackages } from '@tabler/icons-react'; import { IconPackages } from '@tabler/icons-react';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { openCreateApiForm, openEditApiForm } from '../functions/forms';
/** /**
* Construct a set of fields for creating / editing a Part instance * Construct a set of fields for creating / editing a Part instance
@ -98,39 +96,6 @@ export function partFields({
return fields; return fields;
} }
/**
* Launch a dialog to create a new Part instance
*/
export function createPart() {
openCreateApiForm({
title: t`Create Part`,
url: ApiEndpoints.part_list,
successMessage: t`Part created`,
fields: partFields({})
});
}
/**
* Launch a dialog to edit an existing Part instance
* @param part The ID of the part to edit
*/
export function editPart({
part_id,
callback
}: {
part_id: number;
callback?: () => void;
}) {
openEditApiForm({
title: t`Edit Part`,
url: ApiEndpoints.part_list,
pk: part_id,
fields: partFields({ editing: true }),
successMessage: t`Part updated`,
onFormSuccess: callback
});
}
/** /**
* Construct a set of fields for creating / editing a PartCategory instance * Construct a set of fields for creating / editing a PartCategory instance
*/ */
@ -154,26 +119,3 @@ export function partCategoryFields({}: {}): ApiFormFieldSet {
return fields; return fields;
} }
export function partParameterTemplateFields(): ApiFormFieldSet {
return {
name: {},
description: {},
units: {},
choices: {},
checkbox: {}
};
}
export function partTestTemplateFields(): ApiFormFieldSet {
return {
part: {
hidden: true
},
test_name: {},
description: {},
required: {},
requires_value: {},
requires_attachment: {}
};
}

View File

@ -1,46 +1,42 @@
import { import {
IconAddressBook,
IconCalendar, IconCalendar,
IconCoins, IconCoins,
IconCurrencyDollar, IconCurrencyDollar,
IconHash,
IconLink, IconLink,
IconList,
IconNotes, IconNotes,
IconSitemap IconSitemap,
IconUser,
IconUsers
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import {
ApiFormAdjustFilterType,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
/* /*
* Construct a set of fields for creating / editing a PurchaseOrderLineItem instance * Construct a set of fields for creating / editing a PurchaseOrderLineItem instance
*/ */
export function purchaseOrderLineItemFields({ export function purchaseOrderLineItemFields() {
supplierId,
orderId,
create = false
}: {
supplierId?: number;
orderId?: number;
create?: boolean;
}) {
let fields: ApiFormFieldSet = { let fields: ApiFormFieldSet = {
order: { order: {
filters: { filters: {
supplier_detail: true supplier_detail: true
}, },
value: orderId, hidden: true
hidden: create != true || orderId != undefined
}, },
part: { part: {
filters: { filters: {
part_detail: true, part_detail: true,
supplier_detail: true, supplier_detail: true
supplier: supplierId
}, },
adjustFilters: (filters: any) => { adjustFilters: (value: ApiFormAdjustFilterType) => {
// TODO: Filter by the supplier associated with the order // TODO: Adjust part based on the supplier associated with the supplier
return filters; return value.filters;
} }
// TODO: Custom onEdit callback (see purchase_order.js)
// TODO: secondary modal (see purchase_order.js)
}, },
quantity: {}, quantity: {},
reference: {}, reference: {},
@ -66,3 +62,52 @@ export function purchaseOrderLineItemFields({
return fields; return fields;
} }
/**
* Construct a set of fields for creating / editing a PurchaseOrder instance
*/
export function purchaseOrderFields(): ApiFormFieldSet {
return {
reference: {
icon: <IconHash />
},
description: {},
supplier: {
filters: {
is_supplier: true
}
},
supplier_reference: {},
project_code: {
icon: <IconList />
},
order_currency: {
icon: <IconCoins />
},
target_date: {
icon: <IconCalendar />
},
link: {},
contact: {
icon: <IconUser />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.supplier
};
}
},
address: {
icon: <IconAddressBook />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.supplier
};
}
},
responsible: {
icon: <IconUsers />
}
};
}

View File

@ -0,0 +1,44 @@
import { IconAddressBook, IconUser, IconUsers } from '@tabler/icons-react';
import {
ApiFormAdjustFilterType,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
export function salesOrderFields(): ApiFormFieldSet {
return {
reference: {},
description: {},
customer: {
filters: {
is_customer: true
}
},
customer_reference: {},
project_code: {},
order_currency: {},
target_date: {},
link: {},
contact: {
icon: <IconUser />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.customer
};
}
},
address: {
icon: <IconAddressBook />,
adjustFilters: (value: ApiFormAdjustFilterType) => {
return {
...value.filters,
company: value.data.customer
};
}
},
responsible: {
icon: <IconUsers />
}
};
}

View File

@ -107,7 +107,7 @@ export function useCreateStockItem() {
return useCreateApiFormModal({ return useCreateApiFormModal({
url: ApiEndpoints.stock_item_list, url: ApiEndpoints.stock_item_list,
fields: fields, fields: fields,
title: t`Create Stock Item` title: t`Add Stock Item`
}); });
} }

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Divider, Stack } from '@mantine/core'; import { Alert, Divider, Stack } from '@mantine/core';
import { useId } from '@mantine/hooks'; import { useId } from '@mantine/hooks';
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useMemo, useRef } from 'react';
@ -82,6 +82,8 @@ export function useCreateApiFormModal(props: ApiFormModalProps) {
const createProps = useMemo<ApiFormModalProps>( const createProps = useMemo<ApiFormModalProps>(
() => ({ () => ({
...props, ...props,
fetchInitialData: props.fetchInitialData ?? false,
successMessage: props.successMessage ?? t`Item Created`,
method: 'POST' method: 'POST'
}), }),
[props] [props]
@ -98,7 +100,8 @@ export function useEditApiFormModal(props: ApiFormModalProps) {
() => ({ () => ({
...props, ...props,
fetchInitialData: props.fetchInitialData ?? true, fetchInitialData: props.fetchInitialData ?? true,
method: 'PUT' successMessage: props.successMessage ?? t`Item Updated`,
method: 'PATCH'
}), }),
[props] [props]
); );
@ -116,6 +119,12 @@ export function useDeleteApiFormModal(props: ApiFormModalProps) {
method: 'DELETE', method: 'DELETE',
submitText: t`Delete`, submitText: t`Delete`,
submitColor: 'red', submitColor: 'red',
successMessage: props.successMessage ?? t`Item Deleted`,
preFormContent: props.preFormContent ?? (
<Alert
color={'red'}
>{t`Are you sure you want to delete this item?`}</Alert>
),
fields: {} fields: {}
}), }),
[props] [props]

View File

@ -42,6 +42,14 @@ window.INVENTREE_SETTINGS = {
host: `${window.location.origin}/`, host: `${window.location.origin}/`,
name: 'Current Server' name: 'Current Server'
}, },
...(IS_DEV
? {
'mantine-2j5j5j5j5': {
host: 'http://localhost:8000',
name: 'Localhost'
}
}
: {}),
...(IS_DEV_OR_DEMO ...(IS_DEV_OR_DEMO
? { ? {
'mantine-u56l5jt85': { 'mantine-u56l5jt85': {
@ -51,7 +59,11 @@ window.INVENTREE_SETTINGS = {
} }
: {}) : {})
}, },
default_server: IS_DEMO ? 'mantine-u56l5jt85' : 'mantine-cqj63coxn', default_server: IS_DEV
? 'mantine-2j5j5j5j5'
: IS_DEMO
? 'mantine-u56l5jt85'
: 'mantine-cqj63coxn',
show_server_selector: IS_DEV_OR_DEMO, show_server_selector: IS_DEV_OR_DEMO,
// merge in settings that are already set via django's spa_view or for development // merge in settings that are already set via django's spa_view or for development

View File

@ -10,42 +10,54 @@ import { StylishText } from '../../components/items/StylishText';
import { StatusRenderer } from '../../components/render/StatusRenderer'; import { StatusRenderer } from '../../components/render/StatusRenderer';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { import { partCategoryFields, partFields } from '../../forms/PartForms';
createPart,
editPart,
partCategoryFields,
partFields
} from '../../forms/PartForms';
import { useCreateStockItem } from '../../forms/StockForms'; import { useCreateStockItem } from '../../forms/StockForms';
import { import {
OpenApiFormProps, useCreateApiFormModal,
openCreateApiForm, useEditApiFormModal
openEditApiForm } from '../../hooks/UseForm';
} from '../../functions/forms';
import { useCreateApiFormModal } from '../../hooks/UseForm';
// Generate some example forms using the modal API forms interface // Generate some example forms using the modal API forms interface
const fields = partCategoryFields({}); const fields = partCategoryFields({});
function ApiFormsPlayground() { function ApiFormsPlayground() {
const editCategoryForm: OpenApiFormProps = { const editCategory = useEditApiFormModal({
url: ApiEndpoints.category_list, url: ApiEndpoints.category_list,
pk: 2, pk: 2,
title: 'Edit Category', title: 'Edit Category',
fields: fields fields: fields
}; });
const createAttachmentForm: OpenApiFormProps = { const newPart = useCreateApiFormModal({
url: ApiEndpoints.part_list,
title: 'Create Part',
fields: partFields({}),
initialData: {
description: 'A part created via the API'
}
});
const editPart = useEditApiFormModal({
url: ApiEndpoints.part_list,
pk: 1,
title: 'Edit Part',
fields: partFields({ editing: true })
});
const newAttachment = useCreateApiFormModal({
url: ApiEndpoints.part_attachment_list, url: ApiEndpoints.part_attachment_list,
title: 'Create Attachment', title: 'Create Attachment',
successMessage: 'Attachment uploaded',
fields: { fields: {
part: { part: {},
value: 1
},
attachment: {}, attachment: {},
comment: {} comment: {}
} },
}; initialData: {
part: 1
},
successMessage: 'Attachment uploaded'
});
const [active, setActive] = useState(true); const [active, setActive] = useState(true);
const [name, setName] = useState('Hello'); const [name, setName] = useState('Hello');
@ -73,6 +85,14 @@ function ApiFormsPlayground() {
url: ApiEndpoints.part_list, url: ApiEndpoints.part_list,
title: 'Create part', title: 'Create part',
fields: partFieldsState, fields: partFieldsState,
initialData: {
is_template: true,
virtual: true,
minimum_stock: 10,
description: 'An example part description',
keywords: 'apple, banana, carrottt',
'initial_supplier.sku': 'SKU-123'
},
preFormContent: ( preFormContent: (
<Button onClick={() => setName('Hello world')}> <Button onClick={() => setName('Hello world')}>
Set name="Hello world" Set name="Hello world"
@ -86,19 +106,20 @@ function ApiFormsPlayground() {
return ( return (
<Stack> <Stack>
<Group> <Group>
<Button onClick={() => createPart()}>Create New Part</Button> <Button onClick={() => newPart.open()}>Create New Part</Button>
<Button onClick={() => editPart({ part_id: 1 })}>Edit Part</Button> {newPart.modal}
<Button onClick={() => editPart.open()}>Edit Part</Button>
{editPart.modal}
<Button onClick={() => openCreateStockItem()}>Create Stock Item</Button> <Button onClick={() => openCreateStockItem()}>Create Stock Item</Button>
{createStockItemModal} {createStockItemModal}
<Button onClick={() => openEditApiForm(editCategoryForm)}> <Button onClick={() => editCategory.open()}>Edit Category</Button>
Edit Category {editCategory.modal}
</Button>
<Button onClick={() => openCreateApiForm(createAttachmentForm)}> <Button onClick={() => newAttachment.open()}>Create Attachment</Button>
Create Attachment {newAttachment.modal}
</Button>
<Button onClick={() => openCreatePart()}>Create Part new Modal</Button> <Button onClick={() => openCreatePart()}>Create Part new Modal</Button>
{createPartModal} {createPartModal}

View File

@ -14,12 +14,11 @@ import {
IconQrcode, IconQrcode,
IconSitemap IconSitemap
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react'; import { useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { import {
ActionDropdown, ActionDropdown,
DeleteItemAction,
DuplicateItemAction, DuplicateItemAction,
EditItemAction, EditItemAction,
LinkBarcodeAction, LinkBarcodeAction,
@ -36,8 +35,9 @@ import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { buildOrderFields } from '../../forms/BuildForms'; import { buildOrderFields } from '../../forms/BuildForms';
import { openEditApiForm } from '../../functions/forms'; import { useEditApiFormModal } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -152,12 +152,10 @@ export default function BuildDetail() {
name: 'child-orders', name: 'child-orders',
label: t`Child Build Orders`, label: t`Child Build Orders`,
icon: <IconSitemap />, icon: <IconSitemap />,
content: ( content: build.pk ? (
<BuildOrderTable <BuildOrderTable parentBuildId={build.pk} />
params={{ ) : (
parent: id <Skeleton />
}}
/>
) )
}, },
{ {
@ -187,24 +185,15 @@ export default function BuildDetail() {
]; ];
}, [build, id]); }, [build, id]);
const editBuildOrder = useCallback(() => { const editBuild = useEditApiFormModal({
let fields = buildOrderFields(); url: ApiEndpoints.build_order_list,
pk: build.pk,
// Cannot edit part field after creation title: t`Edit Build Order`,
fields['part'].hidden = true; fields: buildOrderFields(),
onFormSuccess: () => {
build.pk && refreshInstance();
openEditApiForm({ }
url: ApiEndpoints.build_order_list, });
pk: build.pk,
title: t`Edit Build Order`,
fields: fields,
successMessage: t`Build Order updated`,
onFormSuccess: () => {
refreshInstance();
}
});
}, [build]);
const buildActions = useMemo(() => { const buildActions = useMemo(() => {
// TODO: Disable certain actions based on user permissions // TODO: Disable certain actions based on user permissions
@ -241,10 +230,10 @@ export default function BuildDetail() {
icon={<IconDots />} icon={<IconDots />}
actions={[ actions={[
EditItemAction({ EditItemAction({
onClick: editBuildOrder onClick: () => editBuild.open(),
disabled: !user.hasChangeRole(UserRoles.build)
}), }),
DuplicateItemAction({}), DuplicateItemAction({})
DeleteItemAction({})
]} ]}
/> />
]; ];
@ -263,6 +252,7 @@ export default function BuildDetail() {
return ( return (
<> <>
{editBuild.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail <PageDetail

View File

@ -1,45 +1,17 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Button, Stack } from '@mantine/core'; import { Stack } from '@mantine/core';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { buildOrderFields } from '../../forms/BuildForms';
import { openCreateApiForm } from '../../functions/forms';
/** /**
* Build Order index page * Build Order index page
*/ */
export default function BuildIndex() { export default function BuildIndex() {
const navigate = useNavigate();
const newBuildOrder = useCallback(() => {
openCreateApiForm({
url: ApiEndpoints.build_order_list,
title: t`Add Build Order`,
fields: buildOrderFields(),
successMessage: t`Build order created`,
onFormSuccess: (data: any) => {
if (data.pk) {
navigate(`/build/${data.pk}`);
}
}
});
}, []);
return ( return (
<> <>
<Stack> <Stack>
<PageDetail <PageDetail title={t`Build Orders`} actions={[]} />
title={t`Build Orders`}
actions={[
<Button key="new-build" color="green" onClick={newBuildOrder}>
{t`New Build Order`}
</Button>
]}
/>
<BuildOrderTable /> <BuildOrderTable />
</Stack> </Stack>
</> </>

View File

@ -39,7 +39,8 @@ import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { editCompany } from '../../forms/CompanyForms'; import { companyFields } from '../../forms/CompanyForms';
import { useEditApiFormModal } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -98,9 +99,7 @@ export default function CompanyDetail(props: CompanyDetailProps) {
label: t`Purchase Orders`, label: t`Purchase Orders`,
icon: <IconShoppingCart />, icon: <IconShoppingCart />,
hidden: !company?.is_supplier, hidden: !company?.is_supplier,
content: company?.pk && ( content: company?.pk && <PurchaseOrderTable supplierId={company.pk} />
<PurchaseOrderTable params={{ supplier: company.pk }} />
)
}, },
{ {
name: 'stock-items', name: 'stock-items',
@ -116,9 +115,7 @@ export default function CompanyDetail(props: CompanyDetailProps) {
label: t`Sales Orders`, label: t`Sales Orders`,
icon: <IconTruckDelivery />, icon: <IconTruckDelivery />,
hidden: !company?.is_customer, hidden: !company?.is_customer,
content: company?.pk && ( content: company?.pk && <SalesOrderTable customerId={company.pk} />
<SalesOrderTable params={{ customer: company.pk }} />
)
}, },
{ {
name: 'return-orders', name: 'return-orders',
@ -179,6 +176,14 @@ export default function CompanyDetail(props: CompanyDetailProps) {
]; ];
}, [id, company]); }, [id, company]);
const editCompany = useEditApiFormModal({
url: ApiEndpoints.company_list,
pk: company?.pk,
title: t`Edit Company`,
fields: companyFields(),
onFormSuccess: refreshInstance
});
const companyActions = useMemo(() => { const companyActions = useMemo(() => {
return [ return [
<ActionDropdown <ActionDropdown
@ -188,14 +193,7 @@ export default function CompanyDetail(props: CompanyDetailProps) {
actions={[ actions={[
EditItemAction({ EditItemAction({
disabled: !user.hasChangeRole(UserRoles.purchase_order), disabled: !user.hasChangeRole(UserRoles.purchase_order),
onClick: () => { onClick: () => editCompany.open()
if (company?.pk) {
editCompany({
pk: company?.pk,
callback: refreshInstance
});
}
}
}), }),
DeleteItemAction({ DeleteItemAction({
disabled: !user.hasDeleteRole(UserRoles.purchase_order) disabled: !user.hasDeleteRole(UserRoles.purchase_order)
@ -206,16 +204,19 @@ export default function CompanyDetail(props: CompanyDetailProps) {
}, [id, company, user]); }, [id, company, user]);
return ( return (
<Stack spacing="xs"> <>
<LoadingOverlay visible={instanceQuery.isFetching} /> {editCompany.modal}
<PageDetail <Stack spacing="xs">
title={t`Company` + `: ${company.name}`} <LoadingOverlay visible={instanceQuery.isFetching} />
subtitle={company.description} <PageDetail
actions={companyActions} title={t`Company` + `: ${company.name}`}
imageUrl={company.image} subtitle={company.description}
breadcrumbs={props.breadcrumbs} actions={companyActions}
/> imageUrl={company.image}
<PanelGroup pageKey="company" panels={companyPanels} /> breadcrumbs={props.breadcrumbs}
</Stack> />
<PanelGroup pageKey="company" panels={companyPanels} />
</Stack>
</>
); );
} }

View File

@ -45,7 +45,7 @@ export default function SupplierPartDetail() {
label: t`Purchase Orders`, label: t`Purchase Orders`,
icon: <IconShoppingCart />, icon: <IconShoppingCart />,
content: supplierPart?.pk ? ( content: supplierPart?.pk ? (
<PurchaseOrderTable params={{ supplier_part: supplierPart.pk }} /> <PurchaseOrderTable supplierPartId={supplierPart.pk} />
) : ( ) : (
<Skeleton /> <Skeleton />
) )

View File

@ -63,7 +63,8 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { formatPriceRange } from '../../defaults/formatters'; import { formatPriceRange } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { editPart } from '../../forms/PartForms'; import { partFields } from '../../forms/PartForms';
import { useEditApiFormModal } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -504,14 +505,7 @@ export default function PartDetail() {
label: t`Build Orders`, label: t`Build Orders`,
icon: <IconTools />, icon: <IconTools />,
hidden: !part.assembly, hidden: !part.assembly,
content: ( content: part?.pk ? <BuildOrderTable partId={part.pk} /> : <Skeleton />
<BuildOrderTable
params={{
part_detail: true,
part: part.pk ?? -1
}}
/>
)
}, },
{ {
name: 'used_in', name: 'used_in',
@ -562,15 +556,7 @@ export default function PartDetail() {
label: t`Sales Orders`, label: t`Sales Orders`,
icon: <IconTruckDelivery />, icon: <IconTruckDelivery />,
hidden: !part.salable, hidden: !part.salable,
content: part.pk ? ( content: part.pk ? <SalesOrderTable partId={part.pk} /> : <Skeleton />
<SalesOrderTable
params={{
part: part.pk ?? -1
}}
/>
) : (
<Skeleton />
)
}, },
{ {
name: 'scheduling', name: 'scheduling',
@ -647,6 +633,14 @@ export default function PartDetail() {
); );
}, [part, id]); }, [part, id]);
const editPart = useEditApiFormModal({
url: ApiEndpoints.part_list,
pk: part.pk,
title: t`Edit Part`,
fields: partFields({ editing: true }),
onFormSuccess: refreshInstance
});
const partActions = useMemo(() => { const partActions = useMemo(() => {
// TODO: Disable actions based on user permissions // TODO: Disable actions based on user permissions
return [ return [
@ -685,13 +679,8 @@ export default function PartDetail() {
actions={[ actions={[
DuplicateItemAction({}), DuplicateItemAction({}),
EditItemAction({ EditItemAction({
onClick: () => { disabled: !user.hasChangeRole(UserRoles.part),
part.pk && onClick: () => editPart.open()
editPart({
part_id: part.pk,
callback: refreshInstance
});
}
}), }),
DeleteItemAction({ DeleteItemAction({
disabled: part?.active disabled: part?.active
@ -703,6 +692,7 @@ export default function PartDetail() {
return ( return (
<> <>
{editPart.modal}
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
<PartCategoryTree <PartCategoryTree

View File

@ -20,7 +20,6 @@ export default function PurchasingIndex() {
label: t`Purchase Orders`, label: t`Purchase Orders`,
icon: <IconShoppingCart />, icon: <IconShoppingCart />,
content: <PurchaseOrderTable /> content: <PurchaseOrderTable />
// TODO: Add optional "calendar" display here...
}, },
{ {
name: 'suppliers', name: 'suppliers',

View File

@ -62,11 +62,7 @@ export default function SalesOrderDetail() {
label: t`Build Orders`, label: t`Build Orders`,
icon: <IconTools />, icon: <IconTools />,
content: order?.pk ? ( content: order?.pk ? (
<BuildOrderTable <BuildOrderTable salesOrderId={order.pk} />
params={{
sales_order: order.pk
}}
/>
) : ( ) : (
<Skeleton /> <Skeleton />
) )