App directory refactoring
This commit is contained in:
parent
a53a26f4c0
commit
b312a8c862
153 changed files with 1532 additions and 1447 deletions
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button as MUIButton } from "@mui/material";
|
||||
22
apps/link/app/_components/DisplayError.tsx
Normal file
22
apps/link/app/_components/DisplayError.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { Box, Grid } from "@mui/material";
|
||||
|
||||
type DisplayErrorProps = {
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export const DisplayError: FC<DisplayErrorProps> = ({ error }) => (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="space-around"
|
||||
alignItems="center"
|
||||
style={{ height: 600, width: "100%", color: "red", fontSize: 20 }}
|
||||
>
|
||||
<Grid item>
|
||||
<Box>{`Error: ${error.message}`}</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
6
apps/link/app/_components/Home.tsx
Normal file
6
apps/link/app/_components/Home.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { ZammadWrapper } from "@/app/_components/ZammadWrapper";
|
||||
|
||||
export const Home: FC = () => <ZammadWrapper path="/#dashboard" hideSidebar />;
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { FC, useState } from "react";
|
||||
"use client";
|
||||
|
||||
import { FC, PropsWithChildren, useState } from "react";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
export const Layout = ({ children }) => {
|
||||
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
|
|
@ -12,7 +14,7 @@ export const Layout = ({ children }) => {
|
|||
item
|
||||
sx={{ ml: open ? "270px" : "100px", width: "100%", height: "100vh" }}
|
||||
>
|
||||
{children}
|
||||
{children as any}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
57
apps/link/app/_components/MultiProvider.tsx
Normal file
57
apps/link/app/_components/MultiProvider.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"use client";
|
||||
|
||||
import { FC, PropsWithChildren, useState } from "react";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
|
||||
import { SWRConfig } from "swr";
|
||||
import { GraphQLClient } from "graphql-request";
|
||||
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
|
||||
|
||||
export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [csrfToken, setCsrfToken] = useState("");
|
||||
const origin =
|
||||
typeof window !== "undefined" && window.location.origin
|
||||
? window.location.origin
|
||||
: null;
|
||||
const client = new GraphQLClient(`${origin}/proxy/zammad/graphql`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
const graphQLFetcher = async ({ document, variables }: any) => {
|
||||
const requestHeaders = {
|
||||
"X-CSRF-Token": csrfToken,
|
||||
};
|
||||
const { data, headers } = await client.rawRequest(
|
||||
document,
|
||||
variables,
|
||||
requestHeaders
|
||||
);
|
||||
|
||||
const token = headers.get("CSRF-Token");
|
||||
setCsrfToken(token);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<NextAppDirEmotionCacheProvider options={{ key: "css" }}>
|
||||
<SWRConfig value={{ fetcher: graphQLFetcher }}>
|
||||
<SessionProvider>
|
||||
<CookiesProvider>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
{children}
|
||||
</LocalizationProvider>
|
||||
</CookiesProvider>
|
||||
</SessionProvider>
|
||||
</SWRConfig>
|
||||
</NextAppDirEmotionCacheProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
|
|
@ -22,12 +24,12 @@ import {
|
|||
ExpandCircleDown as ExpandCircleDownIcon,
|
||||
Dvr as DvrIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import LinkLogo from "public/link-logo-small.png";
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import { getTicketOverviewCountsQuery } from "graphql/getTicketOverviewCountsQuery";
|
||||
import { getTicketOverviewCountsQuery } from "@/app/_graphql/getTicketOverviewCountsQuery";
|
||||
|
||||
const openWidth = 270;
|
||||
const closedWidth = 100;
|
||||
|
|
@ -157,8 +159,7 @@ interface SidebarProps {
|
|||
}
|
||||
|
||||
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||
const router = useRouter();
|
||||
const { pathname } = router;
|
||||
const pathname = usePathname();
|
||||
const { data: session } = useSession();
|
||||
const username = session?.user?.name || "User";
|
||||
const { data: overviewData, error: overviewError }: any = useSWR(
|
||||
|
|
@ -358,14 +359,20 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
/>
|
||||
<MenuItem
|
||||
name="Tickets"
|
||||
href="/tickets/assigned"
|
||||
href="/overview/assigned"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
selected={pathname.startsWith("/tickets")}
|
||||
selected={
|
||||
pathname.startsWith("/overview") ||
|
||||
pathname.startsWith("/tickets")
|
||||
}
|
||||
iconSize={20}
|
||||
open={open}
|
||||
/>
|
||||
<Collapse
|
||||
in={pathname.startsWith("/tickets")}
|
||||
in={
|
||||
pathname.startsWith("/overview") ||
|
||||
pathname.startsWith("/tickets")
|
||||
}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
onClick={undefined}
|
||||
|
|
@ -373,37 +380,37 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
<List component="div" disablePadding>
|
||||
<MenuItem
|
||||
name="Assigned"
|
||||
href="/tickets/assigned"
|
||||
href="/overview/assigned"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/tickets/assigned")}
|
||||
selected={pathname.endsWith("/overview/assigned")}
|
||||
badge={assignedCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Urgent"
|
||||
href="/tickets/urgent"
|
||||
href="/overview/urgent"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/tickets/urgent")}
|
||||
selected={pathname.endsWith("/overview/urgent")}
|
||||
badge={urgentCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Pending"
|
||||
href="/tickets/pending"
|
||||
href="/overview/pending"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/tickets/pending")}
|
||||
selected={pathname.endsWith("/overview/pending")}
|
||||
badge={pendingCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Unassigned"
|
||||
href="/tickets/unassigned"
|
||||
href="/overview/unassigned"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/tickets/unassigned")}
|
||||
selected={pathname.endsWith("/overview/unassigned")}
|
||||
badge={unassignedCount}
|
||||
open={open}
|
||||
/>
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import {
|
||||
|
|
@ -1,22 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import getConfig from "next/config";
|
||||
import { useRouter } from "next/router";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
type InternalZammadWrapperProps = {
|
||||
type ZammadWrapperProps = {
|
||||
path: string;
|
||||
hideSidebar?: boolean;
|
||||
};
|
||||
|
||||
export const InternalZammadWrapper: FC<InternalZammadWrapperProps> = ({
|
||||
export const ZammadWrapper: FC<ZammadWrapperProps> = ({
|
||||
path,
|
||||
hideSidebar = true,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [display, setDisplay] = useState("none");
|
||||
const {
|
||||
publicRuntimeConfig: { linkURL },
|
||||
} = getConfig();
|
||||
const [display, setDisplay] = useState("inherit");
|
||||
//const {
|
||||
// publicRuntimeConfig: { linkURL },
|
||||
// } = getConfig();
|
||||
const linkURL = "http://localhost:3000";
|
||||
const url = `${linkURL}/proxy/zammad${path}`;
|
||||
console.log({ url });
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { FC } from "react";
|
||||
import { Grid } from "@mui/material";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
export const LabelStudioWrapper: FC = () => (
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||
<Iframe
|
||||
id="link"
|
||||
url={"https://label-studio:3000"}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder={0}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
10
apps/link/app/admin/label-studio/page.tsx
Normal file
10
apps/link/app/admin/label-studio/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { LabelStudioWrapper } from "./_components/LabelStudioWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Label Studio",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <LabelStudioWrapper />;
|
||||
}
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Head from "next/head";
|
||||
import getConfig from "next/config";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Layout } from "components/Layout";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
const LabelStudio: FC = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
type MetamigoWrapperProps = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const MetamigoWrapper: FC<MetamigoWrapperProps> = ({ path }) => {
|
||||
const {
|
||||
publicRuntimeConfig: { linkURL },
|
||||
} = getConfig();
|
||||
const fullMetamigoURL = `${linkURL}/metamigo/${path}`;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
|
|
@ -18,14 +25,12 @@ const LabelStudio: FC = () => (
|
|||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||
<Iframe
|
||||
id="link"
|
||||
url={"https://label-studio:3000"}
|
||||
url={fullMetamigoURL}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder={0}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export default LabelStudio;
|
||||
);
|
||||
};
|
||||
16
apps/link/app/admin/metamigo/[...path]/page.tsx
Normal file
16
apps/link/app/admin/metamigo/[...path]/page.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Metadata } from "next";
|
||||
import { MetamigoWrapper } from "./_components/MetamigoWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Metamigo",
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
path: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Page({ params: { path } }: PageProps) {
|
||||
return <MetamigoWrapper path={path} />;
|
||||
}
|
||||
10
apps/link/app/admin/zammad/page.tsx
Normal file
10
apps/link/app/admin/zammad/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { ZammadWrapper } from "@/app/_components/ZammadWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Zammad",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <ZammadWrapper path="/#manage" hideSidebar={false} />;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import NextAuth from "next-auth";
|
|||
import Google from "next-auth/providers/google";
|
||||
// import Apple from "next-auth/providers/apple";
|
||||
|
||||
export default NextAuth({
|
||||
const handler = NextAuth({
|
||||
providers: [
|
||||
Google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
|
|
@ -17,3 +17,6 @@ export default NextAuth({
|
|||
],
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
});
|
||||
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
7
apps/link/app/api/v1/users/route.ts
Normal file
7
apps/link/app/api/v1/users/route.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const handler = (req: NextRequest) => {
|
||||
NextResponse.redirect('/proxy/zammad/api/v1' + req.url.substring('/api/v1'.length));
|
||||
};
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
11
apps/link/app/error.tsx
Normal file
11
apps/link/app/error.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { DisplayError } from "./_components/DisplayError";
|
||||
|
||||
type PageProps = {
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export default function Page({ error }: PageProps) {
|
||||
return <DisplayError error={error} />;
|
||||
}
|
||||
10
apps/link/app/knowledge/page.tsx
Normal file
10
apps/link/app/knowledge/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { ZammadWrapper } from "@/app/_components/ZammadWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Knowledge Base",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <ZammadWrapper path="/#knowledge_base/1/locale/en-us" />;
|
||||
}
|
||||
35
apps/link/app/layout.tsx
Normal file
35
apps/link/app/layout.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import "./_styles/global.css";
|
||||
import "@fontsource/poppins/400.css";
|
||||
import "@fontsource/poppins/700.css";
|
||||
import "@fontsource/roboto/400.css";
|
||||
import "@fontsource/roboto/700.css";
|
||||
import "@fontsource/playfair-display/900.css";
|
||||
// import getConfig from "next/config";
|
||||
// import { LicenseInfo } from "@mui/x-data-grid-pro";
|
||||
import { MultiProvider } from "./_components/MultiProvider";
|
||||
import { InternalLayout } from "./_components/InternalLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Link",
|
||||
};
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
// const { publicRuntimeConfig } = getConfig();
|
||||
// LicenseInfo.setLicenseKey(publicRuntimeConfig.muiLicenseKey);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<MultiProvider>
|
||||
<InternalLayout>{children}</InternalLayout>
|
||||
</MultiProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import getConfig from "next/config";
|
||||
import { Grid } from "@mui/material";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
type LeafcutterWrapperProps = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const LeafcutterWrapper: FC<LeafcutterWrapperProps> = ({ path }) => {
|
||||
const {
|
||||
publicRuntimeConfig: { linkURL },
|
||||
} = getConfig();
|
||||
const fullLeafcutterURL = `${linkURL}/proxy/leafcutter/${path}`;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||
<Iframe
|
||||
id="leafcutter"
|
||||
url={fullLeafcutterURL}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder={0}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
11
apps/link/app/leafcutter/[view]/page.tsx
Normal file
11
apps/link/app/leafcutter/[view]/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { LeafcutterWrapper } from "./_components/LeafcutterWrapper";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
view: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Page({ params: { view } }: PageProps) {
|
||||
<LeafcutterWrapper path={view} />;
|
||||
}
|
||||
5
apps/link/app/leafcutter/page.tsx
Normal file
5
apps/link/app/leafcutter/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
redirect("/leafcutter/home");
|
||||
}
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import Head from "next/head";
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { Box, Grid, Container, IconButton } from "@mui/material";
|
||||
import { Apple as AppleIcon, Google as GoogleIcon } from "@mui/icons-material";
|
||||
import { signIn, getSession } from "next-auth/react";
|
||||
import { signIn } from "next-auth/react";
|
||||
|
||||
type LoginProps = {
|
||||
session: any;
|
||||
};
|
||||
|
||||
const Login: FC<LoginProps> = ({ session }) => {
|
||||
export const Login: FC<LoginProps> = ({ session }) => {
|
||||
const origin =
|
||||
typeof window !== "undefined" && window.location.origin
|
||||
? window.location.origin
|
||||
|
|
@ -22,9 +23,6 @@ const Login: FC<LoginProps> = ({ session }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Login</title>
|
||||
</Head>
|
||||
<Grid container direction="row-reverse" sx={{ p: 3 }}>
|
||||
<Grid item />
|
||||
</Grid>
|
||||
|
|
@ -84,13 +82,3 @@ const Login: FC<LoginProps> = ({ session }) => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
const session = (await getSession(context)) ?? null;
|
||||
|
||||
return {
|
||||
props: { session },
|
||||
};
|
||||
}
|
||||
12
apps/link/app/login/page.tsx
Normal file
12
apps/link/app/login/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Metadata } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { Login } from "./_components/Login";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Login",
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
const session = await getSession();
|
||||
return <Login session={session} />;
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { Grid, Box } from "@mui/material";
|
||||
import { GridColDef } from "@mui/x-data-grid-pro";
|
||||
import { StyledDataGrid } from "./StyledDataGrid";
|
||||
import { typography } from "styles/theme";
|
||||
import { useRouter } from "next/router";
|
||||
import { StyledDataGrid } from "../../../_components/StyledDataGrid";
|
||||
import { typography } from "@/app/_styles/theme";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface TicketListProps {
|
||||
title: string;
|
||||
|
|
@ -1,15 +1,20 @@
|
|||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
import { NextPage } from "next";
|
||||
import { Layout } from "components/Layout";
|
||||
import { TicketList } from "components/TicketList";
|
||||
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
|
||||
"use client";
|
||||
|
||||
const Assigned: NextPage = () => {
|
||||
import { FC } from "react";
|
||||
import useSWR from "swr";
|
||||
import { TicketList } from "./TicketList";
|
||||
import { getTicketsByOverviewQuery } from "@/app/_graphql/getTicketsByOverviewQuery";
|
||||
|
||||
type ZammadOverviewProps = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name, id }) => {
|
||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||
{
|
||||
document: getTicketsByOverviewQuery,
|
||||
variables: { overviewId: "gid://zammad/Overview/1" },
|
||||
variables: { overviewId: `gid://zammad/Overview/${id}` },
|
||||
},
|
||||
{ refreshInterval: 10000 }
|
||||
);
|
||||
|
|
@ -19,14 +24,9 @@ const Assigned: NextPage = () => {
|
|||
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell – Assigned Tickets</title>
|
||||
</Head>
|
||||
<>
|
||||
{shouldRender && <TicketList title="Assigned" tickets={tickets} />}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Assigned;
|
||||
11
apps/link/app/overview/[overview]/error.tsx
Normal file
11
apps/link/app/overview/[overview]/error.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { DisplayError } from "app/_components/DisplayError";
|
||||
|
||||
type PageProps = {
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export default function Page({ error }: PageProps) {
|
||||
return <DisplayError error={error} />;
|
||||
}
|
||||
36
apps/link/app/overview/[overview]/page.tsx
Normal file
36
apps/link/app/overview/[overview]/page.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { Metadata } from "next";
|
||||
import { ZammadOverview } from "./_components/ZammadOverview";
|
||||
|
||||
type MetadataProps = {
|
||||
params: {
|
||||
overview: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { overview },
|
||||
}: MetadataProps): Promise<Metadata> {
|
||||
const section = overview.charAt(0).toUpperCase() + overview.slice(1);
|
||||
|
||||
return {
|
||||
title: `Link - ${section} Tickets`,
|
||||
};
|
||||
}
|
||||
|
||||
const overviews = {
|
||||
assigned: 1,
|
||||
unassigned: 2,
|
||||
pending: 3,
|
||||
urgent: 7,
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
overview: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Page({ params: { overview } }: PageProps) {
|
||||
console.log({ overview });
|
||||
return <ZammadOverview name={overview} id={overviews[overview]} />;
|
||||
}
|
||||
10
apps/link/app/page.tsx
Normal file
10
apps/link/app/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { Home } from "@/app/_components/Home";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Link",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <Home />;
|
||||
}
|
||||
10
apps/link/app/profile/page.tsx
Normal file
10
apps/link/app/profile/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from "next";
|
||||
import { ZammadWrapper } from "@/app/_components/ZammadWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Profile",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <ZammadWrapper path="/#profile" hideSidebar={false} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import {
|
||||
Grid,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { updateTicketMutation } from "@/app/_graphql/updateTicketMutation";
|
||||
|
||||
interface ArticleCreateDialogProps {
|
||||
ticketID: string;
|
||||
open: boolean;
|
||||
closeDialog: () => void;
|
||||
kind: "reply" | "note";
|
||||
}
|
||||
|
||||
export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
|
||||
ticketID,
|
||||
open,
|
||||
closeDialog,
|
||||
kind,
|
||||
}) => {
|
||||
const [body, setBody] = useState("");
|
||||
const backgroundColor = kind === "reply" ? "#1982FC" : "#FFB620";
|
||||
const color = kind === "reply" ? "white" : "black";
|
||||
const { fetcher } = useSWRConfig();
|
||||
const createArticle = async () => {
|
||||
await fetcher({
|
||||
document: updateTicketMutation,
|
||||
variables: {
|
||||
ticketId: `gid://zammad/Ticket/${ticketID}`,
|
||||
input: {
|
||||
article: {
|
||||
body,
|
||||
type: kind === "note" ? "note" : "phone",
|
||||
internal: kind === "note",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
closeDialog();
|
||||
setBody("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} maxWidth="sm" fullWidth>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label={kind === "reply" ? "Write reply" : "Write internal note"}
|
||||
multiline
|
||||
rows={10}
|
||||
fullWidth
|
||||
value={body}
|
||||
onChange={(e: any) => setBody(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pt: 0, pb: 3 }}>
|
||||
<Grid container justifyContent="space-between">
|
||||
<Grid item>
|
||||
<Button
|
||||
sx={{
|
||||
backgroundColor: "white",
|
||||
color: "#666",
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 700,
|
||||
borderRadius: 2,
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
setBody("");
|
||||
closeDialog();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
sx={{
|
||||
backgroundColor,
|
||||
color,
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 700,
|
||||
borderRadius: 2,
|
||||
textTransform: "none",
|
||||
px: 3,
|
||||
}}
|
||||
onClick={createArticle}
|
||||
>
|
||||
{kind === "reply" ? "Send Reply" : "Save Note"}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
177
apps/link/app/tickets/[id]/@detail/_components/TicketDetail.tsx
Normal file
177
apps/link/app/tickets/[id]/@detail/_components/TicketDetail.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { getTicketQuery } from "@/app/_graphql/getTicketQuery";
|
||||
import {
|
||||
Grid,
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
} from "@mui/material";
|
||||
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||
import {
|
||||
MainContainer,
|
||||
ChatContainer,
|
||||
MessageList,
|
||||
Message,
|
||||
ConversationHeader,
|
||||
} from "@chatscope/chat-ui-kit-react";
|
||||
import { ArticleCreateDialog } from "./ArticleCreateDialog";
|
||||
|
||||
interface TicketDetailProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
|
||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||
{
|
||||
document: getTicketQuery,
|
||||
variables: { ticketId: `gid://zammad/Ticket/${id}` },
|
||||
},
|
||||
{ refreshInterval: 100000 }
|
||||
);
|
||||
|
||||
console.log({ ticketData, ticketError });
|
||||
const ticket = ticketData?.ticket;
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [articleKind, setArticleKind] = useState<"reply" | "note">("reply");
|
||||
const closeDialog = () => setDialogOpen(false);
|
||||
|
||||
const shouldRender = ticketData && !ticketError;
|
||||
|
||||
return (
|
||||
shouldRender && (
|
||||
<>
|
||||
<MainContainer>
|
||||
<ChatContainer>
|
||||
<ConversationHeader>
|
||||
<ConversationHeader.Content>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ fontFamily: "Poppins", fontWeight: 700 }}
|
||||
>
|
||||
{ticket.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ fontFamily: "Roboto", fontWeight: 400 }}
|
||||
>{`Ticket #${ticket.number} (created ${new Date(
|
||||
ticket.createdAt
|
||||
).toLocaleDateString()})`}</Typography>
|
||||
</Box>
|
||||
</ConversationHeader.Content>
|
||||
</ConversationHeader>
|
||||
<MessageList style={{ marginBottom: 80 }}>
|
||||
{ticket.articles.edges.map(({ node: article }: any) => (
|
||||
<Message
|
||||
key={article.id}
|
||||
className={
|
||||
article.internal
|
||||
? "internal-note"
|
||||
: article.sender.name === "Agent"
|
||||
? "outgoing-message"
|
||||
: "incoming-message"
|
||||
}
|
||||
model={{
|
||||
message: article.body.replace(/<div>*<br>*<div>/g, ""),
|
||||
sentTime: article.updated_at,
|
||||
sender: article.from,
|
||||
direction:
|
||||
article.sender === "Agent" ? "outgoing" : "incoming",
|
||||
position: "single",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</MessageList>
|
||||
</ChatContainer>
|
||||
<Box
|
||||
sx={{
|
||||
height: 80,
|
||||
background: "#eeeeee",
|
||||
borderTop: "1px solid #ddd",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="contained"
|
||||
disableElevation
|
||||
sx={{
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 700,
|
||||
borderRadius: 2,
|
||||
textTransform: "none",
|
||||
backgroundColor: "#1982FC",
|
||||
padding: "6px 30px",
|
||||
margin: "20px 0px",
|
||||
whiteSpace: "nowrap",
|
||||
py: "10px",
|
||||
mt: 2,
|
||||
}}
|
||||
onClick={() => {
|
||||
setArticleKind("reply");
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Reply to ticket
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="contained"
|
||||
disableElevation
|
||||
sx={{
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 700,
|
||||
borderRadius: 2,
|
||||
textTransform: "none",
|
||||
color: "black",
|
||||
backgroundColor: "#FFB620",
|
||||
padding: "6px 30px",
|
||||
margin: "20px 0px",
|
||||
whiteSpace: "nowrap",
|
||||
py: "10px",
|
||||
mt: 2,
|
||||
}}
|
||||
onClick={() => {
|
||||
setArticleKind("note");
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Write note to agent
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</MainContainer>
|
||||
<ArticleCreateDialog
|
||||
ticketID={ticket.internalId}
|
||||
open={dialogOpen}
|
||||
closeDialog={closeDialog}
|
||||
kind={articleKind}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
11
apps/link/app/tickets/[id]/@detail/page.tsx
Normal file
11
apps/link/app/tickets/[id]/@detail/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { TicketDetail } from "./_components/TicketDetail";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Page({ params: { id } }: PageProps) {
|
||||
return <TicketDetail id={id} />;
|
||||
}
|
||||
167
apps/link/app/tickets/[id]/@edit/_components/TicketEdit.tsx
Normal file
167
apps/link/app/tickets/[id]/@edit/_components/TicketEdit.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import {
|
||||
Grid,
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
Stack,
|
||||
Chip,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from "@mui/material";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { updateTicketMutation } from "@/app/_graphql/updateTicketMutation";
|
||||
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||
|
||||
interface TicketEditProps {
|
||||
ticket: any;
|
||||
}
|
||||
|
||||
export const TicketEdit: FC<TicketEditProps> = ({ ticket }) => {
|
||||
const [selectedGroup, setSelectedGroup] = useState(1);
|
||||
const [selectedOwner, setSelectedOwner] = useState(1);
|
||||
const [selectedPriority, setSelectedPriority] = useState(1);
|
||||
const [selectedState, setSelectedState] = useState(1);
|
||||
const [selectedTags, setSelectedTags] = useState(["tag1", "tag2"]);
|
||||
const handleDelete = () => {
|
||||
console.info("You clicked the delete icon.");
|
||||
};
|
||||
const restFetcher = (url: string) => fetch(url).then((r) => r.json());
|
||||
/*
|
||||
const { data: groups } = useSWR("/api/v1/groups", restFetcher);
|
||||
console.log({ groups });
|
||||
const { data: users } = useSWR("/api/v1/users", restFetcher);
|
||||
console.log({ users });
|
||||
const { data: states } = useSWR("/api/v1/ticket_states", restFetcher);
|
||||
console.log({ states });
|
||||
const { data: priorities } = useSWR("/api/v1/ticket_priorities", restFetcher);
|
||||
console.log({ priorities });
|
||||
|
||||
*/
|
||||
const groups = [];
|
||||
const users = [];
|
||||
const states = [];
|
||||
const priorities = [];
|
||||
|
||||
const { fetcher } = useSWRConfig();
|
||||
const updateTicket = async () => {
|
||||
await fetcher({
|
||||
document: updateTicketMutation,
|
||||
variables: {
|
||||
ticketId: ticket.id,
|
||||
input: {
|
||||
ownerId: `gid://zammad/User/${selectedOwner}`,
|
||||
tags: ["tag1", "tag2"],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ height: "100vh", background: "#ddd", p: 2 }}>
|
||||
<Grid container direction="column" spacing={3}>
|
||||
<Grid item>
|
||||
<Box sx={{ m: 1 }}>Group</Box>
|
||||
<Select
|
||||
defaultValue={selectedGroup}
|
||||
value={selectedGroup}
|
||||
onChange={(e: any) => {
|
||||
setSelectedGroup(e.target.value);
|
||||
updateTicket();
|
||||
}}
|
||||
size="small"
|
||||
sx={{
|
||||
width: "100%",
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
{groups?.map((group: any) => (
|
||||
<MenuItem key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Box sx={{ m: 1, mt: 0 }}>Owner</Box>
|
||||
<Select
|
||||
value={selectedOwner}
|
||||
onChange={(e: any) => {
|
||||
setSelectedOwner(e.target.value);
|
||||
updateTicket();
|
||||
}}
|
||||
size="small"
|
||||
sx={{
|
||||
width: "100%",
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
{users?.map((user: any) => (
|
||||
<MenuItem key={user.id} value={user.id}>
|
||||
{user.firstname} {user.lastname}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Box sx={{ m: 1, mt: 0 }}>State</Box>
|
||||
<Select
|
||||
value={selectedState}
|
||||
onChange={(e: any) => setSelectedState(e.target.value)}
|
||||
size="small"
|
||||
sx={{
|
||||
width: "100%",
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
{states?.map((state: any) => (
|
||||
<MenuItem key={state.id} value={state.id}>
|
||||
{state.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Box sx={{ m: 1, mt: 0 }}>Priority</Box>
|
||||
<Select
|
||||
value={selectedPriority}
|
||||
onChange={(e: any) => setSelectedPriority(e.target.value)}
|
||||
size="small"
|
||||
sx={{
|
||||
width: "100%",
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
{priorities?.map((priority: any) => (
|
||||
<MenuItem key={priority.id} value={priority.id}>
|
||||
{priority.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Box sx={{ mb: 1 }}>Tags</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{
|
||||
backgroundColor: "white",
|
||||
p: 1,
|
||||
borderRadius: "6px",
|
||||
border: "1px solid #bbb",
|
||||
minHeight: 120,
|
||||
}}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
{selectedTags.map((tag: string) => (
|
||||
<Chip key={tag} label={tag} onDelete={handleDelete} />
|
||||
))}
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
5
apps/link/app/tickets/[id]/@edit/page.tsx
Normal file
5
apps/link/app/tickets/[id]/@edit/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { TicketEdit } from "./_components/TicketEdit";
|
||||
|
||||
export default function Page() {
|
||||
return <TicketEdit ticket={undefined} />;
|
||||
}
|
||||
11
apps/link/app/tickets/[id]/error.tsx
Normal file
11
apps/link/app/tickets/[id]/error.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { DisplayError } from "app/_components/DisplayError";
|
||||
|
||||
type PageProps = {
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export default function Page({ error }: PageProps) {
|
||||
return <DisplayError error={error} />;
|
||||
}
|
||||
25
apps/link/app/tickets/[id]/layout.tsx
Normal file
25
apps/link/app/tickets/[id]/layout.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { Grid } from "@mui/material";
|
||||
|
||||
type LayoutProps = {
|
||||
detail: any;
|
||||
edit: any;
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Layout({ detail, edit, params: { id } }: LayoutProps) {
|
||||
return (
|
||||
<Grid container spacing={0} sx={{ height: "100vh" }} direction="row">
|
||||
<Grid item sx={{ height: "100vh" }} xs={9}>
|
||||
{detail}
|
||||
</Grid>
|
||||
<Grid item xs={3} sx={{ height: "100vh" }}>
|
||||
{edit}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
15
apps/link/app/tickets/[id]/not-found.tsx
Normal file
15
apps/link/app/tickets/[id]/not-found.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Not Found</h2>
|
||||
<p>Could not find requested resource</p>
|
||||
<p>
|
||||
View <Link href="/blog">all posts</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +1,22 @@
|
|||
type PageProps = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Page({ params: { id } }: PageProps) {
|
||||
return <div>Page</div>;
|
||||
}
|
||||
|
||||
/*
|
||||
import { GetServerSideProps, GetServerSidePropsContext } from "next";
|
||||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
import { NextPage } from "next";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Layout } from "components/Layout";
|
||||
import { TicketDetail } from "components/TicketDetail";
|
||||
import { TicketEdit } from "components/TicketEdit";
|
||||
import { getTicketQuery } from "graphql/getTicketQuery";
|
||||
import { TicketDetail } from "@/app/_components/TicketDetail";
|
||||
import { TicketEdit } from "@/app/_components/TicketEdit";
|
||||
import { getTicketQuery } from "@/app/_graphql/getTicketQuery";
|
||||
|
||||
type TicketProps = {
|
||||
id: string;
|
||||
|
|
@ -24,10 +34,7 @@ const Ticket: NextPage<TicketProps> = ({ id }) => {
|
|||
const shouldRender = !ticketError && ticketData;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<>
|
||||
{shouldRender && (
|
||||
<Grid container spacing={0} sx={{ height: "100vh" }} direction="row">
|
||||
<Grid item sx={{ height: "100vh" }} xs={9}>
|
||||
|
|
@ -39,7 +46,7 @@ const Ticket: NextPage<TicketProps> = ({ id }) => {
|
|||
</Grid>
|
||||
)}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -50,4 +57,6 @@ export const getServerSideProps: GetServerSideProps = async (
|
|||
return { props: { id } };
|
||||
};
|
||||
|
||||
|
||||
export default Ticket;
|
||||
*/
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import { FC, useState } from "react";
|
||||
import { Grid, Button, Dialog, DialogActions, DialogContent, TextField } from "@mui/material";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { updateTicketMutation } from "graphql/updateTicketMutation";
|
||||
|
||||
interface ArticleCreateDialogProps {
|
||||
ticketID: string;
|
||||
open: boolean;
|
||||
closeDialog: () => void;
|
||||
kind: "reply" | "note";
|
||||
}
|
||||
|
||||
export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({ ticketID, open, closeDialog, kind }) => {
|
||||
const [body, setBody] = useState("");
|
||||
const backgroundColor = kind === "reply" ? "#1982FC" : "#FFB620";
|
||||
const color = kind === "reply" ? "white" : "black";
|
||||
const { fetcher } = useSWRConfig();
|
||||
const createArticle = async () => {
|
||||
await fetcher(
|
||||
{
|
||||
document: updateTicketMutation,
|
||||
variables: {
|
||||
ticketId: `gid://zammad/Ticket/${ticketID}`,
|
||||
input: {
|
||||
article: {
|
||||
body,
|
||||
type: kind === "note" ? "note" : "phone",
|
||||
internal: kind === "note"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
closeDialog();
|
||||
setBody("");
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} maxWidth="sm" fullWidth >
|
||||
<DialogContent>
|
||||
<TextField label={kind === "reply" ? "Write reply" : "Write internal note"} multiline rows={10} fullWidth value={body} onChange={(e: any) => setBody(e.target.value)} />
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pt: 0, pb: 3 }}>
|
||||
<Grid container justifyContent="space-between">
|
||||
<Grid item>
|
||||
<Button sx={{
|
||||
backgroundColor: "white", color: "#666", fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 700,
|
||||
borderRadius: 2,
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={() => { setBody(""); closeDialog() }}>Cancel</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button sx={{
|
||||
backgroundColor, color, fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 700,
|
||||
borderRadius: 2,
|
||||
textTransform: "none",
|
||||
px: 3
|
||||
}}
|
||||
onClick={createArticle}
|
||||
>{kind === "reply" ? "Send Reply" : "Save Note"}</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</Dialog >
|
||||
|
||||
);
|
||||
};
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import getConfig from "next/config";
|
||||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Layout } from "components/Layout";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
type LeafcutterWrapperProps = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const LeafcutterWrapper: FC<LeafcutterWrapperProps> = ({ path }) => {
|
||||
const {
|
||||
publicRuntimeConfig: { linkURL },
|
||||
} = getConfig();
|
||||
const fullLeafcutterURL = `${linkURL}/proxy/leafcutter/${path}`;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||
<Iframe
|
||||
id="leafcutter"
|
||||
url={fullLeafcutterURL}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder={0}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import getConfig from "next/config";
|
||||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Layout } from "components/Layout";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
type MetamigoWrapperProps = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const MetamigoWrapper: FC<MetamigoWrapperProps> = ({ path }) => {
|
||||
const {
|
||||
publicRuntimeConfig: { linkURL },
|
||||
} = getConfig();
|
||||
const fullMetamigoURL = `${linkURL}/metamigo/${path}`;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||
<Iframe
|
||||
id="link"
|
||||
url={fullMetamigoURL}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder={0}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
import { FC, useState } from "react";
|
||||
import { Grid, Box, Typography, Button, Dialog, DialogActions, DialogContent } from "@mui/material";
|
||||
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||
import {
|
||||
MainContainer,
|
||||
ChatContainer,
|
||||
MessageList,
|
||||
Message,
|
||||
ConversationHeader,
|
||||
} from "@chatscope/chat-ui-kit-react";
|
||||
import { ArticleCreateDialog } from "./ArticleCreateDialog";
|
||||
|
||||
interface TicketDetailProps {
|
||||
ticket: any;
|
||||
}
|
||||
|
||||
export const TicketDetail: FC<TicketDetailProps> = ({ ticket }) => {
|
||||
console.log({ ticket })
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [articleKind, setArticleKind] = useState<"reply" | "note">("reply");
|
||||
const closeDialog = () => setDialogOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainContainer>
|
||||
<ChatContainer>
|
||||
<ConversationHeader>
|
||||
<ConversationHeader.Content>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ fontFamily: "Poppins", fontWeight: 700 }}
|
||||
>
|
||||
{ticket.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ fontFamily: "Roboto", fontWeight: 400 }}
|
||||
>{`Ticket #${ticket.number} (created ${new Date(
|
||||
ticket.createdAt
|
||||
).toLocaleDateString()})`}</Typography>
|
||||
</Box>
|
||||
</ConversationHeader.Content>
|
||||
</ConversationHeader>
|
||||
<MessageList style={{ marginBottom: 80 }}>
|
||||
{ticket.articles.edges.map(({ node: article }: any) => (
|
||||
<Message
|
||||
key={article.id}
|
||||
className={
|
||||
article.internal
|
||||
? "internal-note"
|
||||
: article.sender.name === "Agent"
|
||||
? "outgoing-message"
|
||||
: "incoming-message"
|
||||
}
|
||||
model={{
|
||||
message: article.body.replace(/<div>*<br>*<div>/g, ""),
|
||||
sentTime: article.updated_at,
|
||||
sender: article.from,
|
||||
direction:
|
||||
article.sender === "Agent" ? "outgoing" : "incoming",
|
||||
position: "single",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</MessageList>
|
||||
{/* <MessageInput
|
||||
placeholder="Type message here"
|
||||
sendOnReturnDisabled
|
||||
attachButton={false}
|
||||
sendButton={false}
|
||||
/> */}
|
||||
</ChatContainer>
|
||||
<Box
|
||||
sx={{
|
||||
height: 80,
|
||||
background: "#eeeeee",
|
||||
borderTop: "1px solid #ddd",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="contained"
|
||||
disableElevation
|
||||
sx={{
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 700,
|
||||
borderRadius: 2,
|
||||
textTransform: "none",
|
||||
backgroundColor: "#1982FC",
|
||||
padding: "6px 30px",
|
||||
margin: "20px 0px",
|
||||
whiteSpace: "nowrap",
|
||||
py: "10px",
|
||||
mt: 2
|
||||
}}
|
||||
onClick={() => {
|
||||
setArticleKind("reply");
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Reply to ticket
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="contained"
|
||||
disableElevation
|
||||
sx={{
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
fontWeight: 700,
|
||||
borderRadius: 2,
|
||||
textTransform: "none",
|
||||
color: "black",
|
||||
backgroundColor: "#FFB620",
|
||||
padding: "6px 30px",
|
||||
margin: "20px 0px",
|
||||
whiteSpace: "nowrap",
|
||||
py: "10px",
|
||||
mt: 2
|
||||
}}
|
||||
onClick={() => {
|
||||
setArticleKind("note");
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Write note to agent
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</MainContainer>
|
||||
<ArticleCreateDialog ticketID={ticket.internalId} open={dialogOpen} closeDialog={closeDialog} kind={articleKind} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import { FC, useEffect, useState } from "react";
|
||||
import { Grid, Box, Typography, TextField, Stack, Chip, Select, MenuItem } from "@mui/material";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { updateTicketMutation } from "graphql/updateTicketMutation";
|
||||
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
||||
|
||||
interface TicketEditProps {
|
||||
ticket: any;
|
||||
}
|
||||
|
||||
export const TicketEdit: FC<TicketEditProps> = ({ ticket }) => {
|
||||
const [selectedGroup, setSelectedGroup] = useState(1);
|
||||
const [selectedOwner, setSelectedOwner] = useState(1);
|
||||
const [selectedPriority, setSelectedPriority] = useState(1);
|
||||
const [selectedState, setSelectedState] = useState(1);
|
||||
const [selectedTags, setSelectedTags] = useState(["tag1", "tag2"]);
|
||||
const handleDelete = () => {
|
||||
console.info("You clicked the delete icon.");
|
||||
};
|
||||
const restFetcher = (url: string) => fetch(url).then((r) => r.json());
|
||||
const { data: groups } = useSWR("/api/v1/groups", restFetcher);
|
||||
console.log({ groups });
|
||||
const { data: users } = useSWR("/api/v1/users", restFetcher);
|
||||
console.log({ users });
|
||||
const { data: states } = useSWR("/api/v1/ticket_states", restFetcher);
|
||||
console.log({ states });
|
||||
const { data: priorities } = useSWR("/api/v1/ticket_priorities", restFetcher);
|
||||
console.log({ priorities });
|
||||
|
||||
const { fetcher } = useSWRConfig();
|
||||
const updateTicket = async () => {
|
||||
await fetcher(
|
||||
{
|
||||
document: updateTicketMutation,
|
||||
variables: {
|
||||
ticketId: ticket.id,
|
||||
input: {
|
||||
ownerId: `gid://zammad/User/${selectedOwner}`,
|
||||
tags: ["tag1", "tag2"],
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ height: "100vh", background: "#ddd", p: 2 }}>
|
||||
<Grid container direction="column" spacing={3}>
|
||||
<Grid item>
|
||||
<Box sx={{ m: 1 }}>Group</Box>
|
||||
<Select defaultValue={selectedGroup} value={selectedGroup} onChange={(e: any) => { setSelectedGroup(e.target.value); updateTicket() }} size="small" sx={{
|
||||
width: "100%",
|
||||
backgroundColor: "white"
|
||||
}} >
|
||||
{groups?.map((group: any) => <MenuItem key={group.id} value={group.id}>{group.name}</MenuItem>)}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Box sx={{ m: 1, mt: 0 }}>Owner</Box>
|
||||
<Select value={selectedOwner} onChange={(e: any) => { setSelectedOwner(e.target.value); updateTicket() }} size="small" sx={{
|
||||
width: "100%",
|
||||
backgroundColor: "white",
|
||||
}} >
|
||||
{users?.map((user: any) => <MenuItem key={user.id} value={user.id}>{user.firstname} {user.lastname}</MenuItem>)}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Box sx={{ m: 1, mt: 0 }}>State</Box>
|
||||
<Select value={selectedState} onChange={(e: any) => setSelectedState(e.target.value)} size="small" sx={{
|
||||
width: "100%",
|
||||
backgroundColor: "white"
|
||||
}} >
|
||||
{states?.map((state: any) => <MenuItem key={state.id} value={state.id}>{state.name}</MenuItem>)}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Box sx={{ m: 1, mt: 0 }}>Priority</Box>
|
||||
<Select value={selectedPriority} onChange={(e: any) => setSelectedPriority(e.target.value)} size="small" sx={{
|
||||
width: "100%",
|
||||
backgroundColor: "white"
|
||||
}} >
|
||||
{priorities?.map((priority: any) => <MenuItem key={priority.id} value={priority.id}>{priority.name}</MenuItem>)}
|
||||
</Select>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Box sx={{ mb: 1, }}>Tags</Box>
|
||||
<Stack direction="row" spacing={1} sx={{ backgroundColor: "white", p: 1, borderRadius: "6px", border: "1px solid #bbb", minHeight: 120 }} flexWrap="wrap">
|
||||
{selectedTags.map((tag: string) => <Chip key={tag} label={tag} onDelete={handleDelete} />)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box >
|
||||
);
|
||||
};
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const InternalZammadWrapper = dynamic(
|
||||
import("./InternalZammadWrapper").then((mod) => mod.InternalZammadWrapper),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
type ZammadWrapperProps = {
|
||||
path: string;
|
||||
hideSidebar?: boolean;
|
||||
};
|
||||
|
||||
export const ZammadWrapper: FC<ZammadWrapperProps> = ({
|
||||
path,
|
||||
hideSidebar,
|
||||
}) => <InternalZammadWrapper path={path} hideSidebar={hideSidebar} />;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import createCache from "@emotion/cache";
|
||||
|
||||
export default function createEmotionCache() {
|
||||
return createCache({ key: "css" });
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ const rewriteURL = (request: NextRequestWithAuth, originBaseURL: string, destina
|
|||
|
||||
requestHeaders.delete('connection');
|
||||
|
||||
console.log({ finalHeaders: requestHeaders });
|
||||
// console.log({ finalHeaders: requestHeaders });
|
||||
|
||||
return NextResponse.rewrite(new URL(destinationURL), { request: { headers: requestHeaders } });
|
||||
};
|
||||
|
|
@ -43,14 +43,14 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
|
|||
console.log('proxying to zammad');
|
||||
const { token } = request.nextauth;
|
||||
|
||||
console.log({ nextauth: request.nextauth });
|
||||
// console.log({ nextauth: request.nextauth });
|
||||
|
||||
const headers = {
|
||||
'X-Forwarded-User': token.email.toLowerCase(),
|
||||
host: 'link-stack-dev.digiresilience.org'
|
||||
};
|
||||
|
||||
console.log({ headers });
|
||||
// console.log({ headers });
|
||||
|
||||
return rewriteURL(request, `${linkBaseURL}/proxy/zammad`, zammadURL, headers);
|
||||
} else if (request.nextUrl.pathname.startsWith('/assets') || request.nextUrl.pathname.startsWith('/api/v1')) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
modularizeImports: {
|
||||
"@mui/material": {
|
||||
transform: "@mui/material/{{member}}",
|
||||
},
|
||||
"@mui/icons-material": {
|
||||
transform: "@mui/icons-material/{{member}}",
|
||||
},
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
linkURL: process.env.LINK_URL ?? "http://localhost:3000",
|
||||
leafcutterURL: process.env.LEAFCUTTER_URL ?? "http://localhost:3001",
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@
|
|||
"@mui/icons-material": "^5",
|
||||
"@mui/lab": "^5.0.0-alpha.134",
|
||||
"@mui/material": "^5",
|
||||
"@mui/x-data-grid-pro": "^6.8.0",
|
||||
"@mui/x-date-pickers-pro": "^6.8.0",
|
||||
"@mui/x-data-grid-pro": "^6.9.0",
|
||||
"@mui/x-date-pickers-pro": "^6.9.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"graphql-request": "^6.1.0",
|
||||
"material-ui-popup-state": "^5.0.9",
|
||||
"next": "13.4.6",
|
||||
"next": "13.4.7",
|
||||
"next-auth": "^4.22.1",
|
||||
"react": "18.2.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
|
|
@ -34,17 +34,18 @@
|
|||
"react-iframe": "^1.8.5",
|
||||
"react-polyglot": "^0.7.2",
|
||||
"sharp": "^0.32.1",
|
||||
"swr": "^2.1.5"
|
||||
"swr": "^2.2.0",
|
||||
"tss-react": "^4.8.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/react": "18.2.13",
|
||||
"@types/react": "18.2.14",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"babel-loader": "^9.1.2",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-next": "^13.4.6",
|
||||
"eslint-config-next": "^13.4.7",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
|
|
|
|||
BIN
apps/link/pages/.DS_Store
vendored
BIN
apps/link/pages/.DS_Store
vendored
Binary file not shown.
|
|
@ -1,13 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import Head from "next/head";
|
||||
import { Layout } from "components/Layout";
|
||||
|
||||
const FourOhFour: FC = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export default FourOhFour;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import Head from "next/head";
|
||||
import { Layout } from "components/Layout";
|
||||
|
||||
const FiveHundred: FC = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export default FiveHundred;
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { useState } from "react";
|
||||
import { AppProps } from "next/app";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { CacheProvider, EmotionCache } from "@emotion/react";
|
||||
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
|
||||
import createEmotionCache from "lib/createEmotionCache";
|
||||
import "@fontsource/poppins/400.css";
|
||||
import "@fontsource/poppins/700.css";
|
||||
import "@fontsource/roboto/400.css";
|
||||
import "@fontsource/roboto/700.css";
|
||||
import "@fontsource/playfair-display/900.css";
|
||||
import "styles/global.css";
|
||||
import { LicenseInfo } from "@mui/x-data-grid-pro";
|
||||
import { SWRConfig } from "swr";
|
||||
import { GraphQLClient } from "graphql-request";
|
||||
import getConfig from "next/config";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
LicenseInfo.setLicenseKey(publicRuntimeConfig.muiLicenseKey);
|
||||
|
||||
const clientSideEmotionCache: any = createEmotionCache();
|
||||
|
||||
interface LinkWebProps extends AppProps {
|
||||
// eslint-disable-next-line react/require-default-props
|
||||
emotionCache?: EmotionCache;
|
||||
}
|
||||
|
||||
export default function LinkWeb(props: LinkWebProps) {
|
||||
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
|
||||
|
||||
const [csrfToken, setCsrfToken] = useState("");
|
||||
const origin =
|
||||
typeof window !== "undefined" && window.location.origin
|
||||
? window.location.origin
|
||||
: null;
|
||||
const client = new GraphQLClient(`${origin}/proxy/zammad/graphql`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
const graphQLFetcher = async ({ document, variables }: any) => {
|
||||
const requestHeaders = {
|
||||
"X-CSRF-Token": csrfToken,
|
||||
};
|
||||
const { data, headers } = await client.rawRequest(
|
||||
document,
|
||||
variables,
|
||||
requestHeaders
|
||||
);
|
||||
|
||||
const token = headers.get("CSRF-Token");
|
||||
setCsrfToken(token);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return (
|
||||
<SessionProvider session={(pageProps as any).session}>
|
||||
<CacheProvider value={emotionCache}>
|
||||
<SWRConfig value={{ fetcher: graphQLFetcher }}>
|
||||
<CssBaseline />
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<Component {...pageProps} />
|
||||
</LocalizationProvider>
|
||||
</SWRConfig>
|
||||
</CacheProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
// eslint-disable-next-line no-use-before-define
|
||||
import * as React from "react";
|
||||
import Document, { Html, Head, Main, NextScript } from "next/document";
|
||||
import createEmotionServer from "@emotion/server/create-instance";
|
||||
import createEmotionCache from "lib/createEmotionCache";
|
||||
|
||||
export default class LinkDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LinkDocument.getInitialProps = async (ctx) => {
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
const cache = createEmotionCache();
|
||||
const { extractCriticalToChunks } = createEmotionServer(cache as any);
|
||||
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: (App: any) => (props: any) =>
|
||||
<App emotionCache={cache} {...props} />,
|
||||
});
|
||||
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
const emotionStyles = extractCriticalToChunks(initialProps.html);
|
||||
const emotionStyleTags = emotionStyles.styles.map((style) => (
|
||||
<style
|
||||
data-emotion={`${style.key} ${style.ids.join(" ")}`}
|
||||
key={style.key}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: style.css }}
|
||||
/>
|
||||
));
|
||||
|
||||
return {
|
||||
...initialProps,
|
||||
styles: [
|
||||
...React.Children.toArray(initialProps.styles),
|
||||
...emotionStyleTags,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { NextPage } from "next";
|
||||
import getConfig from "next/config";
|
||||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Layout } from "components/Layout";
|
||||
import Iframe from "react-iframe";
|
||||
|
||||
const Metamigo: NextPage = () => {
|
||||
const {
|
||||
publicRuntimeConfig: { metamigoURL },
|
||||
} = getConfig();
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid item sx={{ height: "100vh", width: "100%" }}>
|
||||
<Iframe
|
||||
id="link"
|
||||
url={metamigoURL}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder={0}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Metamigo;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Layout } from "components/Layout";
|
||||
import { ZammadWrapper } from "components/ZammadWrapper";
|
||||
|
||||
const Zammad: FC = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ZammadWrapper path="/#manage" hideSidebar={false} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export default Zammad;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.redirect(307, '/proxy/zammad/api/v1' + req.url.substring('/api/v1'.length));
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Layout } from "components/Layout";
|
||||
import { ZammadWrapper } from "components/ZammadWrapper";
|
||||
|
||||
const Home = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ZammadWrapper path="/#dashboard" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export default Home;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Layout } from "components/Layout";
|
||||
import { ZammadWrapper } from "components/ZammadWrapper";
|
||||
|
||||
const Assigned: FC = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ZammadWrapper path="/#knowledge_base/1/locale/en-us" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export default Assigned;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { NextPage } from "next";
|
||||
import { LeafcutterWrapper } from "components/LeafcutterWrapper";
|
||||
|
||||
const About: NextPage = () => <LeafcutterWrapper path="about" />;
|
||||
|
||||
export default About;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { NextPage } from "next";
|
||||
import { LeafcutterWrapper } from "components/LeafcutterWrapper";
|
||||
|
||||
const Create: NextPage = () => <LeafcutterWrapper path="create" />;
|
||||
|
||||
export default Create;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { NextPage } from "next";
|
||||
import { LeafcutterWrapper } from "components/LeafcutterWrapper";
|
||||
|
||||
const FAQ: NextPage = () => <LeafcutterWrapper path="faq" />;
|
||||
|
||||
export default FAQ;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { NextPage } from "next";
|
||||
import { LeafcutterWrapper } from "components/LeafcutterWrapper";
|
||||
|
||||
const Dashboard: NextPage = () => <LeafcutterWrapper path="" />;
|
||||
|
||||
export default Dashboard;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { NextPage } from "next";
|
||||
import { LeafcutterWrapper } from "components/LeafcutterWrapper";
|
||||
|
||||
const Trends: NextPage = () => <LeafcutterWrapper path="trends" />;
|
||||
|
||||
export default Trends;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Layout } from "components/Layout";
|
||||
import { ZammadWrapper } from "components/ZammadWrapper";
|
||||
|
||||
const Profile: NextPage = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
sx={{ height: "100%", width: "100%" }}
|
||||
direction="column"
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ZammadWrapper path="/#profile" hideSidebar={false} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export default Profile;
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
import { NextPage } from "next";
|
||||
import { Layout } from "components/Layout";
|
||||
import { TicketList } from "components/TicketList";
|
||||
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
|
||||
|
||||
const Pending: NextPage = () => {
|
||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||
{
|
||||
document: getTicketsByOverviewQuery,
|
||||
variables: { overviewId: "gid://zammad/Overview/3" },
|
||||
},
|
||||
{ refreshInterval: 10000 }
|
||||
);
|
||||
|
||||
const shouldRender = !ticketError && ticketData;
|
||||
const tickets =
|
||||
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell – Assigned Tickets</title>
|
||||
</Head>
|
||||
{shouldRender && <TicketList title="Pending" tickets={tickets} />}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pending;
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
import { NextPage } from "next";
|
||||
import { Layout } from "components/Layout";
|
||||
import { TicketList } from "components/TicketList";
|
||||
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
|
||||
|
||||
const Unassigned: NextPage = () => {
|
||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||
{
|
||||
document: getTicketsByOverviewQuery,
|
||||
variables: { overviewId: "gid://zammad/Overview/2" },
|
||||
},
|
||||
{ refreshInterval: 10000 }
|
||||
);
|
||||
|
||||
const shouldRender = !ticketError && ticketData;
|
||||
const tickets =
|
||||
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell – Assigned Tickets</title>
|
||||
</Head>
|
||||
{shouldRender && <TicketList title="Unassigned" tickets={tickets} />}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Unassigned;
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import Head from "next/head";
|
||||
import useSWR from "swr";
|
||||
import { NextPage } from "next";
|
||||
import { Layout } from "components/Layout";
|
||||
import { TicketList } from "components/TicketList";
|
||||
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
|
||||
|
||||
const Urgent: NextPage = () => {
|
||||
const { data: ticketData, error: ticketError }: any = useSWR(
|
||||
{
|
||||
document: getTicketsByOverviewQuery,
|
||||
variables: { overviewId: "gid://zammad/Overview/7" },
|
||||
},
|
||||
{ refreshInterval: 10000 }
|
||||
);
|
||||
|
||||
const shouldRender = !ticketError && ticketData;
|
||||
const tickets =
|
||||
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell – Urgent Tickets</title>
|
||||
</Head>
|
||||
{shouldRender && <TicketList title="Urgent" tickets={tickets} />}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Urgent;
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
|
|
@ -15,10 +19,25 @@
|
|||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["./*", "../../node_modules/*"]
|
||||
"@/*": [
|
||||
"./*",
|
||||
"../../node_modules/*"
|
||||
]
|
||||
},
|
||||
"baseUrl": "."
|
||||
"baseUrl": ".",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue