[PUI] Refactoring forms (#7239)

* Refactor "plugin activate" dialog

- Use hooked modal

* Remove actions from drawer

- Used dynamic modal code which is super buggy

* Update plugin drawer / table

* Refactor settings management:

- Use proper hooked form
- Reduce code duplication
- Run single callback for each <SettingList>

* Add error boundary

* Update ErrorTable

- Use useDeleteApiFormModal

* Refactor ManufacturerPartParameter table

- Use hooked modals

* Refactor existing tables

- Pass table state to forms

* Ensure table is reloaded

* Refactor ManufacturerPartTable

* Code cleanup

* More cleanup
This commit is contained in:
Oliver 2024-05-16 11:58:50 +10:00 committed by GitHub
parent 548ecf58a2
commit b7b320cf61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 442 additions and 471 deletions

View File

@ -35,6 +35,7 @@ import {
} from '../../functions/forms'; } from '../../functions/forms';
import { invalidResponse } from '../../functions/notifications'; import { invalidResponse } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { TableState } from '../../hooks/UseTable';
import { PathParams } from '../../states/ApiState'; import { PathParams } from '../../states/ApiState';
import { Boundary } from '../Boundary'; import { Boundary } from '../Boundary';
import { import {
@ -54,6 +55,7 @@ export interface ApiFormAction {
* Properties for the ApiForm component * Properties for the ApiForm component
* @param url : The API endpoint to fetch the form data from * @param url : The API endpoint to fetch the form data from
* @param pk : Optional primary-key value when editing an existing object * @param pk : Optional primary-key value when editing an existing object
* @param pk_field : Optional primary-key field name (default: pk)
* @param pathParams : Optional path params for the url * @param pathParams : Optional path params for the url
* @param method : Optional HTTP method to use when submitting the form (default: GET) * @param method : Optional HTTP method to use when submitting the form (default: GET)
* @param fields : The fields to render in the form * @param fields : The fields to render in the form
@ -67,10 +69,12 @@ export interface ApiFormAction {
* @param onFormError : A callback function to call when the form is submitted with errors. * @param onFormError : A callback function to call when the form is submitted with errors.
* @param modelType : Define a model type for this form * @param modelType : Define a model type for this form
* @param follow : Boolean, follow the result of the form (if possible) * @param follow : Boolean, follow the result of the form (if possible)
* @param table : Table to update on success (if provided)
*/ */
export interface ApiFormProps { export interface ApiFormProps {
url: ApiEndpoints | string; url: ApiEndpoints | string;
pk?: number | string | undefined; pk?: number | string | undefined;
pk_field?: string;
pathParams?: PathParams; pathParams?: PathParams;
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
fields?: ApiFormFieldSet; fields?: ApiFormFieldSet;
@ -87,6 +91,7 @@ export interface ApiFormProps {
successMessage?: string; successMessage?: string;
onFormSuccess?: (data: any) => void; onFormSuccess?: (data: any) => void;
onFormError?: () => void; onFormError?: () => void;
table?: TableState;
modelType?: ModelType; modelType?: ModelType;
follow?: boolean; follow?: boolean;
actions?: ApiFormAction[]; actions?: ApiFormAction[];
@ -391,14 +396,22 @@ export function ApiForm({
case 204: case 204:
// Form was submitted successfully // Form was submitted successfully
// Optionally call the onFormSuccess callback
if (props.onFormSuccess) { if (props.onFormSuccess) {
// A custom callback hook is provided
props.onFormSuccess(response.data); props.onFormSuccess(response.data);
} }
if (props.follow) { if (props.follow && props.modelType && response.data?.pk) {
if (props.modelType && response.data?.pk) { // If we want to automatically follow the returned data
navigate(getDetailUrl(props.modelType, response.data?.pk)); navigate(getDetailUrl(props.modelType, response.data?.pk));
} else if (props.table) {
// If we want to automatically update or reload a linked table
let pk_field = props.pk_field ?? 'pk';
if (props.pk && response?.data[pk_field]) {
props.table.updateRecord(response.data);
} else {
props.table.refreshTable();
} }
} }

View File

@ -1,4 +1,3 @@
import { t } from '@lingui/macro';
import { import {
Button, Button,
Group, Group,
@ -9,103 +8,25 @@ import {
Text, Text,
useMantineColorScheme useMantineColorScheme
} from '@mantine/core'; } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { api } from '../../App'; import { Setting } from '../../states/states';
import { ModelType } from '../../enums/ModelType';
import { openModalApiForm } from '../../functions/forms';
import { apiUrl } from '../../states/ApiState';
import { SettingsStateProps } from '../../states/SettingsState';
import { Setting, SettingType } from '../../states/states';
import { vars } from '../../theme'; import { vars } from '../../theme';
import { ApiFormFieldType } from '../forms/fields/ApiFormField'; import { Boundary } from '../Boundary';
/** /**
* Render a single setting value * Render a single setting value
*/ */
function SettingValue({ function SettingValue({
settingsState,
setting, setting,
onChange onEdit,
onToggle
}: { }: {
settingsState: SettingsStateProps;
setting: Setting; setting: Setting;
onChange?: () => void; onEdit: (setting: Setting) => void;
onToggle: (setting: Setting, value: boolean) => void;
}) { }) {
// Callback function when a boolean value is changed
function onToggle(value: boolean) {
api
.patch(
apiUrl(settingsState.endpoint, setting.key, settingsState.pathParams),
{ value: value }
)
.then(() => {
showNotification({
title: t`Setting updated`,
message: t`${setting?.name} updated successfully`,
color: 'green'
});
settingsState.fetchSettings();
onChange?.();
})
.catch((error) => {
showNotification({
title: t`Error editing setting`,
message: error.message,
color: 'red'
});
});
}
// Callback function to open the edit dialog (for non-boolean settings)
function onEditButton() {
const fieldDefinition: ApiFormFieldType = {
value: setting?.value ?? '',
field_type: setting?.type ?? 'string',
label: setting?.name,
description: setting?.description
};
// Match related field
if (
fieldDefinition.field_type === SettingType.Model &&
setting.api_url &&
setting.model_name
) {
fieldDefinition.api_url = setting.api_url;
// TODO: improve this model matching mechanism
fieldDefinition.model = setting.model_name.split('.')[1] as ModelType;
} else if (setting.choices?.length > 0) {
// Match choices
fieldDefinition.field_type = SettingType.Choice;
fieldDefinition.choices = setting?.choices || [];
}
openModalApiForm({
url: settingsState.endpoint,
pk: setting.key,
pathParams: settingsState.pathParams,
method: 'PATCH',
title: t`Edit Setting`,
ignorePermissionCheck: true,
fields: {
value: fieldDefinition
},
onFormSuccess() {
showNotification({
title: t`Setting updated`,
message: t`${setting?.name} updated successfully`,
color: 'green'
});
settingsState.fetchSettings();
onChange?.();
}
});
}
// Determine the text to display for the setting value // Determine the text to display for the setting value
const valueText: string = useMemo(() => { const valueText: string = useMemo(() => {
let value = setting.value; let value = setting.value;
@ -130,7 +51,7 @@ function SettingValue({
size="sm" size="sm"
radius="lg" radius="lg"
checked={setting.value.toLowerCase() == 'true'} checked={setting.value.toLowerCase() == 'true'}
onChange={(event) => onToggle(event.currentTarget.checked)} onChange={(event) => onToggle(setting, event.currentTarget.checked)}
style={{ style={{
paddingRight: '20px' paddingRight: '20px'
}} }}
@ -140,12 +61,12 @@ function SettingValue({
return valueText ? ( return valueText ? (
<Group gap="xs" justify="right"> <Group gap="xs" justify="right">
<Space /> <Space />
<Button variant="subtle" onClick={onEditButton}> <Button variant="subtle" onClick={() => onEdit(setting)}>
{valueText} {valueText}
</Button> </Button>
</Group> </Group>
) : ( ) : (
<Button variant="subtle" onClick={onEditButton}> <Button variant="subtle" onClick={() => onEdit(setting)}>
<IconEdit /> <IconEdit />
</Button> </Button>
); );
@ -156,15 +77,15 @@ function SettingValue({
* Display a single setting item, and allow editing of the value * Display a single setting item, and allow editing of the value
*/ */
export function SettingItem({ export function SettingItem({
settingsState,
setting, setting,
shaded, shaded,
onChange onEdit,
onToggle
}: { }: {
settingsState: SettingsStateProps;
setting: Setting; setting: Setting;
shaded: boolean; shaded: boolean;
onChange?: () => void; onEdit: (setting: Setting) => void;
onToggle: (setting: Setting, value: boolean) => void;
}) { }) {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
@ -184,11 +105,9 @@ export function SettingItem({
</Text> </Text>
<Text size="xs">{setting.description}</Text> <Text size="xs">{setting.description}</Text>
</Stack> </Stack>
<SettingValue <Boundary label={`setting-value-${setting.key}`}>
settingsState={settingsState} <SettingValue setting={setting} onEdit={onEdit} onToggle={onToggle} />
setting={setting} </Boundary>
onChange={onChange}
/>
</Group> </Group>
</Paper> </Paper>
); );

View File

@ -1,8 +1,19 @@
import { Trans } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { Stack, Text } from '@mantine/core'; import { Stack, Text } from '@mantine/core';
import React, { useEffect, useMemo, useRef } from 'react'; import { notifications } from '@mantine/notifications';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { api } from '../../App';
import { ModelType } from '../../enums/ModelType';
import { useEditApiFormModal } from '../../hooks/UseForm';
import { apiUrl } from '../../states/ApiState';
import { import {
SettingsStateProps, SettingsStateProps,
createMachineSettingsState, createMachineSettingsState,
@ -10,6 +21,7 @@ import {
useGlobalSettingsState, useGlobalSettingsState,
useUserSettingsState useUserSettingsState
} from '../../states/SettingsState'; } from '../../states/SettingsState';
import { Setting } from '../../states/states';
import { SettingItem } from './SettingItem'; import { SettingItem } from './SettingItem';
/** /**
@ -33,8 +45,82 @@ export function SettingList({
[settingsState?.settings] [settingsState?.settings]
); );
const [setting, setSetting] = useState<Setting | undefined>(undefined);
const editSettingModal = useEditApiFormModal({
url: settingsState.endpoint,
pk: setting?.key,
pathParams: settingsState.pathParams,
title: t`Edit Setting`,
fields: {
value: {
value: setting?.value ?? '',
field_type:
setting?.type ?? (setting?.choices?.length ?? 0) > 0
? 'choice'
: 'string',
label: setting?.name,
description: setting?.description,
api_url: setting?.api_url ?? '',
model: (setting?.model_name?.split('.')[1] as ModelType) ?? null,
choices: setting?.choices ?? undefined
}
},
successMessage: t`Setting ${setting?.key} updated successfully`,
onFormSuccess: () => {
settingsState.fetchSettings();
onChange?.();
}
});
// Callback for editing a single setting instance
const onValueEdit = useCallback(
(setting: Setting) => {
setSetting(setting);
editSettingModal.open();
},
[editSettingModal]
);
// Callback for toggling a single boolean setting instance
const onValueToggle = useCallback(
(setting: Setting, value: boolean) => {
api
.patch(
apiUrl(settingsState.endpoint, setting.key, settingsState.pathParams),
{
value: value
}
)
.then(() => {
notifications.hide('setting');
notifications.show({
title: t`Setting updated`,
message: t`Setting ${setting.key} updated successfully`,
color: 'green',
id: 'setting'
});
onChange?.();
})
.catch((error) => {
notifications.hide('setting');
notifications.show({
title: t`Error editing setting`,
message: error.message,
color: 'red',
id: 'setting'
});
})
.finally(() => {
settingsState.fetchSettings();
});
},
[settingsState]
);
return ( return (
<> <>
{editSettingModal.modal}
<Stack gap="xs"> <Stack gap="xs">
{(keys || allKeys).map((key, i) => { {(keys || allKeys).map((key, i) => {
const setting = settingsState?.settings?.find( const setting = settingsState?.settings?.find(
@ -45,10 +131,10 @@ export function SettingList({
<React.Fragment key={key}> <React.Fragment key={key}>
{setting ? ( {setting ? (
<SettingItem <SettingItem
settingsState={settingsState}
setting={setting} setting={setting}
shaded={i % 2 === 0} shaded={i % 2 === 0}
onChange={onChange} onEdit={onValueEdit}
onToggle={onValueToggle}
/> />
) : ( ) : (
<Text size="sm" style={{ fontStyle: 'italic' }} color="red"> <Text size="sm" style={{ fontStyle: 'italic' }} color="red">

View File

@ -82,6 +82,9 @@ export function useManufacturerPartFields() {
export function useManufacturerPartParameterFields() { export function useManufacturerPartParameterFields() {
return useMemo(() => { return useMemo(() => {
const fields: ApiFormFieldSet = { const fields: ApiFormFieldSet = {
manufacturer_part: {
disabled: true
},
name: {}, name: {},
value: {}, value: {},
units: {} units: {}

View File

@ -84,9 +84,7 @@ export default function PriceBreakPanel({
url: tableUrl, url: tableUrl,
pk: selectedPriceBreak, pk: selectedPriceBreak,
title: t`Delete Price Break`, title: t`Delete Price Break`,
onFormSuccess: () => { table: table
table.refreshTable();
}
}); });
const columns: TableColumn[] = useMemo(() => { const columns: TableColumn[] = useMemo(() => {

View File

@ -312,7 +312,7 @@ export function BomTable({
part: partId part: partId
}, },
successMessage: t`BOM item created`, successMessage: t`BOM item created`,
onFormSuccess: table.refreshTable table: table
}); });
const editBomItem = useEditApiFormModal({ const editBomItem = useEditApiFormModal({
@ -321,7 +321,7 @@ export function BomTable({
title: t`Edit BOM Item`, title: t`Edit BOM Item`,
fields: bomItemFields(), fields: bomItemFields(),
successMessage: t`BOM item updated`, successMessage: t`BOM item updated`,
onFormSuccess: table.refreshTable table: table
}); });
const deleteBomItem = useDeleteApiFormModal({ const deleteBomItem = useDeleteApiFormModal({
@ -329,7 +329,7 @@ export function BomTable({
pk: selectedBomItem, pk: selectedBomItem,
title: t`Delete BOM Item`, title: t`Delete BOM Item`,
successMessage: t`BOM item deleted`, successMessage: t`BOM item deleted`,
onFormSuccess: table.refreshTable table: table
}); });
const rowActions = useCallback( const rowActions = useCallback(

View File

@ -113,9 +113,7 @@ export default function BuildOutputTable({ build }: { build: any }) {
url: apiUrl(ApiEndpoints.build_output_create, buildId), url: apiUrl(ApiEndpoints.build_output_create, buildId),
title: t`Add Build Output`, title: t`Add Build Output`,
fields: buildOutputFields, fields: buildOutputFields,
onFormSuccess: () => { table: table
table.refreshTable();
}
}); });
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]); const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);

View File

@ -124,7 +124,7 @@ export function AddressTable({
company: companyId company: companyId
}, },
successMessage: t`Address created`, successMessage: t`Address created`,
onFormSuccess: table.refreshTable table: table
}); });
const [selectedAddress, setSelectedAddress] = useState<number>(-1); const [selectedAddress, setSelectedAddress] = useState<number>(-1);
@ -134,15 +134,15 @@ export function AddressTable({
pk: selectedAddress, pk: selectedAddress,
title: t`Edit Address`, title: t`Edit Address`,
fields: addressFields, fields: addressFields,
onFormSuccess: (record: any) => table.updateRecord(record) table: table
}); });
const deleteAddress = useDeleteApiFormModal({ const deleteAddress = useDeleteApiFormModal({
url: ApiEndpoints.address_list, url: ApiEndpoints.address_list,
pk: selectedAddress, pk: selectedAddress,
title: t`Delete Address`, title: t`Delete Address`,
onFormSuccess: table.refreshTable, preFormWarning: t`Are you sure you want to delete this address?`,
preFormWarning: t`Are you sure you want to delete this address?` table: table
}); });
const rowActions = useCallback( const rowActions = useCallback(

View File

@ -80,14 +80,14 @@ export function ContactTable({
company: companyId company: companyId
}, },
fields: contactFields, fields: contactFields,
onFormSuccess: table.refreshTable table: table
}); });
const deleteContact = useDeleteApiFormModal({ const deleteContact = useDeleteApiFormModal({
url: ApiEndpoints.contact_list, url: ApiEndpoints.contact_list,
pk: selectedContact, pk: selectedContact,
title: t`Delete Contact`, title: t`Delete Contact`,
onFormSuccess: table.refreshTable table: table
}); });
const rowActions = useCallback( const rowActions = useCallback(

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { YesNoButton } from '../../components/buttons/YesNoButton'; import { YesNoButton } from '../../components/buttons/YesNoButton';
@ -8,7 +7,6 @@ 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 { getDetailUrl } from '../../functions/urls';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useEditApiFormModal useEditApiFormModal
@ -26,8 +24,6 @@ import { RowEditAction } from '../RowActions';
* PartCategoryTable - Displays a table of part categories * PartCategoryTable - Displays a table of part categories
*/ */
export function PartCategoryTable({ parentId }: { parentId?: any }) { export function PartCategoryTable({ parentId }: { parentId?: any }) {
const navigate = useNavigate();
const table = useTable('partcategory'); const table = useTable('partcategory');
const user = useUserState(); const user = useUserState();
@ -79,13 +75,9 @@ export function PartCategoryTable({ parentId }: { parentId?: any }) {
initialData: { initialData: {
parent: parentId parent: parentId
}, },
onFormSuccess(data: any) { follow: true,
if (data.pk) { modelType: ModelType.partcategory,
navigate(getDetailUrl(ModelType.partcategory, data.pk)); table: table
} else {
table.refreshTable();
}
}
}); });
const [selectedCategory, setSelectedCategory] = useState<number>(-1); const [selectedCategory, setSelectedCategory] = useState<number>(-1);

View File

@ -37,7 +37,7 @@ export default function PartCategoryTemplateTable({}: {}) {
url: ApiEndpoints.category_parameter_list, url: ApiEndpoints.category_parameter_list,
title: t`Add Category Parameter`, title: t`Add Category Parameter`,
fields: useMemo(() => ({ ...formFields }), [formFields]), fields: useMemo(() => ({ ...formFields }), [formFields]),
onFormSuccess: table.refreshTable table: table
}); });
const editTemplate = useEditApiFormModal({ const editTemplate = useEditApiFormModal({
@ -45,14 +45,14 @@ export default function PartCategoryTemplateTable({}: {}) {
pk: selectedTemplate, pk: selectedTemplate,
title: t`Edit Category Parameter`, title: t`Edit Category Parameter`,
fields: useMemo(() => ({ ...formFields }), [formFields]), fields: useMemo(() => ({ ...formFields }), [formFields]),
onFormSuccess: (record: any) => table.updateRecord(record) table: table
}); });
const deleteTemplate = useDeleteApiFormModal({ const deleteTemplate = useDeleteApiFormModal({
url: ApiEndpoints.category_parameter_list, url: ApiEndpoints.category_parameter_list,
pk: selectedTemplate, pk: selectedTemplate,
title: t`Delete Category Parameter`, title: t`Delete Category Parameter`,
onFormSuccess: table.refreshTable table: table
}); });
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {

View File

@ -115,7 +115,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
initialData: { initialData: {
part: partId part: partId
}, },
onFormSuccess: table.refreshTable table: table
}); });
const [selectedParameter, setSelectedParameter] = useState< const [selectedParameter, setSelectedParameter] = useState<
@ -127,14 +127,14 @@ export function PartParameterTable({ partId }: { partId: any }) {
pk: selectedParameter, pk: selectedParameter,
title: t`Edit Part Parameter`, title: t`Edit Part Parameter`,
fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]), fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
onFormSuccess: table.refreshTable table: table
}); });
const deleteParameter = useDeleteApiFormModal({ const deleteParameter = useDeleteApiFormModal({
url: ApiEndpoints.part_parameter_list, url: ApiEndpoints.part_parameter_list,
pk: selectedParameter, pk: selectedParameter,
title: t`Delete Part Parameter`, title: t`Delete Part Parameter`,
onFormSuccess: table.refreshTable table: table
}); });
// Callback for row actions // Callback for row actions
@ -171,6 +171,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
return [ return [
<AddItemButton <AddItemButton
key="add-parameter"
hidden={!user.hasAddRole(UserRoles.part)} hidden={!user.hasAddRole(UserRoles.part)}
tooltip={t`Add parameter`} tooltip={t`Add parameter`}
onClick={() => newParameter.open()} onClick={() => newParameter.open()}

View File

@ -83,11 +83,11 @@ export default function PartParameterTemplateTable() {
const newTemplate = useCreateApiFormModal({ const newTemplate = useCreateApiFormModal({
url: ApiEndpoints.part_parameter_template_list, url: ApiEndpoints.part_parameter_template_list,
title: t`Add Parameter Template`, title: t`Add Parameter Template`,
table: table,
fields: useMemo( fields: useMemo(
() => ({ ...partParameterTemplateFields }), () => ({ ...partParameterTemplateFields }),
[partParameterTemplateFields] [partParameterTemplateFields]
), )
onFormSuccess: table.refreshTable
}); });
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>( const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
@ -98,18 +98,18 @@ export default function PartParameterTemplateTable() {
url: ApiEndpoints.part_parameter_template_list, url: ApiEndpoints.part_parameter_template_list,
pk: selectedTemplate, pk: selectedTemplate,
title: t`Edit Parameter Template`, title: t`Edit Parameter Template`,
table: table,
fields: useMemo( fields: useMemo(
() => ({ ...partParameterTemplateFields }), () => ({ ...partParameterTemplateFields }),
[partParameterTemplateFields] [partParameterTemplateFields]
), )
onFormSuccess: (record: any) => table.updateRecord(record)
}); });
const deleteTemplate = useDeleteApiFormModal({ const deleteTemplate = useDeleteApiFormModal({
url: ApiEndpoints.part_parameter_template_list, url: ApiEndpoints.part_parameter_template_list,
pk: selectedTemplate, pk: selectedTemplate,
title: t`Delete Parameter Template`, title: t`Delete Parameter Template`,
onFormSuccess: table.refreshTable table: table
}); });
// Callback for row actions // Callback for row actions

View File

@ -131,7 +131,7 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
initialData: { initialData: {
part: partId part: partId
}, },
onFormSuccess: table.refreshTable table: table
}); });
const [selectedTest, setSelectedTest] = useState<number>(-1); const [selectedTest, setSelectedTest] = useState<number>(-1);
@ -140,11 +140,11 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
url: ApiEndpoints.part_test_template_list, url: ApiEndpoints.part_test_template_list,
pk: selectedTest, pk: selectedTest,
title: t`Edit Test Template`, title: t`Edit Test Template`,
table: table,
fields: useMemo( fields: useMemo(
() => ({ ...partTestTemplateFields }), () => ({ ...partTestTemplateFields }),
[partTestTemplateFields] [partTestTemplateFields]
), )
onFormSuccess: (record: any) => table.updateRecord(record)
}); });
const deleteTestTemplate = useDeleteApiFormModal({ const deleteTestTemplate = useDeleteApiFormModal({
@ -160,7 +160,7 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
</Text> </Text>
</Alert> </Alert>
), ),
onFormSuccess: table.refreshTable table: table
}); });
const rowActions = useCallback( const rowActions = useCallback(

View File

@ -86,7 +86,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
initialData: { initialData: {
part_1: partId part_1: partId
}, },
onFormSuccess: table.refreshTable table: table
}); });
const [selectedRelatedPart, setSelectedRelatedPart] = useState< const [selectedRelatedPart, setSelectedRelatedPart] = useState<
@ -97,7 +97,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
url: ApiEndpoints.related_part_list, url: ApiEndpoints.related_part_list,
pk: selectedRelatedPart, pk: selectedRelatedPart,
title: t`Delete Related Part`, title: t`Delete Related Part`,
onFormSuccess: table.refreshTable table: table
}); });
const tableActions: ReactNode[] = useMemo(() => { const tableActions: ReactNode[] = useMemo(() => {

View File

@ -10,12 +10,10 @@ import {
Title, Title,
Tooltip Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { modals } from '@mantine/modals'; import { showNotification } from '@mantine/notifications';
import { notifications, showNotification } from '@mantine/notifications';
import { import {
IconCircleCheck, IconCircleCheck,
IconCircleX, IconCircleX,
IconDots,
IconHelpCircle, IconHelpCircle,
IconInfoCircle, IconInfoCircle,
IconPlaylistAdd, IconPlaylistAdd,
@ -26,16 +24,11 @@ import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ActionButton } from '../../components/buttons/ActionButton'; import { ActionButton } from '../../components/buttons/ActionButton';
import { import { YesNoButton } from '../../components/buttons/YesNoButton';
ActionDropdown,
EditItemAction
} from '../../components/items/ActionDropdown';
import { InfoItem } from '../../components/items/InfoItem'; import { InfoItem } from '../../components/items/InfoItem';
import { StylishText } from '../../components/items/StylishText';
import { DetailDrawer } from '../../components/nav/DetailDrawer'; import { DetailDrawer } from '../../components/nav/DetailDrawer';
import { PluginSettingList } from '../../components/settings/SettingList'; import { PluginSettingList } from '../../components/settings/SettingList';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { openEditApiForm } from '../../functions/forms';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useDeleteApiFormModal, useDeleteApiFormModal,
@ -80,16 +73,9 @@ export interface PluginI {
>; >;
} }
export function PluginDrawer({ export function PluginDrawer({ pluginKey }: { pluginKey: Readonly<string> }) {
pluginKey,
refreshTable
}: {
pluginKey: string;
refreshTable: () => void;
}) {
const { const {
instance: plugin, instance: plugin,
refreshInstance,
instanceQuery: { isFetching, error } instanceQuery: { isFetching, error }
} = useInstance<PluginI>({ } = useInstance<PluginI>({
endpoint: ApiEndpoints.plugin_list, endpoint: ApiEndpoints.plugin_list,
@ -98,11 +84,6 @@ export function PluginDrawer({
throwError: true throwError: true
}); });
const refetch = useCallback(() => {
refreshTable();
refreshInstance();
}, [refreshTable, refreshInstance]);
if (!pluginKey || isFetching) { if (!pluginKey || isFetching) {
return <LoadingOverlay visible={true} />; return <LoadingOverlay visible={true} />;
} }
@ -121,7 +102,8 @@ export function PluginDrawer({
return ( return (
<Stack gap={'xs'}> <Stack gap={'xs'}>
<Group justify="space-between"> <Card withBorder>
<Group justify="left">
<Box></Box> <Box></Box>
<Group gap={'xs'}> <Group gap={'xs'}>
@ -130,35 +112,8 @@ export function PluginDrawer({
{plugin?.meta?.human_name ?? plugin?.name ?? '-'} {plugin?.meta?.human_name ?? plugin?.name ?? '-'}
</Title> </Title>
</Group> </Group>
<ActionDropdown
tooltip={t`Plugin Actions`}
icon={<IconDots />}
actions={[
EditItemAction({
tooltip: t`Edit plugin`,
onClick: () => {
openEditApiForm({
title: t`Edit plugin`,
url: ApiEndpoints.plugin_list,
pathParams: { key: pluginKey },
fields: {
active: {}
},
onClose: refetch
});
}
}),
{
name: t`Reload`,
tooltip: t`Reload`,
icon: <IconRefresh />,
onClick: refreshInstance
}
]}
/>
</Group> </Group>
</Card>
<LoadingOverlay visible={isFetching} overlayProps={{ opacity: 0 }} /> <LoadingOverlay visible={isFetching} overlayProps={{ opacity: 0 }} />
<Card withBorder> <Card withBorder>
@ -166,6 +121,7 @@ export function PluginDrawer({
<Title order={4}> <Title order={4}>
<Trans>Plugin information</Trans> <Trans>Plugin information</Trans>
</Title> </Title>
{plugin.active ? (
<Stack pos="relative" gap="xs"> <Stack pos="relative" gap="xs">
<InfoItem type="text" name={t`Name`} value={plugin?.name} /> <InfoItem type="text" name={t`Name`} value={plugin?.name} />
<InfoItem <InfoItem
@ -188,11 +144,19 @@ export function PluginDrawer({
name={t`Version`} name={t`Version`}
value={plugin?.meta.version} value={plugin?.meta.version}
/> />
<InfoItem type="boolean" name={t`Active`} value={plugin?.active} /> <InfoItem
type="boolean"
name={t`Active`}
value={plugin?.active}
/>
</Stack> </Stack>
) : (
<Text color="red">{t`Plugin is not active`}</Text>
)}
</Stack> </Stack>
</Card> </Card>
{plugin.active && (
<Card withBorder> <Card withBorder>
<Stack gap="md"> <Stack gap="md">
<Title order={4}> <Title order={4}>
@ -224,6 +188,7 @@ export function PluginDrawer({
</Stack> </Stack>
</Stack> </Stack>
</Card> </Card>
)}
{plugin && plugin?.active && ( {plugin && plugin?.active && (
<Card withBorder> <Card withBorder>
@ -300,6 +265,12 @@ export default function PluginListTable() {
); );
} }
}, },
{
accessor: 'active',
sortable: true,
title: t`Active`,
render: (record: any) => <YesNoButton value={record.active} />
},
{ {
accessor: 'meta.description', accessor: 'meta.description',
title: t`Description`, title: t`Description`,
@ -312,6 +283,7 @@ export default function PluginListTable() {
return ( return (
<Text <Text
style={{ fontStyle: 'italic' }} style={{ fontStyle: 'italic' }}
size="sm"
>{t`Description not available`}</Text> >{t`Description not available`}</Text>
); );
} }
@ -333,87 +305,49 @@ export default function PluginListTable() {
[] []
); );
const activatePlugin = useCallback( const [selectedPlugin, setSelectedPlugin] = useState<string>('');
(plugin_key: string, plugin_name: string, active: boolean) => { const [activate, setActivate] = useState<boolean>(false);
modals.openConfirmModal({
title: ( const activateModalContent = useMemo(() => {
<StylishText> return (
{active ? t`Activate Plugin` : t`Deactivate Plugin`} <Stack gap="xs">
</StylishText>
),
children: (
<Alert <Alert
color="green" color={activate ? 'green' : 'red'}
icon={<IconCircleCheck />} icon={<IconCircleCheck />}
title={ title={
active activate
? t`Confirm plugin activation` ? t`Confirm plugin activation`
: t`Confirm plugin deactivation` : t`Confirm plugin deactivation`
} }
> >
<Stack gap="xs">
<Text> <Text>
{active {activate
? t`The following plugin will be activated` ? t`The selected plugin will be activated`
: t`The following plugin will be deactivated`} : t`The selected plugin will be deactivated`}
:
</Text> </Text>
<Text size="lg" style={{ fontStyle: 'italic' }}>
{plugin_name}
</Text>
</Stack>
</Alert> </Alert>
), </Stack>
labels: {
cancel: t`Cancel`,
confirm: t`Confirm`
},
onConfirm: () => {
let url = apiUrl(ApiEndpoints.plugin_activate, null, {
key: plugin_key
});
const id = 'plugin-activate';
// Show a progress notification
notifications.show({
id: id,
message: active ? t`Activating plugin` : t`Deactivating plugin`,
loading: true
});
api
.patch(
url,
{ active: active },
{
timeout: 30 * 1000
}
)
.then(() => {
table.refreshTable();
notifications.hide(id);
notifications.show({
title: t`Plugin updated`,
message: active
? t`The plugin was activated`
: t`The plugin was deactivated`,
color: 'green'
});
})
.catch((_err) => {
notifications.hide(id);
notifications.show({
title: t`Error`,
message: t`Error updating plugin`,
color: 'red'
});
});
}
});
},
[]
); );
}, [activate]);
const activatePluginModal = useEditApiFormModal({
title: t`Activate Plugin`,
url: ApiEndpoints.plugin_activate,
pathParams: { key: selectedPlugin },
preFormContent: activateModalContent,
fetchInitialData: false,
method: 'POST',
successMessage: activate
? `The plugin was activated`
: `The plugin was deactivated`,
fields: {
active: {
value: activate,
hidden: true
}
},
table: table
});
// Determine available actions for a given plugin // Determine available actions for a given plugin
const rowActions = useCallback( const rowActions = useCallback(
@ -429,7 +363,9 @@ export default function PluginListTable() {
color: 'red', color: 'red',
icon: <IconCircleX />, icon: <IconCircleX />,
onClick: () => { onClick: () => {
activatePlugin(record.key, record.name, false); setSelectedPlugin(record.key);
setActivate(false);
activatePluginModal.open();
} }
}); });
} else { } else {
@ -438,7 +374,9 @@ export default function PluginListTable() {
color: 'green', color: 'green',
icon: <IconCircleCheck />, icon: <IconCircleCheck />,
onClick: () => { onClick: () => {
activatePlugin(record.key, record.name, true); setSelectedPlugin(record.key);
setActivate(true);
activatePluginModal.open();
} }
}); });
} }
@ -511,21 +449,10 @@ export default function PluginListTable() {
}, },
closeOnClickOutside: false, closeOnClickOutside: false,
submitText: t`Install`, submitText: t`Install`,
successMessage: undefined, successMessage: t`Plugin installed successfully`,
onFormSuccess: (data) => { table: table
notifications.show({
title: t`Plugin installed successfully`,
message: data.result,
autoClose: 30000,
color: 'green'
}); });
table.refreshTable();
}
});
const [selectedPlugin, setSelectedPlugin] = useState<string>('');
const uninstallPluginModal = useEditApiFormModal({ const uninstallPluginModal = useEditApiFormModal({
title: t`Uninstall Plugin`, title: t`Uninstall Plugin`,
url: ApiEndpoints.plugin_uninstall, url: ApiEndpoints.plugin_uninstall,
@ -547,24 +474,16 @@ export default function PluginListTable() {
</Stack> </Stack>
</Alert> </Alert>
), ),
onFormSuccess: (data) => { successMessage: t`Plugin uninstalled successfully`,
notifications.show({ table: table
title: t`Plugin uninstalled successfully`,
message: data.result,
autoClose: 30000,
color: 'green'
});
table.refreshTable();
}
}); });
const deletePluginModal = useDeleteApiFormModal({ const deletePluginModal = useDeleteApiFormModal({
url: ApiEndpoints.plugin_list, url: ApiEndpoints.plugin_list,
pathParams: { key: selectedPlugin }, pathParams: { key: selectedPlugin },
title: t`Delete Plugin`, title: t`Delete Plugin`,
onFormSuccess: table.refreshTable, preFormWarning: t`Deleting this plugin configuration will remove all associated settings and data. Are you sure you want to delete this plugin?`,
preFormWarning: t`Deleting this plugin configuration will remove all associated settings and data. Are you sure you want to delete this plugin?` table: table
}); });
const reloadPlugins = useCallback(() => { const reloadPlugins = useCallback(() => {
@ -617,6 +536,7 @@ export default function PluginListTable() {
return ( return (
<> <>
{activatePluginModal.modal}
{installPluginModal.modal} {installPluginModal.modal}
{uninstallPluginModal.modal} {uninstallPluginModal.modal}
{deletePluginModal.modal} {deletePluginModal.modal}
@ -625,12 +545,7 @@ export default function PluginListTable() {
size={'50%'} size={'50%'}
renderContent={(pluginKey) => { renderContent={(pluginKey) => {
if (!pluginKey) return; if (!pluginKey) return;
return ( return <PluginDrawer pluginKey={pluginKey} />;
<PluginDrawer
pluginKey={pluginKey}
refreshTable={table.refreshTable}
/>
);
}} }}
/> />
<InvenTreeTable <InvenTreeTable

View File

@ -1,10 +1,15 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useManufacturerPartParameterFields } from '../../forms/CompanyForms'; import { useManufacturerPartParameterFields } from '../../forms/CompanyForms';
import { openDeleteApiForm, openEditApiForm } from '../../functions/forms'; 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';
@ -45,34 +50,50 @@ export default function ManufacturerPartParameterTable({
const fields = useManufacturerPartParameterFields(); const fields = useManufacturerPartParameterFields();
const [selectedParameter, setSelectedParameter] = useState<
number | undefined
>(undefined);
const createParameter = useCreateApiFormModal({
url: ApiEndpoints.manufacturer_part_parameter_list,
title: t`Add Parameter`,
fields: fields,
table: table,
initialData: {
manufacturer_part: params.manufacturer_part
}
});
const editParameter = useEditApiFormModal({
url: ApiEndpoints.manufacturer_part_parameter_list,
pk: selectedParameter,
title: t`Edit Parameter`,
fields: fields,
table: table
});
const deleteParameter = useDeleteApiFormModal({
url: ApiEndpoints.manufacturer_part_parameter_list,
pk: selectedParameter,
title: t`Delete Parameter`,
table: table
});
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
return [ return [
RowEditAction({ RowEditAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order), hidden: !user.hasChangeRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
openEditApiForm({ setSelectedParameter(record.pk);
url: ApiEndpoints.manufacturer_part_parameter_list, editParameter.open();
pk: record.pk,
title: t`Edit Parameter`,
fields: fields,
onFormSuccess: table.refreshTable,
successMessage: t`Parameter updated`
});
} }
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order), hidden: !user.hasDeleteRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
record.pk && setSelectedParameter(record.pk);
openDeleteApiForm({ deleteParameter.open();
url: ApiEndpoints.manufacturer_part_parameter_list,
pk: record.pk,
title: t`Delete Parameter`,
onFormSuccess: table.refreshTable,
successMessage: t`Parameter deleted`,
preFormWarning: t`Are you sure you want to delete this parameter?`
});
} }
}) })
]; ];
@ -80,7 +101,24 @@ export default function ManufacturerPartParameterTable({
[user] [user]
); );
const tableActions = useMemo(() => {
return [
<AddItemButton
key="add-parameter"
tooltip={t`Add Parameter`}
onClick={() => {
createParameter.open();
}}
hidden={!user.hasAddRole(UserRoles.purchase_order)}
/>
];
}, [user]);
return ( return (
<>
{createParameter.modal}
{editParameter.modal}
{deleteParameter.modal}
<InvenTreeTable <InvenTreeTable
url={apiUrl(ApiEndpoints.manufacturer_part_parameter_list)} url={apiUrl(ApiEndpoints.manufacturer_part_parameter_list)}
tableState={table} tableState={table}
@ -89,8 +127,10 @@ export default function ManufacturerPartParameterTable({
params: { params: {
...params ...params
}, },
rowActions: rowActions rowActions: rowActions,
tableActions: tableActions
}} }}
/> />
</>
); );
} }

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ReactNode, useCallback, useMemo } from 'react'; import { ReactNode, useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail'; import { Thumbnail } from '../../components/images/Thumbnail';
@ -7,8 +7,11 @@ 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 { useManufacturerPartFields } from '../../forms/CompanyForms'; import { useManufacturerPartFields } from '../../forms/CompanyForms';
import { openDeleteApiForm, openEditApiForm } from '../../functions/forms'; import {
import { useCreateApiFormModal } from '../../hooks/UseForm'; 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';
@ -58,16 +61,37 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode {
]; ];
}, [params]); }, [params]);
const manufacturerPartFields = useManufacturerPartFields();
const [selectedPart, setSelectedPart] = useState<number | undefined>(
undefined
);
const createManufacturerPart = useCreateApiFormModal({ const createManufacturerPart = useCreateApiFormModal({
url: ApiEndpoints.manufacturer_part_list, url: ApiEndpoints.manufacturer_part_list,
title: t`Add Manufacturer Part`, title: t`Add Manufacturer Part`,
fields: useManufacturerPartFields(), fields: manufacturerPartFields,
onFormSuccess: table.refreshTable, table: table,
initialData: { initialData: {
manufacturer: params?.manufacturer manufacturer: params?.manufacturer
} }
}); });
const editManufacturerPart = useEditApiFormModal({
url: ApiEndpoints.manufacturer_part_list,
pk: selectedPart,
title: t`Edit Manufacturer Part`,
fields: manufacturerPartFields,
table: table
});
const deleteManufacturerPart = useDeleteApiFormModal({
url: ApiEndpoints.manufacturer_part_list,
pk: selectedPart,
title: t`Delete Manufacturer Part`,
table: table
});
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
let can_add = let can_add =
user.hasAddRole(UserRoles.purchase_order) && user.hasAddRole(UserRoles.purchase_order) &&
@ -82,37 +106,21 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode {
]; ];
}, [user]); }, [user]);
const editManufacturerPartFields = useManufacturerPartFields();
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
return [ return [
RowEditAction({ RowEditAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order), hidden: !user.hasChangeRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
record.pk && setSelectedPart(record.pk);
openEditApiForm({ editManufacturerPart.open();
url: ApiEndpoints.manufacturer_part_list,
pk: record.pk,
title: t`Edit Manufacturer Part`,
fields: editManufacturerPartFields,
onFormSuccess: table.refreshTable,
successMessage: t`Manufacturer part updated`
});
} }
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order), hidden: !user.hasDeleteRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
record.pk && setSelectedPart(record.pk);
openDeleteApiForm({ deleteManufacturerPart.open();
url: ApiEndpoints.manufacturer_part_list,
pk: record.pk,
title: t`Delete Manufacturer Part`,
successMessage: t`Manufacturer part deleted`,
onFormSuccess: table.refreshTable,
preFormWarning: t`Are you sure you want to remove this manufacturer part?`
});
} }
}) })
]; ];
@ -123,6 +131,8 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode {
return ( return (
<> <>
{createManufacturerPart.modal} {createManufacturerPart.modal}
{editManufacturerPart.modal}
{deleteManufacturerPart.modal}
<InvenTreeTable <InvenTreeTable
url={apiUrl(ApiEndpoints.manufacturer_part_list)} url={apiUrl(ApiEndpoints.manufacturer_part_list)}
tableState={table} tableState={table}

View File

@ -198,7 +198,7 @@ export function PurchaseOrderLineItemTable({
title: t`Add Line Item`, title: t`Add Line Item`,
fields: addPurchaseOrderFields, fields: addPurchaseOrderFields,
initialData: initialData, initialData: initialData,
onFormSuccess: table.refreshTable table: table
}); });
const [selectedLine, setSelectedLine] = useState<number>(0); const [selectedLine, setSelectedLine] = useState<number>(0);
@ -214,14 +214,14 @@ export function PurchaseOrderLineItemTable({
pk: selectedLine, pk: selectedLine,
title: t`Edit Line Item`, title: t`Edit Line Item`,
fields: editPurchaseOrderFields, fields: editPurchaseOrderFields,
onFormSuccess: table.refreshTable table: table
}); });
const deleteLine = useDeleteApiFormModal({ const deleteLine = useDeleteApiFormModal({
url: ApiEndpoints.purchase_order_line_list, url: ApiEndpoints.purchase_order_line_list,
pk: selectedLine, pk: selectedLine,
title: t`Delete Line Item`, title: t`Delete Line Item`,
onFormSuccess: table.refreshTable table: table
}); });
const rowActions = useCallback( const rowActions = useCallback(

View File

@ -166,7 +166,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
part: params?.part, part: params?.part,
supplier: params?.supplier supplier: params?.supplier
}, },
onFormSuccess: table.refreshTable, table: table,
successMessage: t`Supplier part created` successMessage: t`Supplier part created`
}); });
@ -209,14 +209,14 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
pk: selectedSupplierPart, pk: selectedSupplierPart,
title: t`Edit Supplier Part`, title: t`Edit Supplier Part`,
fields: editSupplierPartFields, fields: editSupplierPartFields,
onFormSuccess: () => table.refreshTable() table: table
}); });
const deleteSupplierPart = useDeleteApiFormModal({ const deleteSupplierPart = useDeleteApiFormModal({
url: ApiEndpoints.supplier_part_list, url: ApiEndpoints.supplier_part_list,
pk: selectedSupplierPart, pk: selectedSupplierPart,
title: t`Delete Supplier Part`, title: t`Delete Supplier Part`,
onFormSuccess: () => table.refreshTable() table: table
}); });
// Row action callback // Row action callback

