From 7ce2668b5bd750cc18011e81fddf63c4d4da1173 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 10 Sep 2026 22:05:45 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20redirect=20create=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 2 +- server/db/pg/schema/schema.ts | 2 +- server/db/sqlite/schema/schema.ts | 1 + server/routers/redirect/createRedirect.ts | 3 + server/routers/redirect/getRedirect.ts | 2 + server/routers/redirect/listRedirects.ts | 2 + server/routers/redirect/updateRedirect.ts | 4 + src/app/[orgId]/settings/redirects/page.tsx | 1 + src/components/RedirectForm.tsx | 170 +++++++++++--------- src/components/RedirectsTable.tsx | 19 ++- src/components/Settings.tsx | 82 ++++++++-- 11 files changed, 192 insertions(+), 96 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 2e925ad14..8e42ca361 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -4397,13 +4397,13 @@ "redirectSettings": "Redirect Settings", "selectedRedirectDomain": "Selected Domain", "selectedRedirectResource": "Selected Resource", + "redirectResourceNoDomain": "This resource has no domain", "redirectSettingsGeneralDescription": "Configure the basic redirect settings", "redirectSettingsDescription": "Configure where requests come from and where they are sent", "redirectEnabledDescription": "Turn the redirect off to stop forwarding requests without deleting it", "redirectAttachedToDescription": "Choose whether this redirect applies to a whole domain or a single resource", "redirectAttachDomain": "Domain", "redirectAttachResource": "Resource", - "redirectDomainSelect": "Select a domain", "redirectDomainRequired": "Select a domain to attach this redirect to", "redirectResourceRequired": "Select a resource to attach this redirect to", "redirectPermanent": "Permanent Redirect", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 4811400e6..e3517a533 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -248,7 +248,7 @@ export const redirects = pgTable("redirects", { .$type<"exact" | "prefix" | "regex">() .notNull() .default("regex"), // exact, prefix, regex - matchPath: varchar("matchPath").notNull().default("*"), + matchPath: varchar("matchPath").notNull().default(".*"), rewritePath: varchar("rewritePath"), // if set, rewrites the path to this value, // else, the original path will be kept rewritePathType: varchar("rewritePathType").$type< diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index f7f4884b9..b29a83f71 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -258,6 +258,7 @@ export const redirects = sqliteTable("redirects", { }), niceId: text("niceId").notNull(), name: text("name").notNull(), + subdomain: text("subdomain"), destinationDomain: text("destinationDomain").notNull(), pathMatchType: text("pathMatchType") .$type<"exact" | "prefix" | "regex">() diff --git a/server/routers/redirect/createRedirect.ts b/server/routers/redirect/createRedirect.ts index d8da0040d..a5b77a913 100644 --- a/server/routers/redirect/createRedirect.ts +++ b/server/routers/redirect/createRedirect.ts @@ -29,6 +29,7 @@ const bodySchema = z.strictObject({ name: z.string().nonempty(), resourceId: z.number().int().positive().optional().nullable(), domainId: z.string().nonempty().optional().nullable(), + subdomain: z.string().nonempty().optional().nullable(), destinationDomain: z.string().nonempty(), pathMatchType: redirectPathMatchTypeSchema.optional(), matchPath: redirectMatchPathSchema, @@ -103,6 +104,7 @@ export async function createRedirect( name, resourceId, domainId, + subdomain, destinationDomain, pathMatchType, matchPath, @@ -170,6 +172,7 @@ export async function createRedirect( niceId, resourceId: resourceId ?? null, domainId: domainId ?? null, + subdomain: subdomain ?? null, destinationDomain, pathMatchType: pathMatchType ?? "regex", matchPath, diff --git a/server/routers/redirect/getRedirect.ts b/server/routers/redirect/getRedirect.ts index 03f7aa186..cba2048e9 100644 --- a/server/routers/redirect/getRedirect.ts +++ b/server/routers/redirect/getRedirect.ts @@ -16,6 +16,7 @@ export type GetRedirectResponse = { orgId: string; niceId: string; name: string; + subdomain: string | null; destinationDomain: string; pathMatchType: "exact" | "prefix" | "regex"; matchPath: string; @@ -39,6 +40,7 @@ const redirectColumns = { orgId: redirects.orgId, niceId: redirects.niceId, name: redirects.name, + subdomain: redirects.subdomain, destinationDomain: redirects.destinationDomain, pathMatchType: redirects.pathMatchType, matchPath: redirects.matchPath, diff --git a/server/routers/redirect/listRedirects.ts b/server/routers/redirect/listRedirects.ts index d5984f53e..47c918728 100644 --- a/server/routers/redirect/listRedirects.ts +++ b/server/routers/redirect/listRedirects.ts @@ -16,6 +16,7 @@ export type ListRedirectsResponse = PaginatedResponse<{ orgId: string; niceId: string; name: string; + subdomain: string | null; destinationDomain: string; pathMatchType: "exact" | "prefix" | "regex"; matchPath: string; @@ -137,6 +138,7 @@ export async function listRedirects( orgId: redirects.orgId, niceId: redirects.niceId, name: redirects.name, + subdomain: redirects.subdomain, destinationDomain: redirects.destinationDomain, pathMatchType: redirects.pathMatchType, matchPath: redirects.matchPath, diff --git a/server/routers/redirect/updateRedirect.ts b/server/routers/redirect/updateRedirect.ts index 9158fd270..0dfd27722 100644 --- a/server/routers/redirect/updateRedirect.ts +++ b/server/routers/redirect/updateRedirect.ts @@ -31,6 +31,7 @@ const bodySchema = z.strictObject({ niceId: redirectNiceIdSchema.optional(), resourceId: z.number().int().positive().optional().nullable(), domainId: z.string().nonempty().optional().nullable(), + subdomain: z.string().nonempty().optional().nullable(), destinationDomain: z.string().nonempty().optional(), pathMatchType: redirectPathMatchTypeSchema.optional(), matchPath: redirectMatchPathSchema.optional(), @@ -196,6 +197,9 @@ export async function updateRedirect( if (body.domainId !== undefined) { updateData.domainId = body.domainId; } + if (body.subdomain !== undefined) { + updateData.subdomain = body.subdomain; + } if (body.destinationDomain !== undefined) { updateData.destinationDomain = body.destinationDomain; } diff --git a/src/app/[orgId]/settings/redirects/page.tsx b/src/app/[orgId]/settings/redirects/page.tsx index f2113c4e1..5837b438e 100644 --- a/src/app/[orgId]/settings/redirects/page.tsx +++ b/src/app/[orgId]/settings/redirects/page.tsx @@ -55,6 +55,7 @@ export default async function RedirectIndexPage(props: RedirectIndexPageProps) { redirectId: redirect.redirectId, niceId: redirect.niceId, name: redirect.name, + subdomain: redirect.subdomain, destinationDomain: redirect.destinationDomain, pathMatchType: redirect.pathMatchType, matchPath: redirect.matchPath, diff --git a/src/components/RedirectForm.tsx b/src/components/RedirectForm.tsx index fed65b713..5a85e58f1 100644 --- a/src/components/RedirectForm.tsx +++ b/src/components/RedirectForm.tsx @@ -41,7 +41,6 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; -import { orgQueries } from "@app/lib/queries"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { zodResolver } from "@hookform/resolvers/zod"; import type { @@ -49,7 +48,6 @@ import type { GetRedirectResponse } from "@server/routers/redirect"; import type { AxiosResponse } from "axios"; -import { useQuery } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useMemo, useState } from "react"; @@ -63,9 +61,10 @@ import { PathRewriteModal } from "@app/components/PathMatchRenameModal"; import { Plus } from "lucide-react"; +import DomainPicker from "@app/components/DomainPicker"; import Link from "next/link"; -const DEFAULT_MATCH_PATH = "*"; +const DEFAULT_MATCH_PATH = ".*"; const DEFAULT_PATH_MATCH_TYPE = "regex" as const; export type ExistingRedirect = GetRedirectResponse["redirect"]; @@ -95,8 +94,6 @@ export default function RedirectForm({ const [selectedResource, setSelectedResource] = useState(initialResource); - const { data: domains = [] } = useQuery(orgQueries.domains({ orgId })); - const formSchema = useMemo( () => z @@ -107,6 +104,7 @@ export default function RedirectForm({ .min(1, { message: t("nameRequired") }), attachTo: z.enum(["domain", "resource"]), domainId: z.string().nullable(), + subdomain: z.string().nullable(), resourceId: z.number().int().positive().nullable(), destinationDomain: z .string() @@ -163,6 +161,7 @@ export default function RedirectForm({ name: redirect?.name ?? "", attachTo: redirect?.resourceId ? "resource" : "domain", domainId: redirect?.domainId ?? null, + subdomain: redirect?.subdomain ?? null, resourceId: redirect?.resourceId ?? null, destinationDomain: redirect?.destinationDomain ?? "", pathMatchType: redirect?.pathMatchType ?? DEFAULT_PATH_MATCH_TYPE, @@ -190,6 +189,8 @@ export default function RedirectForm({ const body = { name: values.name.trim(), domainId: values.attachTo === "domain" ? values.domainId : null, + subdomain: + values.attachTo === "domain" ? values.subdomain || null : null, resourceId: values.attachTo === "resource" ? values.resourceId : null, destinationDomain: values.destinationDomain.trim(), @@ -283,7 +284,7 @@ export default function RedirectForm({ )} - + {t("general")} @@ -301,6 +302,35 @@ export default function RedirectForm({ id="redirect-form" > + + ( + + + + + + + )} + /> + + {attachTo === "domain" ? ( - + ( + render={() => ( - - {t( - "selectedRedirectDomain" - )} - - + onDomainChange={( + res + ) => { + form.setValue( + "domainId", + res?.domainId ?? + null, + { + shouldValidate: true + } + ); + form.setValue( + "subdomain", + res?.subdomain || + null + ); + }} + /> )} @@ -484,6 +499,33 @@ export default function RedirectForm({ /> )} + + {attachTo === "resource" && ( + + + + {t("resourceDomain")} + + + + + )} @@ -491,7 +533,7 @@ export default function RedirectForm({ - + {t("redirectSettings")} @@ -504,33 +546,9 @@ export default function RedirectForm({
- + - ( - - - {t("name")} - - - - - - - )} - /> - - - { const redirect = row.original; - const host = - redirect.resourceFullDomain ?? redirect.baseDomain; + // A domain-attached redirect may target a specific host + // under the base domain, e.g. old.example.com. + const domainHost = redirect.baseDomain + ? [redirect.subdomain, redirect.baseDomain] + .filter(Boolean) + .join(".") + : null; + const host = redirect.resourceFullDomain ?? domainHost; return (
@@ -237,7 +244,13 @@ export default function RedirectsTable({ } if (redirect.baseDomain) { - return {redirect.baseDomain}; + return ( + + {[redirect.subdomain, redirect.baseDomain] + .filter(Boolean) + .join(".")} + + ); } return -; diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index ec3c99809..27c19136d 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -1,23 +1,46 @@ import { cn } from "@app/lib/cn"; -export function SettingsContainer({ children }: { children: React.ReactNode }) { - return
{children}
; +export function SettingsContainer({ + children, + className +}: { + children: React.ReactNode; + className?: string; +}) { + return
{children}
; } -export function SettingsSection({ children }: { children: React.ReactNode }) { +export function SettingsSection({ + children, + className +}: { + children: React.ReactNode; + className?: string; +}) { return ( -
+
{children}
); } export function SettingsSectionHeader({ - children + children, + className }: { children: React.ReactNode; + className?: string; }) { - return
{children}
; + return ( +
+ {children} +
+ ); } export function SettingsSectionForm({ @@ -87,23 +110,36 @@ export function SettingsFormCell({ } export function SettingsSectionTitle({ - children + children, + className }: { children: React.ReactNode; + className?: string; }) { return ( -

+

{children}

); } export function SettingsSectionDescription({ - children + children, + className }: { children: React.ReactNode; + className?: string; }) { - return

{children}

; + return ( +

+ {children} +

+ ); } export function SettingsSubsectionHeader({ @@ -141,11 +177,15 @@ export function SettingsSubsectionDescription({ } export function SettingsSectionBody({ - children + children, + className }: { children: React.ReactNode; + className?: string; }) { - return
{children}
; + return ( +
{children}
+ ); } export function SettingsSectionFooter({ @@ -169,10 +209,22 @@ export function SettingsSectionFooter({ export function SettingsSectionGrid({ children, - cols + cols = 4, + className }: { children: React.ReactNode; - cols: number; + cols?: number; + className?: string; }) { - return
{children}
; + return ( +
+ {children} +
+ ); }