2
0
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:
Oliver 2024-11-13 06:49:28 +11:00 committed by GitHub
parent 113b9e9fd9
commit 7b50f3b1d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 149 additions and 80 deletions

View File

@ -5,6 +5,7 @@ import { Fragment, type ReactNode } from 'react';
import { ApiImage } from '../images/ApiImage'; import { ApiImage } from '../images/ApiImage';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
import { type Breadcrumb, BreadcrumbList } from './BreadcrumbList'; import { type Breadcrumb, BreadcrumbList } from './BreadcrumbList';
import PageTitle from './PageTitle';
interface PageDetailInterface { interface PageDetailInterface {
title?: string; title?: string;
@ -51,50 +52,53 @@ export function PageDetail({
]); ]);
return ( return (
<Stack gap='xs'> <>
{breadcrumbs && breadcrumbs.length > 0 && ( <PageTitle title={title} />
<BreadcrumbList <Stack gap='xs'>
navCallback={breadcrumbAction} {breadcrumbs && breadcrumbs.length > 0 && (
breadcrumbs={breadcrumbs} <BreadcrumbList
/> navCallback={breadcrumbAction}
)} breadcrumbs={breadcrumbs}
<Paper p='xs' radius='xs' shadow='xs'> />
<Stack gap='xs'> )}
<Group justify='space-between' wrap='nowrap'> <Paper p='xs' radius='xs' shadow='xs'>
<Group justify='left' wrap='nowrap'> <Stack gap='xs'>
{imageUrl && ( <Group justify='space-between' wrap='nowrap'>
<ApiImage src={imageUrl} radius='sm' mah={42} maw={42} /> <Group justify='left' wrap='nowrap'>
)} {imageUrl && (
<Stack gap='xs'> <ApiImage src={imageUrl} radius='sm' mah={42} maw={42} />
{title && <StylishText size='lg'>{title}</StylishText>}
{subtitle && (
<Group gap='xs'>
{icon}
<Text size='md' truncate>
{subtitle}
</Text>
</Group>
)} )}
</Stack> <Stack gap='xs'>
</Group> {title && <StylishText size='lg'>{title}</StylishText>}
<Space /> {subtitle && (
{detail} <Group gap='xs'>
<Group justify='right' gap='xs' wrap='nowrap'> {icon}
{badges?.map((badge, idx) => ( <Text size='sm' truncate>
<Fragment key={idx}>{badge}</Fragment> {subtitle}
))} </Text>
</Group> </Group>
<Space /> )}
{actions && ( </Stack>
<Group gap={5} justify='right'> </Group>
{actions.map((action, idx) => ( <Space />
<Fragment key={idx}>{action}</Fragment> {detail}
<Group justify='right' gap='xs' wrap='nowrap'>
{badges?.map((badge, idx) => (
<Fragment key={idx}>{badge}</Fragment>
))} ))}
</Group> </Group>
)} <Space />
</Group> {actions && (
</Stack> <Group gap={5} justify='right'>
</Paper> {actions.map((action, idx) => (
</Stack> <Fragment key={idx}>{action}</Fragment>
))}
</Group>
)}
</Group>
</Stack>
</Paper>
</Stack>
</>
); );
} }

View 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>;
}

View File

