mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	UI cleanup (#9140)
* Refactor SecurityContext page: - Add Accordion to separate different groups - Fix "make primary" button (requires PATCH) - Responsive grid design * Add splash screen background to more pages * Adds playwright testing for email setup * Refactoring * Fix playwright tests
This commit is contained in:
		
							
								
								
									
										24
									
								
								src/frontend/src/components/SplashScreen.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/frontend/src/components/SplashScreen.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					import { BackgroundImage } from '@mantine/core';
 | 
				
			||||||
 | 
					import { generateUrl } from '../functions/urls';
 | 
				
			||||||
 | 
					import { useServerApiState } from '../states/ApiState';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Render content within a "splash screen" container.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default function SplashScreen({
 | 
				
			||||||
 | 
					  children
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  children: React.ReactNode;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const [server] = useServerApiState((state) => [state.server]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (server.customize?.splash) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <BackgroundImage src={generateUrl(server.customize.splash)}>
 | 
				
			||||||
 | 
					        {children}
 | 
				
			||||||
 | 
					      </BackgroundImage>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return <>{children}</>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -342,7 +342,7 @@ export async function ProviderLogin(
 | 
				
			|||||||
export function authApi(
 | 
					export function authApi(
 | 
				
			||||||
  url: string,
 | 
					  url: string,
 | 
				
			||||||
  config: AxiosRequestConfig | undefined = undefined,
 | 
					  config: AxiosRequestConfig | undefined = undefined,
 | 
				
			||||||
  method: 'get' | 'post' | 'put' | 'delete' = 'get',
 | 
					  method: 'get' | 'patch' | 'post' | 'put' | 'delete' = 'get',
 | 
				
			||||||
  data?: any
 | 
					  data?: any
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const requestConfig = config || {};
 | 
					  const requestConfig = config || {};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ import { notifications } from '@mantine/notifications';
 | 
				
			|||||||
import { useNavigate } from 'react-router-dom';
 | 
					import { useNavigate } from 'react-router-dom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { api } from '../../App';
 | 
					import { api } from '../../App';
 | 
				
			||||||
 | 
					import SplashScreen from '../../components/SplashScreen';
 | 
				
			||||||
import { StylishText } from '../../components/items/StylishText';
 | 
					import { StylishText } from '../../components/items/StylishText';
 | 
				
			||||||
import { ProtectedRoute } from '../../components/nav/Layout';
 | 
					import { ProtectedRoute } from '../../components/nav/Layout';
 | 
				
			||||||
import { LanguageContext } from '../../contexts/LanguageContext';
 | 
					import { LanguageContext } from '../../contexts/LanguageContext';
 | 
				
			||||||
@@ -104,49 +105,53 @@ export default function Set_Password() {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <LanguageContext>
 | 
					    <LanguageContext>
 | 
				
			||||||
      <ProtectedRoute>
 | 
					      <ProtectedRoute>
 | 
				
			||||||
        <Center mih='100vh'>
 | 
					        <SplashScreen>
 | 
				
			||||||
          <Container w='md' miw={425}>
 | 
					          <Center mih='100vh'>
 | 
				
			||||||
            <Stack>
 | 
					            <Container w='md' miw={425}>
 | 
				
			||||||
              <StylishText size='xl'>{t`Reset Password`}</StylishText>
 | 
					              <Paper p='xl' withBorder>
 | 
				
			||||||
              <Divider />
 | 
					                <Stack>
 | 
				
			||||||
              {user.username() && (
 | 
					                  <StylishText size='xl'>{t`Reset Password`}</StylishText>
 | 
				
			||||||
                <Paper>
 | 
					                  <Divider />
 | 
				
			||||||
                  <Group>
 | 
					                  {user.username() && (
 | 
				
			||||||
                    <StylishText size='md'>{t`User`}</StylishText>
 | 
					                    <Paper>
 | 
				
			||||||
                    <Text>{user.username()}</Text>
 | 
					                      <Group>
 | 
				
			||||||
                  </Group>
 | 
					                        <StylishText size='md'>{t`Username`}</StylishText>
 | 
				
			||||||
                </Paper>
 | 
					                        <Text>{user.username()}</Text>
 | 
				
			||||||
              )}
 | 
					                      </Group>
 | 
				
			||||||
              <Divider />
 | 
					                    </Paper>
 | 
				
			||||||
              <Stack gap='xs'>
 | 
					                  )}
 | 
				
			||||||
                <PasswordInput
 | 
					                  <Divider />
 | 
				
			||||||
                  required
 | 
					                  <Stack gap='xs'>
 | 
				
			||||||
                  aria-label='password'
 | 
					                    <PasswordInput
 | 
				
			||||||
                  label={t`Current Password`}
 | 
					                      required
 | 
				
			||||||
                  description={t`Enter your current password`}
 | 
					                      aria-label='password'
 | 
				
			||||||
                  {...simpleForm.getInputProps('current_password')}
 | 
					                      label={t`Current Password`}
 | 
				
			||||||
                />
 | 
					                      description={t`Enter your current password`}
 | 
				
			||||||
                <PasswordInput
 | 
					                      {...simpleForm.getInputProps('current_password')}
 | 
				
			||||||
                  required
 | 
					                    />
 | 
				
			||||||
                  aria-label='input-password-1'
 | 
					                    <PasswordInput
 | 
				
			||||||
                  label={t`New Password`}
 | 
					                      required
 | 
				
			||||||
                  description={t`Enter your new password`}
 | 
					                      aria-label='input-password-1'
 | 
				
			||||||
                  {...simpleForm.getInputProps('new_password1')}
 | 
					                      label={t`New Password`}
 | 
				
			||||||
                />
 | 
					                      description={t`Enter your new password`}
 | 
				
			||||||
                <PasswordInput
 | 
					                      {...simpleForm.getInputProps('new_password1')}
 | 
				
			||||||
                  required
 | 
					                    />
 | 
				
			||||||
                  aria-label='input-password-2'
 | 
					                    <PasswordInput
 | 
				
			||||||
                  label={t`Confirm New Password`}
 | 
					                      required
 | 
				
			||||||
                  description={t`Confirm your new password`}
 | 
					                      aria-label='input-password-2'
 | 
				
			||||||
                  {...simpleForm.getInputProps('new_password2')}
 | 
					                      label={t`Confirm New Password`}
 | 
				
			||||||
                />
 | 
					                      description={t`Confirm your new password`}
 | 
				
			||||||
              </Stack>
 | 
					                      {...simpleForm.getInputProps('new_password2')}
 | 
				
			||||||
              <Button type='submit' onClick={handleSet}>
 | 
					                    />
 | 
				
			||||||
                <Trans>Confirm</Trans>
 | 
					                  </Stack>
 | 
				
			||||||
              </Button>
 | 
					                  <Button type='submit' onClick={handleSet}>
 | 
				
			||||||
            </Stack>
 | 
					                    <Trans>Confirm</Trans>
 | 
				
			||||||
          </Container>
 | 
					                  </Button>
 | 
				
			||||||
        </Center>
 | 
					                </Stack>
 | 
				
			||||||
 | 
					              </Paper>
 | 
				
			||||||
 | 
					            </Container>
 | 
				
			||||||
 | 
					          </Center>
 | 
				
			||||||
 | 
					        </SplashScreen>
 | 
				
			||||||
      </ProtectedRoute>
 | 
					      </ProtectedRoute>
 | 
				
			||||||
    </LanguageContext>
 | 
					    </LanguageContext>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,10 @@
 | 
				
			|||||||
import { t } from '@lingui/macro';
 | 
					import { t } from '@lingui/macro';
 | 
				
			||||||
import {
 | 
					import { Center, Container, Divider, Paper, Text } from '@mantine/core';
 | 
				
			||||||
  BackgroundImage,
 | 
					 | 
				
			||||||
  Center,
 | 
					 | 
				
			||||||
  Container,
 | 
					 | 
				
			||||||
  Divider,
 | 
					 | 
				
			||||||
  Paper,
 | 
					 | 
				
			||||||
  Text
 | 
					 | 
				
			||||||
} from '@mantine/core';
 | 
					 | 
				
			||||||
import { useDisclosure, useToggle } from '@mantine/hooks';
 | 
					import { useDisclosure, useToggle } from '@mantine/hooks';
 | 
				
			||||||
import { useEffect, useMemo } from 'react';
 | 
					import { useEffect, useMemo } from 'react';
 | 
				
			||||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
 | 
					import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
 | 
				
			||||||
import { setApiDefaults } from '../../App';
 | 
					import { setApiDefaults } from '../../App';
 | 
				
			||||||
 | 
					import SplashScreen from '../../components/SplashScreen';
 | 
				
			||||||
import { AuthFormOptions } from '../../components/forms/AuthFormOptions';
 | 
					import { AuthFormOptions } from '../../components/forms/AuthFormOptions';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AuthenticationForm,
 | 
					  AuthenticationForm,
 | 
				
			||||||
@@ -25,7 +19,6 @@ import {
 | 
				
			|||||||
  doBasicLogin,
 | 
					  doBasicLogin,
 | 
				
			||||||
  followRedirect
 | 
					  followRedirect
 | 
				
			||||||
} from '../../functions/auth';
 | 
					} from '../../functions/auth';
 | 
				
			||||||
import { generateUrl } from '../../functions/urls';
 | 
					 | 
				
			||||||
import { useServerApiState } from '../../states/ApiState';
 | 
					import { useServerApiState } from '../../states/ApiState';
 | 
				
			||||||
import { useLocalState } from '../../states/LocalState';
 | 
					import { useLocalState } from '../../states/LocalState';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -73,16 +66,6 @@ export default function Login() {
 | 
				
			|||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }, [server.customize]);
 | 
					  }, [server.customize]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const SplashComponent = useMemo(() => {
 | 
					 | 
				
			||||||
    const temp = server.customize?.splash;
 | 
					 | 
				
			||||||
    if (temp) {
 | 
					 | 
				
			||||||
      return ({ children }: { children: React.ReactNode }) => (
 | 
					 | 
				
			||||||
        <BackgroundImage src={generateUrl(temp)}>{children}</BackgroundImage>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return ({ children }: { children: React.ReactNode }) => <>{children}</>;
 | 
					 | 
				
			||||||
  }, [server.customize]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Data manipulation functions
 | 
					  // Data manipulation functions
 | 
				
			||||||
  function ChangeHost(newHost: string | null): void {
 | 
					  function ChangeHost(newHost: string | null): void {
 | 
				
			||||||
    if (newHost === null) return;
 | 
					    if (newHost === null) return;
 | 
				
			||||||
@@ -120,7 +103,7 @@ export default function Login() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Main rendering block
 | 
					  // Main rendering block
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <SplashComponent>
 | 
					    <SplashScreen>
 | 
				
			||||||
      <Center mih='100vh'>
 | 
					      <Center mih='100vh'>
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          style={{
 | 
					          style={{
 | 
				
			||||||
@@ -159,6 +142,6 @@ export default function Login() {
 | 
				
			|||||||
          </Container>
 | 
					          </Container>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </Center>
 | 
					      </Center>
 | 
				
			||||||
    </SplashComponent>
 | 
					    </SplashScreen>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,14 +3,16 @@ import {
 | 
				
			|||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
  Center,
 | 
					  Center,
 | 
				
			||||||
  Container,
 | 
					  Container,
 | 
				
			||||||
 | 
					  Paper,
 | 
				
			||||||
  Stack,
 | 
					  Stack,
 | 
				
			||||||
  TextInput,
 | 
					  TextInput
 | 
				
			||||||
  Title
 | 
					 | 
				
			||||||
} from '@mantine/core';
 | 
					} from '@mantine/core';
 | 
				
			||||||
import { useForm } from '@mantine/form';
 | 
					import { useForm } from '@mantine/form';
 | 
				
			||||||
import { useLocation, useNavigate } from 'react-router-dom';
 | 
					import { useLocation, useNavigate } from 'react-router-dom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useState } from 'react';
 | 
					import { useState } from 'react';
 | 
				
			||||||
 | 
					import SplashScreen from '../../components/SplashScreen';
 | 
				
			||||||
 | 
					import { StylishText } from '../../components/items/StylishText';
 | 
				
			||||||
import { LanguageContext } from '../../contexts/LanguageContext';
 | 
					import { LanguageContext } from '../../contexts/LanguageContext';
 | 
				
			||||||
import { handleMfaLogin } from '../../functions/auth';
 | 
					import { handleMfaLogin } from '../../functions/auth';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,38 +24,40 @@ export default function MFALogin() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <LanguageContext>
 | 
					    <LanguageContext>
 | 
				
			||||||
      <Center mih='100vh'>
 | 
					      <SplashScreen>
 | 
				
			||||||
        <Container w='md' miw={425}>
 | 
					        <Center mih='100vh'>
 | 
				
			||||||
          <Stack>
 | 
					          <Container w='md' miw={425}>
 | 
				
			||||||
            <Title>
 | 
					            <Paper p='xl' withBorder>
 | 
				
			||||||
              <Trans>MFA Login</Trans>
 | 
					              <Stack>
 | 
				
			||||||
            </Title>
 | 
					                <StylishText size='xl'>{t`Multi-Factor Login`}</StylishText>
 | 
				
			||||||
            <Stack>
 | 
					                <Stack>
 | 
				
			||||||
              <TextInput
 | 
					                  <TextInput
 | 
				
			||||||
                required
 | 
					                    required
 | 
				
			||||||
                label={t`TOTP Code`}
 | 
					                    label={t`TOTP Code`}
 | 
				
			||||||
                name='TOTP'
 | 
					                    name='TOTP'
 | 
				
			||||||
                description={t`Enter your TOTP or recovery code`}
 | 
					                    description={t`Enter your TOTP or recovery code`}
 | 
				
			||||||
                {...simpleForm.getInputProps('code')}
 | 
					                    {...simpleForm.getInputProps('code')}
 | 
				
			||||||
                error={loginError}
 | 
					                    error={loginError}
 | 
				
			||||||
              />
 | 
					                  />
 | 
				
			||||||
            </Stack>
 | 
					                </Stack>
 | 
				
			||||||
            <Button
 | 
					                <Button
 | 
				
			||||||
              type='submit'
 | 
					                  type='submit'
 | 
				
			||||||
              onClick={() =>
 | 
					                  onClick={() =>
 | 
				
			||||||
                handleMfaLogin(
 | 
					                    handleMfaLogin(
 | 
				
			||||||
                  navigate,
 | 
					                      navigate,
 | 
				
			||||||
                  location,
 | 
					                      location,
 | 
				
			||||||
                  simpleForm.values,
 | 
					                      simpleForm.values,
 | 
				
			||||||
                  setLoginError
 | 
					                      setLoginError
 | 
				
			||||||
                )
 | 
					                    )
 | 
				
			||||||
              }
 | 
					                  }
 | 
				
			||||||
            >
 | 
					                >
 | 
				
			||||||
              <Trans>Log in</Trans>
 | 
					                  <Trans>Log In</Trans>
 | 
				
			||||||
            </Button>
 | 
					                </Button>
 | 
				
			||||||
          </Stack>
 | 
					              </Stack>
 | 
				
			||||||
        </Container>
 | 
					            </Paper>
 | 
				
			||||||
      </Center>
 | 
					          </Container>
 | 
				
			||||||
 | 
					        </Center>
 | 
				
			||||||
 | 
					      </SplashScreen>
 | 
				
			||||||
    </LanguageContext>
 | 
					    </LanguageContext>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import { Trans, t } from '@lingui/macro';
 | 
					import { Trans, t } from '@lingui/macro';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  Accordion,
 | 
				
			||||||
  Alert,
 | 
					  Alert,
 | 
				
			||||||
  Badge,
 | 
					  Badge,
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
@@ -9,11 +10,13 @@ import {
 | 
				
			|||||||
  Loader,
 | 
					  Loader,
 | 
				
			||||||
  Modal,
 | 
					  Modal,
 | 
				
			||||||
  Radio,
 | 
					  Radio,
 | 
				
			||||||
 | 
					  SimpleGrid,
 | 
				
			||||||
  Stack,
 | 
					  Stack,
 | 
				
			||||||
  Table,
 | 
					  Table,
 | 
				
			||||||
  Text,
 | 
					  Text,
 | 
				
			||||||
  TextInput,
 | 
					  TextInput,
 | 
				
			||||||
  Title
 | 
					  Title,
 | 
				
			||||||
 | 
					  Tooltip
 | 
				
			||||||
} from '@mantine/core';
 | 
					} from '@mantine/core';
 | 
				
			||||||
import { useDisclosure } from '@mantine/hooks';
 | 
					import { useDisclosure } from '@mantine/hooks';
 | 
				
			||||||
import { hideNotification, showNotification } from '@mantine/notifications';
 | 
					import { hideNotification, showNotification } from '@mantine/notifications';
 | 
				
			||||||
@@ -43,91 +46,116 @@ export function SecurityContent() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Stack>
 | 
					    <Stack>
 | 
				
			||||||
      <Title order={5}>
 | 
					      <Accordion multiple defaultValue={['email', 'sso', 'mfa', 'token']}>
 | 
				
			||||||
        <Trans>Email Addresses</Trans>
 | 
					        <Accordion.Item value='email'>
 | 
				
			||||||
      </Title>
 | 
					          <Accordion.Control>
 | 
				
			||||||
      <EmailSection />
 | 
					            <StylishText size='lg'>{t`Email Addresses`}</StylishText>
 | 
				
			||||||
      <Title order={5}>
 | 
					          </Accordion.Control>
 | 
				
			||||||
        <Trans>Single Sign On</Trans>
 | 
					          <Accordion.Panel>
 | 
				
			||||||
      </Title>
 | 
					            <EmailSection />
 | 
				
			||||||
      {sso_enabled() ? (
 | 
					          </Accordion.Panel>
 | 
				
			||||||
        <ProviderSection auth_config={auth_config} />
 | 
					        </Accordion.Item>
 | 
				
			||||||
      ) : (
 | 
					        <Accordion.Item value='sso'>
 | 
				
			||||||
        <Alert
 | 
					          <Accordion.Control>
 | 
				
			||||||
          icon={<IconAlertCircle size='1rem' />}
 | 
					            <StylishText size='lg'>{t`Single Sign On`}</StylishText>
 | 
				
			||||||
          title={t`Not enabled`}
 | 
					          </Accordion.Control>
 | 
				
			||||||
          color='yellow'
 | 
					          <Accordion.Panel>
 | 
				
			||||||
        >
 | 
					            {sso_enabled() ? (
 | 
				
			||||||
          <Trans>Single Sign On is not enabled for this server </Trans>
 | 
					              <ProviderSection auth_config={auth_config} />
 | 
				
			||||||
        </Alert>
 | 
					            ) : (
 | 
				
			||||||
      )}
 | 
					              <Alert
 | 
				
			||||||
      <Title order={5}>
 | 
					                icon={<IconAlertCircle size='1rem' />}
 | 
				
			||||||
        <Trans>Multifactor authentication</Trans>
 | 
					                title={t`Not enabled`}
 | 
				
			||||||
      </Title>
 | 
					                color='yellow'
 | 
				
			||||||
      <MfaSection />
 | 
					              >
 | 
				
			||||||
      <Title order={5}>
 | 
					                <Trans>Single Sign On is not enabled for this server </Trans>
 | 
				
			||||||
        <Trans>Access Tokens</Trans>
 | 
					              </Alert>
 | 
				
			||||||
      </Title>
 | 
					            )}
 | 
				
			||||||
      <TokenSection />
 | 
					          </Accordion.Panel>
 | 
				
			||||||
 | 
					        </Accordion.Item>
 | 
				
			||||||
 | 
					        <Accordion.Item value='mfa'>
 | 
				
			||||||
 | 
					          <Accordion.Control>
 | 
				
			||||||
 | 
					            <StylishText size='lg'>{t`Multi-Factor Authentication`}</StylishText>
 | 
				
			||||||
 | 
					          </Accordion.Control>
 | 
				
			||||||
 | 
					          <Accordion.Panel>
 | 
				
			||||||
 | 
					            <MfaSection />
 | 
				
			||||||
 | 
					          </Accordion.Panel>
 | 
				
			||||||
 | 
					        </Accordion.Item>
 | 
				
			||||||
 | 
					        <Accordion.Item value='token'>
 | 
				
			||||||
 | 
					          <Accordion.Control>
 | 
				
			||||||
 | 
					            <StylishText size='lg'>{t`Access Tokens`}</StylishText>
 | 
				
			||||||
 | 
					          </Accordion.Control>
 | 
				
			||||||
 | 
					          <Accordion.Panel>
 | 
				
			||||||
 | 
					            <TokenSection />
 | 
				
			||||||
 | 
					          </Accordion.Panel>
 | 
				
			||||||
 | 
					        </Accordion.Item>
 | 
				
			||||||
 | 
					      </Accordion>
 | 
				
			||||||
    </Stack>
 | 
					    </Stack>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function EmailSection() {
 | 
					function EmailSection() {
 | 
				
			||||||
  const [value, setValue] = useState<string>('');
 | 
					  const [selectedEmail, setSelectedEmail] = useState<string>('');
 | 
				
			||||||
  const [newEmailValue, setNewEmailValue] = useState('');
 | 
					  const [newEmailValue, setNewEmailValue] = useState('');
 | 
				
			||||||
  const { isLoading, data, refetch } = useQuery({
 | 
					  const { isLoading, data, refetch } = useQuery({
 | 
				
			||||||
    queryKey: ['emails'],
 | 
					    queryKey: ['emails'],
 | 
				
			||||||
    queryFn: () =>
 | 
					    queryFn: () =>
 | 
				
			||||||
      authApi(apiUrl(ApiEndpoints.auth_email)).then((res) => res.data.data)
 | 
					      authApi(apiUrl(ApiEndpoints.auth_email)).then((res) => res.data.data)
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const emailAvailable = useMemo(() => {
 | 
					  const emailAvailable = useMemo(() => {
 | 
				
			||||||
    return data == undefined || data.length == 0;
 | 
					    return data == undefined || data.length == 0;
 | 
				
			||||||
  }, [data]);
 | 
					  }, [data]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function runServerAction(
 | 
					  function runServerAction(
 | 
				
			||||||
    action: 'post' | 'put' | 'delete' = 'post',
 | 
					    action: 'patch' | 'post' | 'put' | 'delete' = 'post',
 | 
				
			||||||
    data?: any
 | 
					    data?: any
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    const vals: any = data || { email: value };
 | 
					    const vals: any = data || { email: selectedEmail };
 | 
				
			||||||
    return authApi(
 | 
					    return authApi(apiUrl(ApiEndpoints.auth_email), undefined, action, vals)
 | 
				
			||||||
      apiUrl(ApiEndpoints.auth_email),
 | 
					      .then(() => {
 | 
				
			||||||
      undefined,
 | 
					        refetch();
 | 
				
			||||||
      action,
 | 
					      })
 | 
				
			||||||
      vals
 | 
					      .catch((err) => {
 | 
				
			||||||
    ).then(() => {
 | 
					        hideNotification('email-error');
 | 
				
			||||||
      refetch();
 | 
					
 | 
				
			||||||
    });
 | 
					        showNotification({
 | 
				
			||||||
 | 
					          id: 'email-error',
 | 
				
			||||||
 | 
					          title: t`Error`,
 | 
				
			||||||
 | 
					          message: t`Error while updating email`,
 | 
				
			||||||
 | 
					          color: 'red'
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (isLoading) return <Loader />;
 | 
					  if (isLoading) return <Loader />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Grid>
 | 
					    <SimpleGrid cols={{ xs: 1, md: 2 }} spacing='sm'>
 | 
				
			||||||
      <Grid.Col span={6}>
 | 
					      {emailAvailable ? (
 | 
				
			||||||
        {emailAvailable ? (
 | 
					        <Alert
 | 
				
			||||||
          <Alert
 | 
					          icon={<IconAlertCircle size='1rem' />}
 | 
				
			||||||
            icon={<IconAlertCircle size='1rem' />}
 | 
					          title={t`Not Configured`}
 | 
				
			||||||
            title={t`Not configured`}
 | 
					          color='yellow'
 | 
				
			||||||
            color='yellow'
 | 
					        >
 | 
				
			||||||
          >
 | 
					          <Trans>Currently no email addresses are registered.</Trans>
 | 
				
			||||||
            <Trans>Currently no email addresses are registered.</Trans>
 | 
					        </Alert>
 | 
				
			||||||
          </Alert>
 | 
					      ) : (
 | 
				
			||||||
        ) : (
 | 
					        <Radio.Group
 | 
				
			||||||
          <Radio.Group
 | 
					          value={selectedEmail}
 | 
				
			||||||
            value={value}
 | 
					          onChange={setSelectedEmail}
 | 
				
			||||||
            onChange={setValue}
 | 
					          name='email_accounts'
 | 
				
			||||||
            name='email_accounts'
 | 
					          label={t`The following email addresses are associated with your account:`}
 | 
				
			||||||
            label={t`The following email addresses are associated with your account:`}
 | 
					        >
 | 
				
			||||||
          >
 | 
					          <Stack mt='xs'>
 | 
				
			||||||
            <Stack mt='xs'>
 | 
					            {data.map((email: any) => (
 | 
				
			||||||
              {data.map((email: any) => (
 | 
					              <Radio
 | 
				
			||||||
                <Radio
 | 
					                key={email.email}
 | 
				
			||||||
                  key={email.email}
 | 
					                value={String(email.email)}
 | 
				
			||||||
                  value={String(email.email)}
 | 
					                label={
 | 
				
			||||||
                  label={
 | 
					                  <Group justify='space-apart'>
 | 
				
			||||||
                    <Group justify='space-between'>
 | 
					                    {email.email}
 | 
				
			||||||
                      {email.email}
 | 
					                    <Group justify='right'>
 | 
				
			||||||
                      {email.primary && (
 | 
					                      {email.primary && (
 | 
				
			||||||
                        <Badge color='blue'>
 | 
					                        <Badge color='blue'>
 | 
				
			||||||
                          <Trans>Primary</Trans>
 | 
					                          <Trans>Primary</Trans>
 | 
				
			||||||
@@ -143,53 +171,51 @@ function EmailSection() {
 | 
				
			|||||||
                        </Badge>
 | 
					                        </Badge>
 | 
				
			||||||
                      )}
 | 
					                      )}
 | 
				
			||||||
                    </Group>
 | 
					                    </Group>
 | 
				
			||||||
                  }
 | 
					                  </Group>
 | 
				
			||||||
                />
 | 
					                }
 | 
				
			||||||
              ))}
 | 
					              />
 | 
				
			||||||
            </Stack>
 | 
					            ))}
 | 
				
			||||||
          </Radio.Group>
 | 
					            <Group>
 | 
				
			||||||
        )}
 | 
					              <Button
 | 
				
			||||||
      </Grid.Col>
 | 
					                onClick={() =>
 | 
				
			||||||
      <Grid.Col span={6}>
 | 
					                  runServerAction('patch', {
 | 
				
			||||||
        <Stack>
 | 
					                    email: selectedEmail,
 | 
				
			||||||
          <Text>
 | 
					                    primary: true
 | 
				
			||||||
            <Trans>Add Email Address</Trans>
 | 
					                  })
 | 
				
			||||||
          </Text>
 | 
					                }
 | 
				
			||||||
          <TextInput
 | 
					                disabled={!selectedEmail}
 | 
				
			||||||
            label={t`E-Mail`}
 | 
					              >
 | 
				
			||||||
            placeholder={t`E-Mail address`}
 | 
					                <Trans>Make Primary</Trans>
 | 
				
			||||||
            leftSection={<IconAt />}
 | 
					              </Button>
 | 
				
			||||||
            value={newEmailValue}
 | 
					              <Button
 | 
				
			||||||
            onChange={(event) => setNewEmailValue(event.currentTarget.value)}
 | 
					                onClick={() => runServerAction('put')}
 | 
				
			||||||
          />
 | 
					                disabled={!selectedEmail}
 | 
				
			||||||
        </Stack>
 | 
					              >
 | 
				
			||||||
      </Grid.Col>
 | 
					                <Trans>Re-send Verification</Trans>
 | 
				
			||||||
      <Grid.Col span={6}>
 | 
					              </Button>
 | 
				
			||||||
        <Group>
 | 
					              <Button
 | 
				
			||||||
          <Button
 | 
					                onClick={() => runServerAction('delete')}
 | 
				
			||||||
            onClick={() =>
 | 
					                disabled={!selectedEmail}
 | 
				
			||||||
              runServerAction('post', { email: value, primary: true })
 | 
					                color='red'
 | 
				
			||||||
            }
 | 
					              >
 | 
				
			||||||
            disabled={emailAvailable}
 | 
					                <Trans>Remove</Trans>
 | 
				
			||||||
          >
 | 
					              </Button>
 | 
				
			||||||
            <Trans>Make Primary</Trans>
 | 
					            </Group>
 | 
				
			||||||
          </Button>
 | 
					          </Stack>
 | 
				
			||||||
          <Button
 | 
					        </Radio.Group>
 | 
				
			||||||
            onClick={() => runServerAction('put')}
 | 
					      )}
 | 
				
			||||||
            disabled={emailAvailable}
 | 
					      <Stack>
 | 
				
			||||||
          >
 | 
					        <StylishText size='md'>{t`Add Email Address`}</StylishText>
 | 
				
			||||||
            <Trans>Re-send Verification</Trans>
 | 
					        <TextInput
 | 
				
			||||||
          </Button>
 | 
					          label={t`E-Mail`}
 | 
				
			||||||
          <Button
 | 
					          placeholder={t`E-Mail address`}
 | 
				
			||||||
            onClick={() => runServerAction('delete')}
 | 
					          leftSection={<IconAt />}
 | 
				
			||||||
            disabled={emailAvailable}
 | 
					          aria-label='email-address-input'
 | 
				
			||||||
          >
 | 
					          value={newEmailValue}
 | 
				
			||||||
            <Trans>Remove</Trans>
 | 
					          onChange={(event) => setNewEmailValue(event.currentTarget.value)}
 | 
				
			||||||
          </Button>
 | 
					        />
 | 
				
			||||||
        </Group>
 | 
					 | 
				
			||||||
      </Grid.Col>
 | 
					 | 
				
			||||||
      <Grid.Col span={6}>
 | 
					 | 
				
			||||||
        <Button
 | 
					        <Button
 | 
				
			||||||
 | 
					          aria-label='email-address-submit'
 | 
				
			||||||
          onClick={() =>
 | 
					          onClick={() =>
 | 
				
			||||||
            runServerAction('post', { email: newEmailValue }).catch((err) => {
 | 
					            runServerAction('post', { email: newEmailValue }).catch((err) => {
 | 
				
			||||||
              if (err.status == 400) {
 | 
					              if (err.status == 400) {
 | 
				
			||||||
@@ -207,8 +233,8 @@ function EmailSection() {
 | 
				
			|||||||
        >
 | 
					        >
 | 
				
			||||||
          <Trans>Add Email</Trans>
 | 
					          <Trans>Add Email</Trans>
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
      </Grid.Col>
 | 
					      </Stack>
 | 
				
			||||||
    </Grid>
 | 
					    </SimpleGrid>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -263,7 +289,7 @@ function ProviderSection({
 | 
				
			|||||||
        {data.length == 0 ? (
 | 
					        {data.length == 0 ? (
 | 
				
			||||||
          <Alert
 | 
					          <Alert
 | 
				
			||||||
            icon={<IconAlertCircle size='1rem' />}
 | 
					            icon={<IconAlertCircle size='1rem' />}
 | 
				
			||||||
            title={t`Not configured`}
 | 
					            title={t`Not Configured`}
 | 
				
			||||||
            color='yellow'
 | 
					            color='yellow'
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <Trans>There are no providers connected to this account.</Trans>
 | 
					            <Trans>There are no providers connected to this account.</Trans>
 | 
				
			||||||
@@ -395,61 +421,61 @@ function MfaSection() {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <ReauthModal />
 | 
					      <ReauthModal />
 | 
				
			||||||
      <Grid>
 | 
					      <SimpleGrid cols={{ xs: 1, md: 2 }} spacing='sm'>
 | 
				
			||||||
        <Grid.Col span={6}>
 | 
					        {data.length == 0 ? (
 | 
				
			||||||
          {data.length == 0 ? (
 | 
					          <Alert
 | 
				
			||||||
            <Alert icon={<IconAlertCircle size='1rem' />} color='yellow'>
 | 
					            title={t`Not Configured`}
 | 
				
			||||||
              <Trans>No factors configured</Trans>
 | 
					            icon={<IconAlertCircle size='1rem' />}
 | 
				
			||||||
            </Alert>
 | 
					            color='yellow'
 | 
				
			||||||
          ) : (
 | 
					 | 
				
			||||||
            <Table stickyHeader striped highlightOnHover withTableBorder>
 | 
					 | 
				
			||||||
              <Table.Thead>
 | 
					 | 
				
			||||||
                <Table.Tr>
 | 
					 | 
				
			||||||
                  <Table.Th>
 | 
					 | 
				
			||||||
                    <Trans>Type</Trans>
 | 
					 | 
				
			||||||
                  </Table.Th>
 | 
					 | 
				
			||||||
                  <Table.Th>
 | 
					 | 
				
			||||||
                    <Trans>Last used at</Trans>
 | 
					 | 
				
			||||||
                  </Table.Th>
 | 
					 | 
				
			||||||
                  <Table.Th>
 | 
					 | 
				
			||||||
                    <Trans>Created at</Trans>
 | 
					 | 
				
			||||||
                  </Table.Th>
 | 
					 | 
				
			||||||
                  <Table.Th>
 | 
					 | 
				
			||||||
                    <Trans>Actions</Trans>
 | 
					 | 
				
			||||||
                  </Table.Th>
 | 
					 | 
				
			||||||
                </Table.Tr>
 | 
					 | 
				
			||||||
              </Table.Thead>
 | 
					 | 
				
			||||||
              <Table.Tbody>{rows}</Table.Tbody>
 | 
					 | 
				
			||||||
            </Table>
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
        </Grid.Col>
 | 
					 | 
				
			||||||
        <Grid.Col span={6}>
 | 
					 | 
				
			||||||
          <MfaAddSection
 | 
					 | 
				
			||||||
            usedFactors={usedFactors}
 | 
					 | 
				
			||||||
            refetch={refetch}
 | 
					 | 
				
			||||||
            showRecoveryCodes={showRecoveryCodes}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <Modal
 | 
					 | 
				
			||||||
            opened={recoveryCodesOpen}
 | 
					 | 
				
			||||||
            onClose={() => {
 | 
					 | 
				
			||||||
              refetch();
 | 
					 | 
				
			||||||
              closeRecoveryCodes();
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
            title={t`Recovery Codes`}
 | 
					 | 
				
			||||||
            centered
 | 
					 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <Title order={3}>
 | 
					            <Trans>No multi-factor tokens configured for this account</Trans>
 | 
				
			||||||
              <Trans>Unused Codes</Trans>
 | 
					          </Alert>
 | 
				
			||||||
            </Title>
 | 
					        ) : (
 | 
				
			||||||
            <Code>{recoveryCodes?.unused_codes?.join('\n')}</Code>
 | 
					          <Table stickyHeader striped highlightOnHover withTableBorder>
 | 
				
			||||||
 | 
					            <Table.Thead>
 | 
				
			||||||
 | 
					              <Table.Tr>
 | 
				
			||||||
 | 
					                <Table.Th>
 | 
				
			||||||
 | 
					                  <Trans>Type</Trans>
 | 
				
			||||||
 | 
					                </Table.Th>
 | 
				
			||||||
 | 
					                <Table.Th>
 | 
				
			||||||
 | 
					                  <Trans>Last used at</Trans>
 | 
				
			||||||
 | 
					                </Table.Th>
 | 
				
			||||||
 | 
					                <Table.Th>
 | 
				
			||||||
 | 
					                  <Trans>Created at</Trans>
 | 
				
			||||||
 | 
					                </Table.Th>
 | 
				
			||||||
 | 
					                <Table.Th>
 | 
				
			||||||
 | 
					                  <Trans>Actions</Trans>
 | 
				
			||||||
 | 
					                </Table.Th>
 | 
				
			||||||
 | 
					              </Table.Tr>
 | 
				
			||||||
 | 
					            </Table.Thead>
 | 
				
			||||||
 | 
					            <Table.Tbody>{rows}</Table.Tbody>
 | 
				
			||||||
 | 
					          </Table>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        <MfaAddSection
 | 
				
			||||||
 | 
					          usedFactors={usedFactors}
 | 
				
			||||||
 | 
					          refetch={refetch}
 | 
				
			||||||
 | 
					          showRecoveryCodes={showRecoveryCodes}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <Modal
 | 
				
			||||||
 | 
					          opened={recoveryCodesOpen}
 | 
				
			||||||
 | 
					          onClose={() => {
 | 
				
			||||||
 | 
					            refetch();
 | 
				
			||||||
 | 
					            closeRecoveryCodes();
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          title={t`Recovery Codes`}
 | 
				
			||||||
 | 
					          centered
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Title order={3}>
 | 
				
			||||||
 | 
					            <Trans>Unused Codes</Trans>
 | 
				
			||||||
 | 
					          </Title>
 | 
				
			||||||
 | 
					          <Code>{recoveryCodes?.unused_codes?.join('\n')}</Code>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <Title order={3}>
 | 
					          <Title order={3}>
 | 
				
			||||||
              <Trans>Used Codes</Trans>
 | 
					            <Trans>Used Codes</Trans>
 | 
				
			||||||
            </Title>
 | 
					          </Title>
 | 
				
			||||||
            <Code>{recoveryCodes?.used_codes?.join('\n')}</Code>
 | 
					          <Code>{recoveryCodes?.used_codes?.join('\n')}</Code>
 | 
				
			||||||
          </Modal>
 | 
					        </Modal>
 | 
				
			||||||
        </Grid.Col>
 | 
					      </SimpleGrid>
 | 
				
			||||||
      </Grid>
 | 
					 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -554,16 +580,17 @@ function MfaAddSection({
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Stack>
 | 
					    <Stack>
 | 
				
			||||||
      <ReauthModal />
 | 
					      <ReauthModal />
 | 
				
			||||||
      <Text>Add Factor</Text>
 | 
					      <StylishText size='md'>{t`Add Token`}</StylishText>
 | 
				
			||||||
      {possibleFactors.map((factor) => (
 | 
					      {possibleFactors.map((factor) => (
 | 
				
			||||||
        <Button
 | 
					        <Tooltip label={factor.description} key={factor.type}>
 | 
				
			||||||
          key={factor.type}
 | 
					          <Button
 | 
				
			||||||
          onClick={factor.function}
 | 
					            onClick={factor.function}
 | 
				
			||||||
          disabled={factor.used}
 | 
					            disabled={factor.used}
 | 
				
			||||||
          variant='outline'
 | 
					            variant='outline'
 | 
				
			||||||
        >
 | 
					          >
 | 
				
			||||||
          {factor.name}
 | 
					            {factor.name}
 | 
				
			||||||
        </Button>
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </Tooltip>
 | 
				
			||||||
      ))}
 | 
					      ))}
 | 
				
			||||||
      <Modal
 | 
					      <Modal
 | 
				
			||||||
        opened={totpQrOpen}
 | 
					        opened={totpQrOpen}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -89,6 +89,17 @@ export const navigate = async (page, url: string) => {
 | 
				
			|||||||
  await page.waitForLoadState('networkidle');
 | 
					  await page.waitForLoadState('networkidle');
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * CLick on the 'tab' element with the provided name
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const loadTab = async (page, tabName) => {
 | 
				
			||||||
 | 
					  await page
 | 
				
			||||||
 | 
					    .getByLabel(/panel-tabs-/)
 | 
				
			||||||
 | 
					    .getByRole('tab', { name: tabName })
 | 
				
			||||||
 | 
					    .click();
 | 
				
			||||||
 | 
					  await page.waitForLoadState('networkidle');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Perform a 'global search' on the provided page, for the provided query text
 | 
					 * Perform a 'global search' on the provided page, for the provided query text
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import { test } from '../baseFixtures.ts';
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  clearTableFilters,
 | 
					  clearTableFilters,
 | 
				
			||||||
  getRowFromCell,
 | 
					  getRowFromCell,
 | 
				
			||||||
 | 
					  loadTab,
 | 
				
			||||||
  navigate,
 | 
					  navigate,
 | 
				
			||||||
  setTableChoiceFilter
 | 
					  setTableChoiceFilter
 | 
				
			||||||
} from '../helpers.ts';
 | 
					} from '../helpers.ts';
 | 
				
			||||||
@@ -12,7 +13,7 @@ test('Build Order - Basic Tests', async ({ page }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Navigate to the correct build order
 | 
					  // Navigate to the correct build order
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Manufacturing', exact: true }).click();
 | 
					  await page.getByRole('tab', { name: 'Manufacturing', exact: true }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Build Orders', exact: true }).click();
 | 
					  await loadTab(page, 'Build Orders');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await clearTableFilters(page);
 | 
					  await clearTableFilters(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -57,11 +58,11 @@ test('Build Order - Basic Tests', async ({ page }) => {
 | 
				
			|||||||
  await page.getByRole('button', { name: 'Cancel' }).click();
 | 
					  await page.getByRole('button', { name: 'Cancel' }).click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Click on some tabs
 | 
					  // Click on some tabs
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Attachments' }).click();
 | 
					  await loadTab(page, 'Attachments');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Notes' }).click();
 | 
					  await loadTab(page, 'Notes');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Incomplete Outputs' }).click();
 | 
					  await loadTab(page, 'Incomplete Outputs');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Line Items' }).click();
 | 
					  await loadTab(page, 'Line Items');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Allocated Stock' }).click();
 | 
					  await loadTab(page, 'Allocated Stock');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Check for expected text in the table
 | 
					  // Check for expected text in the table
 | 
				
			||||||
  await page.getByText('R_10R_0402_1%').waitFor();
 | 
					  await page.getByText('R_10R_0402_1%').waitFor();
 | 
				
			||||||
@@ -70,7 +71,7 @@ test('Build Order - Basic Tests', async ({ page }) => {
 | 
				
			|||||||
    .waitFor();
 | 
					    .waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Check "test results"
 | 
					  // Check "test results"
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Test Results' }).click();
 | 
					  await loadTab(page, 'Test Results');
 | 
				
			||||||
  await page.getByText('Quantity: 25').waitFor();
 | 
					  await page.getByText('Quantity: 25').waitFor();
 | 
				
			||||||
  await page.getByText('Continuity Checks').waitFor();
 | 
					  await page.getByText('Continuity Checks').waitFor();
 | 
				
			||||||
  await page
 | 
					  await page
 | 
				
			||||||
@@ -80,7 +81,7 @@ test('Build Order - Basic Tests', async ({ page }) => {
 | 
				
			|||||||
  await page.getByText('Add Test Result').waitFor();
 | 
					  await page.getByText('Add Test Result').waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Click through to the "parent" build
 | 
					  // Click through to the "parent" build
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Build Details' }).click();
 | 
					  await loadTab(page, 'Build Details');
 | 
				
			||||||
  await page.getByRole('link', { name: 'BO0010' }).click();
 | 
					  await page.getByRole('link', { name: 'BO0010' }).click();
 | 
				
			||||||
  await page
 | 
					  await page
 | 
				
			||||||
    .getByLabel('Build Details')
 | 
					    .getByLabel('Build Details')
 | 
				
			||||||
@@ -119,7 +120,7 @@ test('Build Order - Build Outputs', async ({ page }) => {
 | 
				
			|||||||
  await doQuickLogin(page);
 | 
					  await doQuickLogin(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await navigate(page, 'manufacturing/index/');
 | 
					  await navigate(page, 'manufacturing/index/');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Build Orders', exact: true }).click();
 | 
					  await loadTab(page, 'Build Orders');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await clearTableFilters(page);
 | 
					  await clearTableFilters(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -128,7 +129,7 @@ test('Build Order - Build Outputs', async ({ page }) => {
 | 
				
			|||||||
  await page.getByText('Pending').first().waitFor();
 | 
					  await page.getByText('Pending').first().waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('cell', { name: 'BO0011' }).click();
 | 
					  await page.getByRole('cell', { name: 'BO0011' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Incomplete Outputs' }).click();
 | 
					  await loadTab(page, 'Incomplete Outputs');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Create a new build output
 | 
					  // Create a new build output
 | 
				
			||||||
  await page.getByLabel('action-button-add-build-output').click();
 | 
					  await page.getByLabel('action-button-add-build-output').click();
 | 
				
			||||||
@@ -213,7 +214,7 @@ test('Build Order - Allocation', async ({ page }) => {
 | 
				
			|||||||
  await page.getByRole('cell', { name: 'Reel Storage', exact: true }).waitFor();
 | 
					  await page.getByRole('cell', { name: 'Reel Storage', exact: true }).waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Navigate to the "Incomplete Outputs" tab
 | 
					  // Navigate to the "Incomplete Outputs" tab
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Incomplete Outputs' }).click();
 | 
					  await loadTab(page, 'Incomplete Outputs');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Find output #7
 | 
					  // Find output #7
 | 
				
			||||||
  const output7 = await page
 | 
					  const output7 = await page
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { test } from '../baseFixtures.js';
 | 
					import { test } from '../baseFixtures.js';
 | 
				
			||||||
import { navigate } from '../helpers.js';
 | 
					import { loadTab, navigate } from '../helpers.js';
 | 
				
			||||||
import { doQuickLogin } from '../login.js';
 | 
					import { doQuickLogin } from '../login.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('Company', async ({ page }) => {
 | 
					test('Company', async ({ page }) => {
 | 
				
			||||||
@@ -8,23 +8,23 @@ test('Company', async ({ page }) => {
 | 
				
			|||||||
  await navigate(page, 'company/1/details');
 | 
					  await navigate(page, 'company/1/details');
 | 
				
			||||||
  await page.getByLabel('Details').getByText('DigiKey Electronics').waitFor();
 | 
					  await page.getByLabel('Details').getByText('DigiKey Electronics').waitFor();
 | 
				
			||||||
  await page.getByRole('cell', { name: 'https://www.digikey.com/' }).waitFor();
 | 
					  await page.getByRole('cell', { name: 'https://www.digikey.com/' }).waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Supplied Parts' }).click();
 | 
					  await loadTab(page, 'Supplied Parts');
 | 
				
			||||||
  await page
 | 
					  await page
 | 
				
			||||||
    .getByRole('cell', { name: 'RR05P100KDTR-ND', exact: true })
 | 
					    .getByRole('cell', { name: 'RR05P100KDTR-ND', exact: true })
 | 
				
			||||||
    .waitFor();
 | 
					    .waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchase Orders' }).click();
 | 
					  await loadTab(page, 'Purchase Orders');
 | 
				
			||||||
  await page.getByRole('cell', { name: 'Molex connectors' }).first().waitFor();
 | 
					  await page.getByRole('cell', { name: 'Molex connectors' }).first().waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Items' }).click();
 | 
					  await loadTab(page, 'Stock Items');
 | 
				
			||||||
  await page
 | 
					  await page
 | 
				
			||||||
    .getByRole('cell', { name: 'Blue plastic enclosure' })
 | 
					    .getByRole('cell', { name: 'Blue plastic enclosure' })
 | 
				
			||||||
    .first()
 | 
					    .first()
 | 
				
			||||||
    .waitFor();
 | 
					    .waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Contacts' }).click();
 | 
					  await loadTab(page, 'Contacts');
 | 
				
			||||||
  await page.getByRole('cell', { name: 'jimmy.mcleod@digikey.com' }).waitFor();
 | 
					  await page.getByRole('cell', { name: 'jimmy.mcleod@digikey.com' }).waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Addresses' }).click();
 | 
					  await loadTab(page, 'Addresses');
 | 
				
			||||||
  await page.getByRole('cell', { name: 'Carla Tunnel' }).waitFor();
 | 
					  await page.getByRole('cell', { name: 'Carla Tunnel' }).waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Attachments' }).click();
 | 
					  await loadTab(page, 'Attachments');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Notes' }).click();
 | 
					  await loadTab(page, 'Notes');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Let's edit the company details
 | 
					  // Let's edit the company details
 | 
				
			||||||
  await page.getByLabel('action-menu-company-actions').click();
 | 
					  await page.getByLabel('action-menu-company-actions').click();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,10 @@
 | 
				
			|||||||
import { test } from '../baseFixtures';
 | 
					import { test } from '../baseFixtures';
 | 
				
			||||||
import { clearTableFilters, getRowFromCell, navigate } from '../helpers';
 | 
					import {
 | 
				
			||||||
 | 
					  clearTableFilters,
 | 
				
			||||||
 | 
					  getRowFromCell,
 | 
				
			||||||
 | 
					  loadTab,
 | 
				
			||||||
 | 
					  navigate
 | 
				
			||||||
 | 
					} from '../helpers';
 | 
				
			||||||
import { doQuickLogin } from '../login';
 | 
					import { doQuickLogin } from '../login';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@@ -19,35 +24,35 @@ test('Parts - Tabs', async ({ page }) => {
 | 
				
			|||||||
  await page.getByPlaceholder('Search').fill('1551');
 | 
					  await page.getByPlaceholder('Search').fill('1551');
 | 
				
			||||||
  await page.getByText('1551ABK').click();
 | 
					  await page.getByText('1551ABK').click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Allocations' }).click();
 | 
					  await loadTab(page, 'Allocations');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Used In' }).click();
 | 
					  await loadTab(page, 'Used In');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Pricing' }).click();
 | 
					  await loadTab(page, 'Pricing');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Suppliers' }).click();
 | 
					  await loadTab(page, 'Suppliers');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchase Orders' }).click();
 | 
					  await loadTab(page, 'Purchase Orders');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Scheduling' }).click();
 | 
					  await loadTab(page, 'Scheduling');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock History' }).click();
 | 
					  await loadTab(page, 'Stock History');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Attachments' }).click();
 | 
					  await loadTab(page, 'Attachments');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Notes' }).click();
 | 
					  await loadTab(page, 'Notes');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Related Parts' }).click();
 | 
					  await loadTab(page, 'Related Parts');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Related Parts
 | 
					  // Related Parts
 | 
				
			||||||
  await page.getByText('1551ACLR').click();
 | 
					  await page.getByText('1551ACLR').click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Part Details' }).click();
 | 
					  await loadTab(page, 'Part Details');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Parameters' }).click();
 | 
					  await loadTab(page, 'Parameters');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page
 | 
					  await page
 | 
				
			||||||
    .getByLabel('panel-tabs-part')
 | 
					    .getByLabel('panel-tabs-part')
 | 
				
			||||||
    .getByRole('tab', { name: 'Stock', exact: true })
 | 
					    .getByRole('tab', { name: 'Stock', exact: true })
 | 
				
			||||||
    .click();
 | 
					    .click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Allocations' }).click();
 | 
					  await loadTab(page, 'Allocations');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Used In' }).click();
 | 
					  await loadTab(page, 'Used In');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Pricing' }).click();
 | 
					  await loadTab(page, 'Pricing');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await navigate(page, 'part/category/index/parts');
 | 
					  await navigate(page, 'part/category/index/parts');
 | 
				
			||||||
  await page.getByText('Blue Chair').click();
 | 
					  await page.getByText('Blue Chair').click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Bill of Materials' }).click();
 | 
					  await loadTab(page, 'Bill of Materials');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Build Orders' }).click();
 | 
					  await loadTab(page, 'Build Orders');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('Parts - Manufacturer Parts', async ({ page }) => {
 | 
					test('Parts - Manufacturer Parts', async ({ page }) => {
 | 
				
			||||||
@@ -55,11 +60,11 @@ test('Parts - Manufacturer Parts', async ({ page }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  await navigate(page, 'part/84/suppliers');
 | 
					  await navigate(page, 'part/84/suppliers');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Suppliers' }).click();
 | 
					  await loadTab(page, 'Suppliers');
 | 
				
			||||||
  await page.getByText('Hammond Manufacturing').click();
 | 
					  await page.getByText('Hammond Manufacturing').click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Parameters' }).click();
 | 
					  await loadTab(page, 'Parameters');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Suppliers' }).click();
 | 
					  await loadTab(page, 'Suppliers');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Attachments' }).click();
 | 
					  await loadTab(page, 'Attachments');
 | 
				
			||||||
  await page.getByText('1551ACLR - 1551ACLR').waitFor();
 | 
					  await page.getByText('1551ACLR - 1551ACLR').waitFor();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -68,11 +73,11 @@ test('Parts - Supplier Parts', async ({ page }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  await navigate(page, 'part/15/suppliers');
 | 
					  await navigate(page, 'part/15/suppliers');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Suppliers' }).click();
 | 
					  await loadTab(page, 'Suppliers');
 | 
				
			||||||
  await page.getByRole('cell', { name: 'DIG-84670-SJI' }).click();
 | 
					  await page.getByRole('cell', { name: 'DIG-84670-SJI' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Received Stock' }).click(); //
 | 
					  await loadTab(page, 'Received Stock'); //
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchase Orders' }).click();
 | 
					  await loadTab(page, 'Purchase Orders');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Pricing' }).click();
 | 
					  await loadTab(page, 'Pricing');
 | 
				
			||||||
  await page.getByText('DIG-84670-SJI - R_550R_0805_1%').waitFor();
 | 
					  await page.getByText('DIG-84670-SJI - R_550R_0805_1%').waitFor();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,14 +86,14 @@ test('Parts - Locking', async ({ page }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Navigate to a known assembly which is *not* locked
 | 
					  // Navigate to a known assembly which is *not* locked
 | 
				
			||||||
  await navigate(page, 'part/104/bom');
 | 
					  await navigate(page, 'part/104/bom');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Bill of Materials' }).click();
 | 
					  await loadTab(page, 'Bill of Materials');
 | 
				
			||||||
  await page.getByLabel('action-button-add-bom-item').waitFor();
 | 
					  await page.getByLabel('action-button-add-bom-item').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Parameters' }).click();
 | 
					  await loadTab(page, 'Parameters');
 | 
				
			||||||
  await page.getByLabel('action-button-add-parameter').waitFor();
 | 
					  await page.getByLabel('action-button-add-parameter').waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Navigate to a known assembly which *is* locked
 | 
					  // Navigate to a known assembly which *is* locked
 | 
				
			||||||
  await navigate(page, 'part/100/bom');
 | 
					  await navigate(page, 'part/100/bom');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Bill of Materials' }).click();
 | 
					  await loadTab(page, 'Bill of Materials');
 | 
				
			||||||
  await page.getByLabel('part-lock-icon').waitFor();
 | 
					  await page.getByLabel('part-lock-icon').waitFor();
 | 
				
			||||||
  await page.getByText('Part is Locked', { exact: true }).waitFor();
 | 
					  await page.getByText('Part is Locked', { exact: true }).waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -98,7 +103,7 @@ test('Parts - Locking', async ({ page }) => {
 | 
				
			|||||||
  await page.getByText('In Production: 50').waitFor();
 | 
					  await page.getByText('In Production: 50').waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Check the "parameters" tab also
 | 
					  // Check the "parameters" tab also
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Parameters' }).click();
 | 
					  await loadTab(page, 'Parameters');
 | 
				
			||||||
  await page.getByText('Part parameters cannot be').waitFor();
 | 
					  await page.getByText('Part parameters cannot be').waitFor();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -107,7 +112,7 @@ test('Parts - Allocations', async ({ page }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Let's look at the allocations for a single stock item
 | 
					  // Let's look at the allocations for a single stock item
 | 
				
			||||||
  await navigate(page, 'stock/item/324/');
 | 
					  await navigate(page, 'stock/item/324/');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Allocations' }).click();
 | 
					  await loadTab(page, 'Allocations');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('button', { name: 'Build Order Allocations' }).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 some blue chairs' }).waitFor();
 | 
				
			||||||
@@ -124,7 +129,7 @@ test('Parts - Allocations', async ({ page }) => {
 | 
				
			|||||||
  await page.waitForTimeout(500);
 | 
					  await page.waitForTimeout(500);
 | 
				
			||||||
  await page.waitForLoadState('networkidle');
 | 
					  await page.waitForLoadState('networkidle');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Allocations' }).click();
 | 
					  await loadTab(page, 'Allocations');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor();
 | 
					  await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor();
 | 
				
			||||||
  await page.getByRole('button', { name: 'Sales Order Allocations' }).waitFor();
 | 
					  await page.getByRole('button', { name: 'Sales Order Allocations' }).waitFor();
 | 
				
			||||||
@@ -177,7 +182,7 @@ test('Parts - Pricing (Nothing, BOM)', async ({ page }) => {
 | 
				
			|||||||
  await navigate(page, 'part/82/pricing');
 | 
					  await navigate(page, 'part/82/pricing');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByText('Small plastic enclosure, black').waitFor();
 | 
					  await page.getByText('Small plastic enclosure, black').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Part Pricing' }).click();
 | 
					  await loadTab(page, 'Part Pricing');
 | 
				
			||||||
  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();
 | 
				
			||||||
  await page.getByText('Last Updated').waitFor();
 | 
					  await page.getByText('Last Updated').waitFor();
 | 
				
			||||||
@@ -188,7 +193,7 @@ test('Parts - Pricing (Nothing, BOM)', async ({ page }) => {
 | 
				
			|||||||
  // Part with history
 | 
					  // Part with history
 | 
				
			||||||
  await navigate(page, 'part/108/pricing');
 | 
					  await navigate(page, 'part/108/pricing');
 | 
				
			||||||
  await page.getByText('A chair - with blue paint').waitFor();
 | 
					  await page.getByText('A chair - with blue paint').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Part Pricing' }).click();
 | 
					  await loadTab(page, 'Part Pricing');
 | 
				
			||||||
  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();
 | 
				
			||||||
  await page.getByText('Last Updated').waitFor();
 | 
					  await page.getByText('Last Updated').waitFor();
 | 
				
			||||||
@@ -226,7 +231,7 @@ test('Parts - Pricing (Supplier)', async ({ page }) => {
 | 
				
			|||||||
  // Part
 | 
					  // Part
 | 
				
			||||||
  await navigate(page, 'part/55/pricing');
 | 
					  await navigate(page, 'part/55/pricing');
 | 
				
			||||||
  await page.getByText('Ceramic capacitor, 100nF in').waitFor();
 | 
					  await page.getByText('Ceramic capacitor, 100nF in').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Part Pricing' }).click();
 | 
					  await loadTab(page, 'Part Pricing');
 | 
				
			||||||
  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();
 | 
				
			||||||
  await page.getByText('Last Updated').waitFor();
 | 
					  await page.getByText('Last Updated').waitFor();
 | 
				
			||||||
@@ -252,7 +257,7 @@ test('Parts - Pricing (Variant)', async ({ page }) => {
 | 
				
			|||||||
  // Part
 | 
					  // Part
 | 
				
			||||||
  await navigate(page, 'part/106/pricing');
 | 
					  await navigate(page, 'part/106/pricing');
 | 
				
			||||||
  await page.getByText('A chair - available in multiple colors').waitFor();
 | 
					  await page.getByText('A chair - available in multiple colors').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Part Pricing' }).click();
 | 
					  await loadTab(page, 'Part Pricing');
 | 
				
			||||||
  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();
 | 
				
			||||||
  await page.getByText('Last Updated').waitFor();
 | 
					  await page.getByText('Last Updated').waitFor();
 | 
				
			||||||
@@ -278,7 +283,7 @@ test('Parts - Pricing (Internal)', async ({ page }) => {
 | 
				
			|||||||
  // Part
 | 
					  // Part
 | 
				
			||||||
  await navigate(page, 'part/65/pricing');
 | 
					  await navigate(page, 'part/65/pricing');
 | 
				
			||||||
  await page.getByText('Socket head cap screw, M2').waitFor();
 | 
					  await page.getByText('Socket head cap screw, M2').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Part Pricing' }).click();
 | 
					  await loadTab(page, 'Part Pricing');
 | 
				
			||||||
  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();
 | 
				
			||||||
  await page.getByText('Last Updated').waitFor();
 | 
					  await page.getByText('Last Updated').waitFor();
 | 
				
			||||||
@@ -303,7 +308,7 @@ test('Parts - Pricing (Purchase)', async ({ page }) => {
 | 
				
			|||||||
  // Part
 | 
					  // Part
 | 
				
			||||||
  await navigate(page, 'part/69/pricing');
 | 
					  await navigate(page, 'part/69/pricing');
 | 
				
			||||||
  await page.getByText('1.25mm Pitch, PicoBlade PCB').waitFor();
 | 
					  await page.getByText('1.25mm Pitch, PicoBlade PCB').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Part Pricing' }).click();
 | 
					  await loadTab(page, 'Part Pricing');
 | 
				
			||||||
  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();
 | 
				
			||||||
  await page.getByText('Last Updated').waitFor();
 | 
					  await page.getByText('Last Updated').waitFor();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import {
 | 
				
			|||||||
  clearTableFilters,
 | 
					  clearTableFilters,
 | 
				
			||||||
  clickButtonIfVisible,
 | 
					  clickButtonIfVisible,
 | 
				
			||||||
  clickOnRowMenu,
 | 
					  clickOnRowMenu,
 | 
				
			||||||
 | 
					  loadTab,
 | 
				
			||||||
  navigate,
 | 
					  navigate,
 | 
				
			||||||
  openFilterDrawer,
 | 
					  openFilterDrawer,
 | 
				
			||||||
  setTableChoiceFilter
 | 
					  setTableChoiceFilter
 | 
				
			||||||
@@ -13,7 +14,7 @@ test('Purchase Orders - List', async ({ page }) => {
 | 
				
			|||||||
  await doQuickLogin(page);
 | 
					  await doQuickLogin(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
					  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchase Orders' }).click();
 | 
					  await loadTab(page, 'Purchase Orders');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await clearTableFilters(page);
 | 
					  await clearTableFilters(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -101,29 +102,32 @@ test('Purchase Orders - General', async ({ page }) => {
 | 
				
			|||||||
  await doQuickLogin(page);
 | 
					  await doQuickLogin(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
					  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('cell', { name: 'PO0012' }).click();
 | 
					  await page.getByRole('cell', { name: 'PO0012' }).click();
 | 
				
			||||||
  await page.waitForTimeout(200);
 | 
					  await page.waitForTimeout(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Line Items' }).click();
 | 
					  await loadTab(page, 'Line Items');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Received Stock' }).click();
 | 
					  await loadTab(page, 'Received Stock');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Attachments' }).click();
 | 
					  await loadTab(page, 'Attachments');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
					  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Suppliers' }).click();
 | 
					  await loadTab(page, 'Suppliers');
 | 
				
			||||||
  await page.getByText('Arrow', { exact: true }).click();
 | 
					  await page.getByText('Arrow', { exact: true }).click();
 | 
				
			||||||
  await page.waitForTimeout(200);
 | 
					  await page.waitForTimeout(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Supplied Parts' }).click();
 | 
					  await loadTab(page, 'Supplied Parts');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchase Orders' }).click();
 | 
					  await loadTab(page, 'Purchase Orders');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Items' }).click();
 | 
					  await loadTab(page, 'Stock Items');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Contacts' }).click();
 | 
					  await loadTab(page, 'Contacts');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Addresses' }).click();
 | 
					  await loadTab(page, 'Addresses');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Attachments' }).click();
 | 
					  await loadTab(page, 'Attachments');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
					  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Manufacturers' }).click();
 | 
					  await loadTab(page, 'Manufacturers');
 | 
				
			||||||
  await page.getByText('AVX Corporation').click();
 | 
					  await page.getByText('AVX Corporation').click();
 | 
				
			||||||
  await page.waitForTimeout(200);
 | 
					  await page.waitForTimeout(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Addresses' }).click();
 | 
					  await loadTab(page, 'Addresses');
 | 
				
			||||||
  await page.getByRole('cell', { name: 'West Branch' }).click();
 | 
					  await page.getByRole('cell', { name: 'West Branch' }).click();
 | 
				
			||||||
  await page.locator('.mantine-ScrollArea-root').click();
 | 
					  await page.locator('.mantine-ScrollArea-root').click();
 | 
				
			||||||
  await page
 | 
					  await page
 | 
				
			||||||
@@ -151,7 +155,7 @@ test('Purchase Orders - Filters', async ({ page }) => {
 | 
				
			|||||||
  await doQuickLogin(page, 'reader', 'readonly');
 | 
					  await doQuickLogin(page, 'reader', 'readonly');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
					  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchase Orders' }).click();
 | 
					  await loadTab(page, 'Purchase Orders');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Open filters drawer
 | 
					  // Open filters drawer
 | 
				
			||||||
  await openFilterDrawer(page);
 | 
					  await openFilterDrawer(page);
 | 
				
			||||||
@@ -197,7 +201,7 @@ test('Purchase Orders - Order Parts', async ({ page }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Open "Order Parts" wizard from the "Stock Items" table
 | 
					  // Open "Order Parts" wizard from the "Stock Items" table
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock' }).click();
 | 
					  await page.getByRole('tab', { name: 'Stock' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Items' }).click();
 | 
					  await loadTab(page, 'Stock Items');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Select multiple stock items
 | 
					  // Select multiple stock items
 | 
				
			||||||
  for (let ii = 2; ii < 7; ii += 2) {
 | 
					  for (let ii = 2; ii < 7; ii += 2) {
 | 
				
			||||||
@@ -257,10 +261,10 @@ test('Purchase Orders - Receive Items', async ({ page }) => {
 | 
				
			|||||||
  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
					  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
				
			||||||
  await page.getByRole('cell', { name: 'PO0014' }).click();
 | 
					  await page.getByRole('cell', { name: 'PO0014' }).click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Order Details' }).click();
 | 
					  await loadTab(page, 'Order Details');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Select all line items to receive
 | 
					  // Select all line items to receive
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Line Items' }).click();
 | 
					  await loadTab(page, 'Line Items');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByLabel('Select all records').click();
 | 
					  await page.getByLabel('Select all records').click();
 | 
				
			||||||
  await page.waitForTimeout(200);
 | 
					  await page.waitForTimeout(200);
 | 
				
			||||||
@@ -311,7 +315,7 @@ test('Purchase Orders - Receive Items', async ({ page }) => {
 | 
				
			|||||||
  await page.getByRole('button', { name: 'Submit' }).click();
 | 
					  await page.getByRole('button', { name: 'Submit' }).click();
 | 
				
			||||||
  await page.getByText('Items received').waitFor();
 | 
					  await page.getByText('Items received').waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Received Stock' }).click();
 | 
					  await loadTab(page, 'Received Stock');
 | 
				
			||||||
  await clearTableFilters(page);
 | 
					  await clearTableFilters(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('cell', { name: 'my-batch-code' }).first().waitFor();
 | 
					  await page.getByRole('cell', { name: 'my-batch-code' }).first().waitFor();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import { test } from '../baseFixtures.ts';
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  clearTableFilters,
 | 
					  clearTableFilters,
 | 
				
			||||||
  globalSearch,
 | 
					  globalSearch,
 | 
				
			||||||
 | 
					  loadTab,
 | 
				
			||||||
  navigate,
 | 
					  navigate,
 | 
				
			||||||
  setTableChoiceFilter
 | 
					  setTableChoiceFilter
 | 
				
			||||||
} from '../helpers.ts';
 | 
					} from '../helpers.ts';
 | 
				
			||||||
@@ -13,27 +14,27 @@ test('Sales Orders - Tabs', async ({ page }) => {
 | 
				
			|||||||
  await navigate(page, 'sales/index/');
 | 
					  await navigate(page, 'sales/index/');
 | 
				
			||||||
  await page.waitForURL('**/platform/sales/**');
 | 
					  await page.waitForURL('**/platform/sales/**');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Sales Orders' }).click();
 | 
					  await loadTab(page, 'Sales Orders');
 | 
				
			||||||
  await page.waitForURL('**/platform/sales/index/salesorders');
 | 
					  await page.waitForURL('**/platform/sales/index/salesorders');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Return Orders' }).click();
 | 
					  await loadTab(page, 'Return Orders');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Customers
 | 
					  // Customers
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Customers' }).click();
 | 
					  await loadTab(page, 'Customers');
 | 
				
			||||||
  await page.getByText('Customer A').click();
 | 
					  await page.getByText('Customer A').click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Notes' }).click();
 | 
					  await loadTab(page, 'Notes');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Attachments' }).click();
 | 
					  await loadTab(page, 'Attachments');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Contacts' }).click();
 | 
					  await loadTab(page, 'Contacts');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Assigned Stock' }).click();
 | 
					  await loadTab(page, 'Assigned Stock');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Return Orders' }).click();
 | 
					  await loadTab(page, 'Return Orders');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Sales Orders' }).click();
 | 
					  await loadTab(page, 'Sales Orders');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Contacts' }).click();
 | 
					  await loadTab(page, 'Contacts');
 | 
				
			||||||
  await page.getByRole('cell', { name: 'Dorathy Gross' }).waitFor();
 | 
					  await page.getByRole('cell', { name: 'Dorathy Gross' }).waitFor();
 | 
				
			||||||
  await page
 | 
					  await page
 | 
				
			||||||
    .getByRole('row', { name: 'Dorathy Gross 	dorathy.gross@customer.com' })
 | 
					    .getByRole('row', { name: 'Dorathy Gross 	dorathy.gross@customer.com' })
 | 
				
			||||||
    .waitFor();
 | 
					    .waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Sales Order Details
 | 
					  // Sales Order Details
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Sales Orders' }).click();
 | 
					  await loadTab(page, 'Sales Orders');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await clearTableFilters(page);
 | 
					  await clearTableFilters(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,30 +43,30 @@ test('Sales Orders - Tabs', async ({ page }) => {
 | 
				
			|||||||
    .getByLabel('Order Details')
 | 
					    .getByLabel('Order Details')
 | 
				
			||||||
    .getByText('Selling some stuff')
 | 
					    .getByText('Selling some stuff')
 | 
				
			||||||
    .waitFor();
 | 
					    .waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Line Items' }).click();
 | 
					  await loadTab(page, 'Line Items');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Shipments' }).click();
 | 
					  await loadTab(page, 'Shipments');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Build Orders' }).click();
 | 
					  await loadTab(page, 'Build Orders');
 | 
				
			||||||
  await page.getByText('No records found').first().waitFor();
 | 
					  await page.getByText('No records found').first().waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Attachments' }).click();
 | 
					  await loadTab(page, 'Attachments');
 | 
				
			||||||
  await page.getByText('No attachments found').first().waitFor();
 | 
					  await page.getByText('No attachments found').first().waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Notes' }).click();
 | 
					  await loadTab(page, 'Notes');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Order Details' }).click();
 | 
					  await loadTab(page, 'Order Details');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Return Order Details
 | 
					  // Return Order Details
 | 
				
			||||||
  await page.getByRole('link', { name: 'Customer A' }).click();
 | 
					  await page.getByRole('link', { name: 'Customer A' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Return Orders' }).click();
 | 
					  await loadTab(page, 'Return Orders');
 | 
				
			||||||
  await page.getByRole('cell', { name: 'RMA-' }).click();
 | 
					  await page.getByRole('cell', { name: 'RMA-' }).click();
 | 
				
			||||||
  await page.getByText('RMA-0001', { exact: true }).waitFor();
 | 
					  await page.getByText('RMA-0001', { exact: true }).waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Line Items' }).click();
 | 
					  await loadTab(page, 'Line Items');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Attachments' }).click();
 | 
					  await loadTab(page, 'Attachments');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Notes' }).click();
 | 
					  await loadTab(page, 'Notes');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('Sales Orders - Basic Tests', async ({ page }) => {
 | 
					test('Sales Orders - Basic Tests', async ({ page }) => {
 | 
				
			||||||
  await doQuickLogin(page);
 | 
					  await doQuickLogin(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Sales' }).click();
 | 
					  await page.getByRole('tab', { name: 'Sales' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Sales Orders' }).click();
 | 
					  await loadTab(page, 'Sales Orders');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await clearTableFilters(page);
 | 
					  await clearTableFilters(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -104,12 +105,12 @@ test('Sales Orders - Shipments', async ({ page }) => {
 | 
				
			|||||||
  await doQuickLogin(page);
 | 
					  await doQuickLogin(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Sales' }).click();
 | 
					  await page.getByRole('tab', { name: 'Sales' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Sales Orders' }).click();
 | 
					  await loadTab(page, 'Sales Orders');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await clearTableFilters(page);
 | 
					  await clearTableFilters(page);
 | 
				
			||||||
  // Click through to a particular sales order
 | 
					  // Click through to a particular sales order
 | 
				
			||||||
  await page.getByRole('cell', { name: 'SO0006' }).first().click();
 | 
					  await page.getByRole('cell', { name: 'SO0006' }).first().click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Shipments' }).click();
 | 
					  await loadTab(page, 'Shipments');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Create a new shipment
 | 
					  // Create a new shipment
 | 
				
			||||||
  await page.getByLabel('action-button-add-shipment').click();
 | 
					  await page.getByLabel('action-button-add-shipment').click();
 | 
				
			||||||
@@ -155,14 +156,14 @@ test('Sales Orders - Shipments', async ({ page }) => {
 | 
				
			|||||||
  await page.getByRole('menuitem', { name: 'View Shipment' }).click();
 | 
					  await page.getByRole('menuitem', { name: 'View Shipment' }).click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Click through the various tabs
 | 
					  // Click through the various tabs
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Attachments' }).click();
 | 
					  await loadTab(page, 'Attachments');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Notes' }).click();
 | 
					  await loadTab(page, 'Notes');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Allocated Stock' }).click();
 | 
					  await loadTab(page, 'Allocated Stock');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Ensure assigned items table loads correctly
 | 
					  // Ensure assigned items table loads correctly
 | 
				
			||||||
  await page.getByRole('cell', { name: 'BATCH-001' }).first().waitFor();
 | 
					  await page.getByRole('cell', { name: 'BATCH-001' }).first().waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Shipment Details' }).click();
 | 
					  await loadTab(page, 'Shipment Details');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // The "new" tracking number should be visible
 | 
					  // The "new" tracking number should be visible
 | 
				
			||||||
  await page.getByText(tracking_number).waitFor();
 | 
					  await page.getByText(tracking_number).waitFor();
 | 
				
			||||||
@@ -171,7 +172,7 @@ test('Sales Orders - Shipments', async ({ page }) => {
 | 
				
			|||||||
  await page.getByRole('link', { name: 'SO0006' }).click();
 | 
					  await page.getByRole('link', { name: 'SO0006' }).click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Let's try to allocate some stock
 | 
					  // Let's try to allocate some stock
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Line Items' }).click();
 | 
					  await loadTab(page, 'Line Items');
 | 
				
			||||||
  await page.getByLabel('row-action-menu-1').click();
 | 
					  await page.getByLabel('row-action-menu-1').click();
 | 
				
			||||||
  await page.getByRole('menuitem', { name: 'Allocate stock' }).click();
 | 
					  await page.getByRole('menuitem', { name: 'Allocate stock' }).click();
 | 
				
			||||||
  await page
 | 
					  await page
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import { test } from '../baseFixtures.js';
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  clearTableFilters,
 | 
					  clearTableFilters,
 | 
				
			||||||
  clickButtonIfVisible,
 | 
					  clickButtonIfVisible,
 | 
				
			||||||
 | 
					  loadTab,
 | 
				
			||||||
  navigate,
 | 
					  navigate,
 | 
				
			||||||
  openFilterDrawer,
 | 
					  openFilterDrawer,
 | 
				
			||||||
  setTableChoiceFilter
 | 
					  setTableChoiceFilter
 | 
				
			||||||
@@ -14,28 +15,28 @@ test('Stock - Basic Tests', async ({ page }) => {
 | 
				
			|||||||
  await navigate(page, 'stock/location/index/');
 | 
					  await navigate(page, 'stock/location/index/');
 | 
				
			||||||
  await page.waitForURL('**/platform/stock/location/**');
 | 
					  await page.waitForURL('**/platform/stock/location/**');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Location Details' }).click();
 | 
					  await loadTab(page, 'Location Details');
 | 
				
			||||||
  await page.waitForURL('**/platform/stock/location/index/details');
 | 
					  await page.waitForURL('**/platform/stock/location/index/details');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Items' }).click();
 | 
					  await loadTab(page, 'Stock Items');
 | 
				
			||||||
  await page.getByText('1551ABK').first().click();
 | 
					  await page.getByText('1551ABK').first().click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock', exact: true }).click();
 | 
					  await page.getByRole('tab', { name: 'Stock', exact: true }).click();
 | 
				
			||||||
  await page.waitForURL('**/platform/stock/**');
 | 
					  await page.waitForURL('**/platform/stock/**');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Locations' }).click();
 | 
					  await loadTab(page, 'Stock Locations');
 | 
				
			||||||
  await page.getByRole('cell', { name: 'Electronics Lab' }).first().click();
 | 
					  await page.getByRole('cell', { name: 'Electronics Lab' }).first().click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Default Parts' }).click();
 | 
					  await loadTab(page, 'Default Parts');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Locations' }).click();
 | 
					  await loadTab(page, 'Stock Locations');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Items' }).click();
 | 
					  await loadTab(page, 'Stock Items');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Location Details' }).click();
 | 
					  await loadTab(page, 'Location Details');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await navigate(page, 'stock/item/1194/details');
 | 
					  await navigate(page, 'stock/item/1194/details');
 | 
				
			||||||
  await page.getByText('D.123 | Doohickey').waitFor();
 | 
					  await page.getByText('D.123 | Doohickey').waitFor();
 | 
				
			||||||
  await page.getByText('Batch Code: BX-123-2024-2-7').waitFor();
 | 
					  await page.getByText('Batch Code: BX-123-2024-2-7').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Tracking' }).click();
 | 
					  await loadTab(page, 'Stock Tracking');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Test Data' }).click();
 | 
					  await loadTab(page, 'Test Data');
 | 
				
			||||||
  await page.getByText('395c6d5586e5fb656901d047be27e1f7').waitFor();
 | 
					  await page.getByText('395c6d5586e5fb656901d047be27e1f7').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Installed Items' }).click();
 | 
					  await loadTab(page, 'Installed Items');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('Stock - Location Tree', async ({ page }) => {
 | 
					test('Stock - Location Tree', async ({ page }) => {
 | 
				
			||||||
@@ -43,7 +44,7 @@ test('Stock - Location Tree', async ({ page }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  await navigate(page, 'stock/location/index/');
 | 
					  await navigate(page, 'stock/location/index/');
 | 
				
			||||||
  await page.waitForURL('**/platform/stock/location/**');
 | 
					  await page.waitForURL('**/platform/stock/location/**');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Location Details' }).click();
 | 
					  await loadTab(page, 'Location Details');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByLabel('nav-breadcrumb-action').click();
 | 
					  await page.getByLabel('nav-breadcrumb-action').click();
 | 
				
			||||||
  await page.getByLabel('nav-tree-toggle-1}').click();
 | 
					  await page.getByLabel('nav-tree-toggle-1}').click();
 | 
				
			||||||
@@ -59,7 +60,7 @@ test('Stock - Filters', async ({ page }) => {
 | 
				
			|||||||
  await doQuickLogin(page, 'steven', 'wizardstaff');
 | 
					  await doQuickLogin(page, 'steven', 'wizardstaff');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await navigate(page, 'stock/location/index/');
 | 
					  await navigate(page, 'stock/location/index/');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Items' }).click();
 | 
					  await loadTab(page, 'Stock Items');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await openFilterDrawer(page);
 | 
					  await openFilterDrawer(page);
 | 
				
			||||||
  await clickButtonIfVisible(page, 'Clear Filters');
 | 
					  await clickButtonIfVisible(page, 'Clear Filters');
 | 
				
			||||||
@@ -238,7 +239,7 @@ test('Stock - Tracking', async ({ page }) => {
 | 
				
			|||||||
  await page.getByRole('link', { name: 'Widget Assembly # 2' }).waitFor();
 | 
					  await page.getByRole('link', { name: 'Widget Assembly # 2' }).waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Navigate to the "stock tracking" tab
 | 
					  // Navigate to the "stock tracking" tab
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Tracking' }).click();
 | 
					  await loadTab(page, 'Stock Tracking');
 | 
				
			||||||
  await page.getByText('- - Factory/Office Block/Room').first().waitFor();
 | 
					  await page.getByText('- - Factory/Office Block/Room').first().waitFor();
 | 
				
			||||||
  await page.getByRole('link', { name: 'Widget Assembly' }).waitFor();
 | 
					  await page.getByRole('link', { name: 'Widget Assembly' }).waitFor();
 | 
				
			||||||
  await page.getByRole('cell', { name: 'Installed into assembly' }).waitFor();
 | 
					  await page.getByRole('cell', { name: 'Installed into assembly' }).waitFor();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import test from 'playwright/test';
 | 
					import test from 'playwright/test';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { navigate } from './helpers.js';
 | 
					import { 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';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,19 +29,18 @@ test('Plugins - Panels', async ({ page, request }) => {
 | 
				
			|||||||
  await navigate(page, 'part/69/');
 | 
					  await navigate(page, 'part/69/');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Ensure basic part tab is available
 | 
					  // Ensure basic part tab is available
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Part Details' }).waitFor();
 | 
					  await loadTab(page, 'Part Details');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Allow time for the plugin panels to load (they are loaded asynchronously)
 | 
					  // Allow time for the plugin panels to load (they are loaded asynchronously)
 | 
				
			||||||
  await page.waitForTimeout(1000);
 | 
					  await page.waitForTimeout(1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Check out each of the plugin panels
 | 
					  // Check out each of the plugin panels
 | 
				
			||||||
 | 
					  await loadTab(page, 'Broken Panel');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Broken Panel' }).click();
 | 
					 | 
				
			||||||
  await page.waitForTimeout(500);
 | 
					  await page.waitForTimeout(500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByText('Error occurred while loading plugin content').waitFor();
 | 
					  await page.getByText('Error occurred while loading plugin content').waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Dynamic Panel' }).click();
 | 
					  await loadTab(page, 'Dynamic Panel');
 | 
				
			||||||
  await page.waitForTimeout(500);
 | 
					  await page.waitForTimeout(500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByText('Instance ID: 69');
 | 
					  await page.getByText('Instance ID: 69');
 | 
				
			||||||
@@ -49,7 +48,7 @@ test('Plugins - Panels', async ({ page, request }) => {
 | 
				
			|||||||
    .getByText('This panel has been dynamically rendered by the plugin system')
 | 
					    .getByText('This panel has been dynamically rendered by the plugin system')
 | 
				
			||||||
    .waitFor();
 | 
					    .waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Part Panel', exact: true }).click();
 | 
					  await loadTab(page, 'Part Panel');
 | 
				
			||||||
  await page.waitForTimeout(500);
 | 
					  await page.waitForTimeout(500);
 | 
				
			||||||
  await page.getByText('This content has been rendered by a custom plugin');
 | 
					  await page.getByText('This content has been rendered by a custom plugin');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { expect, test } from './baseFixtures.js';
 | 
					import { expect, test } from './baseFixtures.js';
 | 
				
			||||||
import { navigate } from './helpers.js';
 | 
					import { loadTab, navigate } from './helpers.js';
 | 
				
			||||||
import { doQuickLogin } from './login.js';
 | 
					import { doQuickLogin } from './login.js';
 | 
				
			||||||
import { setPluginState } from './settings.js';
 | 
					import { setPluginState } from './settings.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,7 +14,7 @@ test('Label Printing', async ({ page }) => {
 | 
				
			|||||||
  await navigate(page, 'stock/location/index/');
 | 
					  await navigate(page, 'stock/location/index/');
 | 
				
			||||||
  await page.waitForURL('**/platform/stock/location/**');
 | 
					  await page.waitForURL('**/platform/stock/location/**');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Stock Items' }).click();
 | 
					  await loadTab(page, 'Stock Items');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Select some labels
 | 
					  // Select some labels
 | 
				
			||||||
  await page.getByLabel('Select record 1', { exact: true }).click();
 | 
					  await page.getByLabel('Select record 1', { exact: true }).click();
 | 
				
			||||||
@@ -60,7 +60,7 @@ test('Report Printing', async ({ page }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Navigate to a specific PurchaseOrder
 | 
					  // Navigate to a specific PurchaseOrder
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
					  await page.getByRole('tab', { name: 'Purchasing' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchase Orders' }).click();
 | 
					  await loadTab(page, 'Purchase Orders');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('cell', { name: 'PO0009' }).click();
 | 
					  await page.getByRole('cell', { name: 'PO0009' }).click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -98,7 +98,7 @@ test('Report Editing', async ({ page, request }) => {
 | 
				
			|||||||
  // Navigate to the admin center
 | 
					  // Navigate to the admin center
 | 
				
			||||||
  await page.getByRole('button', { name: 'admin' }).click();
 | 
					  await page.getByRole('button', { name: 'admin' }).click();
 | 
				
			||||||
  await page.getByRole('menuitem', { name: 'Admin Center' }).click();
 | 
					  await page.getByRole('menuitem', { name: 'Admin Center' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Label Templates' }).click();
 | 
					  await loadTab(page, 'Label Templates');
 | 
				
			||||||
  await page
 | 
					  await page
 | 
				
			||||||
    .getByRole('cell', { name: 'InvenTree Stock Item Label (' })
 | 
					    .getByRole('cell', { name: 'InvenTree Stock Item Label (' })
 | 
				
			||||||
    .click();
 | 
					    .click();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import { expect, test } from './baseFixtures.js';
 | 
					import { expect, test } from './baseFixtures.js';
 | 
				
			||||||
import { apiUrl } from './defaults.js';
 | 
					import { apiUrl } from './defaults.js';
 | 
				
			||||||
import { getRowFromCell, navigate } from './helpers.js';
 | 
					import { getRowFromCell, loadTab, navigate } from './helpers.js';
 | 
				
			||||||
import { doQuickLogin } from './login.js';
 | 
					import { doQuickLogin } from './login.js';
 | 
				
			||||||
import { setSettingState } from './settings.js';
 | 
					import { setSettingState } from './settings.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -49,47 +49,47 @@ test('Settings - Admin', async ({ page }) => {
 | 
				
			|||||||
  // User settings
 | 
					  // User settings
 | 
				
			||||||
  await page.getByRole('button', { name: 'admin' }).click();
 | 
					  await page.getByRole('button', { name: 'admin' }).click();
 | 
				
			||||||
  await page.getByRole('menuitem', { name: 'Account settings' }).click();
 | 
					  await page.getByRole('menuitem', { name: 'Account settings' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Security' }).click();
 | 
					  await loadTab(page, 'Security');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Display Options' }).click();
 | 
					  await loadTab(page, 'Display Options');
 | 
				
			||||||
  await page.getByText('Date Format').waitFor();
 | 
					  await page.getByText('Date Format').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Search' }).click();
 | 
					  await loadTab(page, 'Search');
 | 
				
			||||||
  await page.getByText('Regex Search').waitFor();
 | 
					  await page.getByText('Regex Search').waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Notifications' }).click();
 | 
					  await loadTab(page, 'Notifications');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Reporting' }).click();
 | 
					  await loadTab(page, 'Reporting');
 | 
				
			||||||
  await page.getByText('Inline report display').waitFor();
 | 
					  await page.getByText('Inline report display').waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // System Settings
 | 
					  // System Settings
 | 
				
			||||||
  await page.locator('label').filter({ hasText: 'System Settings' }).click();
 | 
					  await page.locator('label').filter({ hasText: 'System Settings' }).click();
 | 
				
			||||||
  await page.getByText('Base URL', { exact: true }).waitFor();
 | 
					  await page.getByText('Base URL', { exact: true }).waitFor();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Login' }).click();
 | 
					  await loadTab(page, 'Login');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Barcodes' }).click();
 | 
					  await loadTab(page, 'Barcodes');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Notifications' }).click();
 | 
					  await loadTab(page, 'Notifications');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Pricing' }).click();
 | 
					  await loadTab(page, 'Pricing');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Labels' }).click();
 | 
					  await loadTab(page, 'Labels');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Reporting' }).click();
 | 
					  await loadTab(page, 'Reporting');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Build Orders' }).click();
 | 
					  await loadTab(page, 'Build Orders');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Purchase Orders' }).click();
 | 
					  await loadTab(page, 'Purchase Orders');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Sales Orders' }).click();
 | 
					  await loadTab(page, 'Sales Orders');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Return Orders' }).click();
 | 
					  await loadTab(page, 'Return Orders');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Admin Center
 | 
					  // Admin Center
 | 
				
			||||||
  await page.getByRole('button', { name: 'admin' }).click();
 | 
					  await page.getByRole('button', { name: 'admin' }).click();
 | 
				
			||||||
  await page.getByRole('menuitem', { name: 'Admin Center' }).click();
 | 
					  await page.getByRole('menuitem', { name: 'Admin Center' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Background Tasks' }).click();
 | 
					  await loadTab(page, 'Background Tasks');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Error Reports' }).click();
 | 
					  await loadTab(page, 'Error Reports');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Currencies' }).click();
 | 
					  await loadTab(page, 'Currencies');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Project Codes' }).click();
 | 
					  await loadTab(page, 'Project Codes');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Custom Units' }).click();
 | 
					  await loadTab(page, 'Custom Units');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Part Parameters' }).click();
 | 
					  await loadTab(page, 'Part Parameters');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Category Parameters' }).click();
 | 
					  await loadTab(page, 'Category Parameters');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Label Templates' }).click();
 | 
					  await loadTab(page, 'Label Templates');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Report Templates' }).click();
 | 
					  await loadTab(page, 'Report Templates');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Plugins' }).click();
 | 
					  await loadTab(page, 'Plugins');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Adjust some "location type" items
 | 
					  // Adjust some "location type" items
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Location Types' }).click();
 | 
					  await loadTab(page, 'Location Types');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Edit first item ('Room')
 | 
					  // Edit first item ('Room')
 | 
				
			||||||
  const roomCell = await page.getByRole('cell', { name: 'Room', exact: true });
 | 
					  const roomCell = await page.getByRole('cell', { name: 'Room', exact: true });
 | 
				
			||||||
@@ -166,7 +166,7 @@ test('Settings - Admin - Barcode History', async ({ page, request }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('button', { name: 'admin' }).click();
 | 
					  await page.getByRole('button', { name: 'admin' }).click();
 | 
				
			||||||
  await page.getByRole('menuitem', { name: 'Admin Center' }).click();
 | 
					  await page.getByRole('menuitem', { name: 'Admin Center' }).click();
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Barcode Scans' }).click();
 | 
					  await loadTab(page, 'Barcode Scans');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.waitForTimeout(500);
 | 
					  await page.waitForTimeout(500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -193,8 +193,8 @@ test('Settings - Admin - Unauthorized', async ({ page }) => {
 | 
				
			|||||||
  await navigate(page, 'settings/user/');
 | 
					  await navigate(page, 'settings/user/');
 | 
				
			||||||
  await page.waitForURL('**/settings/user/**');
 | 
					  await page.waitForURL('**/settings/user/**');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Display Options' }).click();
 | 
					  await loadTab(page, 'Display Options');
 | 
				
			||||||
  await page.getByRole('tab', { name: 'Account' }).click();
 | 
					  await loadTab(page, 'Account');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Try to access global settings page
 | 
					  // Try to access global settings page
 | 
				
			||||||
  await navigate(page, 'settings/system/');
 | 
					  await navigate(page, 'settings/system/');
 | 
				
			||||||
@@ -205,3 +205,25 @@ test('Settings - Admin - Unauthorized', async ({ page }) => {
 | 
				
			|||||||
    .getByRole('button', { name: 'Return to the index page' })
 | 
					    .getByRole('button', { name: 'Return to the index page' })
 | 
				
			||||||
    .waitFor();
 | 
					    .waitFor();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Test for user auth configuration
 | 
				
			||||||
 | 
					test('Settings - Auth - Email', async ({ page }) => {
 | 
				
			||||||
 | 
					  await doQuickLogin(page, 'allaccess', 'nolimits');
 | 
				
			||||||
 | 
					  await navigate(page, 'settings/user/');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await loadTab(page, 'Security');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await page.getByText('Currently no email addresses are registered').waitFor();
 | 
				
			||||||
 | 
					  await page.getByLabel('email-address-input').fill('test-email@domain.org');
 | 
				
			||||||
 | 
					  await page.getByLabel('email-address-submit').click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await page.getByText('Unverified', { exact: true }).waitFor();
 | 
				
			||||||
 | 
					  await page.getByLabel('test-email@domain.').click();
 | 
				
			||||||
 | 
					  await page.getByRole('button', { name: 'Make Primary' }).click();
 | 
				
			||||||
 | 
					  await page.getByText('Primary', { exact: true }).waitFor();
 | 
				
			||||||
 | 
					  await page.getByRole('button', { name: 'Remove' }).click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await page.getByText('Currently no email addresses are registered').waitFor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await page.waitForTimeout(2500);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user