2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-22 23:00:54 +00:00

[UI] Web Prefix ()

* [UI] Change default web prefix

- Adjust default from "platform" to "web"
- Much more standard prefix

* Cleanup

* Fixes for playwright tests

* Fix unit tests

* Refactor base_url into getBaseUrl
This commit is contained in:
Oliver
2025-03-20 00:12:52 +11:00
committed by GitHub
parent 832d884c85
commit 662a0b275e
38 changed files with 80 additions and 98 deletions

@ -549,7 +549,7 @@ jobs:
invoke migrate
platform_ui:
name: Tests - Platform UI
name: Tests - Web UI
runs-on: ubuntu-20.04
timeout-minutes: 60
needs: ["pre-commit", "paths-filter"]
@ -618,7 +618,7 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: inventree/InvenTree
flags: pui
flags: web
- name: Upload bundler info
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@ -628,7 +628,7 @@ jobs:
yarn run build
platform_ui_build:
name: Build - UI Platform
name: Build - Web UI
runs-on: ubuntu-20.04
timeout-minutes: 60

@ -22,7 +22,7 @@ flag_management:
statuses:
- type: project
target: 40%
- name: pui
- name: web
carryforward: true
statuses:
- type: project

@ -73,7 +73,7 @@ You can now set breakpoints and vscode will automatically pause execution if tha
!!! info "React Frontend development"
The React frontend requires additional steps to run. Refer to [Platform UI / React](./react-frontend.md)
The React frontend requires additional steps to run. Refer to [Web UI / React](./react-frontend.md)
### Plugin development

@ -4,7 +4,7 @@ title: Template editor
## Template editor
The Template Editor is integrated into the Admin center of Platform UI (PUI). It allows users to create and edit label and report templates directly with a side by side preview for a more productive workflow.
The Template Editor is integrated into the Admin center of the Web UI. It allows users to create and edit label and report templates directly with a side by side preview for a more productive workflow.
![Template Table](../assets/images/report/template-table.png)