View File

@ -140,9 +140,7 @@ export default function SupplierPriceBreakTable({
initialData: { initialData: {
part: supplierPartId part: supplierPartId
}, },
onFormSuccess: (data: any) => { table: table
table.refreshTable();
}
}); });
const editPriceBreak = useEditApiFormModal({ const editPriceBreak = useEditApiFormModal({
@ -150,18 +148,14 @@ export default function SupplierPriceBreakTable({
pk: selectedPriceBreak, pk: selectedPriceBreak,
title: t`Edit Price Break`, title: t`Edit Price Break`,
fields: supplierPriceBreakFields, fields: supplierPriceBreakFields,
onFormSuccess: (data: any) => { table: table
table.refreshTable();
}
}); });
const deletePriceBreak = useDeleteApiFormModal({ const deletePriceBreak = useDeleteApiFormModal({
url: apiUrl(ApiEndpoints.supplier_part_pricing_list), url: apiUrl(ApiEndpoints.supplier_part_pricing_list),
pk: selectedPriceBreak, pk: selectedPriceBreak,
title: t`Delete Price Break`, title: t`Delete Price Break`,
onFormSuccess: () => { table: table
table.refreshTable();
}
}); });
const tableActions = useMemo(() => { const tableActions = useMemo(() => {

View File

@ -49,7 +49,7 @@ export default function CustomUnitsTable() {
url: ApiEndpoints.custom_unit_list, url: ApiEndpoints.custom_unit_list,
title: t`Add Custom Unit`, title: t`Add Custom Unit`,
fields: customUnitsFields(), fields: customUnitsFields(),
onFormSuccess: table.refreshTable table: table
}); });
const [selectedUnit, setSelectedUnit] = useState<number>(-1); const [selectedUnit, setSelectedUnit] = useState<number>(-1);
@ -66,7 +66,7 @@ export default function CustomUnitsTable() {
url: ApiEndpoints.custom_unit_list, url: ApiEndpoints.custom_unit_list,
pk: selectedUnit, pk: selectedUnit,
title: t`Delete Custom Unit`, title: t`Delete Custom Unit`,
onFormSuccess: table.refreshTable table: table
}); });
const rowActions = useCallback( const rowActions = useCallback(

View File

@ -5,7 +5,7 @@ import { useCallback, useMemo, useState } from 'react';
import { StylishText } from '../../components/items/StylishText'; import { StylishText } from '../../components/items/StylishText';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { openDeleteApiForm } from '../../functions/forms'; import { 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 { TableColumn } from '../Column'; import { TableColumn } from '../Column';
@ -41,18 +41,27 @@ export default function ErrorReportTable() {
]; ];
}, []); }, []);
const [selectedError, setSelectedError] = useState<number | undefined>(
undefined
);
const deleteErrorModal = useDeleteApiFormModal({
url: ApiEndpoints.error_report_list,
pk: selectedError,
title: t`Delete Error Report`,
preFormContent: (
<Text c="red">{t`Are you sure you want to delete this error report?`}</Text>
),
successMessage: t`Error report deleted`,
table: table
});
const rowActions = useCallback((record: any): RowAction[] => { const rowActions = useCallback((record: any): RowAction[] => {
return [ return [
RowDeleteAction({ RowDeleteAction({
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedError(record.pk);
url: ApiEndpoints.error_report_list, deleteErrorModal.open();
pk: record.pk,
title: t`Delete error report`,
onFormSuccess: table.refreshTable,
successMessage: t`Error report deleted`,
preFormWarning: t`Are you sure you want to delete this error report?`
});
} }
}) })
]; ];
@ -60,6 +69,7 @@ export default function ErrorReportTable() {
return ( return (
<> <>
{deleteErrorModal.modal}
<Drawer <Drawer
opened={opened} opened={opened}
size="xl" size="xl"

View File

@ -125,7 +125,7 @@ export function GroupTable() {
pk: selectedGroup, pk: selectedGroup,
title: t`Delete group`, title: t`Delete group`,
successMessage: t`Group deleted`, successMessage: t`Group deleted`,
onFormSuccess: table.refreshTable, table: table,
preFormWarning: t`Are you sure you want to delete this group?` preFormWarning: t`Are you sure you want to delete this group?`
}); });
@ -133,7 +133,7 @@ export function GroupTable() {
url: ApiEndpoints.group_list, url: ApiEndpoints.group_list,
title: t`Add group`, title: t`Add group`,
fields: { name: {} }, fields: { name: {} },
onFormSuccess: table.refreshTable table: table
}); });
const tableActions = useMemo(() => { const tableActions = useMemo(() => {

View File

@ -41,7 +41,7 @@ export default function ProjectCodeTable() {
url: ApiEndpoints.project_code_list, url: ApiEndpoints.project_code_list,
title: t`Add Project Code`, title: t`Add Project Code`,
fields: projectCodeFields(), fields: projectCodeFields(),
onFormSuccess: table.refreshTable table: table
}); });
const [selectedProjectCode, setSelectedProjectCode] = useState< const [selectedProjectCode, setSelectedProjectCode] = useState<
@ -53,14 +53,14 @@ export default function ProjectCodeTable() {
pk: selectedProjectCode, pk: selectedProjectCode,
title: t`Edit Project Code`, title: t`Edit Project Code`,
fields: projectCodeFields(), fields: projectCodeFields(),
onFormSuccess: (record: any) => table.updateRecord(record) table: table
}); });
const deleteProjectCode = useDeleteApiFormModal({ const deleteProjectCode = useDeleteApiFormModal({
url: ApiEndpoints.project_code_list, url: ApiEndpoints.project_code_list,
pk: selectedProjectCode, pk: selectedProjectCode,
title: t`Delete Project Code`, title: t`Delete Project Code`,
onFormSuccess: table.refreshTable table: table
}); });
const rowActions = useCallback( const rowActions = useCallback(

View File

@ -234,7 +234,7 @@ export function TemplateTable({
pathParams: { variant }, pathParams: { variant },
pk: selectedTemplate, pk: selectedTemplate,
title: t`Delete` + ' ' + templateTypeTranslation, title: t`Delete` + ' ' + templateTypeTranslation,
onFormSuccess: table.refreshTable table: table
}); });
const newTemplate = useCreateApiFormModal({ const newTemplate = useCreateApiFormModal({

View File

@ -230,7 +230,7 @@ export function UserTable() {
pk: selectedUser, pk: selectedUser,
title: t`Delete user`, title: t`Delete user`,
successMessage: t`User deleted`, successMessage: t`User deleted`,
onFormSuccess: table.refreshTable, table: table,
preFormWarning: t`Are you sure you want to delete this user?` preFormWarning: t`Are you sure you want to delete this user?`
}); });
@ -244,7 +244,7 @@ export function UserTable() {
first_name: {}, first_name: {},
last_name: {} last_name: {}
}, },
onFormSuccess: table.refreshTable, table: table,
successMessage: t`Added user` successMessage: t`Added user`
}); });

