Compare commits

...
Sign in to create a new pull request.

7 commits
main ... mobile

Author SHA1 Message Date
N-Pex
8c7e19bae8 Hide some columns in ticket list (on mobile) 2024-09-12 15:56:57 +02:00
N-Pex
6440c402cf Mobile friendly version of ticket view
Refactor the header into own component, so it can be shown at "page top" for the mobile one column view.
2024-09-12 15:45:20 +02:00
N-Pex
047ef094fc Hide reporting, admin and profile in sidebar (for mobile)
Also, have the "Zammad Interface" link go to mobile version.
2024-09-12 15:43:48 +02:00
N-Pex
dca61e1459 Close menu on selection (mobile only) 2024-09-12 15:42:08 +02:00
N-Pex
89fdd955fe Fix padding on mobile 2024-09-09 10:24:15 +02:00
N-Pex
566d3643de Hide overflow during load 2024-09-09 10:24:05 +02:00
N-Pex
b621014178 Link mobile layout 2024-09-09 10:19:36 +02:00
10 changed files with 254 additions and 62 deletions

View file

@ -1,9 +1,30 @@
"use client"; "use client";
import { FC, PropsWithChildren, useState } from "react"; import { FC, PropsWithChildren, useEffect, useState } from "react";
import { Grid, Box } from "@mui/material";
import { Sidebar } from "./Sidebar";
import { SetupModeWarning } from "./SetupModeWarning"; import { SetupModeWarning } from "./SetupModeWarning";
import {
AppBar as MuiAppBar,
ToolbarProps,
Box,
Grid,
IconButton,
styled,
Toolbar,
Typography,
Menu,
useTheme,
useMediaQuery,
} from "@mui/material";
import { Sidebar } from "./Sidebar";
import {
ExpandCircleDown,
MenuBookTwoTone,
MenuSharp,
} from "@mui/icons-material";
import { fonts } from "@link-stack/ui";
import Image from "next/image";
import LinkLogo from "public/link-logo-small.png";
import { usePathname } from 'next/navigation';
interface InternalLayoutProps extends PropsWithChildren { interface InternalLayoutProps extends PropsWithChildren {
setupModeActive: boolean; setupModeActive: boolean;
@ -15,20 +36,102 @@ export const InternalLayout: FC<InternalLayoutProps> = ({
setupModeActive, setupModeActive,
leafcutterEnabled, leafcutterEnabled,
}) => { }) => {
const [open, setOpen] = useState(true); const { poppins } = fonts;
const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down("sm"));
const [open, setOpen] = useState(false);
const [openWidth, setOpenWidth] = useState<number>(0);
const [closedWidth, setClosedWidth] = useState<number>(0);
const pathname = usePathname();
useEffect(() => {
// On mobile, close menu when stuff is selected
if (mobile) {
setOpen(false);
}
}, [pathname]);
useEffect(() => {
setClosedWidth(mobile ? 0 : 70);
setOpenWidth(270);
setOpen(!mobile);
}, [mobile]);
interface HeaderBarProps extends ToolbarProps {
open?: boolean;
}
const HeaderBar = styled(Toolbar, {
shouldForwardProp: (prop) => prop !== "open",
})<HeaderBarProps>(({ theme, open }) => ({
backgroundColor: "#25272A",
width: "100%",
height: "56px",
}));
return ( return (
<Box sx={{ position: "relative" }}> <Box sx={{ position: "relative" }}>
<SetupModeWarning setupModeActive={setupModeActive} /> <SetupModeWarning setupModeActive={setupModeActive} />
<Grid container direction="row"> <Grid container direction="row">
{mobile ? (<MuiAppBar>
<HeaderBar>
<IconButton
sx={{ color: "white" }}
aria-label="open drawer"
onClick={() => setOpen(true)}
>
<MenuSharp />
</IconButton>
<Typography
variant="h2"
sx={{
fontSize: 26,
color: "white",
fontWeight: 700,
fontFamily: poppins.style.fontFamily,
flexGrow: 1,
}}
>
CDR Link
</Typography>
<Box
sx={{
width: "40px",
height: "40px",
margin: open ? "0" : "0 auto",
}}
>
<Image
src={LinkLogo}
alt="Link logo"
width={40}
height={40}
style={{
objectFit: "cover",
filter: "grayscale(100) brightness(100)",
}}
/>
</Box>
</HeaderBar>
</MuiAppBar>) : (null)}
<Sidebar <Sidebar
open={open} open={open}
setOpen={setOpen} setOpen={setOpen}
leafcutterEnabled={leafcutterEnabled} openWidth={openWidth}
closedWidth={closedWidth}
/> />
<Grid <Grid
item item
sx={{ ml: open ? "270px" : "70px", width: "100%", height: "100vh" }} sx={{
mt: { xs: "56px", sm: "0" },
ml: { xs: open ? "270px" : "0px", sm: open ? "270px" : "70px" },
width: "100%",
height: { xs: "calc(100vh - 56px)", sm: "100vh" },
position: "relative",
}}
> >
{children as any} {children as any}
</Grid> </Grid>

View file

@ -12,6 +12,8 @@ import {
ListItemSecondaryAction, ListItemSecondaryAction,
Drawer, Drawer,
Collapse, Collapse,
useTheme,
useMediaQuery,
} from "@mui/material"; } from "@mui/material";
import { import {
FeaturedPlayList as FeaturedPlayListIcon, FeaturedPlayList as FeaturedPlayListIcon,
@ -35,9 +37,6 @@ import { getOverviewTicketCountsAction } from "app/_actions/overviews";
import { SearchBox } from "./SearchBox"; import { SearchBox } from "./SearchBox";
import { fonts } from "@link-stack/ui"; import { fonts } from "@link-stack/ui";
const openWidth = 270;
const closedWidth = 70;
const MenuItem = ({ const MenuItem = ({
name, name,
href, href,
@ -178,14 +177,12 @@ const MenuItem = ({
interface SidebarProps { interface SidebarProps {
open: boolean; open: boolean;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
openWidth: number;
closedWidth: number;
leafcutterEnabled?: boolean; leafcutterEnabled?: boolean;
} }
export const Sidebar: FC<SidebarProps> = ({ export const Sidebar: FC<SidebarProps> = ({ open, setOpen, openWidth, closedWidth, leafcutterEnabled = false }) => {
open,
setOpen,
leafcutterEnabled = false,
}) => {
const pathname = usePathname(); const pathname = usePathname();
const { data: session } = useSession(); const { data: session } = useSession();
const [overviewCounts, setOverviewCounts] = useState<any>(null); const [overviewCounts, setOverviewCounts] = useState<any>(null);
@ -207,6 +204,9 @@ export const Sidebar: FC<SidebarProps> = ({
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down('sm'));
const logout = () => { const logout = () => {
signOut({ callbackUrl: "/login" }); signOut({ callbackUrl: "/login" });
}; };
@ -214,14 +214,15 @@ export const Sidebar: FC<SidebarProps> = ({
return ( return (
<Drawer <Drawer
sx={{ width: open ? openWidth : closedWidth, flexShrink: 0 }} sx={{ width: open ? openWidth : closedWidth, flexShrink: 0 }}
variant="permanent" variant={mobile ? "temporary" : "permanent"}
anchor="left" anchor="left"
open={open} open={open}
onClose={() => {if (mobile) setOpen(false)}}
PaperProps={{ PaperProps={{
sx: { sx: {
width: open ? openWidth : closedWidth, width: open ? openWidth : closedWidth,
border: 0, border: 0,
overflow: "visible", overflow: openWidth == 0 ? "hidden" : "visible",
}, },
}} }}
> >
@ -493,14 +494,14 @@ export const Sidebar: FC<SidebarProps> = ({
selected={pathname.endsWith("/docs")} selected={pathname.endsWith("/docs")}
open={open} open={open}
/> />
<MenuItem {!mobile && (<MenuItem
name="Reporting" name="Reporting"
href="/reporting" href="/reporting"
Icon={AssessmentIcon} Icon={AssessmentIcon}
iconSize={20} iconSize={20}
selected={pathname.endsWith("/reporting")} selected={pathname.endsWith("/reporting")}
open={open} open={open}
/> />)}
{leafcutterEnabled && ( {leafcutterEnabled && (
<MenuItem <MenuItem
name="Leafcutter" name="Leafcutter"
@ -557,6 +558,7 @@ export const Sidebar: FC<SidebarProps> = ({
/> />
</List> </List>
</Collapse> </Collapse>
{!mobile && (
<MenuItem <MenuItem
name="Profile" name="Profile"
href="/profile" href="/profile"
@ -565,7 +567,8 @@ export const Sidebar: FC<SidebarProps> = ({
selected={pathname.endsWith("/profile")} selected={pathname.endsWith("/profile")}
open={open} open={open}
/> />
{roles.includes("admin") && ( )}
{roles.includes("admin") && !mobile && (
<> <>
<MenuItem <MenuItem
name="Admin" name="Admin"
@ -655,7 +658,7 @@ export const Sidebar: FC<SidebarProps> = ({
)} )}
<MenuItem <MenuItem
name="Zammad Interface" name="Zammad Interface"
href="/zammad" href={mobile ? "/mobile" : "/zammad"}
Icon={DvrIcon} Icon={DvrIcon}
iconSize={20} iconSize={20}
open={open} open={open}

View file

@ -115,6 +115,14 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
// @ts-ignore // @ts-ignore
linkElement.contentDocument.querySelector("#navigation").style = linkElement.contentDocument.querySelector("#navigation").style =
"display: none"; "display: none";
// @ts-ignore
if (linkElement.contentDocument.querySelector(".content")) {
// If navigation removed, set content margin to 0 to avoid gap.
// @ts-ignore
linkElement.contentDocument.querySelector(".content").style =
"margin-left: 0";
}
// @ts-ignore // @ts-ignore
linkElement.contentDocument.querySelector("body").style = linkElement.contentDocument.querySelector("body").style =
"font-family: Arial"; "font-family: Arial";

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { FC, useState } from "react"; import { FC, useState } from "react";
import { Grid, Box } from "@mui/material"; import { Grid, Box, useTheme, useMediaQuery } from "@mui/material";
import { GridColDef } from "@mui/x-data-grid-pro"; import { GridColDef } from "@mui/x-data-grid-pro";
import { StyledDataGrid } from "app/(main)/_components/StyledDataGrid"; import { StyledDataGrid } from "app/(main)/_components/StyledDataGrid";
import { Button, List, typography } from "@link-stack/ui"; import { Button, List, typography } from "@link-stack/ui";
@ -16,6 +16,10 @@ interface TicketListProps {
export const TicketList: FC<TicketListProps> = ({ title, tickets }) => { export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down("sm"));
let gridColumns: GridColDef[] = [ let gridColumns: GridColDef[] = [
{ {
field: "number", field: "number",
@ -32,7 +36,9 @@ export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
headerName: "Sender", headerName: "Sender",
valueGetter: (value: any) => value?.fullname, valueGetter: (value: any) => value?.fullname,
flex: 1, flex: 1,
}, }];
if (!mobile) {
gridColumns.push(
{ {
field: "createdAt", field: "createdAt",
headerName: "Created At", headerName: "Created At",
@ -50,8 +56,8 @@ export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
headerName: "Group", headerName: "Group",
valueGetter: (value: any) => value?.name, valueGetter: (value: any) => value?.name,
flex: 1, flex: 1,
}, });
]; }
const onRowClick = (id: any) => { const onRowClick = (id: any) => {
router.push(`/tickets/${id}`); router.push(`/tickets/${id}`);

View file

@ -2,7 +2,7 @@
import { FC, useState, useEffect } from "react"; import { FC, useState, useEffect } from "react";
import { getTicketAction, getTicketArticlesAction } from "app/_actions/tickets"; import { getTicketAction, getTicketArticlesAction } from "app/_actions/tickets";
import { Grid, Box, Typography } from "@mui/material"; import { Grid, Box, Typography, useTheme, useMediaQuery } from "@mui/material";
import { Button, fonts, colors } from "@link-stack/ui"; import { Button, fonts, colors } from "@link-stack/ui";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import { import {
@ -13,6 +13,7 @@ import {
ConversationHeader, ConversationHeader,
} from "@chatscope/chat-ui-kit-react"; } from "@chatscope/chat-ui-kit-react";
import { ArticleCreateDialog } from "./ArticleCreateDialog"; import { ArticleCreateDialog } from "./ArticleCreateDialog";
import { TicketHeader } from "../../_components/TicketHeader";
interface TicketDetailProps { interface TicketDetailProps {
id: string; id: string;
@ -26,6 +27,9 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [articleKind, setArticleKind] = useState("note"); const [articleKind, setArticleKind] = useState("note");
const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down("md"));
useEffect(() => { useEffect(() => {
const fetchTicket = async () => { const fetchTicket = async () => {
const result = await getTicketAction(id); const result = await getTicketAction(id);
@ -66,36 +70,11 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
<> <>
<MainContainer> <MainContainer>
<ChatContainer> <ChatContainer>
<ConversationHeader> {!mobile && (<ConversationHeader>
<ConversationHeader.Content> <ConversationHeader.Content>
<Box <TicketHeader ticket={ticket} />
sx={{
width: "100%",
textAlign: "center",
fontWeight: "bold",
}}
>
<Typography
variant="h5"
sx={{
fontFamily: poppins.style.fontFamily,
fontWeight: 700,
}}
>
{ticket.title}
</Typography>
<Typography
variant="h6"
sx={{
fontFamily: roboto.style.fontFamily,
fontWeight: 400,
}}
>{`Ticket #${ticket.number} (created ${new Date(
ticket.createdAt,
).toLocaleDateString()})`}</Typography>
</Box>
</ConversationHeader.Content> </ConversationHeader.Content>
</ConversationHeader> </ConversationHeader>)}
<MessageList style={{ marginBottom: 80 }}> <MessageList style={{ marginBottom: 80 }}>
{ticketArticles.edges.map(({ node: article }: any) => ( {ticketArticles.edges.map(({ node: article }: any) => (
<Message <Message

View file

@ -110,7 +110,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
const shouldRender = !!ticket; const shouldRender = !!ticket;
return ( return (
<Box sx={{ height: "100vh", background: "#ddd", p: 2 }}> <Box sx={{ background: "#ddd", p: 2 }}>
{shouldRender && ( {shouldRender && (
<Grid container direction="column" spacing={3}> <Grid container direction="column" spacing={3}>
<Grid item> <Grid item>

View file

@ -0,0 +1,12 @@
import { getTicketAction } from "@/app/_actions/tickets";
import { TicketHeader } from "../_components/TicketHeader";
type PageProps = {
params: {
id: string;
};
};
export default function Page({ params: { id } }: PageProps) {
return (<TicketHeader id={id} />);
}

View file

@ -0,0 +1,73 @@
"use client";
import { FC, useState, useEffect } from "react";
import { Box, Typography } from "@mui/material";
import { fonts } from "@link-stack/ui";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import {
ConversationHeader,
ConversationHeaderProps,
} from "@chatscope/chat-ui-kit-react";
import { getTicketAction } from "@/app/_actions/tickets";
interface TicketHeaderProps {
id?: string;
ticket?: any;
}
export const TicketHeader: FC<ConversationHeaderProps & TicketHeaderProps> = (
props,
) => {
const { poppins, roboto } = fonts;
const [ticket, setTicket] = useState<any>(props.ticket || null);
useEffect(() => {
if (!ticket) {
const fetchTicket = async () => {
const result = await getTicketAction(props.id);
setTicket(result);
};
fetchTicket();
const interval = setInterval(fetchTicket, 20000);
return () => clearInterval(interval);
}
}, [props.id]);
return (
<>
<Box
sx={{
width: "100%",
textAlign: "center",
fontWeight: "bold",
}}
>
<Typography
variant="h5"
sx={{
fontFamily: poppins.style.fontFamily,
fontWeight: 700,
}}
>
{ticket?.title}
</Typography>
<Typography
variant="h6"
sx={{
fontFamily: roboto.style.fontFamily,
fontWeight: 400,
}}
>
{ticket
? `Ticket #${ticket.number} (created ${new Date(
ticket.createdAt,
).toLocaleDateString()})`
: ""}
</Typography>
</Box>
</>
);
};

View file

@ -1,22 +1,31 @@
"use client"; "use client";
import { ConversationHeader } from "@chatscope/chat-ui-kit-react";
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
type LayoutProps = { type LayoutProps = {
detail: any; detail: any;
edit: any; edit: any;
header: any;
params: { params: {
id: string; id: string;
}; };
}; };
export default function Layout({ detail, edit, params: { id } }: LayoutProps) { export default function Layout({ detail, edit, header, params: { id } }: LayoutProps) {
return ( return (
<Grid container spacing={0} sx={{ height: "100vh" }} direction="row"> <Grid container spacing={0} sx={{ height: "100%" }} direction="row" wrap="wrap">
<Grid item sx={{ height: "100vh" }} xs={9}> <Grid item sx={{ order: 0, display: { xs: "block", md: "none" } }} xs={12}>
<ConversationHeader>
<ConversationHeader.Content>
{header}
</ConversationHeader.Content>
</ConversationHeader>
</Grid>
<Grid item sx={{ height: {xs: "auto", md: "100%"}, order: {xs: 3, md: 1} }} xs={12} md={9}>
{detail} {detail}
</Grid> </Grid>
<Grid item xs={3} sx={{ height: "100vh" }}> <Grid item xs={12} md={3} sx={{ height: {xs: "auto", md: "100%"}, order: 2 }}>
{edit} {edit}
</Grid> </Grid>
</Grid> </Grid>

View file

@ -36,8 +36,8 @@ export const List: FC<ListProps> = ({
}; };
return ( return (
<Box sx={{ height: "100vh", backgroundColor: lightGray, p: 3 }}> <Box sx={{ height: "100%", p: 3 }}>
<Grid container direction="column"> <Grid container direction="column" sx={{ height: "100%", flexWrap: "nowrap" }}>
<Grid <Grid
item item
container container
@ -52,14 +52,13 @@ export const List: FC<ListProps> = ({
{buttons} {buttons}
</Grid> </Grid>
</Grid> </Grid>
<Grid item> <Grid item sx={{flexGrow: 1}}>
<Box <Box
sx={{ sx={{
mt: 2, mt: 2,
backgroundColor: "transparent", backgroundColor: "transparent",
border: 0, border: 0,
width: "100%", width: "100%",
height: "calc(100vh - 100px)",
".MuiDataGrid-row": { ".MuiDataGrid-row": {
cursor: "pointer", cursor: "pointer",
"&:hover": { "&:hover": {