mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36: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:
parent
2cabd02c6b
commit
8df34cefd6
@ -62,7 +62,7 @@ class PluginWithSettings(SettingsMixin, InvenTreePlugin):
|
|||||||
'name': _('Assembled Part'),
|
'name': _('Assembled Part'),
|
||||||
'description': _('Settings can point to internal database models'),
|
'description': _('Settings can point to internal database models'),
|
||||||
'model': 'part.part',
|
'model': 'part.part',
|
||||||
'filters': {
|
'model_filters': {
|
||||||
'active': True,
|
'active': True,
|
||||||
'assembly': True
|
'assembly': True
|
||||||
}
|
}
|
||||||
|
@ -72,14 +72,16 @@ class SampleIntegrationPlugin(
|
|||||||
'default': 'A',
|
'default': 'A',
|
||||||
},
|
},
|
||||||
'SELECT_COMPANY': {
|
'SELECT_COMPANY': {
|
||||||
'name': 'Company',
|
'name': 'Supplier',
|
||||||
'description': 'Select a company object from the database',
|
'description': 'Select a supplier object from the database',
|
||||||
'model': 'company.company',
|
'model': 'company.company',
|
||||||
|
'model_filters': {'is_supplier': True},
|
||||||
},
|
},
|
||||||
'SELECT_PART': {
|
'SELECT_PART': {
|
||||||
'name': 'Part',
|
'name': 'Part',
|
||||||
'description': 'Select a part object from the database',
|
'description': 'Select a part object from the database',
|
||||||
'model': 'part.part',
|
'model': 'part.part',
|
||||||
|
'model_filters': {'active': True},
|
||||||
},
|
},
|
||||||
'PROTECTED_SETTING': {
|
'PROTECTED_SETTING': {
|
||||||
'name': 'Protected Setting',
|
'name': 'Protected Setting',
|
||||||
|
@ -146,7 +146,7 @@ function NameBadge({
|
|||||||
return <Skeleton height={12} radius='md' />;
|
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() {
|
function _render_name() {
|
||||||
if (!data || !data.pk) {
|
if (!data || !data.pk) {
|
||||||
return '';
|
return '';
|
||||||
|
@ -9,11 +9,16 @@ import {
|
|||||||
useMantineColorScheme
|
useMantineColorScheme
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
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 type { Setting } from '../../states/states';
|
||||||
import { vars } from '../../theme';
|
import { vars } from '../../theme';
|
||||||
import { Boundary } from '../Boundary';
|
import { Boundary } from '../Boundary';
|
||||||
|
import { RenderInstance } from '../render/Instance';
|
||||||
|
import { ModelInformationDict } from '../render/ModelType';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a single setting value
|
* Render a single setting value
|
||||||
@ -44,12 +49,61 @@ function SettingValue({
|
|||||||
return value;
|
return value;
|
||||||
}, [setting]);
|
}, [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') {
|
switch (setting?.type || 'string') {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
size='sm'
|
size='sm'
|
||||||
radius='lg'
|
radius='lg'
|
||||||
|
aria-label={`toggle-setting-${setting.key}`}
|
||||||
checked={setting.value.toLowerCase() == 'true'}
|
checked={setting.value.toLowerCase() == 'true'}
|
||||||
onChange={(event) => onToggle(setting, event.currentTarget.checked)}
|
onChange={(event) => onToggle(setting, event.currentTarget.checked)}
|
||||||
style={{
|
style={{
|
||||||
@ -61,12 +115,20 @@ function SettingValue({
|
|||||||
return valueText ? (
|
return valueText ? (
|
||||||
<Group gap='xs' justify='right'>
|
<Group gap='xs' justify='right'>
|
||||||
<Space />
|
<Space />
|
||||||
<Button variant='subtle' onClick={() => onEdit(setting)}>
|
<Button
|
||||||
|
aria-label={`edit-setting-${setting.key}`}
|
||||||
|
variant='subtle'
|
||||||
|
onClick={() => onEdit(setting)}
|
||||||
|
>
|
||||||
{valueText}
|
{valueText}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
) : (
|
) : (
|
||||||
<Button variant='subtle' onClick={() => onEdit(setting)}>
|
<Button
|
||||||
|
aria-label={`edit-setting-${setting.key}`}
|
||||||
|
variant='subtle'
|
||||||
|
onClick={() => onEdit(setting)}
|
||||||
|
>
|
||||||
<IconEdit />
|
<IconEdit />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -22,6 +22,7 @@ export function TableSearchInput({
|
|||||||
<TextInput
|
<TextInput
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
aria-label='table-search-input'
|
||||||
leftSection={<IconSearch />}
|
leftSection={<IconSearch />}
|
||||||
placeholder={t`Search`}
|
placeholder={t`Search`}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
|
@ -1,9 +1,54 @@
|
|||||||
import test from 'playwright/test';
|
import test from 'playwright/test';
|
||||||
|
|
||||||
import { loadTab, navigate } from './helpers.js';
|
import { clearTableFilters, loadTab, navigate } from './helpers.js';
|
||||||
import { doQuickLogin } from './login.js';
|
import { doQuickLogin } from './login.js';
|
||||||
import { setPluginState, setSettingState } from './settings.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 }) => {
|
test('Plugins - Panels', async ({ page, request }) => {
|
||||||
await doQuickLogin(page, 'admin', 'inventree');
|
await doQuickLogin(page, 'admin', 'inventree');
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user