summaryrefslogtreecommitdiffstatshomepage
path: root/webui/src
diff options
context:
space:
mode:
Diffstat (limited to 'webui/src')
-rw-r--r--webui/src/App.tsx23
-rw-r--r--webui/src/components/Author.tsx2
-rw-r--r--webui/src/components/BackToListButton.tsx2
-rw-r--r--webui/src/components/BugTitleForm/BugTitleForm.tsx4
-rw-r--r--webui/src/components/CommentInput/CommentInput.tsx2
-rw-r--r--webui/src/components/Content/AnchorTag.tsx2
-rw-r--r--webui/src/components/Content/index.tsx98
-rw-r--r--webui/src/components/Date.tsx3
-rw-r--r--webui/src/components/Header/Header.tsx61
-rw-r--r--webui/src/components/Identity/CurrentIdentity.tsx2
-rw-r--r--webui/src/components/Identity/CurrentRepository.tsx19
-rw-r--r--webui/src/components/Label.tsx4
-rw-r--r--webui/src/components/Moment.tsx28
-rw-r--r--webui/src/components/Themer.tsx2
-rw-r--r--webui/src/index.tsx12
-rw-r--r--webui/src/pages/bug/BugQuery.tsx5
-rw-r--r--webui/src/pages/bug/LabelChange.tsx4
-rw-r--r--webui/src/pages/bug/MessageHistoryDialog.tsx4
-rw-r--r--webui/src/pages/bug/TimelineQuery.tsx2
-rw-r--r--webui/src/pages/identity/BugList.tsx2
-rw-r--r--webui/src/pages/identity/Identity.tsx4
-rw-r--r--webui/src/pages/identity/IdentityQuery.tsx4
-rw-r--r--webui/src/pages/list/BugRow.tsx2
-rw-r--r--webui/src/pages/list/Filter.tsx2
-rw-r--r--webui/src/pages/list/FilterToolbar.tsx6
-rw-r--r--webui/src/pages/list/ListQuery.tsx2
-rw-r--r--webui/src/pages/new/NewBugPage.tsx2
-rw-r--r--webui/src/setupTests.js6
-rw-r--r--webui/src/themes/highlight-theme.scss13
29 files changed, 199 insertions, 123 deletions
diff --git a/webui/src/App.tsx b/webui/src/App.tsx
index 4bf09606b..a8712a945 100644
--- a/webui/src/App.tsx
+++ b/webui/src/App.tsx
@@ -1,4 +1,5 @@
-import { Route, Routes } from 'react-router-dom';
+import * as React from 'react';
+import { Route, Routes } from 'react-router';
import Layout from './components/Header';
import BugPage from './pages/bug';
@@ -9,14 +10,16 @@ import NotFoundPage from './pages/notfound/NotFoundPage';
export default function App() {
return (
- <Layout>
- <Routes>
- <Route path="/" element={<ListPage />} />
- <Route path="/new" element={<NewBugPage />} />
- <Route path="/bug/:id" element={<BugPage />} />
- <Route path="/user/:id" element={<IdentityPage />} />
- <Route element={<NotFoundPage />} />
- </Routes>
- </Layout>
+ <React.StrictMode>
+ <Layout>
+ <Routes>
+ <Route path="/" element={<ListPage />} />
+ <Route path="/new" element={<NewBugPage />} />
+ <Route path="/bug/:id" element={<BugPage />} />
+ <Route path="/user/:id" element={<IdentityPage />} />
+ <Route element={<NotFoundPage />} />
+ </Routes>
+ </Layout>
+ </React.StrictMode>
);
}
diff --git a/webui/src/components/Author.tsx b/webui/src/components/Author.tsx
index 92e14d40f..51ed336ab 100644
--- a/webui/src/components/Author.tsx
+++ b/webui/src/components/Author.tsx
@@ -1,7 +1,7 @@
import MAvatar from '@mui/material/Avatar';
import Link from '@mui/material/Link';
import Tooltip from '@mui/material/Tooltip/Tooltip';
-import { Link as RouterLink } from 'react-router-dom';
+import { Link as RouterLink } from 'react-router';
import { AuthoredFragment } from '../graphql/fragments.generated';
diff --git a/webui/src/components/BackToListButton.tsx b/webui/src/components/BackToListButton.tsx
index a4e4ea9c8..234d14588 100644
--- a/webui/src/components/BackToListButton.tsx
+++ b/webui/src/components/BackToListButton.tsx
@@ -1,7 +1,7 @@
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import Button from '@mui/material/Button';
import makeStyles from '@mui/styles/makeStyles';
-import { Link } from 'react-router-dom';
+import { Link } from 'react-router';
const useStyles = makeStyles((theme) => ({
backButton: {
diff --git a/webui/src/components/BugTitleForm/BugTitleForm.tsx b/webui/src/components/BugTitleForm/BugTitleForm.tsx
index 78b9e901b..82185d4fd 100644
--- a/webui/src/components/BugTitleForm/BugTitleForm.tsx
+++ b/webui/src/components/BugTitleForm/BugTitleForm.tsx
@@ -1,7 +1,7 @@
import { Button, Typography } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import { useRef, useState } from 'react';
-import { Link } from 'react-router-dom';
+import { Link } from 'react-router';
import { TimelineDocument } from '../../pages/bug/TimelineQuery.generated';
import IfLoggedIn from '../IfLoggedIn/IfLoggedIn';
@@ -71,7 +71,7 @@ function BugTitleForm({ bug }: Props) {
const [setTitle, { loading, error }] = useSetTitleMutation();
const [issueTitle, setIssueTitle] = useState(bug.title);
const classes = useStyles();
- const issueTitleInput = useRef<HTMLInputElement>();
+ const issueTitleInput = useRef<HTMLInputElement>(null);
function isFormValid() {
if (issueTitleInput.current) {
diff --git a/webui/src/components/CommentInput/CommentInput.tsx b/webui/src/components/CommentInput/CommentInput.tsx
index 0fde1d95b..4d4c3b633 100644
--- a/webui/src/components/CommentInput/CommentInput.tsx
+++ b/webui/src/components/CommentInput/CommentInput.tsx
@@ -102,7 +102,7 @@ function CommentInput({ inputProps, inputText, loading, onChange }: Props) {
multiline
value={input}
variant="filled"
- rows="4" // TODO: rowsMin support
+ minRows={4}
onChange={(e: any) => setInput(e.target.value)}
disabled={loading}
/>
diff --git a/webui/src/components/Content/AnchorTag.tsx b/webui/src/components/Content/AnchorTag.tsx
index e9edef954..da861bc2d 100644
--- a/webui/src/components/Content/AnchorTag.tsx
+++ b/webui/src/components/Content/AnchorTag.tsx
@@ -1,6 +1,6 @@
import makeStyles from '@mui/styles/makeStyles';
import * as React from 'react';
-import { Link } from 'react-router-dom';
+import { Link } from 'react-router';
const useStyles = makeStyles((theme) => ({
tag: {
diff --git a/webui/src/components/Content/index.tsx b/webui/src/components/Content/index.tsx
index 9bf9ff7a6..3cfa9eb89 100644
--- a/webui/src/components/Content/index.tsx
+++ b/webui/src/components/Content/index.tsx
@@ -1,11 +1,24 @@
-import { createElement, Fragment, useEffect, useState } from 'react';
+import type { Root as HastRoot } from 'hast';
+import type { Root as MdRoot } from 'mdast';
+import { useEffect, useState } from 'react';
import * as React from 'react';
+import * as production from 'react/jsx-runtime';
+import rehypeHighlight, {
+ Options as RehypeHighlightOpts,
+} from 'rehype-highlight';
import rehypeReact from 'rehype-react';
-import gemoji from 'remark-gemoji';
-import html from 'remark-html';
-import parse from 'remark-parse';
+import rehypeSanitize from 'rehype-sanitize';
+import remarkBreaks from 'remark-breaks';
+import remarkGemoji from 'remark-gemoji';
+import remarkGfm from 'remark-gfm';
+import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
+import type { Options as RemarkRehypeOptions } from 'remark-rehype';
import { unified } from 'unified';
+import type { Plugin, Processor } from 'unified';
+import { Node as UnistNode } from 'unified/lib';
+
+import { ThemeContext } from '../Themer';
import AnchorTag from './AnchorTag';
import BlockQuoteTag from './BlockQuoteTag';
@@ -13,32 +26,71 @@ import ImageTag from './ImageTag';
import PreTag from './PreTag';
type Props = { markdown: string };
+
+// @lygaret 2025/05/16
+// type inference for some of this doesn't work, but the pipeline is fine
+// this might get better when we upgrade typescript
+
+type RemarkPlugin = Plugin<[], MdRoot, HastRoot>;
+type RemarkRehypePlugin = Plugin<RemarkRehypeOptions[], MdRoot, HastRoot>;
+type RehypePlugin<Options extends unknown[] = []> = Plugin<
+ Options,
+ HastRoot,
+ HastRoot
+>;
+
+const markdownPipeline: Processor<
+ UnistNode,
+ undefined,
+ undefined,
+ HastRoot,
+ React.JSX.Element
+> = unified()
+ .use(remarkParse)
+ .use(remarkGemoji as unknown as RemarkPlugin)
+ .use(remarkBreaks as unknown as RemarkPlugin)
+ .use(remarkGfm)
+ .use(remarkRehype as unknown as RemarkRehypePlugin, {
+ allowDangerousHtml: true,
+ })
+ .use(rehypeSanitize as unknown as RehypePlugin)
+ .use(rehypeHighlight as unknown as RehypePlugin<RehypeHighlightOpts[]>, {
+ detect: true,
+ subset: ['text'],
+ })
+ .use(rehypeReact, {
+ ...production,
+ components: {
+ a: AnchorTag,
+ blockquote: BlockQuoteTag,
+ img: ImageTag,
+ pre: PreTag,
+ },
+ });
+
const Content: React.FC<Props> = ({ markdown }: Props) => {
- const [Content, setContent] = useState(<></>);
+ const theme = React.useContext(ThemeContext);
+ const [content, setContent] = useState(<></>);
useEffect(() => {
- unified()
- .use(parse)
- .use(gemoji)
- .use(html)
- .use(remarkRehype)
- .use(rehypeReact, {
- createElement,
- Fragment,
- components: {
- img: ImageTag,
- pre: PreTag,
- a: AnchorTag,
- blockquote: BlockQuoteTag,
- },
- })
+ markdownPipeline
.process(markdown)
- .then((file) => {
- setContent(file.result);
+ .then((file) => setContent(file.result))
+ .catch((err: any) => {
+ setContent(
+ <>
+ <span className="error">{err}</span>
+ <pre>{markdown}</pre>
+ </>
+ );
});
}, [markdown]);
- return Content;
+ return (
+ <div className={'highlight-theme'} data-theme={theme.mode}>
+ {content}
+ </div>
+ );
};
export default Content;
diff --git a/webui/src/components/Date.tsx b/webui/src/components/Date.tsx
index 1454fac77..19efc081e 100644
--- a/webui/src/components/Date.tsx
+++ b/webui/src/components/Date.tsx
@@ -1,6 +1,7 @@
import Tooltip from '@mui/material/Tooltip/Tooltip';
import moment from 'moment';
-import Moment from 'react-moment';
+
+import Moment from './Moment';
const HOUR = 1000 * 3600;
const DAY = 24 * HOUR;
diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx
index 5c03a5ecf..69b3fdfb2 100644
--- a/webui/src/components/Header/Header.tsx
+++ b/webui/src/components/Header/Header.tsx
@@ -1,13 +1,11 @@
import AppBar from '@mui/material/AppBar';
-import Tab, { TabProps } from '@mui/material/Tab';
-import Tabs from '@mui/material/Tabs';
import Toolbar from '@mui/material/Toolbar';
-import Tooltip from '@mui/material/Tooltip/Tooltip';
import { alpha } from '@mui/material/styles';
import makeStyles from '@mui/styles/makeStyles';
-import { Link, useLocation } from 'react-router-dom';
+import { Link } from 'react-router';
import CurrentIdentity from '../Identity/CurrentIdentity';
+import CurrentRepository from '../Identity/CurrentRepository';
import { LightSwitch } from '../Themer';
const useStyles = makeStyles((theme) => ({
@@ -29,7 +27,7 @@ const useStyles = makeStyles((theme) => ({
alignItems: 'center',
},
lightSwitch: {
- marginRight: '20px',
+ marginRight: theme.spacing(2),
color: alpha(theme.palette.primary.contrastText, 0.5),
},
logo: {
@@ -38,43 +36,8 @@ const useStyles = makeStyles((theme) => ({
},
}));
-function a11yProps(index: any) {
- return {
- id: `nav-tab-${index}`,
- 'aria-controls': `nav-tabpanel-${index}`,
- };
-}
-
-const DisabledTabWithTooltip = (props: TabProps) => {
- /*The span elements around disabled tabs are needed, as the tooltip
- * won't be triggered by disabled elements.
- * See: https://material-ui.com/components/tooltips/#disabled-elements
- * This must be done in a wrapper component, otherwise the TabS component
- * cannot pass it styles down to the Tab component. Resulting in (console)
- * warnings. This wrapper accepts the passed down TabProps and pass it around
- * the span element to the Tab component.
- */
- const msg = `This feature doesn't exist yet. Come help us build it.`;
- return (
- <Tooltip title={msg}>
- <span>
- <Tab disabled {...props} />
- </span>
- </Tooltip>
- );
-};
-
function Header() {
const classes = useStyles();
- const location = useLocation();
-
- // Prevents error of invalid tab selection in <Tabs>
- // Will return a valid tab path or false if path is unknown.
- function highlightTab() {
- const validTabs = ['/', '/code', '/pulls', '/settings'];
- const tab = validTabs.find((tabPath) => tabPath === location.pathname);
- return tab === undefined ? false : tab;
- }
return (
<>
@@ -82,28 +45,14 @@ function Header() {
<Toolbar>
<Link to="/" className={classes.appTitle}>
<img src="/logo.svg" className={classes.logo} alt="git-bug logo" />
- git-bug
+ <CurrentRepository default="git-bug" />
</Link>
<div className={classes.filler} />
<LightSwitch className={classes.lightSwitch} />
<CurrentIdentity />
</Toolbar>
</AppBar>
- <div className={classes.offset} />
- <Tabs centered value={highlightTab()} aria-label="nav tabs">
- <DisabledTabWithTooltip label="Code" value="/code" {...a11yProps(1)} />
- <Tab label="Bugs" value="/" component={Link} to="/" {...a11yProps(2)} />
- <DisabledTabWithTooltip
- label="Pull Requests"
- value="/pulls"
- {...a11yProps(3)}
- />
- <DisabledTabWithTooltip
- label="Settings"
- value="/settings"
- {...a11yProps(4)}
- />
- </Tabs>
+ <div className={classes.offset}></div>
</>
);
}
diff --git a/webui/src/components/Identity/CurrentIdentity.tsx b/webui/src/components/Identity/CurrentIdentity.tsx
index e6a396a8e..fb06898ec 100644
--- a/webui/src/components/Identity/CurrentIdentity.tsx
+++ b/webui/src/components/Identity/CurrentIdentity.tsx
@@ -12,7 +12,7 @@ import {
import Avatar from '@mui/material/Avatar';
import makeStyles from '@mui/styles/makeStyles';
import { useState, useRef } from 'react';
-import { Link as RouterLink } from 'react-router-dom';
+import { Link as RouterLink } from 'react-router';
import { useCurrentIdentityQuery } from './CurrentIdentity.generated';
diff --git a/webui/src/components/Identity/CurrentRepository.tsx b/webui/src/components/Identity/CurrentRepository.tsx
new file mode 100644
index 000000000..77aa6839b
--- /dev/null
+++ b/webui/src/components/Identity/CurrentRepository.tsx
@@ -0,0 +1,19 @@
+import { useCurrentIdentityQuery } from './CurrentIdentity.generated';
+
+// same as in multi_repo_cache.go
+const defaultRepoName = '__default';
+
+const CurrentRepository = (props: { default: string }) => {
+ const { loading, error, data } = useCurrentIdentityQuery();
+
+ if (error || loading || !data?.repository?.name) return null;
+
+ let name = data.repository.name;
+ if (name === defaultRepoName) {
+ name = props.default;
+ }
+
+ return <>{name}</>;
+};
+
+export default CurrentRepository;
diff --git a/webui/src/components/Label.tsx b/webui/src/components/Label.tsx
index 709aceff9..a63398487 100644
--- a/webui/src/components/Label.tsx
+++ b/webui/src/components/Label.tsx
@@ -26,14 +26,16 @@ const createStyle = (color: Color, maxWidth?: string) => ({
type Props = {
label: LabelFragment;
+ inline?: boolean;
maxWidth?: string;
className?: string;
};
-function Label({ label, maxWidth, className }: Props) {
+function Label({ label, inline, maxWidth, className }: Props) {
return (
<Chip
size={'small'}
label={label.name}
+ component={inline ? 'span' : 'div'}
className={className}
style={createStyle(label.color, maxWidth)}
/>
diff --git a/webui/src/components/Moment.tsx b/webui/src/components/Moment.tsx
new file mode 100644
index 000000000..4bffbd318
--- /dev/null
+++ b/webui/src/components/Moment.tsx
@@ -0,0 +1,28 @@
+import moment from 'moment';
+
+type Props = {
+ date: moment.MomentInput;
+ format: string;
+ fromNowDuring?: number;
+};
+
+const Moment = ({ date, format, fromNowDuring }: Props) => {
+ let dateString: string | undefined;
+ const dateMoment = moment(date);
+
+ if (fromNowDuring) {
+ const diff = moment().diff(dateMoment, 'ms');
+ if (diff < fromNowDuring) {
+ dateString = dateMoment.fromNow();
+ }
+ }
+
+ // we either are out of range or didn't get asked for fromNow
+ if (dateString === undefined) {
+ dateString = dateMoment.format(format);
+ }
+
+ return <span>{dateString}</span>;
+};
+
+export default Moment;
diff --git a/webui/src/components/Themer.tsx b/webui/src/components/Themer.tsx
index 9934888d1..f03881b4d 100644
--- a/webui/src/components/Themer.tsx
+++ b/webui/src/components/Themer.tsx
@@ -11,7 +11,7 @@ declare module '@mui/styles/defaultTheme' {
interface DefaultTheme extends Theme {}
}
-const ThemeContext = createContext({
+export const ThemeContext = createContext({
toggleMode: () => {},
mode: '',
});
diff --git a/webui/src/index.tsx b/webui/src/index.tsx
index d203eb199..7667c7068 100644
--- a/webui/src/index.tsx
+++ b/webui/src/index.tsx
@@ -1,19 +1,21 @@
import { ApolloProvider } from '@apollo/client';
-import ReactDOM from 'react-dom';
-import { BrowserRouter } from 'react-router-dom';
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { BrowserRouter } from 'react-router';
import App from './App';
import apolloClient from './apollo';
import Themer from './components/Themer';
import { defaultLightTheme, defaultDarkTheme } from './themes/index';
+import './themes/highlight-theme.scss';
-ReactDOM.render(
+const root = createRoot(document.getElementById('root') as HTMLElement);
+root.render(
<ApolloProvider client={apolloClient}>
<BrowserRouter>
<Themer lightTheme={defaultLightTheme} darkTheme={defaultDarkTheme}>
<App />
</Themer>
</BrowserRouter>
- </ApolloProvider>,
- document.getElementById('root')
+ </ApolloProvider>
);
diff --git a/webui/src/pages/bug/BugQuery.tsx b/webui/src/pages/bug/BugQuery.tsx
index 244b08360..704dd0376 100644
--- a/webui/src/pages/bug/BugQuery.tsx
+++ b/webui/src/pages/bug/BugQuery.tsx
@@ -1,6 +1,6 @@
import CircularProgress from '@mui/material/CircularProgress';
import * as React from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams } from 'react-router';
import NotFoundPage from '../notfound/NotFoundPage';
@@ -16,7 +16,8 @@ const BugQuery: React.FC = () => {
});
if (loading) return <CircularProgress />;
if (!data?.repository?.bug) return <NotFoundPage />;
- if (error) return <p>Error: {error}</p>;
+ if (error) return <p>Error: {error.message}</p>;
+
return <Bug bug={data.repository.bug} />;
};
diff --git a/webui/src/pages/bug/LabelChange.tsx b/webui/src/pages/bug/LabelChange.tsx
index 6b356d14a..28fc47433 100644
--- a/webui/src/pages/bug/LabelChange.tsx
+++ b/webui/src/pages/bug/LabelChange.tsx
@@ -35,12 +35,12 @@ function LabelChange({ op }: Props) {
<Author author={op.author} className={classes.author} />
{added.length > 0 && <span> added the </span>}
{added.map((label, index) => (
- <Label key={index} label={label} className={classes.label} />
+ <Label inline key={index} label={label} className={classes.label} />
))}
{added.length > 0 && removed.length > 0 && <span> and</span>}
{removed.length > 0 && <span> removed the </span>}
{removed.map((label, index) => (
- <Label key={index} label={label} className={classes.label} />
+ <Label inline key={index} label={label} className={classes.label} />
))}
<span>
{' '}
diff --git a/webui/src/pages/bug/MessageHistoryDialog.tsx b/webui/src/pages/bug/MessageHistoryDialog.tsx
index 77f82d86f..7523d5c08 100644
--- a/webui/src/pages/bug/MessageHistoryDialog.tsx
+++ b/webui/src/pages/bug/MessageHistoryDialog.tsx
@@ -17,9 +17,9 @@ import createStyles from '@mui/styles/createStyles';
import withStyles from '@mui/styles/withStyles';
import moment from 'moment';
import * as React from 'react';
-import Moment from 'react-moment';
import Content from '../../components/Content';
+import Moment from '../../components/Moment';
import { AddCommentFragment } from './MessageCommentFragment.generated';
import { CreateFragment } from './MessageCreateFragment.generated';
@@ -159,7 +159,7 @@ function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) {
Something went wrong...
</DialogTitle>
<DialogContent dividers>
- <p>Error: {error}</p>
+ <p>Error: {error.message}</p>
</DialogContent>
</Dialog>
);
diff --git a/webui/src/pages/bug/TimelineQuery.tsx b/webui/src/pages/bug/TimelineQuery.tsx
index ab9e4cd6f..3b55270cb 100644
--- a/webui/src/pages/bug/TimelineQuery.tsx
+++ b/webui/src/pages/bug/TimelineQuery.tsx
@@ -17,7 +17,7 @@ const TimelineQuery = ({ bug }: Props) => {
});
if (loading) return <CircularProgress />;
- if (error) return <p>Error: {error}</p>;
+ if (error) return <p>Error: {error.message}</p>;
const nodes = data?.repository?.bug?.timeline.nodes;
if (!nodes) {
diff --git a/webui/src/pages/identity/BugList.tsx b/webui/src/pages/identity/BugList.tsx
index a7c37a32c..7c661dc20 100644
--- a/webui/src/pages/identity/BugList.tsx
+++ b/webui/src/pages/identity/BugList.tsx
@@ -34,7 +34,7 @@ function BugList({ id }: Props) {
});
if (loading) return <CircularProgress />;
- if (error) return <p>Error: {error}</p>;
+ if (error) return <p>Error: {error.message}</p>;
const bugs = data?.repository?.allBugs.nodes;
return (
diff --git a/webui/src/pages/identity/Identity.tsx b/webui/src/pages/identity/Identity.tsx
index 19b80b1c2..beced10d8 100644
--- a/webui/src/pages/identity/Identity.tsx
+++ b/webui/src/pages/identity/Identity.tsx
@@ -5,7 +5,7 @@ import Avatar from '@mui/material/Avatar';
import CircularProgress from '@mui/material/CircularProgress';
import Grid from '@mui/material/Grid';
import makeStyles from '@mui/styles/makeStyles';
-import { Link as RouterLink } from 'react-router-dom';
+import { Link as RouterLink } from 'react-router';
import { IdentityFragment } from '../../components/Identity/IdentityFragment.generated';
@@ -56,7 +56,7 @@ const Identity = ({ identity }: Props) => {
});
if (loading) return <CircularProgress />;
- if (error) return <p>Error: {error}</p>;
+ if (error) return <p>Error: {error.message}</p>;
const statistic = data?.repository;
const authoredCount = statistic?.authored?.totalCount;
const participatedCount = statistic?.participated?.totalCount;
diff --git a/webui/src/pages/identity/IdentityQuery.tsx b/webui/src/pages/identity/IdentityQuery.tsx
index 382116ca5..faa192f6b 100644
--- a/webui/src/pages/identity/IdentityQuery.tsx
+++ b/webui/src/pages/identity/IdentityQuery.tsx
@@ -1,6 +1,6 @@
import CircularProgress from '@mui/material/CircularProgress';
import * as React from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams } from 'react-router';
import { useGetUserByIdQuery } from '../../components/Identity/UserIdentity.generated';
@@ -14,7 +14,7 @@ const UserQuery: React.FC = () => {
variables: { userId: params.id },
});
if (loading) return <CircularProgress />;
- if (error) return <p>Error: {error}</p>;
+ if (error) return <p>Error: {error.message}</p>;
if (!data?.repository?.identity) return <p>404.</p>;
return <Identity identity={data.repository.identity} />;
};
diff --git a/webui/src/pages/list/BugRow.tsx b/webui/src/pages/list/BugRow.tsx
index 82582dbe2..40f436130 100644
--- a/webui/src/pages/list/BugRow.tsx
+++ b/webui/src/pages/list/BugRow.tsx
@@ -6,7 +6,7 @@ import TableRow from '@mui/material/TableRow/TableRow';
import Tooltip from '@mui/material/Tooltip/Tooltip';
import makeStyles from '@mui/styles/makeStyles';
import * as React from 'react';
-import { Link } from 'react-router-dom';
+import { Link } from 'react-router';
import Author from 'src/components/Author';
import Date from 'src/components/Date';
diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx
index 6b3422bec..0ba5e71d0 100644
--- a/webui/src/pages/list/Filter.tsx
+++ b/webui/src/pages/list/Filter.tsx
@@ -10,7 +10,7 @@ import withStyles from '@mui/styles/withStyles';
import clsx from 'clsx';
import * as React from 'react';
import { useRef, useState, useEffect } from 'react';
-import { Location, Link } from 'react-router-dom';
+import { Location, Link } from 'react-router';
import { Color } from '../../gqlTypes';
diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx
index 5e774734e..9b467e20c 100644
--- a/webui/src/pages/list/FilterToolbar.tsx
+++ b/webui/src/pages/list/FilterToolbar.tsx
@@ -4,7 +4,7 @@ import ErrorOutline from '@mui/icons-material/ErrorOutline';
import Toolbar from '@mui/material/Toolbar';
import makeStyles from '@mui/styles/makeStyles';
import * as React from 'react';
-import { Location } from 'react-router-dom';
+import { Location } from 'react-router';
import {
Filter,
@@ -131,8 +131,8 @@ function FilterToolbar({ query, queryLocation }: Props) {
params[key] && params[key].includes(value)
? values.filter((v) => v !== value)
: values
- ? [...values, value]
- : [value],
+ ? [...values, value]
+ : [value],
};
};
const clearParam =
diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx
index 6b508e905..98a7df42d 100644
--- a/webui/src/pages/list/ListQuery.tsx
+++ b/webui/src/pages/list/ListQuery.tsx
@@ -13,7 +13,7 @@ import { Theme } from '@mui/material/styles';
import makeStyles from '@mui/styles/makeStyles';
import * as React from 'react';
import { useState, useEffect, useRef } from 'react';
-import { useLocation, useNavigate, Link } from 'react-router-dom';
+import { useLocation, useNavigate, Link } from 'react-router';
import { useCurrentIdentityQuery } from '../../components/Identity/CurrentIdentity.generated';
import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn';
diff --git a/webui/src/pages/new/NewBugPage.tsx b/webui/src/pages/new/NewBugPage.tsx
index 91e4905a0..c0aa6b3f9 100644
--- a/webui/src/pages/new/NewBugPage.tsx
+++ b/webui/src/pages/new/NewBugPage.tsx
@@ -2,7 +2,7 @@ import { Button, Paper } from '@mui/material';
import { Theme } from '@mui/material/styles';
import makeStyles from '@mui/styles/makeStyles';
import { FormEvent, useRef, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router';
import BugTitleInput from '../../components/BugTitleForm/BugTitleInput';
import CommentInput from '../../components/CommentInput/CommentInput';
diff --git a/webui/src/setupTests.js b/webui/src/setupTests.js
new file mode 100644
index 000000000..012e25a15
--- /dev/null
+++ b/webui/src/setupTests.js
@@ -0,0 +1,6 @@
+import { TextEncoder } from 'util';
+
+// jsdom, used to run tests, doesn't support text-encoder
+// https://github.com/remix-run/react-router/issues/12363
+
+global.TextEncoder = TextEncoder;
diff --git a/webui/src/themes/highlight-theme.scss b/webui/src/themes/highlight-theme.scss
new file mode 100644
index 000000000..e6f145e9b
--- /dev/null
+++ b/webui/src/themes/highlight-theme.scss
@@ -0,0 +1,13 @@
+@use 'sass:meta';
+
+// if highlight.js gets updated to support proper mixins,
+// meta.load-css won't be necessary; see https://sass-lang.com/documentation/breaking-changes/import/#nested-imports
+
+.highlight-theme[data-theme='light'],
+.highlight-theme:not([data-theme='dark']) {
+ @include meta.load-css('~highlight.js/scss/base16/classic-light.scss');
+}
+
+.highlight-theme[data-theme='dark'] {
+ @include meta.load-css('~highlight.js/scss/base16/classic-dark.scss');
+}