[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';
import { invalidResponse } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import { TableState } from '../../hooks/UseTable';
import { PathParams } from '../../states/ApiState';
import { Boundary } from '../Boundary';
import {
@ -54,6 +55,7 @@ export interface ApiFormAction {
* Properties for the ApiForm component
* @param url : The API endpoint to fetch the form data from
* @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 method : Optional HTTP method to use when submitting the form (default: GET)
* @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 modelType : Define a model type for this form
* @param follow : Boolean, follow the result of the form (if possible)
* @param table : Table to update on success (if provided)
*/
export interface ApiFormProps {
url: ApiEndpoints | string;
pk?: number | string | undefined;
pk_field?: string;
pathParams?: PathParams;
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
fields?: ApiFormFieldSet;
@ -87,6 +91,7 @@ export interface ApiFormProps {
successMessage?: string;
onFormSuccess?: (data: any) => void;
onFormError?: () => void;
table?: TableState;
modelType?: ModelType;
follow?: boolean;
actions?: ApiFormAction[];
@ -391,14 +396,22 @@ export function ApiForm({
case 204:
// Form was submitted successfully
// Optionally call the onFormSuccess callback
if (props.onFormSuccess) {
// A custom callback hook is provided
props.onFormSuccess(response.data);
}
if (props.follow) {
if (props.modelType && response.data?.pk) {
navigate(getDetailUrl(props.modelType, response.data?.pk));
if (props.follow && props.modelType && response.data?.pk) {
// If we want to automatically follow the returned data
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 {
Button,
Group,
@ -9,103 +8,25 @@ import {
Text,
useMantineColorScheme
} from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconEdit } from '@tabler/icons-react';
import { useMemo } from 'react';
import { api } from '../../App';
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 { Setting } from '../../states/states';
import { vars } from '../../theme';
import { ApiFormFieldType } from '../forms/fields/ApiFormField';
import { Boundary } from '../Boundary';
/**
* Render a single setting value
*/
function SettingValue({
settingsState,
setting,
onChange
onEdit,
onToggle
}: {
settingsState: SettingsStateProps;
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
const valueText: string = useMemo(() => {
let value = setting.value;
@ -130,7 +51,7 @@ function SettingValue({
size="sm"
radius="lg"
checked={setting.value.toLowerCase() == 'true'}
onChange={(event) => onToggle(event.currentTarget.checked)}
onChange={(event) => onToggle(setting, event.currentTarget.checked)}
style={{
paddingRight: '20px'
}}
@ -140,12 +61,12 @@ function SettingValue({
return valueText ? (
<Group gap="xs" justify="right">
<Space />
<Button variant="subtle" onClick={onEditButton}>
<Button variant="subtle" onClick={() => onEdit(setting)}>
{valueText}
</Button>
</Group>
) : (
<Button variant="subtle" onClick={onEditButton}>
<Button variant="subtle" onClick={() => onEdit(setting)}>
<IconEdit />
</Button>
);
@ -156,15 +77,15 @@ function SettingValue({
* Display a single setting item, and allow editing of the value
*/
export function SettingItem({
settingsState,
setting,
shaded,
onChange
onEdit,
onToggle
}: {
settingsState: SettingsStateProps;
setting: Setting;
shaded: boolean;
onChange?: () => void;
onEdit: (setting: Setting) => void;
onToggle: (setting: Setting, value: boolean) => void;
}) {
const { colorScheme } = useMantineColorScheme();
@ -184,11 +105,9 @@ export function SettingItem({
</Text>
<Text size="xs">{setting.description}</Text>
</Stack>
<SettingValue
settingsState={settingsState}
setting={setting}
onChange={onChange}
/>
<Boundary label={`setting-value-${setting.key}`}>
<SettingValue setting={setting} onEdit={onEdit} onToggle={onToggle} />
</Boundary>
</Group>
</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 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 { api } from '../../App';
import { ModelType } from '../../enums/ModelType';
import { useEditApiFormModal } from '../../hooks/UseForm';
import { apiUrl } from '../../states/ApiState';
import {
SettingsStateProps,
createMachineSettingsState,
@ -10,6 +21,7 @@ import {
useGlobalSettingsState,
useUserSettingsState
} from '../../states/SettingsState';
import { Setting } from '../../states/states';
import { SettingItem } from './SettingItem';
/**
@ -33,8 +45,82 @@ export function SettingList({
[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 (
<>
{editSettingModal.modal}
<Stack gap="xs">
{(keys || allKeys).map((key, i) => {
const setting = settingsState?.settings?.find(
@ -45,10 +131,10 @@ export function SettingList({
<React.Fragment key={key}>
{setting ? (
<SettingItem
settingsState={settingsState}
setting={setting}
shaded={i % 2 === 0}
onChange={onChange}
onEdit={onValueEdit}
onToggle={onValueToggle}
/>
) : (
<Text size="sm" style={{ fontStyle: 'italic' }} color="red">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,12 +10,10 @@ import {
Title,
Tooltip
} from '@mantine/core';
import { modals } from '@mantine/modals';
import { notifications, showNotification } from '@mantine/notifications';
import { showNotification } from '@mantine/notifications';
import {
IconCircleCheck,
IconCircleX,
IconDots,
IconHelpCircle,
IconInfoCircle,
IconPlaylistAdd,
@ -26,16 +24,11 @@ import { useNavigate } from 'react-router-dom';
import { api } from '../../App';
import { ActionButton } from '../../components/buttons/ActionButton';
import {
ActionDropdown,
EditItemAction
} from '../../components/items/ActionDropdown';
import { YesNoButton } from '../../components/buttons/YesNoButton';
import { InfoItem } from '../../components/items/InfoItem';
import { StylishText } from '../../components/items/StylishText';
import { DetailDrawer } from '../../components/nav/DetailDrawer';
import { PluginSettingList } from '../../components/settings/SettingList';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { openEditApiForm } from '../../functions/forms';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
@ -80,16 +73,9 @@ export interface PluginI {
>;
}
export function PluginDrawer({
pluginKey,
refreshTable
}: {
pluginKey: string;
refreshTable: () => void;
}) {
export function PluginDrawer({ pluginKey }: { pluginKey: Readonly<string> }) {
const {
instance: plugin,
refreshInstance,
instanceQuery: { isFetching, error }
} = useInstance<PluginI>({
endpoint: ApiEndpoints.plugin_list,
@ -98,11 +84,6 @@ export function PluginDrawer({
throwError: true
});
const refetch = useCallback(() => {
refreshTable();
refreshInstance();
}, [refreshTable, refreshInstance]);
if (!pluginKey || isFetching) {
return <LoadingOverlay visible={true} />;
}
@ -121,44 +102,18 @@ export function PluginDrawer({
return (
<Stack gap={'xs'}>
<Group justify="space-between">
<Box></Box>
<Card withBorder>
<Group justify="left">
<Box></Box>
<Group gap={'xs'}>
{plugin && <PluginIcon plugin={plugin} />}
<Title order={4}>
{plugin?.meta?.human_name ?? plugin?.name ?? '-'}
</Title>
<Group gap={'xs'}>
{plugin && <PluginIcon plugin={plugin} />}
<Title order={4}>
{plugin?.meta?.human_name ?? plugin?.name ?? '-'}
</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>
</Card>
<LoadingOverlay visible={isFetching} overlayProps={{ opacity: 0 }} />
<Card withBorder>
@ -166,64 +121,74 @@ export function PluginDrawer({
<Title order={4}>
<Trans>Plugin information</Trans>
</Title>
<Stack pos="relative" gap="xs">
<InfoItem type="text" name={t`Name`} value={plugin?.name} />
<InfoItem
type="text"
name={t`Description`}
value={plugin?.meta.description}
/>
<InfoItem
type="text"
name={t`Author`}
value={plugin?.meta.author}
/>
<InfoItem
type="text"
name={t`Date`}
value={plugin?.meta.pub_date}
/>
<InfoItem
type="text"
name={t`Version`}
value={plugin?.meta.version}
/>
<InfoItem type="boolean" name={t`Active`} value={plugin?.active} />
</Stack>
{plugin.active ? (
<Stack pos="relative" gap="xs">
<InfoItem type="text" name={t`Name`} value={plugin?.name} />
<InfoItem
type="text"
name={t`Description`}
value={plugin?.meta.description}
/>
<InfoItem
type="text"
name={t`Author`}
value={plugin?.meta.author}
/>
<InfoItem
type="text"
name={t`Date`}
value={plugin?.meta.pub_date}
/>
<InfoItem
type="text"
name={t`Version`}
value={plugin?.meta.version}
/>
<InfoItem
type="boolean"
name={t`Active`}
value={plugin?.active}
/>
</Stack>
) : (
<Text color="red">{t`Plugin is not active`}</Text>
)}
</Stack>
</Card>
<Card withBorder>
<Stack gap="md">
<Title order={4}>
<Trans>Package information</Trans>
</Title>
<Stack pos="relative" gap="xs">
{plugin?.is_package && (
{plugin.active && (
<Card withBorder>
<Stack gap="md">
<Title order={4}>
<Trans>Package information</Trans>
</Title>
<Stack pos="relative" gap="xs">
{plugin?.is_package && (
<InfoItem
type="text"
name={t`Package Name`}
value={plugin?.package_name}
/>
)}
<InfoItem
type="text"
name={t`Package Name`}
value={plugin?.package_name}
name={t`Installation Path`}
value={plugin?.meta.package_path}
/>
)}
<InfoItem
type="text"
name={t`Installation Path`}
value={plugin?.meta.package_path}
/>
<InfoItem
type="boolean"
name={t`Builtin`}
value={plugin?.is_builtin}
/>
<InfoItem
type="boolean"
name={t`Package`}
value={plugin?.is_package}
/>
<InfoItem
type="boolean"
name={t`Builtin`}
value={plugin?.is_builtin}
/>
<InfoItem
type="boolean"
name={t`Package`}
value={plugin?.is_package}
/>
</Stack>
</Stack>
</Stack>
</Card>
</Card>
)}
{plugin && plugin?.active && (
<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',
title: t`Description`,
@ -312,6 +283,7 @@ export default function PluginListTable() {
return (
<Text
style={{ fontStyle: 'italic' }}
size="sm"
>{t`Description not available`}</Text>
);
}
@ -333,87 +305,49 @@ export default function PluginListTable() {
[]
);
const activatePlugin = useCallback(
(plugin_key: string, plugin_name: string, active: boolean) => {
modals.openConfirmModal({
title: (
<StylishText>
{active ? t`Activate Plugin` : t`Deactivate Plugin`}
</StylishText>
),
children: (
<Alert
color="green"
icon={<IconCircleCheck />}
title={
active
? t`Confirm plugin activation`
: t`Confirm plugin deactivation`
}
>
<Stack gap="xs">
<Text>
{active
? t`The following plugin will be activated`
: t`The following plugin will be deactivated`}
:
</Text>
<Text size="lg" style={{ fontStyle: 'italic' }}>
{plugin_name}
</Text>
</Stack>
</Alert>
),
labels: {
cancel: t`Cancel`,
confirm: t`Confirm`
},
onConfirm: () => {
let url = apiUrl(ApiEndpoints.plugin_activate, null, {
key: plugin_key
});
const [selectedPlugin, setSelectedPlugin] = useState<string>('');
const [activate, setActivate] = useState<boolean>(false);
const id = 'plugin-activate';
const activateModalContent = useMemo(() => {
return (
<Stack gap="xs">
<Alert
color={activate ? 'green' : 'red'}
icon={<IconCircleCheck />}
title={
activate
? t`Confirm plugin activation`
: t`Confirm plugin deactivation`
}
>
<Text>
{activate
? t`The selected plugin will be activated`
: t`The selected plugin will be deactivated`}
</Text>
</Alert>
</Stack>
);
}, [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'
});
});
}
});
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
const rowActions = useCallback(
@ -429,7 +363,9 @@ export default function PluginListTable() {
color: 'red',
icon: <IconCircleX />,
onClick: () => {
activatePlugin(record.key, record.name, false);
setSelectedPlugin(record.key);
setActivate(false);
activatePluginModal.open();
}
});
} else {
@ -438,7 +374,9 @@ export default function PluginListTable() {
color: 'green',
icon: <IconCircleCheck />,
onClick: () => {
activatePlugin(record.key, record.name, true);
setSelectedPlugin(record.key);
setActivate(true);
activatePluginModal.open();
}
});
}
@ -511,21 +449,10 @@ export default function PluginListTable() {
},
closeOnClickOutside: false,
submitText: t`Install`,
successMessage: undefined,
onFormSuccess: (data) => {
notifications.show({
title: t`Plugin installed successfully`,
message: data.result,
autoClose: 30000,
color: 'green'
});
table.refreshTable();
}
successMessage: t`Plugin installed successfully`,
table: table
});
const [selectedPlugin, setSelectedPlugin] = useState<string>('');
const uninstallPluginModal = useEditApiFormModal({
title: t`Uninstall Plugin`,
url: ApiEndpoints.plugin_uninstall,
@ -547,24 +474,16 @@ export default function PluginListTable() {
</Stack>
</Alert>
),
onFormSuccess: (data) => {
notifications.show({
title: t`Plugin uninstalled successfully`,
message: data.result,
autoClose: 30000,
color: 'green'
});
table.refreshTable();
}
successMessage: t`Plugin uninstalled successfully`,
table: table
});
const deletePluginModal = useDeleteApiFormModal({
url: ApiEndpoints.plugin_list,
pathParams: { key: selectedPlugin },
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(() => {
@ -617,6 +536,7 @@ export default function PluginListTable() {
return (
<>
{activatePluginModal.modal}
{installPluginModal.modal}
{uninstallPluginModal.modal}
{deletePluginModal.modal}
@ -625,12 +545,7 @@ export default function PluginListTable() {
size={'50%'}
renderContent={(pluginKey) => {
if (!pluginKey) return;
return (
<PluginDrawer
pluginKey={pluginKey}
refreshTable={table.refreshTable}
/>
);
return <PluginDrawer pluginKey={pluginKey} />;
}}
/>
<InvenTreeTable

View File

@ -1,10 +1,15 @@
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 { UserRoles } from '../../enums/Roles';
import { useManufacturerPartParameterFields } from '../../forms/CompanyForms';
import { openDeleteApiForm, openEditApiForm } from '../../functions/forms';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@ -45,34 +50,50 @@ export default function ManufacturerPartParameterTable({
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(
(record: any) => {
return [
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order),
onClick: () => {
openEditApiForm({
url: ApiEndpoints.manufacturer_part_parameter_list,
pk: record.pk,
title: t`Edit Parameter`,
fields: fields,
onFormSuccess: table.refreshTable,
successMessage: t`Parameter updated`
});
setSelectedParameter(record.pk);
editParameter.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order),
onClick: () => {
record.pk &&
openDeleteApiForm({
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?`
});
setSelectedParameter(record.pk);
deleteParameter.open();
}
})
];
@ -80,17 +101,36 @@ export default function ManufacturerPartParameterTable({
[user]
);
const tableActions = useMemo(() => {
return [
<AddItemButton
key="add-parameter"
tooltip={t`Add Parameter`}
onClick={() => {
createParameter.open();
}}
hidden={!user.hasAddRole(UserRoles.purchase_order)}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiEndpoints.manufacturer_part_parameter_list)}
tableState={table}
columns={tableColumns}
props={{
params: {
...params
},
rowActions: rowActions
}}
/>
<>
{createParameter.modal}
{editParameter.modal}
{deleteParameter.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.manufacturer_part_parameter_list)}
tableState={table}
columns={tableColumns}
props={{
params: {
...params
},
rowActions: rowActions,
tableActions: tableActions
}}
/>
</>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import { useCallback, useMemo, useState } from 'react';
import { StylishText } from '../../components/items/StylishText';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { openDeleteApiForm } from '../../functions/forms';
import { useDeleteApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
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[] => {
return [
RowDeleteAction({
onClick: () => {
openDeleteApiForm({
url: ApiEndpoints.error_report_list,
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?`
});
setSelectedError(record.pk);
deleteErrorModal.open();
}
})
];
@ -60,6 +69,7 @@ export default function ErrorReportTable() {
return (
<>
{deleteErrorModal.modal}
<Drawer
opened={opened}
size="xl"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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