💄 Some UI changes

This commit is contained in:
Fred KISSIE
2026-09-11 21:52:37 +02:00
parent 4324eebc1f
commit 5a6c23e05a
4 changed files with 83 additions and 77 deletions
+4 -1
View File
@@ -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.",
+57 -1
View File
@@ -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<SelectedResource | null>(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<string | null>(
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
);
}}
/>
<FormMessage />
@@ -547,6 +584,22 @@ export default function RedirectForm({
<Form {...form}>
<form action={formAction}>
<SettingsFormGrid>
<SettingsFormCell span="full">
<SwitchInput
id="redirect-same-domain"
label={t(
"redirectSameDomainAsSource"
)}
description={t(
"redirectSameDomainAsSourceDescription"
)}
checked={sameDomainAsSource}
onCheckedChange={
setSameDomainAsSource
}
/>
</SettingsFormCell>
<SettingsFormCell span="full">
<FormField
control={form.control}
@@ -562,6 +615,9 @@ export default function RedirectForm({
<Input
autoComplete="off"
placeholder="example.com"
readOnly={
sameDomainAsSource
}
{...field}
/>
</FormControl>
+21 -75
View File
@@ -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: () => (
<span className="p-3">{t("redirectSourceDomain")}</span>
<span className="p-3">{t("redirectSource")}</span>
),
cell: ({ row }) => {
const redirect = row.original;
@@ -280,71 +262,34 @@ export default function RedirectsTable({
: null;
const host = redirect.resourceFullDomain ?? domainHost;
return host ? (
<code className="text-sm">{host}</code>
) : (
<span>-</span>
);
}
},
{
id: "matchPath",
accessorKey: "matchPath",
friendlyName: t("matchPath"),
header: () => <span className="p-3">{t("matchPath")}</span>,
cell: ({ row }) => {
const redirect = row.original;
return (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="shrink-0">
{matchTypeLabel(redirect.pathMatchType)}
</Badge>
<code className="text-sm truncate">
<code className="text-sm truncate">
{host ?? ""}
<span className="text-muted-foreground">
{redirect.matchPath}
</code>
</div>
</span>
</code>
);
}
},
{
id: "destination",
accessorKey: "destinationDomain",
friendlyName: t("redirectDestinationDomain"),
friendlyName: t("redirectDestination"),
header: () => (
<span className="p-3">
{t("redirectDestinationDomain")}
</span>
<span className="p-3">{t("redirectDestination")}</span>
),
cell: ({ row }) => (
<code className="text-sm">
{row.original.destinationDomain}
</code>
)
},
{
id: "rewritePath",
accessorKey: "rewritePath",
friendlyName: t("rewritePath"),
header: () => <span className="p-3">{t("rewritePath")}</span>,
cell: ({ row }) => {
const redirect = row.original;
const hasRewrite =
Boolean(redirect.rewritePath) ||
redirect.rewritePathType === "stripPrefix";
if (!hasRewrite) {
return <span>-</span>;
}
return (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="shrink-0">
{rewriteTypeLabel(redirect.rewritePathType)}
</Badge>
<code className="text-sm truncate">
{redirect.rewritePath ?? ""}
</code>
</div>
<code className="text-sm truncate">
{redirect.destinationDomain}
{redirect.rewritePath && (
<span className="text-muted-foreground">
{redirect.rewritePath}
</span>
)}
</code>
);
}
},
@@ -468,7 +413,8 @@ export default function RedirectsTable({
rowCount={rowCount}
columnVisibility={{
attachedTo: false,
niceId: false
niceId: false,
permanent: false
}}
enableColumnVisibility
stickyLeftColumn="name"
+1
View File
@@ -44,6 +44,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
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