diff --git a/messages/en-US.json b/messages/en-US.json index 9d9f0633f..f9cdebe8c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -4393,6 +4393,7 @@ "redirectRewritePathDescription": "Optionally change the path before redirecting. Leave unset to keep the original path.", "redirectRewritePathRequired": "Enter a rewrite path, or choose Strip Prefix", "redirectMatchPathInvalidRegex": "Match path must be a valid regular expression", + "redirectPriorityInvalid": "Enter a whole number between 1 and 1000", "redirectCreate": "Create Redirect", "redirectCreateDescription": "Forward requests matching a path to another URL", "redirectEditDescription": "Update how this redirect forwards incoming requests", diff --git a/server/routers/redirect/createRedirect.ts b/server/routers/redirect/createRedirect.ts index 1131243e2..8167428ef 100644 --- a/server/routers/redirect/createRedirect.ts +++ b/server/routers/redirect/createRedirect.ts @@ -15,6 +15,7 @@ import { redirectPathMatchTypeSchema, redirectRewritePathSchema, isValidMatchPath, + redirectPrioritySchema, redirectRewritePathTypeSchema } from "@server/routers/redirect/validation"; import { getUniqueRedirectName } from "@server/db/names"; @@ -39,6 +40,7 @@ const bodySchema = z matchPath: redirectMatchPathSchema.optional().nullable(), rewritePath: redirectRewritePathSchema.optional().nullable(), rewritePathType: redirectRewritePathTypeSchema.optional().nullable(), + priority: redirectPrioritySchema.optional().nullable(), permanent: z.boolean().optional(), enabled: z.boolean().optional() }) @@ -123,6 +125,7 @@ export async function createRedirect( matchPath, rewritePath, rewritePathType, + priority, permanent, enabled } = parsedBody.data; @@ -195,6 +198,7 @@ export async function createRedirect( matchPath: matchPath ?? null, rewritePath: rewritePath ?? null, rewritePathType: rewritePathType ?? null, + priority: priority ?? 100, permanent: permanent ?? false, enabled: enabled ?? true }) diff --git a/server/routers/redirect/getRedirect.ts b/server/routers/redirect/getRedirect.ts index f12a4b0af..6cdea3d6f 100644 --- a/server/routers/redirect/getRedirect.ts +++ b/server/routers/redirect/getRedirect.ts @@ -22,6 +22,7 @@ export type GetRedirectResponse = { matchPath: string | null; rewritePath: string | null; rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null; + priority: number | null; permanent: boolean; enabled: boolean; resourceId: number | null; @@ -46,6 +47,7 @@ const redirectColumns = { matchPath: redirects.matchPath, rewritePath: redirects.rewritePath, rewritePathType: redirects.rewritePathType, + priority: redirects.priority, permanent: redirects.permanent, enabled: redirects.enabled, resourceId: redirects.resourceId, diff --git a/server/routers/redirect/listRedirects.ts b/server/routers/redirect/listRedirects.ts index d90acd6f6..c4421c0f6 100644 --- a/server/routers/redirect/listRedirects.ts +++ b/server/routers/redirect/listRedirects.ts @@ -22,6 +22,7 @@ export type ListRedirectsResponse = PaginatedResponse<{ matchPath: string | null; rewritePath: string | null; rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null; + priority: number | null; permanent: boolean; enabled: boolean; resourceId: number | null; @@ -145,6 +146,7 @@ export async function listRedirects( matchPath: redirects.matchPath, rewritePath: redirects.rewritePath, rewritePathType: redirects.rewritePathType, + priority: redirects.priority, permanent: redirects.permanent, enabled: redirects.enabled, resourceId: redirects.resourceId, @@ -173,7 +175,7 @@ export async function listRedirects( baseQuery .limit(pageSize) .offset(pageSize * (page - 1)) - .orderBy(desc(redirects.redirectId)) + .orderBy(desc(redirects.priority), desc(redirects.redirectId)) ]); return response(res, { diff --git a/server/routers/redirect/updateRedirect.ts b/server/routers/redirect/updateRedirect.ts index 139db0e46..2833ebd70 100644 --- a/server/routers/redirect/updateRedirect.ts +++ b/server/routers/redirect/updateRedirect.ts @@ -16,6 +16,7 @@ import { redirectPathMatchTypeSchema, redirectRewritePathSchema, redirectRewritePathTypeSchema, + redirectPrioritySchema, isValidMatchPath } from "@server/routers/redirect/validation"; import { createCertificate } from "../certificates"; @@ -40,6 +41,7 @@ const bodySchema = z.strictObject({ matchPath: redirectMatchPathSchema.optional().nullable(), rewritePath: redirectRewritePathSchema.optional().nullable(), rewritePathType: redirectRewritePathTypeSchema.optional().nullable(), + priority: redirectPrioritySchema.optional(), permanent: z.boolean().optional(), enabled: z.boolean().optional() }); @@ -255,6 +257,9 @@ export async function updateRedirect( if (body.rewritePathType !== undefined) { updateData.rewritePathType = body.rewritePathType; } + if (body.priority !== undefined) { + updateData.priority = body.priority; + } if (body.permanent !== undefined) { updateData.permanent = body.permanent; } diff --git a/server/routers/redirect/validation.ts b/server/routers/redirect/validation.ts index 93129eaf8..a72910a04 100644 --- a/server/routers/redirect/validation.ts +++ b/server/routers/redirect/validation.ts @@ -45,6 +45,9 @@ export function isValidMatchPath( export const redirectRewritePathSchema = z.string().nonempty(); +// Same range as target priorities; 100 means "let the system order it". +export const redirectPrioritySchema = z.int().min(1).max(1000); + export const redirectDestinationDomainSchema = z .string() .nonempty() diff --git a/src/app/[orgId]/settings/redirects/page.tsx b/src/app/[orgId]/settings/redirects/page.tsx index 99c3e4f5a..1533e4f16 100644 --- a/src/app/[orgId]/settings/redirects/page.tsx +++ b/src/app/[orgId]/settings/redirects/page.tsx @@ -54,6 +54,7 @@ export default async function RedirectIndexPage(props: RedirectIndexPageProps) { matchPath: redirect.matchPath, rewritePath: redirect.rewritePath, rewritePathType: redirect.rewritePathType, + priority: redirect.priority, permanent: redirect.permanent, enabled: redirect.enabled, resourceId: redirect.resourceId, diff --git a/src/components/RedirectForm.tsx b/src/components/RedirectForm.tsx index c024e560a..10340a898 100644 --- a/src/components/RedirectForm.tsx +++ b/src/components/RedirectForm.tsx @@ -67,6 +67,7 @@ import DomainPicker from "@app/components/DomainPicker"; import Link from "next/link"; const DEFAULT_PATH_MATCH_TYPE = "regex" as const; +const DEFAULT_PRIORITY = 100; export type ExistingRedirect = GetRedirectResponse["redirect"]; @@ -132,6 +133,11 @@ export default function RedirectForm({ rewritePathType: z .enum(["exact", "prefix", "regex", "stripPrefix"]) .nullable(), + priority: z + .number() + .int() + .min(1, { message: t("redirectPriorityInvalid") }) + .max(1000, { message: t("redirectPriorityInvalid") }), permanent: z.boolean(), enabled: z.boolean() }) @@ -193,6 +199,7 @@ export default function RedirectForm({ matchPath: redirect?.matchPath ?? null, rewritePath: redirect?.rewritePath ?? null, rewritePathType: redirect?.rewritePathType ?? null, + priority: redirect?.priority ?? DEFAULT_PRIORITY, permanent: redirect?.permanent ?? false, enabled: redirect?.enabled ?? true } @@ -248,6 +255,7 @@ export default function RedirectForm({ matchPath: values.matchPath?.trim() || null, rewritePath: values.rewritePath?.trim() || null, rewritePathType: values.rewritePathType, + priority: values.priority, permanent: values.permanent, enabled: values.enabled }; @@ -792,6 +800,40 @@ export default function RedirectForm({ /> + + ( + + + {t("priority")} + + + + field.onChange( + e.target + .valueAsNumber + ) + } + /> + + + {t( + "priorityDescription" + )} + + + + )} + /> + + {redirect.pathMatchType === "prefix" - ? withPrefixGlob( - redirect.matchPath - ) + ? withPrefixGlob(redirect.matchPath) : redirect.matchPath} )} @@ -320,6 +322,28 @@ export default function RedirectsTable({ ); } }, + { + accessorKey: "priority", + friendlyName: t("priority"), + header: () => {t("priority")}, + cell: ({ row }) => { + // 100 is the automatic default; anything else was set + // deliberately, so flag which way it deviates. + const priority = row.original.priority ?? 100; + return ( + + {priority} + {priority > 100 ? ( + + ) : priority < 100 ? ( + + ) : ( + + )} + + ); + } + }, { accessorKey: "permanent", friendlyName: t("redirectType"), @@ -439,7 +463,8 @@ export default function RedirectsTable({ columnVisibility={{ attachedTo: false, niceId: false, - permanent: false + permanent: false, + priority: false }} enableColumnVisibility stickyLeftColumn="name"