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.
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
token.set_metadata('user_agent', request.META.get('HTTP_USER_AGENT', ''))
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}
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)
if not get_user(request).is_authenticated:
login(request, user)

View File

@ -14,6 +14,7 @@ import { useCallback, useEffect, useMemo } from 'react';
import { useState } from 'react';
import {
FieldValues,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm
@ -65,6 +66,7 @@ export interface ApiFormProps {
pathParams?: PathParams;
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
fields?: ApiFormFieldSet;
initialData?: FieldValues;
submitText?: string;
submitColor?: string;
fetchInitialData?: boolean;
@ -146,6 +148,13 @@ export function OptionsApiForm({
field: v,
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;
@ -163,13 +172,23 @@ export function OptionsApiForm({
* based on an API endpoint.
*/
export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
const defaultValues: FieldValues = useMemo(
() =>
mapFields(props.fields ?? {}, (_path, field) => {
return field.default ?? undefined;
}),
[props.fields]
);
const defaultValues: FieldValues = useMemo(() => {
let defaultValuesMap = mapFields(props.fields ?? {}, (_path, field) => {
return field.value ?? field.default ?? undefined;
});
// 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
const [nonFieldErrors, setNonFieldErrors] = useState<string[]>([]);
@ -179,6 +198,7 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
criteriaMode: 'all',
defaultValues
});
const {
isValid,
isDirty,
@ -390,16 +410,18 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
{props.preFormWarning}
</Alert>
)}
<Stack spacing="xs">
{Object.entries(props.fields ?? {}).map(([fieldName, field]) => (
<ApiFormField
key={fieldName}
fieldName={fieldName}
definition={field}
control={form.control}
/>
))}
</Stack>
<FormProvider {...form}>
<Stack spacing="xs">
{Object.entries(props.fields ?? {}).map(([fieldName, field]) => (
<ApiFormField
key={fieldName}
fieldName={fieldName}
definition={field}
control={form.control}
/>
))}
</Stack>
</FormProvider>
{props.postFormContent}
</Stack>
<Divider />

View File

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

View File

@ -58,6 +58,10 @@ export function ChoiceField({
onChange={onChange}
data={choices}
value={field.value}
label={definition.label}
description={definition.description}
placeholder={definition.placeholder}
icon={definition.icon}
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 { useQuery } from '@tanstack/react-query';
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 { api } from '../../../App';
@ -32,6 +36,8 @@ export function RelatedModelField({
fieldState: { error }
} = controller;
const form = useFormContext();
// Keep track of the primary key value for this field
const [pk, setPk] = useState<number | null>(null);
@ -40,6 +46,8 @@ export function RelatedModelField({
const [data, setData] = useState<any[]>([]);
const dataRef = useRef<any[]>([]);
const [isOpen, setIsOpen] = useState<boolean>(false);
// If an initial value is provided, load from the API
useEffect(() => {
// If the value is unchanged, do nothing
@ -71,28 +79,49 @@ export function RelatedModelField({
const [value, setValue] = useState<string>('');
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
useEffect(() => {
dataRef.current = [];
setData([]);
}, [searchText]);
resetSearch();
}, [searchText, filters]);
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],
queryFn: async () => {
if (!definition.api_url) {
return null;
}
let filters = definition.filters ?? {};
let _filters = definition.filters ?? {};
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 = {
...filters,
..._filters,
search: searchText,
offset: offset,
limit: limit
@ -189,16 +218,19 @@ export function RelatedModelField({
filterOption={null}
onInputChange={(value: any) => {
setValue(value);
setOffset(0);
setData([]);
resetSearch();
}}
onChange={onChange}
onMenuScrollToBottom={() => setOffset(offset + limit)}
onMenuOpen={() => {
setIsOpen(true);
setValue('');
setOffset(0);
resetSearch();
selectQuery.refetch();
}}
onMenuClose={() => {
setIsOpen(false);
}}
isLoading={
selectQuery.isFetching ||
selectQuery.isLoading ||

View File

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

View File

@ -5,9 +5,14 @@ import { useNavigate } from 'react-router-dom';
import { renderDate } from '../../../defaults/formatters';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles';
import { buildOrderFields } from '../../../forms/BuildForms';
import { getDetailUrl } from '../../../functions/urls';
import { useCreateApiFormModal } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { PartHoverCard } from '../../images/Thumbnail';
import { ProgressBar } from '../../items/ProgressBar';
import { RenderUser } from '../../render/User';
@ -88,7 +93,15 @@ function buildOrderTableColumns(): TableColumn[] {
/*
* 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 tableFilters: TableFilter[] = useMemo(() => {
@ -130,23 +143,56 @@ export function BuildOrderTable({ params = {} }: { params?: any }) {
}, []);
const navigate = useNavigate();
const user = useUserState();
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 (
<InvenTreeTable
url={apiUrl(ApiEndpoints.build_order_list)}
tableState={table}
columns={tableColumns}
props={{
enableDownload: true,
params: {
...params,
part_detail: true
},
tableFilters: tableFilters,
onRowClick: (row) => navigate(getDetailUrl(ModelType.build, row.pk))
}}
/>
<>
{newBuild.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.build_order_list)}
tableState={table}
columns={tableColumns}
props={{
enableDownload: true,
params: {
part: partId,
sales_order: salesOrderId,
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 { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { addressFields } from '../../../forms/CompanyForms';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { ApiFormFieldSet } from '../../forms/fields/ApiFormField';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
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(
(record: any) => {
let can_edit =
@ -122,27 +170,15 @@ export function AddressTable({
RowEditAction({
hidden: !can_edit,
onClick: () => {
openEditApiForm({
url: ApiEndpoints.address_list,
pk: record.pk,
title: t`Edit Address`,
fields: addressFields(),
successMessage: t`Address updated`,
onFormSuccess: table.refreshTable
});
setSelectedAddress(record.pk);
editAddress.open();
}
}),
RowDeleteAction({
hidden: !can_delete,
onClick: () => {
openDeleteApiForm({
url: ApiEndpoints.address_list,
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?`
});
setSelectedAddress(record.pk);
deleteAddress.open();
}
})
];
@ -150,20 +186,6 @@ export function AddressTable({
[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(() => {
let can_add =
user.hasChangeRole(UserRoles.purchase_order) ||
@ -172,25 +194,30 @@ export function AddressTable({
return [
<AddItemButton
tooltip={t`Add Address`}
onClick={addAddress}
onClick={() => newAddress.open()}
disabled={!can_add}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiEndpoints.address_list)}
tableState={table}
columns={columns}
props={{
rowActions: rowActions,
tableActions: tableActions,
params: {
...params,
company: companyId
}
}}
/>
<>
{newAddress.modal}
{editAddress.modal}
{deleteAddress.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.address_list)}
tableState={table}
columns={columns}
props={{
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 { 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 { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
import { DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
@ -24,6 +29,7 @@ export function CompanyTable({
const table = useTable('company');
const navigate = useNavigate();
const user = useUserState();
const columns = useMemo(() => {
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 (
<InvenTreeTable
url={apiUrl(ApiEndpoints.company_list)}
tableState={table}
columns={columns}
props={{
params: {
...params
},
onRowClick: (row: any) => {
if (row.pk) {
let base = path ?? 'company';
navigate(`/${base}/${row.pk}`);
<>
{newCompany.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.company_list)}
tableState={table}
columns={columns}
props={{
params: {
...params
},
tableActions: tableActions,
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 { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { contactFields } from '../../../forms/CompanyForms';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { ApiFormFieldSet } from '../../forms/fields/ApiFormField';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
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(
(record: any) => {
let can_edit =
@ -70,27 +109,15 @@ export function ContactTable({
RowEditAction({
hidden: !can_edit,
onClick: () => {
openEditApiForm({
url: ApiEndpoints.contact_list,
pk: record.pk,
title: t`Edit Contact`,
fields: contactFields(),
successMessage: t`Contact updated`,
onFormSuccess: table.refreshTable
});
setSelectedContact(record.pk);
editContact.open();
}
}),
RowDeleteAction({
hidden: !can_delete,
onClick: () => {
openDeleteApiForm({
url: ApiEndpoints.contact_list,
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?`
});
setSelectedContact(record.pk);
deleteContact.open();
}
})
];
@ -98,20 +125,6 @@ export function ContactTable({
[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(() => {
let can_add =
user.hasAddRole(UserRoles.purchase_order) ||
@ -120,25 +133,30 @@ export function ContactTable({
return [
<AddItemButton
tooltip={t`Add contact`}
onClick={addContact}
onClick={() => newContact.open()}
disabled={!can_add}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiEndpoints.contact_list)}
tableState={table}
columns={columns}
props={{
rowActions: rowActions,
tableActions: tableActions,
params: {
...params,
company: companyId
}
}}
/>
<>
{newContact.modal}
{editContact.modal}
{deleteContact.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.contact_list)}
tableState={table}
columns={columns}
props={{
rowActions: rowActions,
tableActions: tableActions,
params: {
...params,
company: companyId
}
}}
/>
</>
);
}

View File

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

View File

@ -1,18 +1,19 @@
import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { ApiFormFieldSet } from '../../forms/fields/ApiFormField';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
import { PartColumn } from '../ColumnRenderers';
@ -36,6 +37,12 @@ export function PartParameterTable({ partId }: { partId: any }) {
sortable: true,
render: (record: any) => PartColumn(record?.part_detail)
},
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: false,
switchable: true
},
{
accessor: 'name',
title: t`Parameter`,
@ -85,6 +92,43 @@ export function PartParameterTable({ partId }: { partId: any }) {
];
}, [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
const rowActions = useCallback(
(record: any) => {
@ -93,107 +137,65 @@ export function PartParameterTable({ partId }: { partId: any }) {
return [];
}
let actions = [];
actions.push(
return [
RowEditAction({
tooltip: t`Edit Part Parameter`,
hidden: !user.hasChangeRole(UserRoles.part),
onClick: () => {
openEditApiForm({
url: ApiEndpoints.part_parameter_list,
pk: record.pk,
title: t`Edit Part Parameter`,
fields: {
part: {
hidden: true
},
template: {},
data: {}
},
successMessage: t`Part parameter updated`,
onFormSuccess: table.refreshTable
});
setSelectedParameter(record.pk);
editParameter.open();
}
})
);
actions.push(
}),
RowDeleteAction({
tooltip: t`Delete Part Parameter`,
hidden: !user.hasDeleteRole(UserRoles.part),
onClick: () => {
openDeleteApiForm({
url: ApiEndpoints.part_parameter_list,
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?`
});
setSelectedParameter(record.pk);
deleteParameter.open();
}
})
);
return actions;
];
},
[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
const tableActions = useMemo(() => {
let actions = [];
// TODO: Hide if user does not have permission to edit parts
actions.push(
<AddItemButton tooltip={t`Add parameter`} onClick={addParameter} />
);
return actions;
}, []);
return [
<AddItemButton
hidden={!user.hasAddRole(UserRoles.part)}
tooltip={t`Add parameter`}
onClick={() => newParameter.open()}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiEndpoints.part_parameter_list)}
tableState={table}
columns={tableColumns}
props={{
rowActions: rowActions,
tableActions: tableActions,
tableFilters: [
{
name: 'include_variants',
label: t`Include Variants`,
type: 'boolean'
<>
{newParameter.modal}
{editParameter.modal}
{deleteParameter.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.part_parameter_list)}
tableState={table}
columns={tableColumns}
props={{
rowActions: rowActions,
tableActions: tableActions,
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 { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { partParameterTemplateFields } from '../../../forms/PartForms';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { ApiFormFieldSet } from '../../forms/fields/ApiFormField';
import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers';
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
const rowActions = useCallback(
(record: any) => {
@ -76,27 +112,15 @@ export default function PartParameterTemplateTable() {
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.part),
onClick: () => {
openEditApiForm({
url: ApiEndpoints.part_parameter_template_list,
pk: record.pk,
title: t`Edit Parameter Template`,
fields: partParameterTemplateFields(),
successMessage: t`Parameter template updated`,
onFormSuccess: table.refreshTable
});
setSelectedTemplate(record.pk);
editTemplate.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.part),
onClick: () => {
openDeleteApiForm({
url: ApiEndpoints.part_parameter_template_list,
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?`
});
setSelectedTemplate(record.pk);
deleteTemplate.open();
}
})
];
@ -104,36 +128,31 @@ export default function PartParameterTemplateTable() {
[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(() => {
return [
<AddItemButton
tooltip={t`Add parameter template`}
onClick={addParameterTemplate}
onClick={() => newTemplate.open()}
disabled={!user.hasAddRole(UserRoles.part)}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiEndpoints.part_parameter_template_list)}
tableState={table}
columns={tableColumns}
props={{
rowActions: rowActions,
tableFilters: tableFilters,
tableActions: tableActions
}}
/>
<>
{newTemplate.modal}
{editTemplate.modal}
{deleteTemplate.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.part_parameter_template_list)}
tableState={table}
columns={tableColumns}
props={{
rowActions: rowActions,
tableFilters: tableFilters,
tableActions: tableActions
}}
/>
</>
);
}

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
import { IconSquareArrowRight } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ProgressBar } from '../../../components/items/ProgressBar';
@ -9,8 +9,12 @@ import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles';
import { purchaseOrderLineItemFields } from '../../../forms/PurchaseOrderForms';
import { openCreateApiForm, openEditApiForm } from '../../../functions/forms';
import { getDetailUrl } from '../../../functions/urls';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
@ -47,52 +51,6 @@ export function PurchaseOrderLineItemTable({
const navigate = useNavigate();
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(() => {
return [
{
@ -220,18 +178,67 @@ export function PurchaseOrderLineItemTable({
];
}, [orderId, user]);
const addLine = useCallback(() => {
openCreateApiForm({
url: ApiEndpoints.purchase_order_line_list,
title: t`Add Line Item`,
fields: purchaseOrderLineItemFields({
create: true,
orderId: orderId
}),
onFormSuccess: table.refreshTable,
successMessage: t`Line item added`
});
}, [orderId]);
const newLine = useCreateApiFormModal({
url: ApiEndpoints.purchase_order_line_list,
title: t`Add Line Item`,
fields: purchaseOrderLineItemFields(),
initialData: {
order: orderId
},
onFormSuccess: table.refreshTable
});
const [selectedLine, setSelectedLine] = useState<number | undefined>(
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
const tableActions = useMemo(() => {
@ -239,7 +246,7 @@ export function PurchaseOrderLineItemTable({
<AddItemButton
key="add-line-item"
tooltip={t`Add line item`}
onClick={addLine}
onClick={() => newLine.open()}
hidden={!user?.hasAddRole(UserRoles.purchase_order)}
/>,
<ActionButton
@ -251,26 +258,31 @@ export function PurchaseOrderLineItemTable({
}, [orderId, user]);
return (
<InvenTreeTable
url={apiUrl(ApiEndpoints.purchase_order_line_list)}
tableState={table}
columns={tableColumns}
props={{
enableSelection: true,
enableDownload: true,
params: {
...params,
order: orderId,
part_detail: true
},
rowActions: rowActions,
tableActions: tableActions,
onRowClick: (row: any) => {
if (row.part) {
navigate(getDetailUrl(ModelType.supplierpart, row.part));
<>
{newLine.modal}
{editLine.modal}
{deleteLine.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.purchase_order_line_list)}
tableState={table}
columns={tableColumns}
props={{
enableSelection: true,
enableDownload: true,
params: {
...params,
order: orderId,
part_detail: true
},
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 { useCallback, useMemo } from 'react';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles';
import { notYetImplemented } from '../../../functions/notifications';
import { purchaseOrderFields } from '../../../forms/PurchaseOrderForms';
import { getDetailUrl } from '../../../functions/urls';
import { useCreateApiFormModal } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
@ -34,7 +35,13 @@ import { InvenTreeTable } from '../InvenTreeTable';
/**
* 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 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(() => {
return [
{
@ -100,38 +103,54 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
];
}, []);
const addPurchaseOrder = useCallback(() => {
notYetImplemented();
}, []);
const newPurchaseOrder = useCreateApiFormModal({
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(() => {
return [
<AddItemButton
tooltip={t`Add Purchase Order`}
onClick={addPurchaseOrder}
onClick={() => newPurchaseOrder.open()}
hidden={!user.hasAddRole(UserRoles.purchase_order)}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiEndpoints.purchase_order_list)}
tableState={table}
columns={tableColumns}
props={{
params: {
...params,
supplier_detail: true
},
tableFilters: tableFilters,
tableActions: tableActions,
onRowClick: (row: any) => {
if (row.pk) {
navigate(getDetailUrl(ModelType.purchaseorder, row.pk));
<>
{newPurchaseOrder.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.purchase_order_list)}
tableState={table}
columns={tableColumns}
props={{
params: {
supplier_detail: true,
supplier: supplierId,
supplier_part: supplierPartId
},
tableFilters: tableFilters,
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 { useCallback, useMemo } from 'react';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles';
import { notYetImplemented } from '../../../functions/notifications';
import { salesOrderFields } from '../../../forms/SalesOrderForms';
import { getDetailUrl } from '../../../functions/urls';
import { useCreateApiFormModal } from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
@ -31,7 +32,13 @@ import {
} from '../Filter';
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 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(() => {
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 (
<InvenTreeTable
url={apiUrl(ApiEndpoints.sales_order_list)}
tableState={table}
columns={tableColumns}
props={{
params: {
...params,
customer_detail: true
},
tableFilters: tableFilters,
tableActions: tableActions,
onRowClick: (row: any) => {
if (row.pk) {
navigate(getDetailUrl(ModelType.salesorder, row.pk));
<>
{newSalesOrder.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.sales_order_list)}
tableState={table}
columns={tableColumns}
props={{
params: {
customer_detail: true,
part: partId,
customer: customerId
},
tableFilters: tableFilters,
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 { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { customUnitsFields } from '../../../forms/CommonForms';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
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(
(record: any): RowAction[] => {
return [
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.admin),
onClick: () => {
openEditApiForm({
url: ApiEndpoints.custom_unit_list,
pk: record.pk,
title: t`Edit custom unit`,
fields: {
name: {},
definition: {},
symbol: {}
},
onFormSuccess: table.refreshTable,
successMessage: t`Custom unit updated`
});
setSelectedUnit(record.pk);
editUnit.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.admin),
onClick: () => {
openDeleteApiForm({
url: ApiEndpoints.custom_unit_list,
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?`
});
setSelectedUnit(record.pk);
deleteUnit.open();
}
})
];
@ -85,40 +96,34 @@ export default function CustomUnitsTable() {
[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(() => {
let actions = [];
actions.push(
// 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 (
<InvenTreeTable
url={apiUrl(ApiEndpoints.custom_unit_list)}
tableState={table}
columns={columns}
props={{
rowActions: rowActions,
tableActions: tableActions
}}
/>
<>
{newUnit.modal}
{editUnit.modal}
{deleteUnit.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.custom_unit_list)}
tableState={table}
columns={columns}
props={{
rowActions: rowActions,
tableActions: tableActions
}}
/>
</>
);
}

View File

@ -1,10 +1,13 @@
import { Trans, t } from '@lingui/macro';
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 { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms';
import {
useCreateApiFormModal,
useDeleteApiFormModal
} from '../../../hooks/UseForm';
import { useInstance } from '../../../hooks/UseInstance';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
@ -109,28 +112,31 @@ export function GroupTable() {
}),
RowDeleteAction({
onClick: () => {
openDeleteApiForm({
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?`
});
setSelectedGroup(record.pk), deleteGroup.open();
}
})
];
}, []);
const addGroup = useCallback(() => {
openCreateApiForm({
url: ApiEndpoints.group_list,
title: t`Add group`,
fields: { name: {} },
onFormSuccess: table.refreshTable,
successMessage: t`Added group`
});
}, []);
const [selectedGroup, setSelectedGroup] = useState<number | undefined>(
undefined
);
const deleteGroup = useDeleteApiFormModal({
url: ApiEndpoints.group_list,
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(() => {
let actions = [];
@ -138,7 +144,7 @@ export function GroupTable() {
actions.push(
<AddItemButton
key={'add-group'}
onClick={addGroup}
onClick={() => newGroup.open()}
tooltip={t`Add group`}
/>
);
@ -148,6 +154,8 @@ export function GroupTable() {
return (
<>
{newGroup.modal}
{deleteGroup.modal}
<DetailDrawer
title={t`Edit group`}
renderContent={(id) => {

View File

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

View File

@ -1,13 +1,16 @@
import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles';
import { stockLocationFields } from '../../../forms/StockForms';
import { openCreateApiForm, openEditApiForm } from '../../../functions/forms';
import { getDetailUrl } from '../../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../../hooks/UseForm';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
@ -96,26 +99,33 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
];
}, []);
const addLocation = useCallback(() => {
let fields = stockLocationFields({});
if (parentId) {
fields['parent'].value = parentId;
}
openCreateApiForm({
url: apiUrl(ApiEndpoints.stock_location_list),
title: t`Add Stock Location`,
fields: fields,
onFormSuccess(data: any) {
if (data.pk) {
navigate(`/stock/location/${data.pk}`);
} else {
table.refreshTable();
}
const newLocation = useCreateApiFormModal({
url: ApiEndpoints.stock_location_list,
title: t`Add Stock Location`,
fields: stockLocationFields({}),
initialData: {
parent: parentId
},
onFormSuccess(data: any) {
if (data.pk) {
navigate(getDetailUrl(ModelType.stocklocation, 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(() => {
let can_add = user.hasAddRole(UserRoles.stock_location);
@ -123,7 +133,7 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
return [
<AddItemButton
tooltip={t`Add Stock Location`}
onClick={addLocation}
onClick={() => newLocation.open()}
disabled={!can_add}
/>
];
@ -137,14 +147,8 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
RowEditAction({
hidden: !can_edit,
onClick: () => {
openEditApiForm({
url: ApiEndpoints.stock_location_list,
pk: record.pk,
title: t`Edit Stock Location`,
fields: stockLocationFields({}),
successMessage: t`Stock location updated`,
onFormSuccess: table.refreshTable
});
setSelectedLocation(record.pk);
editLocation.open();
}
})
];
@ -153,23 +157,26 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
);
return (
<InvenTreeTable
url={apiUrl(ApiEndpoints.stock_location_list)}
tableState={table}
columns={tableColumns}
props={{
enableDownload: true,
params: {
parent: parentId ?? 'null'
},
tableFilters: tableFilters,
tableActions: tableActions,
rowActions: rowActions,
onRowClick: (record) => {
navigate(getDetailUrl(ModelType.stocklocation, record.pk));
}
// TODO: allow for "tree view" with cascade
}}
/>
<>
{newLocation.modal}
{editLocation.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.stock_location_list)}
tableState={table}
columns={tableColumns}
props={{
enableDownload: true,
params: {
parent: parentId ?? 'null'
},
tableFilters: tableFilters,
tableActions: tableActions,
rowActions: rowActions,
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 {
IconAt,
IconCurrencyDollar,
@ -12,8 +11,6 @@ import {
import { useEffect, useMemo, useState } from 'react';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { openEditApiForm } from '../functions/forms';
/**
* Field set for SupplierPart instance
@ -131,54 +128,3 @@ export function companyFields(): ApiFormFieldSet {
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 { 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
@ -98,39 +96,6 @@ export function partFields({
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
*/
@ -154,26 +119,3 @@ export function partCategoryFields({}: {}): ApiFormFieldSet {
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 {
IconAddressBook,
IconCalendar,
IconCoins,
IconCurrencyDollar,
IconHash,
IconLink,
IconList,
IconNotes,
IconSitemap
IconSitemap,
IconUser,
IconUsers
} 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
*/
export function purchaseOrderLineItemFields({
supplierId,
orderId,
create = false
}: {
supplierId?: number;
orderId?: number;
create?: boolean;
}) {
export function purchaseOrderLineItemFields() {
let fields: ApiFormFieldSet = {
order: {
filters: {
supplier_detail: true
},
value: orderId,
hidden: create != true || orderId != undefined
hidden: true
},
part: {
filters: {
part_detail: true,
supplier_detail: true,
supplier: supplierId
supplier_detail: true
},
adjustFilters: (filters: any) => {
// TODO: Filter by the supplier associated with the order
return filters;
adjustFilters: (value: ApiFormAdjustFilterType) => {
// TODO: Adjust part based on the supplier associated with the supplier
return value.filters;
}
// TODO: Custom onEdit callback (see purchase_order.js)
// TODO: secondary modal (see purchase_order.js)
},
quantity: {},
reference: {},
@ -66,3 +62,52 @@ export function purchaseOrderLineItemFields({
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({
url: ApiEndpoints.stock_item_list,
fields: fields,
title: t`Create Stock Item`
title: t`Add Stock Item`
});
}

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { Divider, Stack } from '@mantine/core';
import { Alert, Divider, Stack } from '@mantine/core';
import { useId } from '@mantine/hooks';
import { useEffect, useMemo, useRef } from 'react';
@ -82,6 +82,8 @@ export function useCreateApiFormModal(props: ApiFormModalProps) {
const createProps = useMemo<ApiFormModalProps>(
() => ({
...props,
fetchInitialData: props.fetchInitialData ?? false,
successMessage: props.successMessage ?? t`Item Created`,
method: 'POST'
}),
[props]
@ -98,7 +100,8 @@ export function useEditApiFormModal(props: ApiFormModalProps) {
() => ({
...props,
fetchInitialData: props.fetchInitialData ?? true,
method: 'PUT'
successMessage: props.successMessage ?? t`Item Updated`,
method: 'PATCH'
}),
[props]
);
@ -116,6 +119,12 @@ export function useDeleteApiFormModal(props: ApiFormModalProps) {
method: 'DELETE',
submitText: t`Delete`,
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: {}
}),
[props]

View File

@ -42,6 +42,14 @@ window.INVENTREE_SETTINGS = {
host: `${window.location.origin}/`,
name: 'Current Server'
},
...(IS_DEV
? {
'mantine-2j5j5j5j5': {
host: 'http://localhost:8000',
name: 'Localhost'
}
}
: {}),
...(IS_DEV_OR_DEMO
? {
'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,
// 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 { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import {
createPart,
editPart,
partCategoryFields,
partFields
} from '../../forms/PartForms';
import { partCategoryFields, partFields } from '../../forms/PartForms';
import { useCreateStockItem } from '../../forms/StockForms';
import {
OpenApiFormProps,
openCreateApiForm,
openEditApiForm
} from '../../functions/forms';
import { useCreateApiFormModal } from '../../hooks/UseForm';
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
// Generate some example forms using the modal API forms interface
const fields = partCategoryFields({});
function ApiFormsPlayground() {
const editCategoryForm: OpenApiFormProps = {
const editCategory = useEditApiFormModal({
url: ApiEndpoints.category_list,
pk: 2,
title: 'Edit Category',
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,
title: 'Create Attachment',
successMessage: 'Attachment uploaded',
fields: {
part: {
value: 1
},
part: {},
attachment: {},
comment: {}
}
};
},
initialData: {
part: 1
},
successMessage: 'Attachment uploaded'
});
const [active, setActive] = useState(true);
const [name, setName] = useState('Hello');
@ -73,6 +85,14 @@ function ApiFormsPlayground() {
url: ApiEndpoints.part_list,
title: 'Create part',
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: (
<Button onClick={() => setName('Hello world')}>
Set name="Hello world"
@ -86,19 +106,20 @@ function ApiFormsPlayground() {
return (
<Stack>
<Group>
<Button onClick={() => createPart()}>Create New Part</Button>
<Button onClick={() => editPart({ part_id: 1 })}>Edit Part</Button>
<Button onClick={() => newPart.open()}>Create New Part</Button>
{newPart.modal}
<Button onClick={() => editPart.open()}>Edit Part</Button>
{editPart.modal}
<Button onClick={() => openCreateStockItem()}>Create Stock Item</Button>
{createStockItemModal}
<Button onClick={() => openEditApiForm(editCategoryForm)}>
Edit Category
</Button>
<Button onClick={() => editCategory.open()}>Edit Category</Button>
{editCategory.modal}
<Button onClick={() => openCreateApiForm(createAttachmentForm)}>
Create Attachment
</Button>
<Button onClick={() => newAttachment.open()}>Create Attachment</Button>
{newAttachment.modal}
<Button onClick={() => openCreatePart()}>Create Part new Modal</Button>
{createPartModal}

View File

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

View File

@ -1,45 +1,17 @@
import { t } from '@lingui/macro';
import { Button, Stack } from '@mantine/core';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { Stack } from '@mantine/core';
import { PageDetail } from '../../components/nav/PageDetail';
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
*/
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 (
<>
<Stack>
<PageDetail
title={t`Build Orders`}
actions={[
<Button key="new-build" color="green" onClick={newBuildOrder}>
{t`New Build Order`}
</Button>
]}
/>
<PageDetail title={t`Build Orders`} actions={[]} />
<BuildOrderTable />
</Stack>
</>

View File

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

View File

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

View File

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

View File

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

View File

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