diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx index 48a31788eb7..a3648bcbfbc 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx @@ -287,8 +287,7 @@ export default function Page() { Branches docs - - {limits.isAtLimit ? ( + {!hasBranches && limits.isAtLimit && ( - ) : ( - - New branch… - - } - env="preview" - /> )} -
-
- - {autoArchiveAvailable && ( - + {hasBranches && ( +
+ + {autoArchiveAvailable && ( + + )} + +
+ - )} - - -
+ {limits.isAtLimit ? ( + + ) : ( + + New branch… + + } + env="preview" + /> + )} +
+
+ )} {!hasBranches ? ( -
+
(); - const toggleRef = useRef(null); + const triggerRef = useRef(null); const enabled = environment.previewAutoArchiveAfterDays !== null; - function handleToggle(checked: boolean) { - if (checked) { - setOpen(true); - } else { - fetcher.submit( - { - environmentId: environment.id, - intent: "save", - excludedBranches: environment.previewAutoArchiveExcludedBranches.join("\n"), - }, - { method: "post", action: "/resources/branches/auto-archive" } - ); - } - } - return ( -
-
- + - - )} -
- {fetcher.data && !fetcher.data.ok && {fetcher.data.error}} -
+ LeadingIcon={Cog6ToothIcon} + className="shrink-0 whitespace-nowrap" + disabled={!canManage} + > + Manage auto-archive + + + ) : ( + setOpen(true)} + disabled={!canManage} + /> + )} { event.preventDefault(); - toggleRef.current?.focus(); + triggerRef.current?.focus(); }} > - Auto-archive preview branches + + Auto-archive preview branches + {open && setOpen(false)} />}
@@ -216,9 +214,43 @@ function AutoArchiveForm({ }>({ pending: false }); const [requestedKey, setRequestedKey] = useState(null); const [days, setDays] = useState(String(environment.previewAutoArchiveAfterDays ?? 14)); - const [excluded, setExcluded] = useState( - environment.previewAutoArchiveExcludedBranches.join("\n") + const [keepSpecificBranches, setKeepSpecificBranches] = useState( + environment.previewAutoArchiveExcludedBranches.length > 0 + ); + const [excludedRows, setExcludedRows] = useState(() => + [...environment.previewAutoArchiveExcludedBranches, ""].map((name, id) => ({ id, name })) ); + const nextRowId = useRef(excludedRows.length); + const excluded = keepSpecificBranches ? excludedRows.map(({ name }) => name).join("\n") : ""; + const enabled = environment.previewAutoArchiveAfterDays !== null; + + function updateExcludedBranch(id: number, name: string) { + const rows = excludedRows.map((row) => (row.id === id ? { ...row, name } : row)); + if (rows.every((row) => row.name.trim() !== "") && rows.length < 100) { + rows.push({ id: nextRowId.current++, name: "" }); + } + setExcludedRows(rows); + } + + function removeExcludedBranch(id: number) { + const rows = excludedRows.filter((row) => row.id !== id); + if (rows.length === 0 || rows.every((row) => row.name.trim() !== "")) { + rows.push({ id: nextRowId.current++, name: "" }); + } + setExcludedRows(rows); + } + + function disableAutoArchive() { + crumb("disable preview auto-archive", { environmentId: environment.id }); // @crumbs + fetcher.submit( + { + environmentId: environment.id, + intent: "save", + excludedBranches: environment.previewAutoArchiveExcludedBranches.join("\n"), + }, + { method: "post", action: "/resources/branches/auto-archive" } + ); + } const policyKey = JSON.stringify({ days: Number(days), excludedBranches: [ @@ -233,6 +265,13 @@ function AutoArchiveForm({ const valid = PreviewAutoArchivePolicy.safeParse(JSON.parse(policyKey)).success; const reviewed = valid && !preview.pending && preview.data?.ok && preview.data.policyKey === policyKey; + const previewError = + requestedKey === policyKey && !preview.pending && preview.data && !preview.data.ok + ? preview.data.error + : undefined; + const checkingPreview = valid && !reviewed && !previewError; + const protectedBranches = + valid && !previewError && preview.data?.ok ? preview.data.protectedBranches : []; const busy = fetcher.state !== "idle"; useEffect(() => { if (!valid || busy) return; @@ -240,7 +279,8 @@ function AutoArchiveForm({ const timer = setTimeout(async () => { const policy = JSON.parse(policyKey) as { days: number; excludedBranches: string[] }; setRequestedKey(policyKey); - setPreview({ pending: true }); + // Keep the previous results and their height while refreshing the preview. + setPreview((previous) => ({ ...previous, pending: true })); try { // A read-only POST avoids URL-size limits for exclusions and Remix's // page-wide action revalidation while the user edits the form. @@ -283,89 +323,174 @@ function AutoArchiveForm({ > -
- - setDays(event.target.value)} - /> -
-
- -