2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-26 18:46:44 +00:00

[UI] Web Prefix (#9334)

* [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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 80 additions and 98 deletions

View File

@ -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

View File

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

View File

@ -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

View File

@ -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)

View File

@ -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`.

View File

@ -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']

View File

@ -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

View File

@ -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}'

View File

@ -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')

View File

@ -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."""

View File

@ -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."""

View File

@ -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}')

View File

@ -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."""

View File

@ -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."""

View File

@ -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')

View File

@ -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)

View File

@ -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."""

View File

@ -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'

View File

@ -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):

View File

@ -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)

View File

@ -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'),
]

View File

@ -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) => {

View File

@ -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

View File

@ -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}`;

View File

@ -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;

View File

@ -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>
);

View File

@ -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>

View File

@ -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`;

View File

@ -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');
};

View File

@ -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();

View File

@ -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();
});

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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');

View File

@ -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();

View File

@ -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 }) => {