View File

@ -268,7 +268,7 @@ export default function StockItemTestResultTable({
result: true result: true
}, },
title: t`Add Test Result`, title: t`Add Test Result`,
onFormSuccess: () => table.refreshTable(), table: table,
successMessage: t`Test result added` successMessage: t`Test result added`
}); });
@ -279,7 +279,7 @@ export default function StockItemTestResultTable({
pk: selectedTest, pk: selectedTest,
fields: resultFields, fields: resultFields,
title: t`Edit Test Result`, title: t`Edit Test Result`,
onFormSuccess: () => table.refreshTable(), table: table,
successMessage: t`Test result updated` successMessage: t`Test result updated`
}); });
@ -287,7 +287,7 @@ export default function StockItemTestResultTable({
url: ApiEndpoints.stock_test_result_list, url: ApiEndpoints.stock_test_result_list,
pk: selectedTest, pk: selectedTest,
title: t`Delete Test Result`, title: t`Delete Test Result`,
onFormSuccess: () => table.refreshTable(), table: table,
successMessage: t`Test result deleted` successMessage: t`Test result deleted`
}); });

View File

@ -1,13 +1,11 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
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 { getDetailUrl } from '../../functions/urls';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useEditApiFormModal useEditApiFormModal
@ -28,8 +26,6 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
const table = useTable('stocklocation'); const table = useTable('stocklocation');
const user = useUserState(); const user = useUserState();
const navigate = useNavigate();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
{ {
@ -91,13 +87,9 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
initialData: { initialData: {
parent: parentId parent: parentId
}, },
onFormSuccess(data: any) { follow: true,
if (data.pk) { modelType: ModelType.stocklocation,
navigate(getDetailUrl(ModelType.stocklocation, data.pk)); table: table
} else {
table.refreshTable();
}
}
}); });
const [selectedLocation, setSelectedLocation] = useState<number>(-1); const [selectedLocation, setSelectedLocation] = useState<number>(-1);