Skip to content

Commit 9aadf4e

Browse files
committed
feat: can multi-select deployments when pressing shift
closes #1982
1 parent f5ac1d5 commit 9aadf4e

File tree

2 files changed

+98
-11
lines changed

2 files changed

+98
-11
lines changed

apps/deploy-web/src/components/deployments/DeploymentList.tsx

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import { useEffect, useState } from "react";
2+
import { useCallback, useEffect, useState } from "react";
33
import {
44
Button,
55
buttonVariants,
@@ -18,6 +18,7 @@ import {
1818
import { cn } from "@akashnetwork/ui/utils";
1919
import { Refresh, Rocket, Xmark } from "iconoir-react";
2020
import { useAtom } from "jotai";
21+
import { uniq } from "lodash";
2122
import Link from "next/link";
2223
import { NextSeo } from "next-seo";
2324

@@ -64,6 +65,9 @@ export const DeploymentList: React.FunctionComponent = () => {
6465
const [isSignedInWithTrial] = useAtom(walletStore.isSignedInWithTrial);
6566
const { user } = useCustomUser();
6667

68+
const [lastSelectedDeploymentDseq, setLastSelectedDeploymentDseq] = useState<string | null>(null);
69+
const [hoveredDelpoymentDseq, setHoveredDelpoymentDseq] = useState<string | null>(null);
70+
6771
useEffect(() => {
6872
if (isWalletLoaded && isSettingsInit) {
6973
getDeployments();
@@ -110,10 +114,35 @@ export const DeploymentList: React.FunctionComponent = () => {
110114
setSearch(value);
111115
};
112116

113-
const onSelectDeployment = (checked: boolean, dseq: string) => {
117+
const isBetweenDseqs = useCallback(
118+
(dseq: string, dseqA: string, dseqB: string) => {
119+
const dseqIndex = currentPageDeployments?.findIndex(d => d.dseq === dseq);
120+
const dseqAIndex = currentPageDeployments?.findIndex(d => d.dseq === dseqA);
121+
const dseqBIndex = currentPageDeployments?.findIndex(d => d.dseq === dseqB);
122+
123+
return (
124+
dseqIndex !== undefined &&
125+
dseqAIndex !== undefined &&
126+
dseqBIndex !== undefined &&
127+
((dseqAIndex <= dseqIndex && dseqIndex <= dseqBIndex) || (dseqAIndex >= dseqIndex && dseqIndex >= dseqBIndex))
128+
);
129+
},
130+
[currentPageDeployments]
131+
);
132+
133+
const onSelectDeployment = (checked: boolean, dseq: string, eventShiftPressed: boolean) => {
134+
const dseqs =
135+
lastSelectedDeploymentDseq && eventShiftPressed
136+
? currentPageDeployments.filter(deployment => isBetweenDseqs(deployment.dseq, dseq, lastSelectedDeploymentDseq)).map(d => d.dseq)
137+
: [dseq];
138+
114139
setSelectedDeploymentDseqs(prev => {
115-
return checked ? prev.concat([dseq]) : prev.filter(x => x !== dseq);
140+
return checked ? uniq(prev.concat(dseqs)) : prev.filter(x => !dseqs.includes(x));
116141
});
142+
143+
if (!eventShiftPressed) {
144+
setLastSelectedDeploymentDseq(dseq);
145+
}
117146
};
118147

119148
const onCloseSelectedDeployments = async () => {
@@ -148,6 +177,45 @@ export const DeploymentList: React.FunctionComponent = () => {
148177
setPageIndex(0);
149178
};
150179

180+
const [isShiftPressed, setIsShiftPressed] = useState(false);
181+
useEffect(() => {
182+
const handleKeyDown = (event: KeyboardEvent) => {
183+
if (event.key === "Shift") {
184+
setIsShiftPressed(true);
185+
}
186+
};
187+
188+
const handleKeyUp = (event: KeyboardEvent) => {
189+
if (event.key === "Shift") {
190+
setIsShiftPressed(false);
191+
}
192+
};
193+
194+
window.addEventListener("keydown", handleKeyDown);
195+
window.addEventListener("keyup", handleKeyUp);
196+
197+
return () => {
198+
window.removeEventListener("keydown", handleKeyDown);
199+
window.removeEventListener("keyup", handleKeyUp);
200+
};
201+
}, []);
202+
203+
const onRowMouseEnter = (dseq: string) => {
204+
setHoveredDelpoymentDseq(dseq);
205+
};
206+
207+
const isRowHighlightedForMultipleSelection = useCallback(
208+
(dseq: string) => {
209+
if (!isShiftPressed || !hoveredDelpoymentDseq || !lastSelectedDeploymentDseq) {
210+
return false;
211+
}
212+
213+
const dseqIndex = currentPageDeployments?.findIndex(d => d.dseq === dseq);
214+
return dseqIndex !== undefined && isBetweenDseqs(dseq, hoveredDelpoymentDseq, lastSelectedDeploymentDseq);
215+
},
216+
[isShiftPressed, hoveredDelpoymentDseq, lastSelectedDeploymentDseq, currentPageDeployments, isBetweenDseqs]
217+
);
218+
151219
return (
152220
<Layout isLoading={isLoadingDeployments || isLoadingProviders} isUsingSettings isUsingWallet>
153221
<NextSeo title="Deployments" />
@@ -293,6 +361,8 @@ export const DeploymentList: React.FunctionComponent = () => {
293361
isSelectable
294362
onSelectDeployment={onSelectDeployment}
295363
checked={selectedDeploymentDseqs.includes(deployment.dseq)}
364+
onMouseEnter={onRowMouseEnter}
365+
isHighlighted={isRowHighlightedForMultipleSelection(deployment.dseq)}
296366
/>
297367
))}
298368
</TableBody>

apps/deploy-web/src/components/deployments/DeploymentListRow.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
TableCell,
1414
TableRow
1515
} from "@akashnetwork/ui/components";
16+
import { cn } from "@akashnetwork/ui/utils";
1617
import ClickAwayListener from "@mui/material/ClickAwayListener";
1718
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
1819
import formatDistanceToNow from "date-fns/formatDistanceToNow";
@@ -50,14 +51,25 @@ import { LeaseChip } from "./LeaseChip";
5051
type Props = {
5152
deployment: NamedDeploymentDto;
5253
isSelectable?: boolean;
53-
onSelectDeployment?: (isChecked: boolean, dseq: string) => void;
54+
onSelectDeployment?: (isChecked: boolean, dseq: string, eventShiftPressed: boolean) => void;
55+
onMouseEnter?: (dseq: string) => void;
5456
checked?: boolean;
5557
providers: Array<ApiProviderList> | undefined;
56-
refreshDeployments: any;
58+
refreshDeployments: () => void;
5759
children?: ReactNode;
60+
isHighlighted?: boolean;
5861
};
5962

60-
export const DeploymentListRow: React.FunctionComponent<Props> = ({ deployment, isSelectable, onSelectDeployment, checked, providers, refreshDeployments }) => {
63+
export const DeploymentListRow: React.FunctionComponent<Props> = ({
64+
deployment,
65+
isSelectable,
66+
onSelectDeployment,
67+
checked,
68+
providers,
69+
refreshDeployments,
70+
onMouseEnter,
71+
isHighlighted
72+
}) => {
6173
const router = useRouter();
6274
const [open, setOpen] = useState(false);
6375
const [isDepositingDeployment, setIsDepositingDeployment] = useState(false);
@@ -132,7 +144,7 @@ export const DeploymentListRow: React.FunctionComponent<Props> = ({ deployment,
132144
const response = await signAndBroadcastTx([message]);
133145
if (response) {
134146
if (onSelectDeployment) {
135-
onSelectDeployment(false, deployment.dseq);
147+
onSelectDeployment(false, deployment.dseq, false);
136148
}
137149

138150
refreshDeployments();
@@ -168,7 +180,14 @@ export const DeploymentListRow: React.FunctionComponent<Props> = ({ deployment,
168180

169181
return (
170182
<>
171-
<TableRow className="cursor-pointer hover:bg-muted-foreground/10 [&>td]:p-2" role="link" onClick={viewDeployment}>
183+
<TableRow
184+
className={cn("cursor-pointer hover:bg-muted-foreground/10 [&>td]:p-2", {
185+
"bg-muted-foreground/10": isHighlighted
186+
})}
187+
role="link"
188+
onClick={viewDeployment}
189+
onMouseEnter={() => onMouseEnter?.(deployment.dseq)}
190+
>
172191
<TableCell>
173192
<div className="flex items-center justify-center">
174193
<SpecDetailList
@@ -296,9 +315,7 @@ export const DeploymentListRow: React.FunctionComponent<Props> = ({ deployment,
296315
checked={checked}
297316
onClick={event => {
298317
event.stopPropagation();
299-
}}
300-
onCheckedChange={value => {
301-
onSelectDeployment && onSelectDeployment(value as boolean, deployment.dseq);
318+
onSelectDeployment?.(!checked, deployment.dseq, event.shiftKey);
302319
}}
303320
/>
304321
)}

0 commit comments

Comments
 (0)