Skip to content

Commit 8d32f34

Browse files
committed
fix(webapp): don't reload runs list when toggling bulk action inspector
Prevent runs list revalidation and loading states when toggling the bulk action inspector. Filter, pagination, and explicit refresh behavior are unchanged.
1 parent db4074d commit 8d32f34

5 files changed

Lines changed: 256 additions & 51 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Stop reloading the runs list when opening or closing the bulk action inspector

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BeakerIcon, BookOpenIcon } from "@heroicons/react/24/solid";
22
import { type MetaFunction, useNavigation, useRevalidator } from "@remix-run/react";
33
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
4-
import { Suspense } from "react";
4+
import { Suspense, useEffect, useState } from "react";
55
import {
66
TypedAwait,
77
typeddefer,
@@ -26,6 +26,7 @@ import {
2626
ResizablePanel,
2727
ResizablePanelGroup,
2828
collapsibleHandleClassName,
29+
useFrozenValue,
2930
} from "~/components/primitives/Resizable";
3031
import { SelectedItemsProvider } from "~/components/primitives/SelectedItemsProvider";
3132
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
@@ -39,6 +40,7 @@ import { $replica } from "~/db.server";
3940
import { useEnvironment } from "~/hooks/useEnvironment";
4041
import { useOrganization } from "~/hooks/useOrganizations";
4142
import { useProject } from "~/hooks/useProject";
43+
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
4244
import { useSearchParams } from "~/hooks/useSearchParam";
4345
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
4446
import { redirectWithErrorMessage } from "~/models/message.server";
@@ -56,7 +58,6 @@ import { cn } from "~/utils/cn";
5658
import {
5759
docsPath,
5860
EnvironmentParamSchema,
59-
v3CreateBulkActionPath,
6061
v3ProjectPath,
6162
v3TestPath,
6263
v3TestTaskPath,
@@ -65,8 +66,11 @@ import { throwNotFound } from "~/utils/httpErrors";
6566
import { ListPagination } from "../../components/ListPagination";
6667
import { CreateBulkActionInspector } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction";
6768
import { Callout } from "~/components/primitives/Callout";
69+
import { isRunsListLoading, shouldRevalidateRunsList } from "./shouldRevalidateRunsList";
6870
import { useRunsLiveReload } from "./useRunsLiveReload";
6971

72+
export { shouldRevalidateRunsList as shouldRevalidate };
73+
7074
export const meta: MetaFunction = () => {
7175
return [
7276
{
@@ -209,8 +213,9 @@ function RunsList({
209213
filters: TaskRunListSearchFilters;
210214
}) {
211215
const revalidator = useRevalidator();
216+
const location = useOptimisticLocation();
212217
const navigation = useNavigation();
213-
const isLoading = navigation.state !== "idle";
218+
const isLoading = isRunsListLoading(navigation, location.search);
214219
const organization = useOrganization();
215220
const project = useProject();
216221
const environment = useEnvironment();
@@ -244,7 +249,7 @@ function RunsList({
244249
shortcut: { key: "r" },
245250
action: (e) => {
246251
replace({
247-
bulkInspector: "true",
252+
bulkInspector: "show",
248253
action: "replay",
249254
mode: selectedItems.size > 0 ? "selected" : undefined,
250255
});
@@ -254,14 +259,28 @@ function RunsList({
254259
shortcut: { key: "c" },
255260
action: (e) => {
256261
replace({
257-
bulkInspector: "true",
262+
bulkInspector: "show",
258263
action: "cancel",
259264
mode: selectedItems.size > 0 ? "selected" : undefined,
260265
});
261266
},
262267
});
263268

264269
const isShowingBulkActionInspector = has("bulkInspector") && list.hasAnyRuns;
270+
const [isBulkInspectorPanelCollapsed, setIsBulkInspectorPanelCollapsed] = useState(
271+
!isShowingBulkActionInspector
272+
);
273+
const frozenShowBulkInspector = useFrozenValue(isShowingBulkActionInspector || undefined);
274+
const showBulkInspectorContent =
275+
isShowingBulkActionInspector ||
276+
(frozenShowBulkInspector === true && !isBulkInspectorPanelCollapsed);
277+
278+
useEffect(() => {
279+
if (isShowingBulkActionInspector) {
280+
setIsBulkInspectorPanelCollapsed(false);
281+
}
282+
}, [isShowingBulkActionInspector]);
283+
265284
return (
266285
<ResizablePanelGroup orientation="horizontal" className="max-h-full">
267286
<ResizablePanel id="runs-main" min={"100px"}>
@@ -310,39 +329,39 @@ function RunsList({
310329
</Button>
311330
</span>
312331
)}
313-
{!isShowingBulkActionInspector && (
314-
<LinkButton
315-
variant="secondary/small"
316-
to={v3CreateBulkActionPath(
317-
organization,
318-
project,
319-
environment,
320-
filters,
321-
selectedItems.size > 0 ? "selected" : undefined
322-
)}
323-
LeadingIcon={ListCheckedIcon}
324-
className={selectedItems.size > 0 ? "pr-1" : undefined}
325-
tooltip={
326-
<div className="-mr-1 flex items-center gap-3 text-xs text-text-dimmed">
327-
<div className="flex items-center gap-0.5">
328-
<span>Replay</span>
329-
<ShortcutKey shortcut={{ key: "r" }} variant={"small"} />
330-
</div>
331-
<div className="flex items-center gap-0.5">
332-
<span>Cancel</span>
333-
<ShortcutKey shortcut={{ key: "c" }} variant={"small"} />
334-
</div>
332+
<Button
333+
variant="secondary/small"
334+
onClick={() =>
335+
replace({
336+
bulkInspector: "show",
337+
mode: selectedItems.size > 0 ? "selected" : undefined,
338+
})
339+
}
340+
LeadingIcon={ListCheckedIcon}
341+
className={cn(
342+
selectedItems.size > 0 ? "pr-1" : undefined,
343+
isShowingBulkActionInspector && "pointer-events-none invisible"
344+
)}
345+
tooltip={
346+
<div className="-mr-1 flex items-center gap-3 text-xs text-text-dimmed">
347+
<div className="flex items-center gap-0.5">
348+
<span>Replay</span>
349+
<ShortcutKey shortcut={{ key: "r" }} variant={"small"} />
335350
</div>
336-
}
337-
>
338-
<span className="flex items-center gap-x-1 whitespace-nowrap text-text-bright">
339-
<span>Bulk action</span>
340-
{selectedItems.size > 0 && (
341-
<Badge variant="rounded">{selectedItems.size}</Badge>
342-
)}
343-
</span>
344-
</LinkButton>
345-
)}
351+
<div className="flex items-center gap-0.5">
352+
<span>Cancel</span>
353+
<ShortcutKey shortcut={{ key: "c" }} variant={"small"} />
354+
</div>
355+
</div>
356+
}
357+
>
358+
<span className="flex items-center gap-x-1 whitespace-nowrap text-text-bright">
359+
<span>Bulk action</span>
360+
{selectedItems.size > 0 && (
361+
<Badge variant="rounded">{selectedItems.size}</Badge>
362+
)}
363+
</span>
364+
</Button>
346365
<ListPagination list={list} />
347366
</div>
348367
</div>
@@ -374,12 +393,12 @@ function RunsList({
374393
className="overflow-hidden"
375394
collapsible
376395
collapsed={!isShowingBulkActionInspector}
377-
onCollapseChange={() => {}}
396+
onCollapseChange={setIsBulkInspectorPanelCollapsed}
378397
collapsedSize="0px"
379398
collapseAnimation={RESIZABLE_PANEL_ANIMATION}
380399
>
381400
<div className="h-full" style={{ minWidth: 400 }}>
382-
{isShowingBulkActionInspector && (
401+
{showBulkInspectorContent && (
383402
<CreateBulkActionInspector
384403
filters={filters}
385404
selectedItems={selectedItems}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { Navigation, ShouldRevalidateFunction } from "@remix-run/react";
2+
3+
/** Search params that only control the bulk-action inspector UI, not list data. */
4+
export const RUNS_BULK_INSPECTOR_UI_SEARCH_PARAMS = ["bulkInspector", "action", "mode"] as const;
5+
6+
export function searchParamsEqualIgnoringBulkInspectorUiState(
7+
current: URLSearchParams,
8+
next: URLSearchParams
9+
) {
10+
const currentFiltered = new URLSearchParams(current);
11+
const nextFiltered = new URLSearchParams(next);
12+
for (const key of RUNS_BULK_INSPECTOR_UI_SEARCH_PARAMS) {
13+
currentFiltered.delete(key);
14+
nextFiltered.delete(key);
15+
}
16+
return currentFiltered.toString() === nextFiltered.toString();
17+
}
18+
19+
/** True when navigation should show the runs table loading state (excludes bulk-inspector UI toggles). */
20+
export function isRunsListLoading(navigation: Navigation, currentSearch: string): boolean {
21+
if (navigation.state === "idle" || !navigation.location) {
22+
return false;
23+
}
24+
25+
const currentParams = new URLSearchParams(currentSearch);
26+
const nextParams = new URLSearchParams(navigation.location.search);
27+
28+
if (searchParamsEqualIgnoringBulkInspectorUiState(currentParams, nextParams)) {
29+
return false;
30+
}
31+
32+
return true;
33+
}
34+
35+
/**
36+
* Skip runs list loader revalidation when only bulk-inspector UI params change.
37+
* Explicit revalidate() (unchanged URL) and filter/pagination changes still revalidate.
38+
*/
39+
export const shouldRevalidateRunsList: ShouldRevalidateFunction = ({
40+
currentUrl,
41+
nextUrl,
42+
defaultShouldRevalidate,
43+
}) => {
44+
if (currentUrl.pathname !== nextUrl.pathname) {
45+
return defaultShouldRevalidate;
46+
}
47+
48+
const currentParams = new URLSearchParams(currentUrl.search);
49+
const nextParams = new URLSearchParams(nextUrl.search);
50+
51+
if (currentParams.toString() === nextParams.toString()) {
52+
return defaultShouldRevalidate;
53+
}
54+
55+
if (searchParamsEqualIgnoringBulkInspectorUiState(currentParams, nextParams)) {
56+
return false;
57+
}
58+
59+
return defaultShouldRevalidate;
60+
};

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
AccordionItem,
2424
AccordionTrigger,
2525
} from "~/components/primitives/Accordion";
26-
import { Button, LinkButton } from "~/components/primitives/Buttons";
26+
import { Button } from "~/components/primitives/Buttons";
2727
import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
2828
import {
2929
Dialog,
@@ -54,7 +54,7 @@ import { CreateBulkActionPresenter } from "~/presenters/v3/CreateBulkActionPrese
5454
import { logger } from "~/services/logger.server";
5555
import { requireUserId } from "~/services/session.server";
5656
import { cn } from "~/utils/cn";
57-
import { EnvironmentParamSchema, v3BulkActionPath, v3RunsPath } from "~/utils/pathBuilder";
57+
import { EnvironmentParamSchema, v3BulkActionPath } from "~/utils/pathBuilder";
5858
import { BulkActionService } from "~/v3/services/bulk/BulkActionV2.server";
5959

6060
export async function loader({ request, params }: LoaderFunctionArgs) {
@@ -187,7 +187,7 @@ export function CreateBulkActionInspector({
187187
const project = useProject();
188188
const environment = useEnvironment();
189189
const fetcher = useTypedFetcher<typeof loader>();
190-
const { value, replace } = useSearchParams();
190+
const { value, replace, del } = useSearchParams();
191191
const [action, setAction] = useState<BulkActionAction>(
192192
bulkActionActionFromString(value("action"))
193193
);
@@ -208,9 +208,6 @@ export function CreateBulkActionInspector({
208208

209209
const data = fetcher.data != null ? fetcher.data : undefined;
210210

211-
const closedSearchParams = new URLSearchParams(location.search);
212-
closedSearchParams.delete("bulkInspector");
213-
214211
const impactedCountElement =
215212
mode === "selected" ? selectedItems.size : <EstimatedCount count={data?.count} />;
216213

@@ -225,13 +222,10 @@ export function CreateBulkActionInspector({
225222
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
226223
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
227224
<Header2 className="whitespace-nowrap">Create a bulk action</Header2>
228-
<LinkButton
229-
to={`${v3RunsPath(
230-
organization,
231-
project,
232-
environment
233-
)}?${closedSearchParams.toString()}`}
225+
<Button
226+
type="button"
234227
variant="minimal/small"
228+
onClick={() => del(["bulkInspector", "action", "mode"])}
235229
TrailingIcon={ExitIcon}
236230
shortcut={{ key: "esc" }}
237231
shortcutPosition="before-trailing-icon"

0 commit comments

Comments
 (0)