From 1ab772e66d95ea8e2688fe59eaff7a9586fab89d Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 21 Jul 2023 00:54:57 +0200 Subject: [PATCH] P UI: Basic UI & Auth setup (#5207) * update deps * remove @mantine/rte * add icons again * Create dependency-review.yml * Create scan.yml * Create sonar-project.properties * add option to use sections and refactro * translate error messages * remove unneeded vars * move function code * move data inside * add global section * add plugin section * use translated section titles * add translation strings * rename scan action * add user settings * use ordered data * fix settings url * use debounced value for strings (not choices!) * rename contex to context * move i18n provider up * move theme options into seperate context/ component * renmae statrtup vars * move translations out * reactivate sentry * move i18n provider to seperate context * move langauge state completly out of App * use theme out * move theme context * move LanguageContext * move function into state * make sentry optional for now * add key to accordion * init langauge context on top * remove unneeded css files * move errorpage to tsx * add translation for error page * Add error to title * add typecast for error * move type definition out * remove todo -> type was already added * upgrade deps * add bootstrap * remove @mantine/core * readd core * switch to bootstrap * simplify import * Add SPA views for react #2789 * split up frontend urls * Add settings for frontend url loading * add new UI scaffold * remove tracking insert * add platform app * ensure static indexes work too * add lingui * add lingui config * add mgmt tasks * add base locales * settings for frontend dev * fix typo * update deps * add pre-commit * add eslint * add testing scaffold * fix paths * remove error - tests trip correctly * merge workflow * cleanup samples * use name inline with other tests * Add real worl frontend tests * setup env * tun migrations first * optimize setup time * setup demo dataset * optimize run setup * add test for class ui * rename * fix typo * and another typo * do install * run migrations first * fix name * cleanup * use other credentials * use other credentials * fix qc * move envs to qc * remove create_site * reduce testing env * fix test * fix test call * allaccess user * add ui plattform check * add better check * remove unneeded env * enable debug * reduce wait time * also build frontend on static * add sekeleton * fix various issues * add locales * clean output before building * cleanup dir * remove bootstrap * clean up deps * fix settings panel * remove assets * move logo * split out router * split up chunks * fix zustand import syntax * bundl * update pre-render * use vendor splitting * maximes space usage * enlarge breakpoints * remove wired color changes * cleanup tabs * fix error * update auth functions * default to mail login * add placeholder marking * Add text to placeholder * readd codespell * add another test * add sort plugin * add sort plugin * sort imports * fix order * Add mega menu * run pre-commit fixes * add node min version * Docker container (#129) * Fix allocation check for completing build order (#5199) - Allocation check only applies to untracked line items * docker dev Install required node packages to docker development image * add import order settings * cleanup settings * cleanup dashboard * clanup part tab * refactor header to only use 1 line * cleanup reqs for py3.9 * remove compiled UI * revert reqs change * cleanup tasks * cleanout built ui * remove default user * cleanup package.json * fix doctip * remove sentry * optimize loading * reset versions * clean * factor out menu items * refactor Navtabs * refactor HoverMenu * remove part * remove prettier * remove default arg from build * remove eslint * Merge branch 'plattform' of https://github.com/matmair/InvenTree into platform-ui_base * optimize svg * add build step for plattform UI * fix install command * fix test * remove extra test * set default host if none is set * set nicer names * fix tests * fix logged-in test * update translations * ensure more path matches * make loading of serverrefs dynamic * use default radius * fix issues / code smells * clean code smell * fix password reset * fix error messages * detect small screens * use loader meachanism for views * refactor structure * move auth functions out * use text * refactor defaults * Add email login FR] Add email link based logins Closes #3531 * fix name error * fix reqs * fix backend for magic login * fix frontend * remove unused route * remove now unneeded test section * cleanup code * add navigation header * fix logo component for nesting * factor out menu * refactor style * clean code * Translate items and use unique ids * use alpine commands * increase margins to remove drawer scroller * only render plugins if they are defined * remove sample content * fix assertation * open on hover * refactor * merge fix * cleanup navigation drawer * change dependencies for UI testing * add highlight filter * Add correct menu items * move design component out * move pre-commit out * move deps again * move js styles in * revert CI changes * remove unneeded exclution * changed placeholder user * refactored EditButton to component * refactored app loading to useEffect * moved color lookup into global scope * reafactored UserPanel render block * marked placeholder pages in doc links * made doc tooltip optional for MenuLink * changed MenuLinkItem names * fixes missing Link item in MenuLink * fixed merge error --------- Co-authored-by: Oliver --- .pre-commit-config.yaml | 3 +- InvenTree/InvenTree/urls.py | 3 +- InvenTree/web/urls.py | 1 + src/frontend/.linguirc | 3 - src/frontend/index.html | 1 - src/frontend/package.json | 21 +- src/frontend/src/App.tsx | 40 +- .../components/forms/AuthenticationForm.tsx | 154 ++++ .../src/components/forms/HostOptionsForm.tsx | 95 ++ .../src/components/items/ColorToggle.tsx | 27 + .../src/components/items/DocTooltip.tsx | 95 ++ .../components/items/DocumentationLinks.tsx | 41 + .../src/components/items/EditButton.tsx | 18 + .../src/components/items/ErrorItem.tsx | 15 + .../src/components/items/InvenTreeLogo.tsx | 18 + .../src/components/items/MenuLinks.tsx | 73 ++ .../src/components/items/Placeholder.tsx | 17 + .../src/components/items/ScanButton.tsx | 20 + .../src/components/items/StylishText.tsx | 12 + .../src/components/items/inventree.svg | 1 + .../src/components/modals/QrCodeModal.tsx | 201 +++++ src/frontend/src/components/nav/Footer.tsx | 29 + src/frontend/src/components/nav/Header.tsx | 64 ++ src/frontend/src/components/nav/Layout.tsx | 34 + src/frontend/src/components/nav/MainMenu.tsx | 88 ++ .../src/components/nav/NavHoverMenu.tsx | 101 +++ .../src/components/nav/NavigationDrawer.tsx | 85 ++ src/frontend/src/contexts/BaseContext.tsx | 10 + src/frontend/src/contexts/LanguageContext.tsx | 29 + src/frontend/src/contexts/ThemeContext.tsx | 70 ++ src/frontend/src/defaults/defaultHostList.tsx | 19 + src/frontend/src/defaults/defaults.tsx | 25 + src/frontend/src/defaults/links.tsx | 83 ++ src/frontend/src/defaults/menuItems.tsx | 19 + src/frontend/src/functions/auth.tsx | 112 +++ src/frontend/src/functions/loading.tsx | 19 + src/frontend/src/globalStyle.tsx | 184 ++++ src/frontend/src/locales/de/messages.po | 791 +++++++++++++++++ src/frontend/src/locales/de/messages.ts | 4 +- src/frontend/src/locales/en/messages.po | 791 +++++++++++++++++ src/frontend/src/locales/en/messages.ts | 4 +- src/frontend/src/locales/hu/messages.po | 835 ++++++++++++++++++ src/frontend/src/locales/hu/messages.ts | 4 +- .../src/locales/pseudo-LOCALE/messages.po | 835 ++++++++++++++++++ .../src/locales/pseudo-LOCALE/messages.ts | 4 +- src/frontend/src/pages/Auth/Logged-In.tsx | 22 + src/frontend/src/pages/Auth/Login.tsx | 130 +++ src/frontend/src/pages/Auth/Reset.tsx | 48 + src/frontend/src/pages/Auth/Set-Password.tsx | 118 +++ src/frontend/src/pages/ErrorPage.tsx | 36 + src/frontend/src/pages/Index/Home.tsx | 18 + .../src/pages/Index/Profile/Profile.tsx | 33 + .../src/pages/Index/Profile/UserPanel.tsx | 124 +++ .../src/pages/Index/Profile/UserTheme.tsx | 154 ++++ src/frontend/src/pages/NotFound.tsx | 35 + src/frontend/src/router.tsx | 72 ++ src/frontend/src/states/ApiState.tsx | 61 ++ src/frontend/src/states/LocalState.tsx | 48 + src/frontend/src/states/SessionState.tsx | 25 + src/frontend/src/states/states.tsx | 39 + src/frontend/src/views/DesktopAppView.tsx | 42 + src/frontend/src/views/MobileAppView.tsx | 30 + src/frontend/tests/classic.spec.ts | 1 - src/frontend/tests/ui_plattform.spec.ts | 17 +- src/frontend/vite.config.ts | 5 +- src/frontend/yarn.lock | 739 +++++++++++++++- 66 files changed, 6817 insertions(+), 78 deletions(-) create mode 100644 src/frontend/src/components/forms/AuthenticationForm.tsx create mode 100644 src/frontend/src/components/forms/HostOptionsForm.tsx create mode 100644 src/frontend/src/components/items/ColorToggle.tsx create mode 100644 src/frontend/src/components/items/DocTooltip.tsx create mode 100644 src/frontend/src/components/items/DocumentationLinks.tsx create mode 100644 src/frontend/src/components/items/EditButton.tsx create mode 100644 src/frontend/src/components/items/ErrorItem.tsx create mode 100644 src/frontend/src/components/items/InvenTreeLogo.tsx create mode 100644 src/frontend/src/components/items/MenuLinks.tsx create mode 100644 src/frontend/src/components/items/Placeholder.tsx create mode 100644 src/frontend/src/components/items/ScanButton.tsx create mode 100644 src/frontend/src/components/items/StylishText.tsx create mode 100644 src/frontend/src/components/items/inventree.svg create mode 100644 src/frontend/src/components/modals/QrCodeModal.tsx create mode 100644 src/frontend/src/components/nav/Footer.tsx create mode 100644 src/frontend/src/components/nav/Header.tsx create mode 100644 src/frontend/src/components/nav/Layout.tsx create mode 100644 src/frontend/src/components/nav/MainMenu.tsx create mode 100644 src/frontend/src/components/nav/NavHoverMenu.tsx create mode 100644 src/frontend/src/components/nav/NavigationDrawer.tsx create mode 100644 src/frontend/src/contexts/BaseContext.tsx create mode 100644 src/frontend/src/contexts/LanguageContext.tsx create mode 100644 src/frontend/src/contexts/ThemeContext.tsx create mode 100644 src/frontend/src/defaults/defaultHostList.tsx create mode 100644 src/frontend/src/defaults/defaults.tsx create mode 100644 src/frontend/src/defaults/links.tsx create mode 100644 src/frontend/src/defaults/menuItems.tsx create mode 100644 src/frontend/src/functions/auth.tsx create mode 100644 src/frontend/src/functions/loading.tsx create mode 100644 src/frontend/src/globalStyle.tsx create mode 100644 src/frontend/src/pages/Auth/Logged-In.tsx create mode 100644 src/frontend/src/pages/Auth/Login.tsx create mode 100644 src/frontend/src/pages/Auth/Reset.tsx create mode 100644 src/frontend/src/pages/Auth/Set-Password.tsx create mode 100644 src/frontend/src/pages/ErrorPage.tsx create mode 100644 src/frontend/src/pages/Index/Home.tsx create mode 100644 src/frontend/src/pages/Index/Profile/Profile.tsx create mode 100644 src/frontend/src/pages/Index/Profile/UserPanel.tsx create mode 100644 src/frontend/src/pages/Index/Profile/UserTheme.tsx create mode 100644 src/frontend/src/pages/NotFound.tsx create mode 100644 src/frontend/src/router.tsx create mode 100644 src/frontend/src/states/ApiState.tsx create mode 100644 src/frontend/src/states/LocalState.tsx create mode 100644 src/frontend/src/states/SessionState.tsx create mode 100644 src/frontend/src/states/states.tsx create mode 100644 src/frontend/src/views/DesktopAppView.tsx create mode 100644 src/frontend/src/views/MobileAppView.tsx diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a136d41efa..41058f45ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,7 +52,8 @@ repos: (?x)^( docs/docs/stylesheets/.*| docs/docs/javascripts/.*| - docs/docs/webfonts/.* + docs/docs/webfonts/.* | + src/frontend/src/locales/.* | )$ - repo: https://github.com/pre-commit/mirrors-prettier rev: "v3.0.0-alpha.9-for-vscode" diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 662f68208a..95f04a0206 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -33,6 +33,7 @@ from report.api import report_api_urls from stock.api import stock_api_urls from stock.urls import stock_urls from users.api import user_urls +from web.urls import spa_view from web.urls import urlpatterns as platform_urls from .api import APISearchView, InfoView, NotFoundView @@ -210,7 +211,7 @@ classic_frontendpatterns = [ new_frontendpatterns = [ # Platform urls re_path(r'^platform/', include(platform_urls)), - + re_path(r'^platform', spa_view, name='platform'), ] # Load patterns for frontend according to settings diff --git a/InvenTree/web/urls.py b/InvenTree/web/urls.py index b025d1028a..7124e165cb 100644 --- a/InvenTree/web/urls.py +++ b/InvenTree/web/urls.py @@ -22,5 +22,6 @@ spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name="web/index.html urlpatterns = [ path("assets/", RedirectAssetView.as_view()), re_path(r"^(?P.*)/$", spa_view), + path("set-password?uid=&token=", spa_view, name="password_reset_confirm"), path("", spa_view), ] diff --git a/src/frontend/.linguirc b/src/frontend/.linguirc index 9235d3bb76..e1f99865c2 100644 --- a/src/frontend/.linguirc +++ b/src/frontend/.linguirc @@ -12,8 +12,5 @@ "fallbackLocales": { "default": "en", "pseudo-LOCALE": "en" - }, - "extractBabelOptions": { - "presets": ["@babel/preset-typescript"] } } diff --git a/src/frontend/index.html b/src/frontend/index.html index 350af83ef7..3e7fd76816 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -3,7 +3,6 @@ - InvenTree diff --git a/src/frontend/package.json b/src/frontend/package.json index 1c3e16bfae..f34c90ca37 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -11,10 +11,29 @@ "compile": "lingui compile --typescript" }, "dependencies": { + "@emotion/react": "^11.10.5", + "@fortawesome/fontawesome-svg-core": "^6.2.1", + "@fortawesome/free-regular-svg-icons": "^6.2.1", + "@fortawesome/free-solid-svg-icons": "^6.2.1", + "@fortawesome/react-fontawesome": "^0.2.0", "@lingui/core": "^4.2.1", "@lingui/react": "^4.2.1", + "@mantine/core": "^6.0.15", + "@mantine/dates": "^6.0.15", + "@mantine/dropzone": "^6.0.15", + "@mantine/form": "^6.0.15", + "@mantine/hooks": "^6.0.15", + "@mantine/modals": "^6.0.15", + "@mantine/notifications": "^6.0.15", + "@tabler/icons-react": "^2.23.0", + "@tanstack/react-query": "^4.16.1", + "axios": "^1.1.3", + "dayjs": "^1.11.6", + "html5-qrcode": "^2.3.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.4.3", + "zustand": "^4.2.0" }, "devDependencies": { "@babel/core": "^7.20.5", diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index d757519679..2f5b895ef2 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -1,10 +1,38 @@ +import { useViewportSize } from '@mantine/hooks'; +import { QueryClient } from '@tanstack/react-query'; +import axios from 'axios'; +import { lazy } from 'react'; + +import { Loadable } from './functions/loading'; +import { useLocalState } from './states/LocalState'; +import { useSessionState } from './states/SessionState'; + +// API +export const api = axios.create({}); +export function setApiDefaults() { + const host = useLocalState.getState().host; + const token = useSessionState.getState().token; + + api.defaults.baseURL = host; + api.defaults.headers.common['Authorization'] = `Token ${token}`; +} +export const queryClient = new QueryClient(); + +function checkMobile() { + const { height, width } = useViewportSize(); + if (width < 425 || height < 425) return true; + return false; +} + // Main App export default function App() { + // Check if mobile + if (checkMobile()) { + const MobileAppView = Loadable(lazy(() => import('./views/MobileAppView'))); + return ; + } + // Main App component - return ( - <> -

Welcome to the new frontend!

-

This is a placeholder site

- - ); + const DesktopAppView = Loadable(lazy(() => import('./views/DesktopAppView'))); + return ; } diff --git a/src/frontend/src/components/forms/AuthenticationForm.tsx b/src/frontend/src/components/forms/AuthenticationForm.tsx new file mode 100644 index 0000000000..ed652fd5d1 --- /dev/null +++ b/src/frontend/src/components/forms/AuthenticationForm.tsx @@ -0,0 +1,154 @@ +import { Trans, t } from '@lingui/macro'; +import { + Anchor, + Button, + Group, + Paper, + PasswordInput, + Stack, + Text, + TextInput +} from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useDisclosure } from '@mantine/hooks'; +import { notifications } from '@mantine/notifications'; +import { IconCheck } from '@tabler/icons-react'; +import { useNavigate } from 'react-router-dom'; + +import { doClassicLogin, doSimpleLogin } from '../../functions/auth'; +import { EditButton } from '../items/EditButton'; + +export function AuthenticationForm({ + hostname, + editing, + setEditing, + selectElement +}: { + hostname: string; + editing: boolean; + setEditing: (value?: React.SetStateAction | undefined) => void; + selectElement: JSX.Element; +}) { + const classicForm = useForm({ + initialValues: { username: '', password: '' } + }); + const simpleForm = useForm({ initialValues: { email: '' } }); + const [classicLoginMode, setMode] = useDisclosure(true); + const navigate = useNavigate(); + + function handleLogin() { + if (classicLoginMode === true) { + doClassicLogin( + classicForm.values.username, + classicForm.values.password + ).then((ret) => { + if (ret === false) { + notifications.show({ + title: t`Login failed`, + message: t`Check your your input and try again.`, + color: 'red' + }); + } else { + notifications.show({ + title: t`Login successfull`, + message: t`Welcome back!`, + color: 'green', + icon: + }); + navigate('/home'); + } + }); + } else { + doSimpleLogin(simpleForm.values.email).then((ret) => { + if (ret?.status === 'ok') { + notifications.show({ + title: t`Mail delivery successfull`, + message: t`Check your inbox for the login link. If you have an account, you will receive a login link. Check in spam too.`, + color: 'green', + icon: , + autoClose: false + }); + } else { + notifications.show({ + title: t`Input error`, + message: t`Check your your input and try again.`, + color: 'red' + }); + } + }); + } + } + + return ( + + + + {!editing ? hostname : selectElement} + + + +
{})}> + {classicLoginMode ? ( + + + + + navigate('/reset-password')} + > + Reset password + + + + ) : ( + + + + )} + + + setMode.toggle()} + > + {classicLoginMode ? ( + Send me an email + ) : ( + I will use username and password + )} + + + +
+
+ ); +} diff --git a/src/frontend/src/components/forms/HostOptionsForm.tsx b/src/frontend/src/components/forms/HostOptionsForm.tsx new file mode 100644 index 0000000000..c8ee15bc7a --- /dev/null +++ b/src/frontend/src/components/forms/HostOptionsForm.tsx @@ -0,0 +1,95 @@ +import { Trans, t } from '@lingui/macro'; +import { + ActionIcon, + Box, + Button, + Group, + Space, + Text, + TextInput +} from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { randomId } from '@mantine/hooks'; +import { IconSquarePlus, IconTrash } from '@tabler/icons-react'; + +import { HostList } from '../../states/states'; + +export function HostOptionsForm({ + data, + saveOptions +}: { + data: HostList; + saveOptions: (newData: HostList) => void; +}) { + const form = useForm({ initialValues: data }); + function deleteItem(key: string) { + const newData = form.values; + delete newData[key]; + form.setValues(newData); + } + + const fields = Object.entries(form.values).map(([key]) => ( + + {form.values[key] !== undefined && ( + <> + + + { + deleteItem(key); + }} + > + + + + )} + + )); + + return ( +
+ + {fields.length > 0 ? ( + + + Host + + + Name + + + ) : ( + + No one here... + + )} + {fields} + + + + + + +
+ ); +} diff --git a/src/frontend/src/components/items/ColorToggle.tsx b/src/frontend/src/components/items/ColorToggle.tsx new file mode 100644 index 0000000000..6d998bfd72 --- /dev/null +++ b/src/frontend/src/components/items/ColorToggle.tsx @@ -0,0 +1,27 @@ +import { ActionIcon, Group, useMantineColorScheme } from '@mantine/core'; +import { IconMoonStars, IconSun } from '@tabler/icons-react'; + +export function ColorToggle() { + const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + + return ( + + toggleColorScheme()} + size="lg" + sx={(theme) => ({ + backgroundColor: + theme.colorScheme === 'dark' + ? theme.colors.dark[6] + : theme.colors.gray[0], + color: + theme.colorScheme === 'dark' + ? theme.colors.yellow[4] + : theme.colors.blue[6] + })} + > + {colorScheme === 'dark' ? : } + + + ); +} diff --git a/src/frontend/src/components/items/DocTooltip.tsx b/src/frontend/src/components/items/DocTooltip.tsx new file mode 100644 index 0000000000..c09e370dc9 --- /dev/null +++ b/src/frontend/src/components/items/DocTooltip.tsx @@ -0,0 +1,95 @@ +import { Trans } from '@lingui/macro'; +import { Anchor, Container, HoverCard, ScrollArea, Text } from '@mantine/core'; +import { useEffect, useRef, useState } from 'react'; + +import { InvenTreeStyle } from '../../globalStyle'; + +export function DocTooltip({ + children, + text, + detail, + link, + docchildren +}: { + children: React.ReactNode; + text: string | JSX.Element; + detail?: string | JSX.Element; + link?: string; + docchildren?: React.ReactNode; +}) { + const { classes } = InvenTreeStyle(); + + return ( + + +
{children}
+
+ + + +
+ ); +} + +function ConstBody({ + text, + detail, + docchildren, + link +}: { + text: string | JSX.Element; + detail?: string | JSX.Element; + docchildren?: React.ReactNode; + link?: string; +}) { + const [height, setHeight] = useState(0); + const ref = useRef(null); + + // dynamically set height of scroll area based on content to remove unnecessary scroll bar + useEffect(() => { + if (ref.current == null) return; + + let height = ref.current['clientHeight']; + if (height > 250) { + setHeight(250); + } else { + setHeight(height + 1); + } + }); + + return ( + + {text} + {(detail || docchildren) && ( + +
+ {detail && ( + + {detail} + + )} + {docchildren} +
+
+ )} + {link && ( + + + Read More + + + )} +
+ ); +} diff --git a/src/frontend/src/components/items/DocumentationLinks.tsx b/src/frontend/src/components/items/DocumentationLinks.tsx new file mode 100644 index 0000000000..6d8b18a5b7 --- /dev/null +++ b/src/frontend/src/components/items/DocumentationLinks.tsx @@ -0,0 +1,41 @@ +import { Anchor, Group, SimpleGrid, Text } from '@mantine/core'; + +import { DocTooltip } from './DocTooltip'; +import { PlaceholderPill } from './Placeholder'; + +export interface DocumentationLinkItem { + id: string; + title: string | JSX.Element; + description: string | JSX.Element; + link: string; + placeholder?: boolean; +} + +export function DocumentationLinks({ + links +}: { + links: DocumentationLinkItem[]; +}) { + return ( + + {links.map((link) => ( + + + {link.placeholder ? ( + + + {link.title} + + + + ) : ( + + {link.title} + + )} + + + ))} + + ); +} diff --git a/src/frontend/src/components/items/EditButton.tsx b/src/frontend/src/components/items/EditButton.tsx new file mode 100644 index 0000000000..6d68cf62b4 --- /dev/null +++ b/src/frontend/src/components/items/EditButton.tsx @@ -0,0 +1,18 @@ +import { ActionIcon } from '@mantine/core'; +import { IconDeviceFloppy, IconEdit } from '@tabler/icons-react'; + +export function EditButton({ + setEditing, + editing, + disabled +}: { + setEditing: (value?: React.SetStateAction | undefined) => void; + editing: boolean; + disabled?: boolean; +}) { + return ( + setEditing()} disabled={disabled}> + {editing ? : } + + ); +} diff --git a/src/frontend/src/components/items/ErrorItem.tsx b/src/frontend/src/components/items/ErrorItem.tsx new file mode 100644 index 0000000000..fb82b1b342 --- /dev/null +++ b/src/frontend/src/components/items/ErrorItem.tsx @@ -0,0 +1,15 @@ +import { Trans } from '@lingui/macro'; + +export function ErrorItem({ id, error }: { id: string; error?: any }) { + const error_message = error?.message || error?.toString() || ( + Unknown error + ); + return ( + <> +

+ An error occurred: +

+ {error_message} + + ); +} diff --git a/src/frontend/src/components/items/InvenTreeLogo.tsx b/src/frontend/src/components/items/InvenTreeLogo.tsx new file mode 100644 index 0000000000..29862818a3 --- /dev/null +++ b/src/frontend/src/components/items/InvenTreeLogo.tsx @@ -0,0 +1,18 @@ +import { t } from '@lingui/macro'; +import { ActionIcon } from '@mantine/core'; +import { forwardRef } from 'react'; +import { NavLink } from 'react-router-dom'; + +import InvenTreeIcon from './inventree.svg'; + +export const InvenTreeLogo = forwardRef((props, ref) => { + return ( +
+ + + {t`InvenTree + + +
+ ); +}); diff --git a/src/frontend/src/components/items/MenuLinks.tsx b/src/frontend/src/components/items/MenuLinks.tsx new file mode 100644 index 0000000000..b6341f40ba --- /dev/null +++ b/src/frontend/src/components/items/MenuLinks.tsx @@ -0,0 +1,73 @@ +import { Anchor, SimpleGrid, Text, UnstyledButton } from '@mantine/core'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { InvenTreeStyle } from '../../globalStyle'; +import { DocTooltip } from './DocTooltip'; + +export interface MenuLinkItem { + id: string; + text: string | JSX.Element; + link: string; + highlight?: boolean; + doctext?: string | JSX.Element; + docdetail?: string | JSX.Element; + doclink?: string; + docchildren?: React.ReactNode; +} + +function ConditionalDocTooltip({ + item, + children +}: { + item: MenuLinkItem; + children: React.ReactNode; +}) { + if (item.doctext !== undefined) { + return ( + + {children} + + ); + } + return <>{children}; +} + +export function MenuLinks({ + links, + highlighted +}: { + links: MenuLinkItem[]; + highlighted?: boolean; +}) { + const { classes } = InvenTreeStyle(); + highlighted = highlighted || false; + + const filteredLinks = links.filter( + (item) => !highlighted || item.highlight === true + ); + return ( + + {filteredLinks.map((item) => ( + + + + {item.text} + + + + ))} + + ); +} diff --git a/src/frontend/src/components/items/Placeholder.tsx b/src/frontend/src/components/items/Placeholder.tsx new file mode 100644 index 0000000000..0a6d47cba4 --- /dev/null +++ b/src/frontend/src/components/items/Placeholder.tsx @@ -0,0 +1,17 @@ +import { Trans, t } from '@lingui/macro'; +import { Badge, Tooltip } from '@mantine/core'; + +export function PlaceholderPill() { + return ( + + + PLH + + + ); +} diff --git a/src/frontend/src/components/items/ScanButton.tsx b/src/frontend/src/components/items/ScanButton.tsx new file mode 100644 index 0000000000..65c30caef6 --- /dev/null +++ b/src/frontend/src/components/items/ScanButton.tsx @@ -0,0 +1,20 @@ +import { t } from '@lingui/macro'; +import { ActionIcon } from '@mantine/core'; +import { openContextModal } from '@mantine/modals'; +import { IconQrcode } from '@tabler/icons-react'; + +export function ScanButton() { + return ( + + openContextModal({ + modal: 'qr', + title: t`Scan QR code`, + innerProps: {} + }) + } + > + + + ); +} diff --git a/src/frontend/src/components/items/StylishText.tsx b/src/frontend/src/components/items/StylishText.tsx new file mode 100644 index 0000000000..cbd63f019c --- /dev/null +++ b/src/frontend/src/components/items/StylishText.tsx @@ -0,0 +1,12 @@ +import { Text } from '@mantine/core'; + +import { InvenTreeStyle } from '../../globalStyle'; + +export function StylishText({ children }: { children: JSX.Element | string }) { + const { classes } = InvenTreeStyle(); + return ( + + {children} + + ); +} diff --git a/src/frontend/src/components/items/inventree.svg b/src/frontend/src/components/items/inventree.svg new file mode 100644 index 0000000000..1404d2cdaa --- /dev/null +++ b/src/frontend/src/components/items/inventree.svg @@ -0,0 +1 @@ + diff --git a/src/frontend/src/components/modals/QrCodeModal.tsx b/src/frontend/src/components/modals/QrCodeModal.tsx new file mode 100644 index 0000000000..afc9eac3cc --- /dev/null +++ b/src/frontend/src/components/modals/QrCodeModal.tsx @@ -0,0 +1,201 @@ +import { Trans, t } from '@lingui/macro'; +import { + Badge, + Button, + Container, + Group, + ScrollArea, + Space, + Stack, + Text +} from '@mantine/core'; +import { + useDocumentVisibility, + useListState, + useLocalStorage +} from '@mantine/hooks'; +import { ContextModalProps } from '@mantine/modals'; +import { showNotification } from '@mantine/notifications'; +import { IconX } from '@tabler/icons-react'; +import { Html5Qrcode } from 'html5-qrcode'; +import { CameraDevice } from 'html5-qrcode/camera/core'; +import { Html5QrcodeResult } from 'html5-qrcode/core'; +import { useEffect, useState } from 'react'; + +import { api } from '../../App'; + +export function QrCodeModal({ + context, + id +}: ContextModalProps<{ modalBody: string }>) { + const [qrCodeScanner, setQrCodeScanner] = useState(null); + const [camId, setCamId] = useLocalStorage({ + key: 'camId', + defaultValue: null + }); + const [ScanningEnabled, setIsScanning] = useState(false); + const [wasAutoPaused, setWasAutoPaused] = useState(false); + const documentState = useDocumentVisibility(); + + const [values, handlers] = useListState([]); + + // Mount QR code once we are loaded + useEffect(() => { + setQrCodeScanner(new Html5Qrcode('reader')); + }, []); + + // Stop/star when leaving or reentering page + useEffect(() => { + if (ScanningEnabled && documentState === 'hidden') { + stopScanning(); + setWasAutoPaused(true); + } else if (wasAutoPaused && documentState === 'visible') { + startScanning(); + setWasAutoPaused(false); + } + }, [documentState]); + + // Scanner functions + function onScanSuccess( + decodedText: string, + decodedResult: Html5QrcodeResult + ) { + qrCodeScanner?.pause(); + + handlers.append(decodedText); + api.post('/barcode/', { barcode: decodedText }).then((response) => { + showNotification({ + title: response.data?.success || t`Unknown response`, + message: JSON.stringify(response.data), + color: response.data?.success ? 'teal' : 'red' + }); + if (response.data?.url) { + window.location.href = response.data.url; + } + }); + + qrCodeScanner?.resume(); + } + + function onScanFailure(error: string) { + if ( + error != + 'QR code parse error, error = NotFoundException: No MultiFormat Readers were able to detect the code.' + ) { + console.warn(`Code scan error = ${error}`); + } + } + + function selectCamera() { + Html5Qrcode.getCameras() + .then((devices) => { + if (devices?.length) { + setCamId(devices[0]); + } + }) + .catch((err) => { + showNotification({ + title: t`Error while getting camera`, + message: err, + color: 'red', + icon: + }); + }); + } + + function startScanning() { + if (camId && qrCodeScanner) { + qrCodeScanner + .start( + camId.id, + { fps: 10, qrbox: { width: 250, height: 250 } }, + (decodedText, decodedResult) => { + onScanSuccess(decodedText, decodedResult); + }, + (errorMessage) => { + onScanFailure(errorMessage); + } + ) + .catch((err: string) => { + showNotification({ + title: t`Error while scanning`, + message: err, + color: 'red', + icon: + }); + }); + setIsScanning(true); + } + } + + function stopScanning() { + if (qrCodeScanner && ScanningEnabled) { + qrCodeScanner.stop().catch((err: string) => { + showNotification({ + title: t`Error while stopping`, + message: err, + color: 'red', + icon: + }); + }); + setIsScanning(false); + } + } + + return ( + + + {camId?.label} + + {ScanningEnabled ? t`Scanning` : t`Not scanning`} + + + {!camId ? ( + + ) : ( + <> + + + + + {values.length == 0 ? ( + + No scans yet! + + ) : ( + + {values.map((value, index) => ( +
{value}
+ ))} +
+ )} + + )} + +
+ ); +} diff --git a/src/frontend/src/components/nav/Footer.tsx b/src/frontend/src/components/nav/Footer.tsx new file mode 100644 index 0000000000..06af2b2962 --- /dev/null +++ b/src/frontend/src/components/nav/Footer.tsx @@ -0,0 +1,29 @@ +import { Anchor, Container, Group } from '@mantine/core'; + +import { footerLinks } from '../../defaults/links'; +import { InvenTreeStyle } from '../../globalStyle'; +import { InvenTreeLogo } from '../items/InvenTreeLogo'; + +export function Footer() { + const { classes } = InvenTreeStyle(); + const items = footerLinks.map((link) => ( + + color="dimmed" + key={link.key} + href={link.link} + onClick={(event) => event.preventDefault()} + size="sm" + > + {link.label} + + )); + + return ( +
+ + + {items} + +
+ ); +} diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx new file mode 100644 index 0000000000..3fc014e9db --- /dev/null +++ b/src/frontend/src/components/nav/Header.tsx @@ -0,0 +1,64 @@ +import { Container, Group, Tabs } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { navTabs as mainNavTabs } from '../../defaults/links'; +import { InvenTreeStyle } from '../../globalStyle'; +import { ColorToggle } from '../items/ColorToggle'; +import { ScanButton } from '../items/ScanButton'; +import { MainMenu } from './MainMenu'; +import { NavHoverMenu } from './NavHoverMenu'; +import { NavigationDrawer } from './NavigationDrawer'; + +export function Header() { + const { classes } = InvenTreeStyle(); + const [drawerOpened, { open: openDrawer, close }] = useDisclosure(false); + + return ( +
+ + + + + + + + + + + + + + +
+ ); +} + +function NavTabs() { + const { classes } = InvenTreeStyle(); + const { tabValue } = useParams(); + const navigate = useNavigate(); + + return ( + + value == '/' ? navigate('/') : navigate(`/${value}`) + } + > + + {mainNavTabs.map((tab) => ( + + {tab.text} + + ))} + + + ); +} diff --git a/src/frontend/src/components/nav/Layout.tsx b/src/frontend/src/components/nav/Layout.tsx new file mode 100644 index 0000000000..98c78d8fce --- /dev/null +++ b/src/frontend/src/components/nav/Layout.tsx @@ -0,0 +1,34 @@ +import { Container, Flex, Space } from '@mantine/core'; +import { Navigate, Outlet } from 'react-router-dom'; + +import { InvenTreeStyle } from '../../globalStyle'; +import { useSessionState } from '../../states/SessionState'; +import { Footer } from './Footer'; +import { Header } from './Header'; + +export const ProtectedRoute = ({ children }: { children: JSX.Element }) => { + const [token] = useSessionState((state) => [state.token]); + + if (!token) { + return ; + } + + return children; +}; + +export default function LayoutComponent() { + const { classes } = InvenTreeStyle(); + + return ( + + +
+ + + + +