2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-27 19:16:44 +00:00

[UI] Settings render (#9148)

* Update sample plugin

* Inline rendering of model based settings

* Spelling fix

* Add playwright testing
This commit is contained in:
Oliver 2025-02-22 21:59:06 +11:00 committed by GitHub
parent 2cabd02c6b
commit 8df34cefd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 118 additions and 8 deletions

View File

@ -62,7 +62,7 @@ class PluginWithSettings(SettingsMixin, InvenTreePlugin):
'name': _('Assembled Part'),
'description': _('Settings can point to internal database models'),
'model': 'part.part',
'filters': {
'model_filters': {
'active': True,
'assembly': True
}

View File

@ -72,14 +72,16 @@ class SampleIntegrationPlugin(
'default': 'A',
},
'SELECT_COMPANY': {
'name': 'Company',
'description': 'Select a company object from the database',
'name': 'Supplier',
'description': 'Select a supplier object from the database',
'model': 'company.company',
'model_filters': {'is_supplier': True},
},
'SELECT_PART': {
'name': 'Part',
'description': 'Select a part object from the database',
'model': 'part.part',
'model_filters': {'active': True},
},
'PROTECTED_SETTING': {
'name': 'Protected Setting',

View File

@ -146,7 +146,7 @@ function NameBadge({
return <Skeleton height={12} radius='md' />;
}
// Rendering a user's rame for the badge
// Rendering a user's name for the badge
function _render_name() {
if (!data || !data.pk) {
return '';

View File

@ -9,11 +9,16 @@ import {
useMantineColorScheme
} from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { api } from '../../App';
import { ModelType } from '../../enums/ModelType';
import { apiUrl } from '../../states/ApiState';
import type { Setting } from '../../states/states';
import { vars } from '../../theme';
import { Boundary } from '../Boundary';
import { RenderInstance } from '../render/Instance';
import { ModelInformationDict } from '../render/ModelType';
/**
* Render a single setting value
@ -44,12 +49,61 @@ function SettingValue({
return value;
}, [setting]);
const [modelInstance, setModelInstance] = useState<any>(null);
// Does this setting map to an internal database model?
const modelType: ModelType | null = useMemo(() => {
if (setting.model_name) {
const model = setting.model_name.split('.')[1];
return ModelType[model as keyof typeof ModelType] || null;
}
return null;
}, [setting]);
useEffect(() => {
setModelInstance(null);
if (modelType && setting.value) {
const endpoint = ModelInformationDict[modelType].api_endpoint;
api
.get(apiUrl(endpoint, setting.value))
.then((response) => {
if (response.data) {
setModelInstance(response.data);
} else {
setModelInstance(null);
}
})
.catch((error) => {
setModelInstance(null);
});
}
}, [setting, modelType]);
// If a full model instance is available, render it
if (modelInstance && modelType && setting.value) {
return (
<Group justify='right' gap='xs'>
<RenderInstance instance={modelInstance} model={modelType} />
<Button
aria-label={`edit-setting-${setting.key}`}
variant='subtle'
onClick={() => onEdit(setting)}
>
<IconEdit />
</Button>
</Group>
);
}
switch (setting?.type || 'string') {
case 'boolean':
return (
<Switch
size='sm'
radius='lg'
aria-label={`toggle-setting-${setting.key}`}
checked={setting.value.toLowerCase() == 'true'}
onChange={(event) => onToggle(setting, event.currentTarget.checked)}
style={{
@ -61,12 +115,20 @@ function SettingValue({
return valueText ? (
<Group gap='xs' justify='right'>
<Space />
<Button variant='subtle' onClick={() => onEdit(setting)}>
<Button
aria-label={`edit-setting-${setting.key}`}
variant='subtle'
onClick={() => onEdit(setting)}
>
{valueText}
</Button>
</Group>
) : (
<Button variant='subtle' onClick={() => onEdit(setting)}>
<Button
aria-label={`edit-setting-${setting.key}`}
variant='subtle'
onClick={() => onEdit(setting)}
>
<IconEdit />
</Button>
);

View File

@ -22,6 +22,7 @@ export function TableSearchInput({
<TextInput
value={value}
disabled={disabled}
aria-label='table-search-input'
leftSection={<IconSearch />}
placeholder={t`Search`}
onChange={(event) => setValue(event.target.value)}

View File

@ -1,9 +1,54 @@
import test from 'playwright/test';
import { loadTab, navigate } from './helpers.js';
import { clearTableFilters, loadTab, navigate } from './helpers.js';
import { doQuickLogin } from './login.js';
import { setPluginState, setSettingState } from './settings.js';
// Unit test for plugin settings
test('Plugins - Settings', async ({ page, request }) => {
await doQuickLogin(page, 'admin', 'inventree');
// Ensure that the SampleIntegration plugin is enabled
await setPluginState({
request,
plugin: 'sample',
state: true
});
// Navigate and select the plugin
await navigate(page, 'settings/admin/plugin/');
await clearTableFilters(page);
await page.getByLabel('table-search-input').fill('integration');
await page
.getByRole('row', { name: 'SampleIntegrationPlugin' })
.getByRole('paragraph')
.click();
await page.getByRole('button', { name: 'Plugin Information' }).click();
await page
.getByLabel('Plugin Detail -')
.getByRole('button', { name: 'Plugin Settings' })
.waitFor();
// Edit numerical value
await page.getByLabel('edit-setting-NUMERICAL_SETTING').click();
const originalValue = await page.getByLabel('number-field-value').innerText();
await page
.getByLabel('number-field-value')
.fill(originalValue == '999' ? '1000' : '999');
await page.getByRole('button', { name: 'Submit' }).click();
// Change it back
await page.getByLabel('edit-setting-NUMERICAL_SETTING').click();
await page.getByLabel('number-field-value').fill(originalValue);
await page.getByRole('button', { name: 'Submit' }).click();
// Select supplier
await page.getByLabel('edit-setting-SELECT_COMPANY').click();
await page.getByLabel('related-field-value').fill('mouser');
await page.getByText('Mouser Electronics').click();
});
test('Plugins - Panels', async ({ page, request }) => {
await doQuickLogin(page, 'admin', 'inventree');