@ -1,8 +1,11 @@
import { t } from '@lingui/macro';
import DashboardLayout from '../../components/dashboard/DashboardLayout'; import DashboardLayout from '../../components/dashboard/DashboardLayout';
import PageTitle from '../../components/nav/PageTitle';
export default function Home() { export default function Home() {
return ( return (
<> <>
<PageTitle title={t`Dashboard`} />
<DashboardLayout /> <DashboardLayout />
</> </>
); );

View File

@ -47,6 +47,7 @@ import { api } from '../../App';
import { DocInfo } from '../../components/items/DocInfo'; import { DocInfo } from '../../components/items/DocInfo';
import { StylishText } from '../../components/items/StylishText'; import { StylishText } from '../../components/items/StylishText';
import { TitleWithDoc } from '../../components/items/TitleWithDoc'; import { TitleWithDoc } from '../../components/items/TitleWithDoc';
import PageTitle from '../../components/nav/PageTitle';
import { RenderInstance } from '../../components/render/Instance'; import { RenderInstance } from '../../components/render/Instance';
import { ModelInformationDict } from '../../components/render/ModelType'; import { ModelInformationDict } from '../../components/render/ModelType';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
@ -290,6 +291,7 @@ export default function Scan() {
// rendering // rendering
return ( return (
<> <>
<PageTitle title={t`Barcode Scanning`} />
<Group justify='space-between'> <Group justify='space-between'>
<Group justify='left'> <Group justify='left'>
<StylishText> <StylishText>

View File

@ -30,6 +30,7 @@ import { lazy, useMemo } from 'react';
import PermissionDenied from '../../../../components/errors/PermissionDenied'; import PermissionDenied from '../../../../components/errors/PermissionDenied';
import { PlaceholderPill } from '../../../../components/items/Placeholder'; import { PlaceholderPill } from '../../../../components/items/Placeholder';
import PageTitle from '../../../../components/nav/PageTitle';
import { SettingsHeader } from '../../../../components/nav/SettingsHeader'; import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
import type { PanelType } from '../../../../components/panels/Panel'; import type { PanelType } from '../../../../components/panels/Panel';
import { PanelGroup } from '../../../../components/panels/PanelGroup'; import { PanelGroup } from '../../../../components/panels/PanelGroup';
@ -244,6 +245,7 @@ export default function AdminCenter() {
return ( return (
<> <>
<PageTitle title={t`Admin Center`} />
{user.isStaff() ? ( {user.isStaff() ? (
<Stack gap='xs'> <Stack gap='xs'>
<SettingsHeader <SettingsHeader

View File

@ -19,6 +19,7 @@ import { useMemo } from 'react';
import PermissionDenied from '../../../components/errors/PermissionDenied'; import PermissionDenied from '../../../components/errors/PermissionDenied';
import { PlaceholderPanel } from '../../../components/items/Placeholder'; import { PlaceholderPanel } from '../../../components/items/Placeholder';
import PageTitle from '../../../components/nav/PageTitle';
import { SettingsHeader } from '../../../components/nav/SettingsHeader'; import { SettingsHeader } from '../../../components/nav/SettingsHeader';
import type { PanelType } from '../../../components/panels/Panel'; import type { PanelType } from '../../../components/panels/Panel';
import { PanelGroup } from '../../../components/panels/PanelGroup'; import { PanelGroup } from '../../../components/panels/PanelGroup';
@ -303,6 +304,7 @@ export default function SystemSettings() {
return ( return (
<> <>
<PageTitle title={t`System Settings`} />
{user.isStaff() ? ( {user.isStaff() ? (
<Stack gap='xs'> <Stack gap='xs'>
<SettingsHeader <SettingsHeader

View File

@ -11,6 +11,7 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import PageTitle from '../../../components/nav/PageTitle';
import { SettingsHeader } from '../../../components/nav/SettingsHeader'; import { SettingsHeader } from '../../../components/nav/SettingsHeader';
import type { PanelType } from '../../../components/panels/Panel'; import type { PanelType } from '../../../components/panels/Panel';
import { PanelGroup } from '../../../components/panels/PanelGroup'; import { PanelGroup } from '../../../components/panels/PanelGroup';
@ -146,23 +147,26 @@ export default function UserSettings() {
} }
return ( return (
<Stack gap='xs'> <>
<SettingsHeader <PageTitle title={t`User Settings`} />
label='user' <Stack gap='xs'>
title={t`Account Settings`} <SettingsHeader
subtitle={ label='user'
user?.first_name && user?.last_name title={t`Account Settings`}
? `${user?.first_name} ${user?.last_name}` subtitle={
: null user?.first_name && user?.last_name
} ? `${user?.first_name} ${user?.last_name}`
shorthand={user?.username || ''} : null
/> }
<PanelGroup shorthand={user?.username || ''}
pageKey='user-settings' />
panels={userSettingsPanels} <PanelGroup
model='usersettings' pageKey='user-settings'
id={null} panels={userSettingsPanels}
/> model='usersettings'
</Stack> id={null}
/>
</Stack>
</>
); );
} }

View File

@ -528,7 +528,7 @@ export default function BuildDetail() {
<InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}> <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
<Stack gap='xs'> <Stack gap='xs'>
<PageDetail <PageDetail
title={build.reference} title={`${t`Build Order`}: ${build.reference}`}
subtitle={build.title} subtitle={build.title}
badges={buildBadges} badges={buildBadges}
editAction={editBuild.open} editAction={editBuild.open}

View File

@ -34,7 +34,7 @@ export const doQuickLogin = async (
await page.goto(`${url}/login/?login=${username}&password=${password}`); await page.goto(`${url}/login/?login=${username}&password=${password}`);
await page.waitForURL('**/platform/home'); await page.waitForURL('**/platform/home');
await page.getByText(/InvenTree Demo Server/).waitFor(); await page.getByText(/InvenTree Demo Server -/).waitFor();
}; };
export const doLogout = async (page) => { export const doLogout = async (page) => {

View File

@ -100,14 +100,12 @@ test('Parts - Allocations', async ({ page }) => {
await doQuickLogin(page); await doQuickLogin(page);
// Let's look at the allocations for a single stock item // 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.getByRole('button', { name: 'Build Order Allocations' }).waitFor();
// await page.goto(`${baseUrl}/stock/item/324/`); await page.getByRole('cell', { name: 'Making some blue chairs' }).waitFor();
// await page.getByRole('tab', { name: 'Allocations' }).click(); 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 // Let's look at the allocations for an entire part
await page.goto(`${baseUrl}/part/74/details`); await page.goto(`${baseUrl}/part/74/details`);
@ -172,7 +170,8 @@ test('Parts - Pricing (Nothing, BOM)', async ({ page }) => {
// Part with no history // Part with no history
await page.goto(`${baseUrl}/part/82/pricing`); 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.getByRole('tab', { name: 'Part Pricing' }).click();
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
@ -183,7 +182,7 @@ test('Parts - Pricing (Nothing, BOM)', async ({ page }) => {
// Part with history // Part with history
await page.goto(`${baseUrl}/part/108/pricing`); 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.getByRole('tab', { name: 'Part Pricing' }).click();
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
@ -221,7 +220,7 @@ test('Parts - Pricing (Supplier)', async ({ page }) => {
// Part // Part
await page.goto(`${baseUrl}/part/55/pricing`); 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.getByRole('tab', { name: 'Part Pricing' }).click();
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
@ -247,7 +246,7 @@ test('Parts - Pricing (Variant)', async ({ page }) => {
// Part // Part
await page.goto(`${baseUrl}/part/106/pricing`); 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.getByRole('tab', { name: 'Part Pricing' }).click();
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
@ -273,7 +272,7 @@ test('Parts - Pricing (Internal)', async ({ page }) => {
// Part // Part
await page.goto(`${baseUrl}/part/65/pricing`); 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.getByRole('tab', { name: 'Part Pricing' }).click();
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();
@ -298,7 +297,7 @@ test('Parts - Pricing (Purchase)', async ({ page }) => {
// Part // Part
await page.goto(`${baseUrl}/part/69/pricing`); 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.getByRole('tab', { name: 'Part Pricing' }).click();
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor();

View File

@ -18,7 +18,7 @@ test('Sales Orders', async ({ page }) => {
await page.getByRole('cell', { name: 'SO0003' }).click(); await page.getByRole('cell', { name: 'SO0003' }).click();
// Order is "on hold". We will "issue" it and then place on hold again // 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.getByText('On Hold').first().waitFor();
await page.getByRole('button', { name: 'Issue Order' }).click(); await page.getByRole('button', { name: 'Issue Order' }).click();
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();

View File

@ -14,7 +14,7 @@ test('Basic Login Test', async ({ page }) => {
await page.goto(baseUrl); await page.goto(baseUrl);
await page.waitForURL('**/platform'); await page.waitForURL('**/platform');
await page.getByText('InvenTree Demo Server').waitFor(); await page.getByText('InvenTree Demo Server -').waitFor();
// Check that the username is provided // Check that the username is provided
await page.getByText(user.username); await page.getByText(user.username);
@ -45,7 +45,7 @@ test('Quick Login Test', async ({ page }) => {
await page.goto(baseUrl); await page.goto(baseUrl);
await page.waitForURL('**/platform'); await page.waitForURL('**/platform');
await page.getByText('InvenTree Demo Server').waitFor(); await page.getByText('InvenTree Demo Server - ').waitFor();
// Logout (via URL) // Logout (via URL)
await page.goto(`${baseUrl}/logout/`); await page.goto(`${baseUrl}/logout/`);

View File

@ -22,7 +22,7 @@ test('Quick Command - No Keys', async ({ page }) => {
.getByRole('button', { name: 'Dashboard Go to the InvenTree' }) .getByRole('button', { name: 'Dashboard Go to the InvenTree' })
.click(); .click();
await page.getByText('InvenTree Demo Server').waitFor(); await page.getByText('InvenTree Demo Server - ').waitFor();
await page.waitForURL('**/platform/home'); await page.waitForURL('**/platform/home');
// Use navigation menu // Use navigation menu

View File

@ -107,7 +107,7 @@ test('Forms - Supplier Validation', async ({ page, request }) => {
await page.getByLabel('text-field-name').fill(supplierName); await page.getByLabel('text-field-name').fill(supplierName);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText(supplierName).waitFor(); await page.getByText('A description').first().waitFor();
await page await page
.getByRole('link', { name: 'https://www.test-website.co.uk' }) .getByRole('link', { name: 'https://www.test-website.co.uk' })
.waitFor(); .waitFor();