mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-15 19:45:46 +00:00
[WIP] Plugin Updates (#6400)
* Add method to extract "install name" from a plugin * Include more information in plugin meta serializer * Implement better API filtering for PluginConfig list * Add an "update" button to the plugin table row actions - Only for "package" plugins * Adds method to update a plugin: - Add plugin.installer.update_plugin method - Add error logging to existing methods - Add API endpoint and serializer - Integrate into PUI table * Implement lazy loading for plugin tables * Extract package information on registry load - Info is already available via entrypoint data - Significantly faster as introspection operation is expensive - Less code is good code * Frontend updates * Add accordion to plugin page * Add setting to control periodic version check updates * Update API version info * Add "package_name" field to PluginConfig - When the plugin is loaded, save this name to the PluginConfig model - Update the admin view * Update API serializer * Refactor plugin installer code - Add common functions * Adds API endpoint for uninstalling an installed plugin * Allow uninstall of plugin via API - Add API endpoints - Add UI elements * Tweak for admin list display * Update plugin * Refactor "update" method - Just use the "install" function - Add optional "version" specifier - UI updates * Allow deletion of PluginConfig when uninstalling plugin * Add placeholder for deleting database tables * Revert code change - get_object() is required * Use registry.get_plugin() - Instead of registry.plugins.get() - get_plugin checks registry hash - performs registry reload if necessary * Add PluginValidationMixin class - Allows the entire model to be validated via plugins - Called on model.full_clean() - Called on model.save() * Update Validation sample plugin * Fix for InvenTreeTree models * Refactor build.models - Expose models to plugin validation * Update stock.models * Update more models - common.models - company.models * Update more models - label.models - order.models - part.models * More model updates * Update docs * Fix for potential plugin edge case - plugin slug is globally unique - do not use get_or_create with two lookup fields - will throw an IntegrityError if you change the name of a plugin * Inherit DiffMixin into PluginValidationMixin - Allows us to pass model diffs through to validation - Plugins can validate based on what has *changed* * Update documentation * Add get_plugin_config helper function * Bug fix * Bug fix * Update plugin hash when calling set_plugin_state * Working on unit testing * More unit testing * Fix typo (installing -> uninstalling) * Reduce default timeout * set default timeout as part of ApiDefaults * revert changes to launch.json * Remove delete_tables field - Will come back in a future PR * Fix display of nonFIeldErrors in ApiForm.tsx * Allow deletion of deleted plugins - PluginConfig which no longer matches a valid (installed) plugin * Cleanup * Move get_plugin_config into registry.py * Move extract_int into InvenTree.helpers * Fix log formatting * Update model definitions - Ensure there are no changes to the migrations * Update PluginErrorTable.tsx Remove unused var * Update PluginManagementPanel.tsx remove unused var * Comment out format line * Comment out format line * Fix access to get_plugin_config * Fix tests for SimpleActionPlugin * More unit test fixes * Update plugin/installer.py - Account for version string - Remove on uninstall * Fix
This commit is contained in:
@ -21,6 +21,7 @@ export function setApiDefaults() {
|
||||
const token = useSessionState.getState().token;
|
||||
|
||||
api.defaults.baseURL = host;
|
||||
api.defaults.timeout = 1000;
|
||||
|
||||
if (!!token) {
|
||||
api.defaults.headers.common['Authorization'] = `Token ${token}`;
|
||||
|
@ -79,6 +79,7 @@ export interface ApiFormProps {
|
||||
onFormSuccess?: (data: any) => void;
|
||||
onFormError?: () => void;
|
||||
actions?: ApiFormAction[];
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export function OptionsApiForm({
|
||||
@ -296,6 +297,7 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
|
||||
method: method,
|
||||
url: url,
|
||||
data: data,
|
||||
timeout: props.timeout,
|
||||
headers: {
|
||||
'Content-Type': hasFiles ? 'multipart/form-data' : 'application/json'
|
||||
}
|
||||
@ -339,13 +341,15 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
|
||||
switch (error.response.status) {
|
||||
case 400:
|
||||
// Data validation errors
|
||||
const nonFieldErrors: string[] = [];
|
||||
const _nonFieldErrors: string[] = [];
|
||||
const processErrors = (errors: any, _path?: string) => {
|
||||
for (const [k, v] of Object.entries(errors)) {
|
||||
const path = _path ? `${_path}.${k}` : k;
|
||||
|
||||
if (k === 'non_field_errors') {
|
||||
nonFieldErrors.push((v as string[]).join(', '));
|
||||
if (k === 'non_field_errors' || k === '__all__') {
|
||||
if (Array.isArray(v)) {
|
||||
_nonFieldErrors.push(...v);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -358,7 +362,7 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
|
||||
};
|
||||
|
||||
processErrors(error.response.data);
|
||||
setNonFieldErrors(nonFieldErrors);
|
||||
setNonFieldErrors(_nonFieldErrors);
|
||||
break;
|
||||
default:
|
||||
// Unexpected state on form error
|
||||
|
@ -97,6 +97,8 @@ export enum ApiEndpoints {
|
||||
plugin_registry_status = 'plugins/status/',
|
||||
plugin_install = 'plugins/install/',
|
||||
plugin_reload = 'plugins/reload/',
|
||||
plugin_activate = 'plugins/:id/activate/',
|
||||
plugin_uninstall = 'plugins/:id/uninstall/',
|
||||
|
||||
// Miscellaneous API endpoints
|
||||
error_report_list = 'error-report/',
|
||||
|
@ -1,11 +1,20 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Alert, Stack, Title } from '@mantine/core';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Accordion, Alert, Stack } from '@mantine/core';
|
||||
import { IconInfoCircle } from '@tabler/icons-react';
|
||||
import { lazy } from 'react';
|
||||
|
||||
import { StylishText } from '../../../../components/items/StylishText';
|
||||
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
||||
import { Loadable } from '../../../../functions/loading';
|
||||
import { useServerApiState } from '../../../../states/ApiState';
|
||||
import { PluginErrorTable } from '../../../../tables/plugin/PluginErrorTable';
|
||||
import { PluginListTable } from '../../../../tables/plugin/PluginListTable';
|
||||
|
||||
const PluginListTable = Loadable(
|
||||
lazy(() => import('../../../../tables/plugin/PluginListTable'))
|
||||
);
|
||||
|
||||
const PluginErrorTable = Loadable(
|
||||
lazy(() => import('../../../../tables/plugin/PluginErrorTable'))
|
||||
);
|
||||
|
||||
export default function PluginManagementPanel() {
|
||||
const pluginsEnabled = useServerApiState(
|
||||
@ -26,30 +35,44 @@ export default function PluginManagementPanel() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<PluginListTable props={{}} />
|
||||
<Accordion defaultValue="pluginlist">
|
||||
<Accordion.Item value="pluginlist">
|
||||
<Accordion.Control>
|
||||
<StylishText size="lg">{t`Plugins`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<PluginListTable />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Stack spacing={'xs'}>
|
||||
<Title order={5}>
|
||||
<Trans>Plugin Error Stack</Trans>
|
||||
</Title>
|
||||
<PluginErrorTable props={{}} />
|
||||
</Stack>
|
||||
<Accordion.Item value="pluginerror">
|
||||
<Accordion.Control>
|
||||
<StylishText size="lg">{t`Plugin Errors`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<PluginErrorTable />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Stack spacing={'xs'}>
|
||||
<Title order={5}>
|
||||
<Trans>Plugin Settings</Trans>
|
||||
</Title>
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'ENABLE_PLUGINS_SCHEDULE',
|
||||
'ENABLE_PLUGINS_EVENTS',
|
||||
'ENABLE_PLUGINS_URL',
|
||||
'ENABLE_PLUGINS_NAVIGATION',
|
||||
'ENABLE_PLUGINS_APP',
|
||||
'PLUGIN_ON_STARTUP'
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
<Accordion.Item value="pluginsettings">
|
||||
<Accordion.Control>
|
||||
<StylishText size="lg">{t`Plugin Settings`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'ENABLE_PLUGINS_SCHEDULE',
|
||||
'ENABLE_PLUGINS_EVENTS',
|
||||
'ENABLE_PLUGINS_URL',
|
||||
'ENABLE_PLUGINS_NAVIGATION',
|
||||
'ENABLE_PLUGINS_APP',
|
||||
'PLUGIN_ON_STARTUP',
|
||||
'PLUGIN_UPDATE_CHECK'
|
||||
]}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -127,9 +127,7 @@ export function AddressTable({
|
||||
onFormSuccess: table.refreshTable
|
||||
});
|
||||
|
||||
const [selectedAddress, setSelectedAddress] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [selectedAddress, setSelectedAddress] = useState<number>(-1);
|
||||
|
||||
const editAddress = useEditApiFormModal({
|
||||
url: ApiEndpoints.address_list,
|
||||
|
@ -88,9 +88,7 @@ export function PartCategoryTable({ parentId }: { parentId?: any }) {
|
||||
}
|
||||
});
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [selectedCategory, setSelectedCategory] = useState<number>(-1);
|
||||
|
||||
const editCategory = useEditApiFormModal({
|
||||
url: ApiEndpoints.category_list,
|
||||
|
@ -85,9 +85,7 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
|
||||
onFormSuccess: table.refreshTable
|
||||
});
|
||||
|
||||
const [selectedTest, setSelectedTest] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [selectedTest, setSelectedTest] = useState<number>(-1);
|
||||
|
||||
const editTestTemplate = useEditApiFormModal({
|
||||
url: ApiEndpoints.part_test_template_list,
|
||||
|
@ -6,7 +6,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
export interface PluginRegistryErrorI {
|
||||
id: number;
|
||||
@ -18,7 +18,7 @@ export interface PluginRegistryErrorI {
|
||||
/**
|
||||
* Table displaying list of plugin registry errors
|
||||
*/
|
||||
export function PluginErrorTable({ props }: { props: InvenTreeTableProps }) {
|
||||
export default function PluginErrorTable() {
|
||||
const table = useTable('registryErrors');
|
||||
|
||||
const registryErrorTableColumns: TableColumn<PluginRegistryErrorI>[] =
|
||||
@ -47,16 +47,12 @@ export function PluginErrorTable({ props }: { props: InvenTreeTableProps }) {
|
||||
tableState={table}
|
||||
columns={registryErrorTableColumns}
|
||||
props={{
|
||||
...props,
|
||||
dataFormatter: (data: any) =>
|
||||
data.registry_errors.map((e: any, i: number) => ({ id: i, ...e })),
|
||||
idAccessor: 'id',
|
||||
enableDownload: false,
|
||||
enableFilters: false,
|
||||
enableSearch: false,
|
||||
params: {
|
||||
...props.params
|
||||
}
|
||||
enableSearch: false
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -16,11 +16,12 @@ import {
|
||||
IconCircleCheck,
|
||||
IconCircleX,
|
||||
IconHelpCircle,
|
||||
IconInfoCircle,
|
||||
IconPlaylistAdd,
|
||||
IconRefresh
|
||||
} from '@tabler/icons-react';
|
||||
import { IconDots } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
@ -35,13 +36,17 @@ import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
||||
import { PluginSettingList } from '../../components/settings/SettingList';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { openEditApiForm } from '../../functions/forms';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl, useServerApiState } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowAction } from '../RowActions';
|
||||
|
||||
export interface PluginI {
|
||||
@ -52,6 +57,8 @@ export interface PluginI {
|
||||
is_builtin: boolean;
|
||||
is_sample: boolean;
|
||||
is_installed: boolean;
|
||||
is_package: boolean;
|
||||
package_name: string | null;
|
||||
meta: {
|
||||
author: string | null;
|
||||
description: string | null;
|
||||
@ -189,9 +196,16 @@ export function PluginDrawer({
|
||||
<Trans>Package information</Trans>
|
||||
</Title>
|
||||
<Stack pos="relative" spacing="xs">
|
||||
{plugin?.is_package && (
|
||||
<InfoItem
|
||||
type="text"
|
||||
name={t`Package Name`}
|
||||
value={plugin?.package_name}
|
||||
/>
|
||||
)}
|
||||
<InfoItem
|
||||
type="text"
|
||||
name={t`Installation path`}
|
||||
name={t`Installation Path`}
|
||||
value={plugin?.meta.package_path}
|
||||
/>
|
||||
<InfoItem
|
||||
@ -199,6 +213,11 @@ export function PluginDrawer({
|
||||
name={t`Builtin`}
|
||||
value={plugin?.is_builtin}
|
||||
/>
|
||||
<InfoItem
|
||||
type="boolean"
|
||||
name={t`Package`}
|
||||
value={plugin?.is_package}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
@ -247,9 +266,10 @@ function PluginIcon(plugin: PluginI) {
|
||||
/**
|
||||
* Table displaying list of available plugins
|
||||
*/
|
||||
export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
export default function PluginListTable() {
|
||||
const table = useTable('plugin');
|
||||
const navigate = useNavigate();
|
||||
const user = useUserState();
|
||||
|
||||
const pluginsEnabled = useServerApiState(
|
||||
(state) => state.server.plugins_enabled
|
||||
@ -337,7 +357,7 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
confirm: t`Confirm`
|
||||
},
|
||||
onConfirm: () => {
|
||||
let url = apiUrl(ApiEndpoints.plugin_list, plugin_id) + 'activate/';
|
||||
let url = apiUrl(ApiEndpoints.plugin_activate, plugin_id);
|
||||
|
||||
const id = 'plugin-activate';
|
||||
|
||||
@ -349,7 +369,13 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
});
|
||||
|
||||
api
|
||||
.patch(url, { active: active })
|
||||
.patch(
|
||||
url,
|
||||
{ active: active },
|
||||
{
|
||||
timeout: 30 * 1000
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
table.refreshTable();
|
||||
notifications.hide(id);
|
||||
@ -376,42 +402,98 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
);
|
||||
|
||||
// Determine available actions for a given plugin
|
||||
function rowActions(record: any): RowAction[] {
|
||||
let actions: RowAction[] = [];
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
// TODO: Plugin actions should be updated based on on the users's permissions
|
||||
|
||||
if (!record.is_builtin && record.is_installed) {
|
||||
if (record.active) {
|
||||
let actions: RowAction[] = [];
|
||||
|
||||
if (!record.is_builtin && record.is_installed) {
|
||||
if (record.active) {
|
||||
actions.push({
|
||||
title: t`Deactivate`,
|
||||
color: 'red',
|
||||
icon: <IconCircleX />,
|
||||
onClick: () => {
|
||||
activatePlugin(record.pk, record.name, false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
title: t`Activate`,
|
||||
color: 'green',
|
||||
icon: <IconCircleCheck />,
|
||||
onClick: () => {
|
||||
activatePlugin(record.pk, record.name, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Active 'package' plugins can be updated
|
||||
if (record.active && record.is_package && record.package_name) {
|
||||
actions.push({
|
||||
title: t`Deactivate`,
|
||||
color: 'red',
|
||||
icon: <IconCircleX />,
|
||||
title: t`Update`,
|
||||
color: 'blue',
|
||||
icon: <IconRefresh />,
|
||||
onClick: () => {
|
||||
activatePlugin(record.pk, record.name, false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
title: t`Activate`,
|
||||
color: 'green',
|
||||
icon: <IconCircleCheck />,
|
||||
onClick: () => {
|
||||
activatePlugin(record.pk, record.name, true);
|
||||
setPluginPackage(record.package_name);
|
||||
installPluginModal.open();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
// Inactive 'package' plugins can be uninstalled
|
||||
if (
|
||||
!record.active &&
|
||||
record.is_installed &&
|
||||
record.is_package &&
|
||||
record.package_name
|
||||
) {
|
||||
actions.push({
|
||||
title: t`Uninstall`,
|
||||
color: 'red',
|
||||
icon: <IconCircleX />,
|
||||
onClick: () => {
|
||||
setSelectedPlugin(record.pk);
|
||||
uninstallPluginModal.open();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Uninstalled 'package' plugins can be deleted
|
||||
if (!record.is_installed) {
|
||||
actions.push({
|
||||
title: t`Delete`,
|
||||
color: 'red',
|
||||
icon: <IconCircleX />,
|
||||
onClick: () => {
|
||||
setSelectedPlugin(record.pk);
|
||||
deletePluginModal.open();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
},
|
||||
[user, pluginsEnabled]
|
||||
);
|
||||
|
||||
const [pluginPackage, setPluginPackage] = useState<string>('');
|
||||
|
||||
const installPluginModal = useCreateApiFormModal({
|
||||
title: t`Install plugin`,
|
||||
url: ApiEndpoints.plugin_install,
|
||||
timeout: 30000,
|
||||
fields: {
|
||||
packagename: {},
|
||||
url: {},
|
||||
version: {},
|
||||
confirm: {}
|
||||
},
|
||||
initialData: {
|
||||
packagename: pluginPackage
|
||||
},
|
||||
closeOnClickOutside: false,
|
||||
submitText: t`Install`,
|
||||
successMessage: undefined,
|
||||
@ -427,7 +509,48 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
}
|
||||
});
|
||||
|
||||
const user = useUserState();
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<number>(-1);
|
||||
|
||||
const uninstallPluginModal = useEditApiFormModal({
|
||||
title: t`Uninstall Plugin`,
|
||||
url: ApiEndpoints.plugin_uninstall,
|
||||
pk: selectedPlugin,
|
||||
fetchInitialData: false,
|
||||
timeout: 30000,
|
||||
fields: {
|
||||
delete_config: {}
|
||||
},
|
||||
preFormContent: (
|
||||
<Alert
|
||||
color="red"
|
||||
icon={<IconInfoCircle />}
|
||||
title={t`Confirm plugin uninstall`}
|
||||
>
|
||||
<Stack spacing="xs">
|
||||
<Text>{t`The selected plugin will be uninstalled.`}</Text>
|
||||
<Text>{t`This action cannot be undone.`}</Text>
|
||||
</Stack>
|
||||
</Alert>
|
||||
),
|
||||
onFormSuccess: (data) => {
|
||||
notifications.show({
|
||||
title: t`Plugin uninstalled successfully`,
|
||||
message: data.result,
|
||||
autoClose: 30000,
|
||||
color: 'green'
|
||||
});
|
||||
|
||||
table.refreshTable();
|
||||
}
|
||||
});
|
||||
|
||||
const deletePluginModal = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.plugin_list,
|
||||
pk: 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?`
|
||||
});
|
||||
|
||||
const reloadPlugins = useCallback(() => {
|
||||
api
|
||||
@ -465,7 +588,10 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
color="green"
|
||||
icon={<IconPlaylistAdd />}
|
||||
tooltip={t`Install Plugin`}
|
||||
onClick={() => installPluginModal.open()}
|
||||
onClick={() => {
|
||||
setPluginPackage('');
|
||||
installPluginModal.open();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -476,9 +602,11 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
return (
|
||||
<>
|
||||
{installPluginModal.modal}
|
||||
{uninstallPluginModal.modal}
|
||||
{deletePluginModal.modal}
|
||||
<DetailDrawer
|
||||
title={t`Plugin detail`}
|
||||
size={'lg'}
|
||||
size={'xl'}
|
||||
renderContent={(id) => {
|
||||
if (!id) return false;
|
||||
return <PluginDrawer id={id} refreshTable={table.refreshTable} />;
|
||||
@ -489,11 +617,7 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
tableState={table}
|
||||
columns={pluginTableColumns}
|
||||
props={{
|
||||
...props,
|
||||
enableDownload: false,
|
||||
params: {
|
||||
...props.params
|
||||
},
|
||||
rowActions: rowActions,
|
||||
onRowClick: (plugin) => navigate(`${plugin.pk}/`),
|
||||
tableActions: tableActions,
|
||||
|
@ -52,9 +52,7 @@ export default function CustomUnitsTable() {
|
||||
onFormSuccess: table.refreshTable
|
||||
});
|
||||
|
||||
const [selectedUnit, setSelectedUnit] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [selectedUnit, setSelectedUnit] = useState<number>(-1);
|
||||
|
||||
const editUnit = useEditApiFormModal({
|
||||
url: ApiEndpoints.custom_unit_list,
|
||||
|
@ -118,9 +118,7 @@ export function GroupTable() {
|
||||
];
|
||||
}, []);
|
||||
|
||||
const [selectedGroup, setSelectedGroup] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [selectedGroup, setSelectedGroup] = useState<number>(-1);
|
||||
|
||||
const deleteGroup = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.group_list,
|
||||
|
@ -98,9 +98,7 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
|
||||
}
|
||||
});
|
||||
|
||||
const [selectedLocation, setSelectedLocation] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [selectedLocation, setSelectedLocation] = useState<number>(-1);
|
||||
|
||||
const editLocation = useEditApiFormModal({
|
||||
url: ApiEndpoints.stock_location_list,
|
||||
|
Reference in New Issue
Block a user