diff --git a/messages/en-US.json b/messages/en-US.json index c6d731e90..6ea4a470f 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -4369,7 +4369,8 @@ "redirectsDescription": "Forward requests from a path on your domains or resources to another URL", "redirectsSearch": "Search redirects...", "redirectAdd": "Add Redirect", - "redirectSourceDomain": "Source Domain", + "redirectSource": "Source", + "redirectDestination": "Destination", "redirectAttachedTo": "Attached To", "redirectType": "Type", "redirectTypePermanent": "Permanent (308)", @@ -4385,6 +4386,8 @@ "redirectDestinationDomain": "Destination Domain", "redirectDestinationDomainDescription": "The domain requests are sent to, such as example.com", "redirectDestinationDomainRequired": "Enter a destination domain", + "redirectSameDomainAsSource": "Same domain as source", + "redirectSameDomainAsSourceDescription": "Keep the destination on the source domain and only change the path", "redirectDestinationDomainInvalid": "Enter a valid domain, such as example.com", "redirectMatchPathDescription": "Which incoming paths this redirect applies to", "redirectRewritePathDescription": "Optionally change the path before redirecting. Leave unset to keep the original path.", diff --git a/src/components/RedirectForm.tsx b/src/components/RedirectForm.tsx index e9f5230d9..8ee7968a1 100644 --- a/src/components/RedirectForm.tsx +++ b/src/components/RedirectForm.tsx @@ -51,7 +51,7 @@ import type { import type { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; -import { useActionState, useMemo, useState } from "react"; +import { useActionState, useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { ResourceSelector, type SelectedResource } from "./resource-selector"; @@ -95,6 +95,16 @@ export default function RedirectForm({ const [selectedResource, setSelectedResource] = useState(initialResource); + // DomainPicker only hands back the composed host through its callback, so + // keep it locally; seed from the saved redirect for the edit case. + const [domainFullDomain, setDomainFullDomain] = useState( + redirect?.baseDomain + ? [redirect.subdomain, redirect.baseDomain] + .filter(Boolean) + .join(".") + : null + ); + const formSchema = useMemo( () => z @@ -178,6 +188,29 @@ export default function RedirectForm({ }); const attachTo = form.watch("attachTo"); + + const sourceFullDomain = + attachTo === "domain" + ? domainFullDomain + : (selectedResource?.fullDomain ?? null); + + // Mirror is UI-only state; on edit, infer it from whether the saved + // destination already equals the source host. + const [sameDomainAsSource, setSameDomainAsSource] = useState( + Boolean( + redirect && + sourceFullDomain && + redirect.destinationDomain === sourceFullDomain + ) + ); + + useEffect(() => { + if (sameDomainAsSource && sourceFullDomain) { + form.setValue("destinationDomain", sourceFullDomain, { + shouldValidate: true + }); + } + }, [sameDomainAsSource, sourceFullDomain, form]); const pathMatchType = form.watch("pathMatchType"); const rewritePath = form.watch("rewritePath"); const rewritePathType = form.watch("rewritePathType"); @@ -430,6 +463,10 @@ export default function RedirectForm({ res?.subdomain || null ); + setDomainFullDomain( + res?.fullDomain ?? + null + ); }} /> @@ -547,6 +584,22 @@ export default function RedirectForm({
+ + + + diff --git a/src/components/RedirectsTable.tsx b/src/components/RedirectsTable.tsx index 6c33a7457..2e80fdc12 100644 --- a/src/components/RedirectsTable.tsx +++ b/src/components/RedirectsTable.tsx @@ -110,24 +110,6 @@ export default function RedirectsTable({ filter({ searchParams }); }, 300); - function matchTypeLabel(type: RedirectRow["pathMatchType"]) { - return { - prefix: t("pathMatchPrefix"), - exact: t("pathMatchExact"), - regex: t("pathMatchRegex") - }[type]; - } - - function rewriteTypeLabel(type: RedirectRow["rewritePathType"]) { - if (!type) return ""; - return { - prefix: t("pathRewritePrefix"), - exact: t("pathRewriteExact"), - regex: t("pathRewriteRegex"), - stripPrefix: t("pathRewriteStrip") - }[type]; - } - async function toggleEnabled(row: RedirectRow, enabled: boolean) { setRows((prev) => prev.map((r) => @@ -264,10 +246,10 @@ export default function RedirectsTable({ }, { - id: "sourceDomain", - friendlyName: t("redirectSourceDomain"), + id: "source", + friendlyName: t("redirectSource"), header: () => ( - {t("redirectSourceDomain")} + {t("redirectSource")} ), cell: ({ row }) => { const redirect = row.original; @@ -280,71 +262,34 @@ export default function RedirectsTable({ : null; const host = redirect.resourceFullDomain ?? domainHost; - return host ? ( - {host} - ) : ( - - - ); - } - }, - { - id: "matchPath", - accessorKey: "matchPath", - friendlyName: t("matchPath"), - header: () => {t("matchPath")}, - cell: ({ row }) => { - const redirect = row.original; return ( -
- - {matchTypeLabel(redirect.pathMatchType)} - - + + {host ?? ""} + {redirect.matchPath} - -
+ + ); } }, - { + id: "destination", accessorKey: "destinationDomain", - friendlyName: t("redirectDestinationDomain"), + friendlyName: t("redirectDestination"), header: () => ( - - {t("redirectDestinationDomain")} - + {t("redirectDestination")} ), - cell: ({ row }) => ( - - {row.original.destinationDomain} - - ) - }, - { - id: "rewritePath", - accessorKey: "rewritePath", - friendlyName: t("rewritePath"), - header: () => {t("rewritePath")}, cell: ({ row }) => { const redirect = row.original; - const hasRewrite = - Boolean(redirect.rewritePath) || - redirect.rewritePathType === "stripPrefix"; - - if (!hasRewrite) { - return -; - } - return ( -
- - {rewriteTypeLabel(redirect.rewritePathType)} - - - {redirect.rewritePath ?? ""} - -
+ + {redirect.destinationDomain} + {redirect.rewritePath && ( + + {redirect.rewritePath} + + )} + ); } }, @@ -468,7 +413,8 @@ export default function RedirectsTable({ rowCount={rowCount} columnVisibility={{ attachedTo: false, - niceId: false + niceId: false, + permanent: false }} enableColumnVisibility stickyLeftColumn="name" diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index fe87fb0dc..6032d5d8e 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -44,6 +44,7 @@ const Input = React.forwardRef( data-slot="input" className={cn( "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "read-only:opacity-50", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "focus-visible:outline-none focus-visible:border-ring focus-visible:ring-offset-0", className