@ -8,7 +8,7 @@ A stock location represents a physical real-world location where *Stock Items* a
### Icons
Stock locations can be assigned custom icons (either directly or through [Stock Location Types](#stock-location-type)). When using PUI there is a custom icon picker component available that can help to select the right icon. However in CUI the icon needs to be entered manually.
Stock locations can be assigned custom icons (either directly or through [Stock Location Types](#stock-location-type)). In the web interface there is a custom icon picker component available that can help to select the right icon. However in CUI the icon needs to be entered manually.
By default, the tabler icons package (with prefix: `ti`) is available. To manually select an item, search on the [tabler icons](https://tabler.io/icons) page for an icon and copy its name e.g. `bookmark`. Some icons have a filled and an outline version (if no variants are specified, it's an outline variant). Now these values can be put into the format: `<package-prefix>:<icon-name>:<variant>`. E.g. `ti:bookmark:outline` or `ti:bookmark:filled`.

@ -33,8 +33,8 @@ class AllUserRequire2FAMiddleware(MiddlewareMixin):
'api-token',
# web platform urls
'password_reset_confirm',
'platform',
'platform-wildcard',
'web',
'web-wildcard',
'web-assets',
]
app_names = ['headless']

@ -7,7 +7,6 @@ import os
import random
import shutil
import string
import warnings
from pathlib import Path
logger = logging.getLogger('inventree')
@ -401,51 +400,33 @@ def get_custom_file(
def get_frontend_settings(debug=True):
"""Return a dictionary of settings for the frontend interface.
Note that the new config settings use the 'FRONTEND' key,
whereas the legacy key was 'PUI' (platform UI) which is now deprecated
"""
# Legacy settings
pui_settings = get_setting(
'INVENTREE_PUI_SETTINGS', 'pui_settings', {}, typecast=dict
)
if len(pui_settings) > 0:
warnings.warn(
"The 'INVENTREE_PUI_SETTINGS' key is deprecated. Please use 'INVENTREE_FRONTEND_SETTINGS' instead",
DeprecationWarning,
stacklevel=2,
)
"""Return a dictionary of settings for the frontend interface."""
# New settings
frontend_settings = get_setting(
'INVENTREE_FRONTEND_SETTINGS', 'frontend_settings', {}, typecast=dict
)
# Merge settings
settings = {**pui_settings, **frontend_settings}
# Set the base URL
if 'base_url' not in settings:
settings['base_url'] = get_setting(
'INVENTREE_FRONTEND_URL_BASE', 'frontend_url_base', 'platform'
if 'base_url' not in frontend_settings:
frontend_settings['base_url'] = (
get_setting('INVENTREE_FRONTEND_URL_BASE', 'frontend_url_base', 'web')
or 'web'
)
# Set the server list
settings['server_list'] = settings.get('server_list', [])
frontend_settings['server_list'] = frontend_settings.get('server_list', [])
# Set the debug flag
settings['debug'] = debug
frontend_settings['debug'] = debug
if 'environment' not in settings:
settings['environment'] = 'development' if debug else 'production'
if 'environment' not in frontend_settings:
frontend_settings['environment'] = 'development' if debug else 'production'
if (debug and 'show_server_selector' not in settings) or len(
settings['server_list']
if (debug and 'show_server_selector' not in frontend_settings) or len(
frontend_settings['server_list']
) == 0:
# In debug mode, show server selector by default
# If no servers are specified, show server selector
settings['show_server_selector'] = True
frontend_settings['show_server_selector'] = True
return settings
return frontend_settings

@ -1039,7 +1039,7 @@ def inheritors(
def pui_url(subpath: str) -> str:
"""Return the URL for a PUI subpath."""
"""Return the URL for a web subpath."""
if not subpath.startswith('/'):
subpath = '/' + subpath
return f'/{settings.FRONTEND_URL_BASE}{subpath}'

@ -46,4 +46,4 @@ class ViewTests(InvenTreeTestCase):
f'/accounts/login/?next=/&login={self.username}&password={self.password}'
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/platform')
self.assertEqual(response.url, '/web')

@ -31,7 +31,7 @@ class BuildTestSimple(InvenTreeTestCase):
def test_url(self):
"""Test URL lookup."""
b1 = Build.objects.get(pk=1)
self.assertEqual(b1.get_absolute_url(), '/platform/manufacturing/build-order/1')
self.assertEqual(b1.get_absolute_url(), '/web/manufacturing/build-order/1')
def test_is_complete(self):
"""Test build completion status."""

@ -59,7 +59,7 @@ class CompanySimpleTest(TestCase):
def test_company_url(self):
"""Test the detail URL for a company."""
c = Company.objects.get(pk=1)
self.assertEqual(c.get_absolute_url(), '/platform/purchasing/manufacturer/1')
self.assertEqual(c.get_absolute_url(), '/web/purchasing/manufacturer/1')
def test_image_renamer(self):
"""Test the company image upload functionality."""

@ -43,7 +43,7 @@ class OrderTest(TestCase, ExchangeRateMixin):
for pk in range(1, 8):
order = PurchaseOrder.objects.get(pk=pk)
self.assertEqual(
order.get_absolute_url(), f'/platform/purchasing/purchase-order/{pk}'
order.get_absolute_url(), f'/web/purchasing/purchase-order/{pk}'
)
self.assertEqual(order.reference, f'PO-{pk:04d}')

@ -127,9 +127,7 @@ class CategoryTest(TestCase):
def test_url(self):
"""Test that the PartCategory URL works."""
self.assertEqual(
self.capacitors.get_absolute_url(), '/platform/part/category/3'
)
self.assertEqual(self.capacitors.get_absolute_url(), '/web/part/category/3')
def test_part_count(self):
"""Test that the Category part count works."""

@ -245,7 +245,7 @@ class PartTest(TestCase):
def test_attributes(self):
"""Test Part attributes."""
self.assertEqual(self.r1.name, 'R_2K2_0805')
self.assertEqual(self.r1.get_absolute_url(), '/platform/part/3')
self.assertEqual(self.r1.get_absolute_url(), '/web/part/3')
def test_category(self):
"""Test PartCategory path."""

@ -285,7 +285,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
response.data['stocklocation']['api_url'], '/api/stock/location/5/'
)
self.assertEqual(
response.data['stocklocation']['web_url'], '/platform/stock/location/5'
response.data['stocklocation']['web_url'], '/web/stock/location/5'
)
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')
@ -327,7 +327,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
response.data['stocklocation']['api_url'], '/api/stock/location/5/'
)
self.assertEqual(
response.data['stocklocation']['web_url'], '/platform/stock/location/5'
response.data['stocklocation']['web_url'], '/web/stock/location/5'
)
self.assertEqual(response.data['plugin'], 'InvenTreeBarcode')

@ -175,8 +175,8 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
# Test might return one of two results, depending on test env
# If INVENTREE_SITE_URL is not set in the CI environment, the link will be relative
options = [
f'<a href="http://localhost:8000/platform/part/{obj.pk}">test</a>',
f'<a href="/platform/part/{obj.pk}">test</a>',
f'<a href="http://localhost:8000/web/part/{obj.pk}">test</a>',
f'<a href="/web/part/{obj.pk}">test</a>',
]
self.assertIn(link, options)

@ -262,8 +262,8 @@ class StockTest(StockTestBase):
def test_url(self):
"""Test get_absolute_url function."""
it = StockItem.objects.get(pk=2)
self.assertEqual(it.get_absolute_url(), '/platform/stock/item/2')
self.assertEqual(self.home.get_absolute_url(), '/platform/stock/location/1')
self.assertEqual(it.get_absolute_url(), '/web/stock/item/2')
self.assertEqual(self.home.get_absolute_url(), '/web/stock/location/1')
def test_strings(self):
"""Test str function."""

@ -273,7 +273,7 @@ class GetAuthToken(GenericAPIView):
data = {'token': token.key, 'name': token.name, 'expiry': token.expiry}
# Ensure that the users session is logged in (PUI -> CUI login)
# Ensure that the users session is logged in
if not get_user(request).is_authenticated:
login(
request, user, backend='django.contrib.auth.backends.ModelBackend'

@ -113,7 +113,7 @@ class UserAPITests(InvenTreeAPITestCase):
def test_login_redirect(self):
"""Test login redirect endpoint."""
response = self.get(reverse('api-login-redirect'), expected_code=302)
self.assertEqual(response.url, '/platform/logged-in/')
self.assertEqual(response.url, '/web/logged-in/')
class UserTokenTests(InvenTreeAPITestCase):

@ -1,4 +1,4 @@
"""Tests for PUI backend stuff."""
"""Tests for web backend functionality."""
import json
import os
@ -68,7 +68,7 @@ class TemplateTagTest(InvenTreeTestCase):
self.assertSettings(rsp)
# No base_url
envs = {'INVENTREE_PUI_URL_BASE': ''}
envs = {'INVENTREE_FRONTEND_URL_BASE': ''}
with mock.patch.dict(os.environ, envs):
rsp = get_frontend_settings()
self.assertSettings(rsp)
@ -79,7 +79,9 @@ class TemplateTagTest(InvenTreeTestCase):
self.assertTrue(rsp['show_server_selector'])
# No debug, serverlist -> no selector
envs = {'INVENTREE_PUI_SETTINGS': json.dumps({'server_list': ['aa', 'bb']})}
envs = {
'INVENTREE_FRONTEND_SETTINGS': json.dumps({'server_list': ['aa', 'bb']})
}
with mock.patch.dict(os.environ, envs):
rsp = get_frontend_settings(False)
self.assertNotIn('show_server_selector', rsp)

@ -16,8 +16,8 @@ urlpatterns = [
spa_view,
name='password_reset_confirm',
),
re_path('.*', spa_view, name='platform-wildcard'),
re_path('.*', spa_view, name='web-wildcard'),
]),
),
path(settings.FRONTEND_URL_BASE, spa_view, name='platform'),
path(settings.FRONTEND_URL_BASE, spa_view, name='web'),
]

@ -23,7 +23,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import type { ModelType } from '../../enums/ModelType';
import { navigateToLink } from '../../functions/navigation';
import { getDetailUrl } from '../../functions/urls';
import { base_url } from '../../main';
import { getBaseUrl } from '../../main';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { Boundary } from '../Boundary';
@ -67,7 +67,7 @@ function NotificationEntry({
>
<Stack gap={2}>
<Anchor
href={link ? `/${base_url}${link}` : '#'}
href={link ? `/${getBaseUrl()}${link}` : '#'}
underline='hover'
target='_blank'
onClick={(event: any) => {

@ -1,4 +1,4 @@
import { base_url } from '../main';
import { getBaseUrl } from '../main';
import { cancelEvent } from './events';
/*
@ -11,7 +11,7 @@ export const navigateToLink = (link: string, navigate: any, event: any) => {
if (event?.ctrlKey || event?.shiftKey) {
// Open the link in a new tab
const url = `/${base_url}${link}`;
const url = `/${getBaseUrl()}${link}`;
window.open(url, '_blank');
} else {
// Navigate internally

@ -1,6 +1,6 @@
import { ModelInformationDict } from '../components/render/ModelType';
import type { ModelType } from '../enums/ModelType';
import { base_url } from '../main';
import { getBaseUrl } from '../main';
import { useLocalState } from '../states/LocalState';
/**
@ -19,7 +19,7 @@ export function getDetailUrl(
if (!!pk && modelInfo && modelInfo.url_detail) {
const url = modelInfo.url_detail.replace(':pk', pk.toString());
const base = base_url;
const base = getBaseUrl();
if (absolute && base) {
return `/${base}${url}`;

@ -89,7 +89,8 @@ if (window.INVENTREE_SETTINGS.sentry_dsn) {
});
}
export const base_url = window.INVENTREE_SETTINGS.base_url || 'platform';
export const getBaseUrl = (): string =>
window.INVENTREE_SETTINGS?.base_url || 'web';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
@ -99,7 +100,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
// Redirect to base url if on /
if (window.location.pathname === '/') {
window.location.replace(`/${base_url}`);
window.location.replace(`/${getBaseUrl()}`);
}
window.React = React;

@ -5,7 +5,7 @@ import { api, queryClient } from '../App';
import { ApiProvider } from '../contexts/ApiContext';
import { ThemeContext } from '../contexts/ThemeContext';
import { defaultHostList } from '../defaults/defaultHostList';
import { base_url } from '../main';
import { getBaseUrl } from '../main';
import { routes } from '../router';
import { useLocalState } from '../states/LocalState';
@ -21,7 +21,7 @@ export default function DesktopAppView() {
return (
<ApiProvider client={queryClient} api={api}>
<ThemeContext>
<BrowserRouter basename={base_url}>{routes}</BrowserRouter>
<BrowserRouter basename={getBaseUrl()}>{routes}</BrowserRouter>
</ThemeContext>
</ApiProvider>
);

@ -23,7 +23,7 @@ export default function MobileAppView() {
</Title>
<Text>
<Trans>
Platform UI is optimized for Tablets and Desktops, you can use
InvenTree UI is optimized for Tablets and Desktops, you can use
the official app for a mobile experience.
</Trans>
</Text>

@ -1,7 +1,7 @@
export const classicUrl = 'http://127.0.0.1:8000';
export const apiUrl = `${classicUrl}/api`;
export const baseUrl = './platform';
export const baseUrl = './web';
export const loginUrl = `${baseUrl}/login`;
export const logoutUrl = `${baseUrl}/logout`;
export const homeUrl = `${baseUrl}/home`;

@ -11,11 +11,11 @@ export const doLogin = async (page, username?: string, password?: string) => {
await navigate(page, logoutUrl);
await expect(page).toHaveTitle(/^InvenTree.*$/);
await page.waitForURL('**/platform/login');
await page.waitForURL('**/web/login');
await page.getByLabel('username').fill(username);
await page.getByLabel('password').fill(password);
await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL('**/platform/home');
await page.waitForURL('**/web/home');
await page.waitForTimeout(250);
};
@ -33,7 +33,7 @@ export const doQuickLogin = async (
url = url ?? baseUrl;
await navigate(page, `${url}/login?login=${username}&password=${password}`);
await page.waitForURL('**/platform/home');
await page.waitForURL('**/web/home');
await page.getByLabel('navigation-menu').waitFor({ timeout: 5000 });
await page.getByText(/InvenTree Demo Server -/).waitFor();
@ -45,5 +45,5 @@ export const doQuickLogin = async (
export const doLogout = async (page) => {
await navigate(page, 'logout');
await page.waitForURL('**/platform/login');
await page.waitForURL('**/web/login');
};

@ -14,7 +14,7 @@ test('Modals - Admin', async ({ page }) => {
await page.getByRole('cell', { name: 'Instance Name' }).waitFor();
await page.getByRole('button', { name: 'Close' }).click();
await page.waitForURL('**/platform/home');
await page.waitForURL('**/web/home');
// use license info
await page.getByLabel('open-spotlight').click();

@ -416,7 +416,7 @@ test('Parts - Revision', async ({ page }) => {
.getByRole('option', { name: 'Thumbnail Green Round Table No stock' })
.click();
await page.waitForURL('**/platform/part/101/**');
await page.waitForURL('**/web/part/101/**');
await page.getByText('Select Part Revision').waitFor();
});

@ -13,10 +13,10 @@ test('Sales Orders - Tabs', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'sales/index/');
await page.waitForURL('**/platform/sales/**');
await page.waitForURL('**/web/sales/**');
await loadTab(page, 'Sales Orders');
await page.waitForURL('**/platform/sales/index/salesorders');
await page.waitForURL('**/web/sales/index/salesorders');
await loadTab(page, 'Return Orders');
// Customers

@ -13,16 +13,16 @@ test('Stock - Basic Tests', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'stock/location/index/');
await page.waitForURL('**/platform/stock/location/**');
await page.waitForURL('**/web/stock/location/**');
await loadTab(page, 'Location Details');
await page.waitForURL('**/platform/stock/location/index/details');
await page.waitForURL('**/web/stock/location/index/details');
await loadTab(page, 'Stock Items');
await page.getByText('1551ABK').first().click();
await page.getByRole('tab', { name: 'Stock', exact: true }).click();
await page.waitForURL('**/platform/stock/**');
await page.waitForURL('**/web/stock/**');
await loadTab(page, 'Stock Locations');
await page.getByRole('cell', { name: 'Electronics Lab' }).first().click();
await loadTab(page, 'Default Parts');
@ -43,7 +43,7 @@ test('Stock - Location Tree', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'stock/location/index/');
await page.waitForURL('**/platform/stock/location/**');
await page.waitForURL('**/web/stock/location/**');
await loadTab(page, 'Location Details');
await page.getByLabel('nav-breadcrumb-action').click();

@ -10,7 +10,7 @@ test('Quick Command', async ({ page }) => {
await page.getByPlaceholder('Search...').fill('Dashboard');
await page.getByPlaceholder('Search...').press('Tab');
await page.getByPlaceholder('Search...').press('Enter');
await page.waitForURL('**/platform/home');
await page.waitForURL('**/web/home');
});
test('Quick Command - No Keys', async ({ page }) => {
@ -23,7 +23,7 @@ test('Quick Command - No Keys', async ({ page }) => {
.click();
await page.getByText('InvenTree Demo Server - ').waitFor();
await page.waitForURL('**/platform/home');
await page.waitForURL('**/web/home');
// Use navigation menu
await page.getByLabel('open-spotlight').click();
@ -55,7 +55,7 @@ test('Quick Command - No Keys', async ({ page }) => {
await page.getByRole('cell', { name: 'Instance Name' }).waitFor();
await page.getByRole('button', { name: 'Close' }).click();
await page.waitForURL('**/platform/home');
await page.waitForURL('**/web/home');
// use license info
await page.getByLabel('open-spotlight').click();

@ -6,7 +6,7 @@ import { doQuickLogin } from './login';
test('Forms - Stock Item Validation', async ({ page }) => {
await doQuickLogin(page, 'steven', 'wizardstaff');
await navigate(page, 'stock/location/index/stock-items');
await page.waitForURL('**/platform/stock/location/**');
await page.waitForURL('**/web/stock/location/**');
// Create new stock item form
await page.getByLabel('action-button-add-stock-item').click();

@ -13,7 +13,7 @@ test('Login - Basic Test', async ({ page }) => {
await page.getByRole('button', { name: 'Ally Access' }).click();
await page.getByRole('menuitem', { name: 'Logout' }).click();
await page.waitForURL('**/platform/login');
await page.waitForURL('**/web/login');
await page.getByLabel('username');
});
@ -27,13 +27,13 @@ test('Login - Quick Test', async ({ page }) => {
// Go to the dashboard
await navigate(page, '');
await page.waitForURL('**/platform');
await page.waitForURL('**/web');
await page.getByText('InvenTree Demo Server - ').waitFor();
// Logout (via URL)
await navigate(page, 'logout');
await page.waitForURL('**/platform/login');
await page.waitForURL('**/web/login');
await page.getByLabel('username');
});
@ -51,7 +51,7 @@ test('Login - Failures', async ({ page }) => {
// Navigate to the 'login' page
await navigate(page, logoutUrl);
await expect(page).toHaveTitle(/^InvenTree.*$/);
await page.waitForURL('**/platform/login');
await page.waitForURL('**/web/login');
// Attempt login with invalid credentials
await page.getByLabel('login-username').fill('invalid user');

@ -12,7 +12,7 @@ test('Label Printing', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'stock/location/index/');
await page.waitForURL('**/platform/stock/location/**');
await page.waitForURL('**/web/stock/location/**');
await loadTab(page, 'Stock Items');
@ -54,7 +54,7 @@ test('Report Printing', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'stock/location/index/');
await page.waitForURL('**/platform/stock/location/**');
await page.waitForURL('**/web/stock/location/**');
// Navigate to a specific PurchaseOrder
await page.getByRole('tab', { name: 'Purchasing' }).click();

@ -39,7 +39,7 @@ test('Settings - Language / Color', async ({ page }) => {
// .click();
await page.getByRole('tab', { name: 'Dashboard' }).click();
await page.waitForURL('**/platform/home');
await page.waitForURL('**/web/home');
});
test('Settings - User theme', async ({ page }) => {