Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 8 additions & 8 deletions app/api/admin/urls/[id]/url-groups/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe("URL Groups for URL API", () => {
} as JwtPayload);

const request = new NextRequest("http://localhost/api/admin/urls/123/url-groups");
const response = await GET(request, { params: { id: testUrl.id } });
const response = await GET(request, { params: Promise.resolve({ id: testUrl.id }) });
const data = await debugResponse(response);

expect(response.status).toBe(403);
Expand Down Expand Up @@ -122,7 +122,7 @@ describe("URL Groups for URL API", () => {
vi.mocked(prisma.urlsInGroups.findMany).mockResolvedValue(urlInGroups as any);

const request = new NextRequest("http://localhost/api/admin/urls/123/url-groups");
const response = await GET(request, { params: { id: testUrl.id } });
const response = await GET(request, { params: Promise.resolve({ id: testUrl.id }) });
const data = await debugResponse(response);

expect(response.status).toBe(200);
Expand Down Expand Up @@ -150,7 +150,7 @@ describe("URL Groups for URL API", () => {
vi.mocked(prisma.urlsInGroups.findMany).mockRejectedValue(new Error("Database error"));

const request = new NextRequest("http://localhost/api/admin/urls/123/url-groups");
const response = await GET(request, { params: { id: testUrl.id } });
const response = await GET(request, { params: Promise.resolve({ id: testUrl.id }) });
const data = await debugResponse(response);

expect(response.status).toBe(500);
Expand All @@ -176,7 +176,7 @@ describe("URL Groups for URL API", () => {
method: "PUT",
body: JSON.stringify({ urlGroupIds: [testUrlGroup1.id] }),
});
const response = await PUT(request, { params: { id: testUrl.id } });
const response = await PUT(request, { params: Promise.resolve({ id: testUrl.id }) });
const data = await debugResponse(response);

expect(response.status).toBe(403);
Expand All @@ -193,7 +193,7 @@ describe("URL Groups for URL API", () => {
method: "PUT",
body: JSON.stringify({ urlGroupIds: "not-an-array" }),
});
const response = await PUT(request, { params: { id: testUrl.id } });
const response = await PUT(request, { params: Promise.resolve({ id: testUrl.id }) });
const data = await debugResponse(response);

expect(response.status).toBe(400);
Expand All @@ -212,7 +212,7 @@ describe("URL Groups for URL API", () => {
method: "PUT",
body: JSON.stringify({ urlGroupIds: [testUrlGroup1.id] }),
});
const response = await PUT(request, { params: { id: "non-existent-id" } });
const response = await PUT(request, { params: Promise.resolve({ id: "non-existent-id" }) });
const data = await debugResponse(response);

expect(response.status).toBe(404);
Expand Down Expand Up @@ -255,7 +255,7 @@ describe("URL Groups for URL API", () => {
method: "PUT",
body: JSON.stringify({ urlGroupIds: newGroupIds }),
});
const response = await PUT(request, { params: { id: testUrl.id } });
const response = await PUT(request, { params: Promise.resolve({ id: testUrl.id }) });
const data = await debugResponse(response);

expect(response.status).toBe(200);
Expand All @@ -277,7 +277,7 @@ describe("URL Groups for URL API", () => {
method: "PUT",
body: JSON.stringify({ urlGroupIds: [testUrlGroup1.id] }),
});
const response = await PUT(request, { params: { id: testUrl.id } });
const response = await PUT(request, { params: Promise.resolve({ id: testUrl.id }) });
const data = await debugResponse(response);

expect(response.status).toBe(500);
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
89 changes: 89 additions & 0 deletions app/components/url-menu/ExternalUrlItem.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { getEffectiveUrl } from "@/app/lib/utils/iframe-utils";
import { ThemeProvider, createTheme } from "@mui/material";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ExternalUrlItem } from "./ExternalUrlItem";

// Mock the getEffectiveUrl function
vi.mock("@/app/lib/utils/iframe-utils", () => ({
getEffectiveUrl: vi.fn().mockReturnValue("http://localhost:3000/test"),
}));

// Mock window.open
const windowOpenMock = vi.fn();
window.open = windowOpenMock;

describe("ExternalUrlItem", () => {
const theme = createTheme();

beforeEach(() => {
vi.clearAllMocks();
});

it("should use getEffectiveUrl for localhost URLs", () => {
const url = {
id: "localhost-url",
title: "Localhost URL",
url: "http://example-localhost.com",
urlMobile: null,
iconPath: null,
displayOrder: 0,
isLocalhost: true,
port: "3000",
path: "/test",
};

render(
<ThemeProvider theme={theme}>
<ExternalUrlItem
url={url}
tooltipText="Localhost URL - http://localhost:3000/test (opens in new tab)"
menuPosition="top"
theme={theme}
/>
</ThemeProvider>,
);

const button = screen.getByRole("button", { name: /Localhost URL \(opens in new tab\)/i });
fireEvent.click(button);

// Verify getEffectiveUrl was called
expect(getEffectiveUrl).toHaveBeenCalled();

// Verify window.open was called with the effective URL from our mock
expect(windowOpenMock).toHaveBeenCalledWith(
"http://localhost:3000/test",
"_blank",
"noopener,noreferrer",
);
});

it("should open regular URL in new tab", () => {
const url = {
id: "test-url",
title: "Test URL",
url: "https://example.com",
urlMobile: null,
iconPath: null,
displayOrder: 0,
isLocalhost: false,
};

render(
<ThemeProvider theme={theme}>
<ExternalUrlItem
url={url}
tooltipText="Test URL - https://example.com (opens in new tab)"
menuPosition="top"
theme={theme}
/>
</ThemeProvider>,
);

const button = screen.getByRole("button", { name: /Test URL \(opens in new tab\)/i });
fireEvent.click(button);

// For non-localhost URLs, getEffectiveUrl should not be called
expect(windowOpenMock).toHaveBeenCalled();
});
});
Loading
Loading