Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/admin/urls/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface Url {
iconPath: string | null;
idleTimeoutMinutes: number | null;
isLocalhost: boolean;
openInNewTab: boolean;
port?: string | null;
path?: string | null;
localhostMobilePath?: string | null;
Expand Down Expand Up @@ -486,6 +487,7 @@ export default function UrlManagement() {
<TableCell>URL</TableCell>
<TableCell>Mobile URL</TableCell>
<TableCell>Localhost</TableCell>
<TableCell>Open in New Tab</TableCell>
<TableCell>Idle Timeout</TableCell>
<TableCell>Groups</TableCell>
<TableCell>Actions</TableCell>
Expand Down Expand Up @@ -638,6 +640,13 @@ export default function UrlManagement() {
<Chip size="small" color="default" label="Disabled" variant="outlined" />
)}
</TableCell>
<TableCell>
{url.openInNewTab ? (
<Chip size="small" color="primary" label="Yes" variant="outlined" />
) : (
<Chip size="small" color="default" label="No" variant="outlined" />
)}
</TableCell>
<TableCell>{url.idleTimeoutMinutes || "-"}</TableCell>
<TableCell>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
Expand Down
5 changes: 4 additions & 1 deletion app/api/admin/urls/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export async function PUT(request: NextRequest, { params }: RouteContext): Promi
iconPath,
idleTimeoutMinutes,
isLocalhost,
openInNewTab,
port,
path,
localhostMobilePort,
Expand Down Expand Up @@ -143,8 +144,10 @@ export async function PUT(request: NextRequest, { params }: RouteContext): Promi
urlMobile: urlMobile || null,
iconPath: iconPath || null,
idleTimeoutMinutes: timeoutMinutes,
// @ts-ignore - These fields exist in our schema but TypeScript doesn't know about them yet
// @ts-ignore - This field exists in our schema but TypeScript doesn't know about it yet
isLocalhost: isLocalhost || false,
// @ts-ignore - New field added to schema
openInNewTab: openInNewTab || false,
port: port || null,
path: path || null,
localhostMobilePort: localhostMobilePort || null,
Expand Down
3 changes: 3 additions & 0 deletions app/api/admin/urls/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export async function POST(request: Request) {
iconPath,
idleTimeoutMinutes,
isLocalhost,
openInNewTab,
port,
path,
localhostMobilePort,
Expand Down Expand Up @@ -113,6 +114,8 @@ export async function POST(request: Request) {
idleTimeoutMinutes: idleTimeoutMinutes ? Number(idleTimeoutMinutes) : 10,
// @ts-ignore - These fields exist in our schema but TypeScript doesn't know about them yet
isLocalhost: isLocalhost || false,
// @ts-ignore - New field added to schema
openInNewTab: openInNewTab || false,
port: port || null,
path: path || null,
localhostMobilePort: localhostMobilePort || null,
Expand Down
3 changes: 3 additions & 0 deletions app/api/url-groups/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface Url {
idleTimeoutMinutes: number | null;
displayOrder: number;
isLocalhost: boolean;
openInNewTab: boolean;
port: string | null;
path: string | null;
localhostMobilePath: string | null;
Expand Down Expand Up @@ -48,6 +49,7 @@ interface UserUrlGroupItem {
idleTimeoutMinutes: number | null;
// These fields might be missing in the Prisma output but we handle them in the map function
isLocalhost?: boolean;
openInNewTab?: boolean;
port?: string | null;
path?: string | null;
localhostMobilePath?: string | null;
Expand Down Expand Up @@ -116,6 +118,7 @@ export async function GET() {
idleTimeoutMinutes: urlInGroup.url.idleTimeoutMinutes,
displayOrder: urlInGroup.displayOrder,
isLocalhost: urlInGroup.url.isLocalhost || false,
openInNewTab: urlInGroup.url.openInNewTab || false,
port: urlInGroup.url.port || null,
path: urlInGroup.url.path || null,
localhostMobilePath: urlInGroup.url.localhostMobilePath || null,
Expand Down
20 changes: 20 additions & 0 deletions app/components/ui/UrlDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface Url {
iconPath?: string | null;
idleTimeoutMinutes: number | null;
isLocalhost: boolean;
openInNewTab: boolean;
port?: string | null;
path?: string | null;
localhostMobilePath?: string | null;
Expand Down Expand Up @@ -59,6 +60,7 @@ export default function UrlDialog({
iconPath: null,
idleTimeoutMinutes: 0,
isLocalhost: false,
openInNewTab: false,
port: "",
path: "",
enableMobileOverride: false,
Expand All @@ -74,6 +76,7 @@ export default function UrlDialog({
setFormValues({
...formValues,
...initialValues,
openInNewTab: initialValues.openInNewTab ?? false,
enableMobileOverride: hasMobileOverride,
} as Url);
} else if (open) {
Expand All @@ -85,6 +88,7 @@ export default function UrlDialog({
iconPath: null,
idleTimeoutMinutes: 0,
isLocalhost: false,
openInNewTab: false,
port: "",
path: "",
enableMobileOverride: false,
Expand Down Expand Up @@ -160,6 +164,7 @@ export default function UrlDialog({
iconPath: null,
idleTimeoutMinutes: 0,
isLocalhost: false,
openInNewTab: false,
port: "",
path: "",
enableMobileOverride: false,
Expand Down Expand Up @@ -390,6 +395,21 @@ export default function UrlDialog({
/>
</Tooltip>
</Grid>

<Grid item xs={12}>
<Tooltip title="For websites that do not support iframe embedding, this will open the URL in a new tab instead of inside this app.">
<FormControlLabel
control={
<Checkbox
name="openInNewTab"
checked={formValues.openInNewTab}
onChange={handleCheckboxChange}
/>
}
label="Open in new tab"
/>
</Tooltip>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
Expand Down
172 changes: 172 additions & 0 deletions app/components/url-menu/ExternalUrlItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { Url } from "@/app/lib/types";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import {
Box,
Button,
ListItemButton,
ListItemText,
Theme,
Tooltip,
useMediaQuery,
} from "@mui/material";
import { memo, useMemo } from "react";

interface ExternalUrlItemProps {
url: Url;
tooltipText: string;
menuPosition: "top" | "side";
theme: Theme;
}

const ExternalUrlItem = memo(function ExternalUrlItem({
url,
tooltipText,
menuPosition,
theme,
}: ExternalUrlItemProps) {
// Simple click handler that opens the URL in a new tab
const handleClick = () => {
window.open(url.url, "_blank", "noopener,noreferrer");
};

// Check if we're on mobile
const isMobile = useMediaQuery(theme.breakpoints.down("md"));

// For top menu, use Button-based styling
if (menuPosition === "top") {
const styles = useMemo(
() => ({
iconStyles: {
width: 24,
height: 24,
objectFit: "contain" as const,
marginRight: url.title && url.iconPath ? 1 : 0,
},
boxStyles: {
position: "relative" as const,
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
},
buttonStyles: {
height: 50, // Fixed height for consistency
minHeight: 50, // Ensure minimum height is the same
minWidth: 50,
lineHeight: "36px", // Match line height to button height
px: 0, // Slightly more horizontal padding
mx: 0.5,
textTransform: "none",
borderRadius: 1,
color: theme.palette.text.primary,
fontWeight: "normal",
backgroundColor: "transparent",
position: "relative",
overflow: "hidden",
pb: 0,
"&:hover": {
opacity: 0.8,
},
},
containerStyles: {
display: "inline-block",
position: "relative" as const,
},
externalIconStyles: {
position: "relative" as const,
marginLeft: 1,
fontSize: 14,
color: theme.palette.text.secondary,
opacity: 0.7,
padding: "1px",
},
titleStyles: {
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.875rem",
whiteSpace: "nowrap" as const,
overflow: "hidden" as const,
textOverflow: "ellipsis" as const,
maxWidth: 100,
},
}),
[theme.palette.text.primary, theme.palette.text.secondary, url.title, url.iconPath],
);

return (
<Box sx={styles.containerStyles}>
<Tooltip title={tooltipText} placement="bottom" enterDelay={700}>
<Button
onClick={handleClick}
sx={styles.buttonStyles}
disableRipple={false}
aria-label={`${url.title} (opens in new tab)`}
>
<Box sx={styles.boxStyles}>
{url.iconPath ? (
<>
<Box component="img" src={url.iconPath} alt="" sx={styles.iconStyles} />
{url.title && <Box sx={styles.titleStyles}>{url.title}</Box>}
</>
) : (
<Box sx={styles.titleStyles}>{url.title}</Box>
)}
<OpenInNewIcon sx={styles.externalIconStyles} fontSize="small" />
</Box>
</Button>
</Tooltip>
</Box>
);
}

// For side menu, use ListItemButton-based styling to match UrlItem
return (
<ListItemButton
onClick={handleClick}
sx={{
pl: isMobile ? 3 : 4,
position: "relative",
borderRight: 0,
display: "flex",
alignItems: "center",
}}
>
{url.iconPath && (
<Box
component="img"
src={url.iconPath}
alt=""
sx={{
width: 20,
height: 20,
objectFit: "contain",
mr: 1.5,
flexShrink: 0,
}}
/>
)}
<ListItemText
primary={url.title || url.url}
secondary={url.url}
primaryTypographyProps={{
fontSize: isMobile ? "0.875rem" : "inherit", // Smaller font on mobile
}}
secondaryTypographyProps={{
fontSize: isMobile ? "0.75rem" : "inherit", // Smaller font on mobile
}}
/>
<OpenInNewIcon
sx={{
ml: 1,
fontSize: isMobile ? 12 : 14, // Smaller icon on mobile
color: theme.palette.text.secondary,
opacity: 0.7,
}}
fontSize="small"
/>
</ListItemButton>
);
});

export { ExternalUrlItem };
44 changes: 34 additions & 10 deletions app/components/url-menu/TopMenuNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getEffectiveUrl } from "@/app/lib/utils/iframe-utils";
import type { IframeContainerRef, UrlGroup } from "@/app/types/iframe";
import { Box, Paper, Popper, useMediaQuery, useTheme } from "@mui/material";
import { memo, RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ExternalUrlItem } from "./ExternalUrlItem";
import { UrlItem } from "./UrlItem";

interface TopMenuNavigationProps {
Expand Down Expand Up @@ -144,20 +145,43 @@ export const TopMenuNavigation = memo(function TopMenuNavigation({
)
: url.url;

const tooltipText = `${url.title || url.id} - ${tooltipUrl}`;
// Create a complete URL object with all properties
const fullUrlObj = {
id: url.id,
title: urlTitle,
url: url.url,
urlMobile: url.urlMobile ?? null,
iconPath: (url as any).iconPath || null,
idleTimeoutMinutes: (url as any).idleTimeoutMinutes,
displayOrder: (url as any).displayOrder || 0,
isLocalhost: url.isLocalhost || false,
port: url.port || null,
path: url.path || null,
localhostMobilePath: url.localhostMobilePath || null,
localhostMobilePort: url.localhostMobilePort || null,
openInNewTab: (url as any).openInNewTab || false,
Comment thread
michaelbarone marked this conversation as resolved.
Outdated
};

const tooltipText = `${fullUrlObj.title} - ${tooltipUrl}${fullUrlObj.openInNewTab ? " (opens in new tab)" : ""}`;

// Check if URL should open in new tab
if (fullUrlObj.openInNewTab) {
return (
<ExternalUrlItem
key={url.id}
url={fullUrlObj}
tooltipText={tooltipText}
menuPosition="top"
theme={theme}
/>
);
}

// Use regular UrlItem for normal URLs
return (
<UrlItem
key={url.id}
url={{
id: url.id,
title: urlTitle,
url: url.url,
urlMobile: url.urlMobile ?? null,
iconPath: (url as any).iconPath || null,
idleTimeoutMinutes: (url as any).idleTimeoutMinutes,
displayOrder: (url as any).displayOrder || 0,
}}
url={fullUrlObj}
isActive={isActive}
isLoaded={isLoaded}
tooltipText={tooltipText}
Expand Down
Loading
Loading