mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 03:26:45 +00:00
[PUI] Page titles (#8467)
* Add <PageTitle /> component * Use useEffect to override hard-coded value * Ensure page titles are tracked across the site * Adjust unit tests * Playwright test updates * Tweak tests * Update InvenTreeTable.tsx Revert unused change
This commit is contained in:
parent
113b9e9fd9
commit
7b50f3b1d3
@ -5,6 +5,7 @@ import { Fragment, type ReactNode } from 'react';
|
||||
import { ApiImage } from '../images/ApiImage';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
import { type Breadcrumb, BreadcrumbList } from './BreadcrumbList';
|
||||
import PageTitle from './PageTitle';
|
||||
|
||||
interface PageDetailInterface {
|
||||
title?: string;
|
||||
@ -51,50 +52,53 @@ export function PageDetail({
|
||||
]);
|
||||
|
||||
return (
|
||||
<Stack gap='xs'>
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
<BreadcrumbList
|
||||
navCallback={breadcrumbAction}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
)}
|
||||
<Paper p='xs' radius='xs' shadow='xs'>
|
||||
<Stack gap='xs'>
|
||||
<Group justify='space-between' wrap='nowrap'>
|
||||
<Group justify='left' wrap='nowrap'>
|
||||
{imageUrl && (
|
||||
<ApiImage src={imageUrl} radius='sm' mah={42} maw={42} />
|
||||
)}
|
||||
<Stack gap='xs'>
|
||||
{title && <StylishText size='lg'>{title}</StylishText>}
|
||||
{subtitle && (
|
||||
<Group gap='xs'>
|
||||
{icon}
|
||||
<Text size='md' truncate>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</Group>
|
||||
<>
|
||||
<PageTitle title={title} />
|
||||
<Stack gap='xs'>
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
<BreadcrumbList
|
||||
navCallback={breadcrumbAction}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
)}
|
||||
<Paper p='xs' radius='xs' shadow='xs'>
|
||||
<Stack gap='xs'>
|
||||
<Group justify='space-between' wrap='nowrap'>
|
||||
<Group justify='left' wrap='nowrap'>
|
||||
{imageUrl && (
|
||||
<ApiImage src={imageUrl} radius='sm' mah={42} maw={42} />
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
<Space />
|
||||
{detail}
|
||||
<Group justify='right' gap='xs' wrap='nowrap'>
|
||||
{badges?.map((badge, idx) => (
|
||||
<Fragment key={idx}>{badge}</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
<Space />
|
||||
{actions && (
|
||||
<Group gap={5} justify='right'>
|
||||
{actions.map((action, idx) => (
|
||||
<Fragment key={idx}>{action}</Fragment>
|
||||
<Stack gap='xs'>
|
||||
{title && <StylishText size='lg'>{title}</StylishText>}
|
||||
{subtitle && (
|
||||
<Group gap='xs'>
|
||||
{icon}
|
||||
<Text size='sm' truncate>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
<Space />
|
||||
{detail}
|
||||
<Group justify='right' gap='xs' wrap='nowrap'>
|
||||
{badges?.map((badge, idx) => (
|
||||
<Fragment key={idx}>{badge}</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
<Space />
|
||||
{actions && (
|
||||
<Group gap={5} justify='right'>
|
||||
{actions.map((action, idx) => (
|
||||
<Fragment key={idx}>{action}</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
53
src/frontend/src/components/nav/PageTitle.tsx
Normal file
53
src/frontend/src/components/nav/PageTitle.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||
|
||||
/**
|
||||
* Component to set the page title
|
||||
*/
|
||||
export default function PageTitle({
|
||||
title,
|
||||
subtitle
|
||||
}: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
}) {
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const pageTitle = useMemo(() => {
|
||||
const instanceName = globalSettings.getSetting(
|
||||
'INVENTREE_INSTANCE',
|
||||
'InvenTree'
|
||||
);
|
||||
const useInstanceName = globalSettings.isSet(
|
||||
'INVENTREE_INSTANCE_TITLE',
|
||||
false
|
||||
);
|
||||
|
||||
let data = '';
|
||||
|
||||
if (title) {
|
||||
data += title;
|
||||
}
|
||||
|
||||
if (subtitle) {
|
||||
data += ` - ${subtitle}`;
|
||||
}
|
||||
|
||||
if (useInstanceName) {
|
||||
data = `${instanceName} | ${data}`;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
// Backup: No title provided
|
||||
data = instanceName;
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [title, subtitle, globalSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = pageTitle;
|
||||
}, [pageTitle]);
|
||||
|
||||
return <title>{pageTitle}</title>;
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import DashboardLayout from '../../components/dashboard/DashboardLayout';
|
||||
import PageTitle from '../../components/nav/PageTitle';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={t`Dashboard`} />
|
||||
<DashboardLayout />
|
||||
</>
|
||||
);
|
||||
|
@ -47,6 +47,7 @@ import { api } from '../../App';
|
||||
import { DocInfo } from '../../components/items/DocInfo';
|
||||
import { StylishText } from '../../components/items/StylishText';
|
||||
import { TitleWithDoc } from '../../components/items/TitleWithDoc';
|
||||
import PageTitle from '../../components/nav/PageTitle';
|
||||
import { RenderInstance } from '../../components/render/Instance';
|
||||
import { ModelInformationDict } from '../../components/render/ModelType';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
@ -290,6 +291,7 @@ export default function Scan() {
|
||||
// rendering
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={t`Barcode Scanning`} />
|
||||
<Group justify='space-between'>
|
||||
<Group justify='left'>
|
||||
<StylishText>
|
||||
|
@ -30,6 +30,7 @@ import { lazy, useMemo } from 'react';
|
||||
|
||||
import PermissionDenied from '../../../../components/errors/PermissionDenied';
|
||||
import { PlaceholderPill } from '../../../../components/items/Placeholder';
|
||||
import PageTitle from '../../../../components/nav/PageTitle';
|
||||
import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
|
||||
import type { PanelType } from '../../../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../../../components/panels/PanelGroup';
|
||||
@ -244,6 +245,7 @@ export default function AdminCenter() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={t`Admin Center`} />
|
||||
{user.isStaff() ? (
|
||||
<Stack gap='xs'>
|
||||
<SettingsHeader
|
||||
|
@ -19,6 +19,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import PermissionDenied from '../../../components/errors/PermissionDenied';
|
||||
import { PlaceholderPanel } from '../../../components/items/Placeholder';
|
||||
import PageTitle from '../../../components/nav/PageTitle';
|
||||
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
||||
import type { PanelType } from '../../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../../components/panels/PanelGroup';
|
||||
@ -303,6 +304,7 @@ export default function SystemSettings() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={t`System Settings`} />
|
||||
{user.isStaff() ? (
|
||||
<Stack gap='xs'>
|
||||
<SettingsHeader
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import PageTitle from '../../../components/nav/PageTitle';
|
||||
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
||||
import type { PanelType } from '../../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../../components/panels/PanelGroup';
|
||||
@ -146,23 +147,26 @@ export default function UserSettings() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap='xs'>
|
||||
<SettingsHeader
|
||||
label='user'
|
||||
title={t`Account Settings`}
|
||||
subtitle={
|
||||
user?.first_name && user?.last_name
|
||||
? `${user?.first_name} ${user?.last_name}`
|
||||
: null
|
||||
}
|
||||
shorthand={user?.username || ''}
|
||||
/>
|
||||
<PanelGroup
|
||||
pageKey='user-settings'
|
||||
panels={userSettingsPanels}
|
||||
model='usersettings'
|
||||
id={null}
|
||||
/>
|
||||
</Stack>
|
||||
<>
|
||||
<PageTitle title={t`User Settings`} />
|
||||
<Stack gap='xs'>
|
||||
<SettingsHeader
|
||||
label='user'
|
||||
title={t`Account Settings`}
|
||||
subtitle={
|
||||
user?.first_name && user?.last_name
|
||||
? `${user?.first_name} ${user?.last_name}`
|
||||
: null
|
||||
}
|
||||
shorthand={user?.username || ''}
|
||||
/>
|
||||
<PanelGroup
|
||||
pageKey='user-settings'
|
||||
panels={userSettingsPanels}
|
||||
model='usersettings'
|
||||
id={null}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -528,7 +528,7 @@ export default function BuildDetail() {
|
||||
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
|
||||
<Stack gap='xs'>
|
||||
<PageDetail
|
||||
title={build.reference}
|
||||
title={`${t`Build Order`}: ${build.reference}`}
|
||||
subtitle={build.title}
|
||||
badges={buildBadges}
|
||||
editAction={editBuild.open}
|
||||
|
@ -34,7 +34,7 @@ export const doQuickLogin = async (
|
||||
await page.goto(`${url}/login/?login=${username}&password=${password}`);
|
||||
await page.waitForURL('**/platform/home');
|
||||
|
||||
await page.getByText(/InvenTree Demo Server/).waitFor();
|
||||
await page.getByText(/InvenTree Demo Server -/).waitFor();
|
||||
};
|
||||
|
||||
export const doLogout = async (page) => {
|
||||
|
@ -100,14 +100,12 @@ test('Parts - Allocations', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
// Let's look at the allocations for a single stock item
|
||||
await page.goto(`${baseUrl}/stock/item/324/`);
|
||||
await page.getByRole('tab', { name: 'Allocations' }).click();
|
||||
|
||||
// TODO: Un-comment these lines!
|
||||
// await page.goto(`${baseUrl}/stock/item/324/`);
|
||||
// await page.getByRole('tab', { name: 'Allocations' }).click();
|
||||
|
||||
// await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor();
|
||||
// await page.getByRole('cell', { name: 'Making some blue chairs' }).waitFor();
|
||||
// await page.getByRole('cell', { name: 'Making tables for SO 0003' }).waitFor();
|
||||
await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor();
|
||||
await page.getByRole('cell', { name: 'Making some blue chairs' }).waitFor();
|
||||
await page.getByRole('cell', { name: 'Making tables for SO 0003' }).waitFor();
|
||||
|
||||
// Let's look at the allocations for an entire part
|
||||
await page.goto(`${baseUrl}/part/74/details`);
|
||||
@ -172,7 +170,8 @@ test('Parts - Pricing (Nothing, BOM)', async ({ page }) => {
|
||||
|
||||
// Part with no history
|
||||
await page.goto(`${baseUrl}/part/82/pricing`);
|
||||
await page.getByText('1551ABK').waitFor();
|
||||
|
||||
await page.getByText('Small plastic enclosure, black').waitFor();
|
||||
await page.getByRole('tab', { name: 'Part Pricing' }).click();
|
||||
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
|
||||
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
|
||||
@ -183,7 +182,7 @@ test('Parts - Pricing (Nothing, BOM)', async ({ page }) => {
|
||||
|
||||
// Part with history
|
||||
await page.goto(`${baseUrl}/part/108/pricing`);
|
||||
await page.getByText('Part: Blue Chair').waitFor();
|
||||
await page.getByText('A chair - with blue paint').waitFor();
|
||||
await page.getByRole('tab', { name: 'Part Pricing' }).click();
|
||||
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
|
||||
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
|
||||
@ -221,7 +220,7 @@ test('Parts - Pricing (Supplier)', async ({ page }) => {
|
||||
|
||||
// Part
|
||||
await page.goto(`${baseUrl}/part/55/pricing`);
|
||||
await page.getByText('Part: C_100nF_0603').waitFor();
|
||||
await page.getByText('Ceramic capacitor, 100nF in').waitFor();
|
||||
await page.getByRole('tab', { name: 'Part Pricing' }).click();
|
||||
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
|
||||
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
|
||||
@ -247,7 +246,7 @@ test('Parts - Pricing (Variant)', async ({ page }) => {
|
||||
|
||||
// Part
|
||||
await page.goto(`${baseUrl}/part/106/pricing`);
|
||||
await page.getByText('Part: Chair').waitFor();
|
||||
await page.getByText('A chair - available in multiple colors').waitFor();
|
||||
await page.getByRole('tab', { name: 'Part Pricing' }).click();
|
||||
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
|
||||
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
|
||||
@ -273,7 +272,7 @@ test('Parts - Pricing (Internal)', async ({ page }) => {
|
||||
|
||||
// Part
|
||||
await page.goto(`${baseUrl}/part/65/pricing`);
|
||||
await page.getByText('Part: M2x4 SHCS').waitFor();
|
||||
await page.getByText('Socket head cap screw, M2').waitFor();
|
||||
await page.getByRole('tab', { name: 'Part Pricing' }).click();
|
||||
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
|
||||
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
|
||||
@ -298,7 +297,7 @@ test('Parts - Pricing (Purchase)', async ({ page }) => {
|
||||
|
||||
// Part
|
||||
await page.goto(`${baseUrl}/part/69/pricing`);
|
||||
await page.getByText('Part: 530470210').waitFor();
|
||||
await page.getByText('1.25mm Pitch, PicoBlade PCB').waitFor();
|
||||
await page.getByRole('tab', { name: 'Part Pricing' }).click();
|
||||
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
|
||||
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
|
||||
|
@ -18,7 +18,7 @@ test('Sales Orders', async ({ page }) => {
|
||||
await page.getByRole('cell', { name: 'SO0003' }).click();
|
||||
|
||||
// Order is "on hold". We will "issue" it and then place on hold again
|
||||
await page.getByText('Sales Order: SO0003').waitFor();
|
||||
await page.getByText('Selling stuff').first().waitFor();
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
await page.getByRole('button', { name: 'Issue Order' }).click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
@ -14,7 +14,7 @@ test('Basic Login Test', async ({ page }) => {
|
||||
await page.goto(baseUrl);
|
||||
await page.waitForURL('**/platform');
|
||||
|
||||
await page.getByText('InvenTree Demo Server').waitFor();
|
||||
await page.getByText('InvenTree Demo Server -').waitFor();
|
||||
|
||||
// Check that the username is provided
|
||||
await page.getByText(user.username);
|
||||
@ -45,7 +45,7 @@ test('Quick Login Test', async ({ page }) => {
|
||||
await page.goto(baseUrl);
|
||||
await page.waitForURL('**/platform');
|
||||
|
||||
await page.getByText('InvenTree Demo Server').waitFor();
|
||||
await page.getByText('InvenTree Demo Server - ').waitFor();
|
||||
|
||||
// Logout (via URL)
|
||||
await page.goto(`${baseUrl}/logout/`);
|
||||
|
@ -22,7 +22,7 @@ test('Quick Command - No Keys', async ({ page }) => {
|
||||
.getByRole('button', { name: 'Dashboard Go to the InvenTree' })
|
||||
.click();
|
||||
|
||||
await page.getByText('InvenTree Demo Server').waitFor();
|
||||
await page.getByText('InvenTree Demo Server - ').waitFor();
|
||||
await page.waitForURL('**/platform/home');
|
||||
|
||||
// Use navigation menu
|
||||
|
@ -107,7 +107,7 @@ test('Forms - Supplier Validation', async ({ page, request }) => {
|
||||
await page.getByLabel('text-field-name').fill(supplierName);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.getByText(supplierName).waitFor();
|
||||
await page.getByText('A description').first().waitFor();
|
||||
await page
|
||||
.getByRole('link', { name: 'https://www.test-website.co.uk' })
|
||||
.waitFor();
|
||||
|
Loading…
x
Reference in New Issue
Block a user