Skip to content

Commit 6c57b16

Browse files
committed
refactor: centralize relay connection pagination logic
- Add shared relay pagination hook and migrate list pages, selects, tabs, and forms to use it. - Remove duplicated debounce/refetch/loadNext boilerplate and standardize onLoadMore handling across relay pagination fragments. Keeping behavior consistent and making future pagination changes easier Signed-off-by: Omar <omar.brbutovic@secomind.com>
1 parent 4f0293b commit 6c57b16

34 files changed

Lines changed: 838 additions & 1242 deletions

frontend/src/components/BaseImageSelect.tsx

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
* SPDX-License-Identifier: Apache-2.0
1919
*/
2020

21-
import _ from "lodash";
2221
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
2322
import { ErrorBoundary } from "react-error-boundary";
2423
import type { FallbackProps } from "react-error-boundary";
@@ -42,8 +41,9 @@ import { BaseImageSelect_getBaseImageCollection_Query } from "@/api/__generated_
4241
import Button from "@/components/Button";
4342
import Spinner from "@/components/Spinner";
4443
import Stack from "@/components/Stack";
45-
import { RECORDS_TO_LOAD_FIRST, RECORDS_TO_LOAD_NEXT } from "@/constants";
44+
import { RECORDS_TO_LOAD_FIRST } from "@/constants";
4645
import { BaseImageCollectionRecord } from "@/forms/CreateUpdateCampaign";
46+
import useRelayConnectionPagination from "@/hooks/useRelayConnectionPagination";
4747

4848
const GET_BASE_IMAGE_COLLECTION_QUERY = graphql`
4949
query BaseImageSelect_getBaseImageCollection_Query(
@@ -111,40 +111,24 @@ const BaseImageSelect = ({
111111

112112
const [searchText, setSearchText] = useState<string | null>(null);
113113

114-
const debounceRefetch = useMemo(
115-
() =>
116-
_.debounce((text: string) => {
117-
if (text === "") {
118-
refetch(
119-
{
120-
first: RECORDS_TO_LOAD_FIRST,
121-
},
122-
{ fetchPolicy: "network-only" },
123-
);
124-
} else {
125-
refetch(
126-
{
127-
first: RECORDS_TO_LOAD_FIRST,
128-
filter: { version: { ilike: `%${text}%` } },
129-
},
130-
{ fetchPolicy: "network-only" },
131-
);
132-
}
133-
}, 500),
134-
[refetch],
135-
);
136-
137-
useEffect(() => {
138-
if (searchText !== null) {
139-
debounceRefetch(searchText);
140-
}
141-
}, [debounceRefetch, searchText]);
114+
const { onLoadMore } = useRelayConnectionPagination({
115+
hasNext,
116+
isLoadingNext,
117+
loadNext,
118+
refetch,
119+
searchText,
120+
buildFilter: (text) => {
121+
if (text === "") {
122+
return undefined;
123+
}
142124

143-
const loadNextOptions = useCallback(() => {
144-
if (hasNext && !isLoadingNext) {
145-
loadNext(RECORDS_TO_LOAD_NEXT);
146-
}
147-
}, [hasNext, isLoadingNext, loadNext]);
125+
return {
126+
version: {
127+
ilike: `%${text}%`,
128+
},
129+
};
130+
},
131+
});
148132

149133
const baseImages = useMemo(() => {
150134
return (
@@ -187,7 +171,7 @@ const BaseImageSelect = ({
187171
noBaseImageOptionsMessage(inputValue)
188172
}
189173
isLoading={isLoadingNext}
190-
onMenuScrollToBottom={hasNext ? loadNextOptions : undefined}
174+
onMenuScrollToBottom={onLoadMore}
191175
onInputChange={(text) => setSearchText(text)}
192176
/>
193177
);

frontend/src/components/DeploymentTargetsTabs.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* SPDX-License-Identifier: Apache-2.0
1919
*/
2020

21-
import { useCallback, useEffect, useMemo, useState } from "react";
21+
import { useEffect, useMemo, useState } from "react";
2222
import Nav from "react-bootstrap/Nav";
2323
import NavItem from "react-bootstrap/NavItem";
2424
import NavLink from "react-bootstrap/NavLink";
@@ -36,7 +36,8 @@ import type { ColumnId } from "@/components/DeploymentTargetsTable";
3636
import DeploymentTargetsTable, {
3737
columnIds,
3838
} from "@/components/DeploymentTargetsTable";
39-
import { RECORDS_TO_LOAD_FIRST, RECORDS_TO_LOAD_NEXT } from "@/constants";
39+
import { RECORDS_TO_LOAD_FIRST } from "@/constants";
40+
import useRelayConnectionPagination from "@/hooks/useRelayConnectionPagination";
4041
import Spinner from "./Spinner";
4142

4243
/* eslint-disable relay/unused-fields */
@@ -122,11 +123,11 @@ const DeploymentTargetsTabs = ({ campaignRef }: Props) => {
122123
);
123124
}, [activeTab, committedTab, refetch]);
124125

125-
const loadNextDeploymentTargets = useCallback(() => {
126-
if (hasNext && !isLoadingNext) {
127-
loadNext(RECORDS_TO_LOAD_NEXT);
128-
}
129-
}, [hasNext, isLoadingNext, loadNext]);
126+
const { onLoadMore } = useRelayConnectionPagination({
127+
hasNext,
128+
isLoadingNext,
129+
loadNext,
130+
});
130131

131132
const deploymentTargetsRef = data?.campaignTargets;
132133

@@ -171,7 +172,7 @@ const DeploymentTargetsTabs = ({ campaignRef }: Props) => {
171172
campaignTargetsRef={deploymentTargetsRef}
172173
hiddenColumns={hiddenColumns}
173174
loading={isLoadingNext}
174-
onLoadMore={hasNext ? loadNextDeploymentTargets : undefined}
175+
onLoadMore={onLoadMore}
175176
/>
176177
)}
177178
</div>

frontend/src/components/FileDownloadTargetsTabs.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* SPDX-License-Identifier: Apache-2.0
1919
*/
2020

21-
import { useCallback, useEffect, useMemo, useState } from "react";
21+
import { useEffect, useMemo, useState } from "react";
2222
import Nav from "react-bootstrap/Nav";
2323
import NavItem from "react-bootstrap/NavItem";
2424
import NavLink from "react-bootstrap/NavLink";
@@ -36,7 +36,8 @@ import type { ColumnId } from "@/components/FileDownloadTargetsTable";
3636
import FileDownloadTargetsTable, {
3737
columnIds,
3838
} from "@/components/FileDownloadTargetsTable";
39-
import { RECORDS_TO_LOAD_FIRST, RECORDS_TO_LOAD_NEXT } from "@/constants";
39+
import { RECORDS_TO_LOAD_FIRST } from "@/constants";
40+
import useRelayConnectionPagination from "@/hooks/useRelayConnectionPagination";
4041
import Spinner from "./Spinner";
4142

4243
const FILE_DOWNLOAD_TARGETS_FRAGMENT = graphql`
@@ -125,11 +126,11 @@ const FileDownloadTargetsTabs = ({
125126
);
126127
}, [activeTab, committedTab, refetch]);
127128

128-
const loadNextTargets = useCallback(() => {
129-
if (hasNext && !isLoadingNext) {
130-
loadNext(RECORDS_TO_LOAD_NEXT);
131-
}
132-
}, [hasNext, isLoadingNext, loadNext]);
129+
const { onLoadMore } = useRelayConnectionPagination({
130+
hasNext,
131+
isLoadingNext,
132+
loadNext,
133+
});
133134

134135
const targetsRef = data?.campaignTargets;
135136

@@ -189,7 +190,7 @@ const FileDownloadTargetsTabs = ({
189190
campaignTargetsRef={targetsRef}
190191
hiddenColumns={hiddenColumns}
191192
loading={isLoadingNext}
192-
onLoadMore={hasNext ? loadNextTargets : undefined}
193+
onLoadMore={onLoadMore}
193194
/>
194195
)}
195196
</div>

frontend/src/components/FileSelect.tsx

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
* SPDX-License-Identifier: Apache-2.0
1919
*/
2020

21-
import _ from "lodash";
2221
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
2322
import type { FallbackProps } from "react-error-boundary";
2423
import { ErrorBoundary } from "react-error-boundary";
@@ -42,7 +41,8 @@ import type { FileSelect_getRepository_Query } from "@/api/__generated__/FileSel
4241
import Button from "@/components/Button";
4342
import Spinner from "@/components/Spinner";
4443
import Stack from "@/components/Stack";
45-
import { RECORDS_TO_LOAD_FIRST, RECORDS_TO_LOAD_NEXT } from "@/constants";
44+
import { RECORDS_TO_LOAD_FIRST } from "@/constants";
45+
import useRelayConnectionPagination from "@/hooks/useRelayConnectionPagination";
4646

4747
const GET_REPOSITORY_QUERY = graphql`
4848
query FileSelect_getRepository_Query(
@@ -112,40 +112,24 @@ const FileSelect = ({ filesFragmentRef, controllerProps }: FileSelectProps) => {
112112

113113
const [searchText, setSearchText] = useState<string | null>(null);
114114

115-
const debounceRefetch = useMemo(
116-
() =>
117-
_.debounce((text: string) => {
118-
if (text === "") {
119-
refetch(
120-
{
121-
first: RECORDS_TO_LOAD_FIRST,
122-
},
123-
{ fetchPolicy: "network-only" },
124-
);
125-
} else {
126-
refetch(
127-
{
128-
first: RECORDS_TO_LOAD_FIRST,
129-
filter: { name: { ilike: `%${text}%` } },
130-
},
131-
{ fetchPolicy: "network-only" },
132-
);
133-
}
134-
}, 500),
135-
[refetch],
136-
);
137-
138-
useEffect(() => {
139-
if (searchText !== null) {
140-
debounceRefetch(searchText);
141-
}
142-
}, [debounceRefetch, searchText]);
115+
const { onLoadMore } = useRelayConnectionPagination({
116+
hasNext,
117+
isLoadingNext,
118+
loadNext,
119+
refetch,
120+
searchText,
121+
buildFilter: (text) => {
122+
if (text === "") {
123+
return undefined;
124+
}
143125

144-
const loadNextOptions = useCallback(() => {
145-
if (hasNext && !isLoadingNext) {
146-
loadNext(RECORDS_TO_LOAD_NEXT);
147-
}
148-
}, [hasNext, isLoadingNext, loadNext]);
126+
return {
127+
name: {
128+
ilike: `%${text}%`,
129+
},
130+
};
131+
},
132+
});
149133

150134
const files = useMemo(() => {
151135
return (
@@ -185,7 +169,7 @@ const FileSelect = ({ filesFragmentRef, controllerProps }: FileSelectProps) => {
185169
getOptionValue={getFileValue}
186170
noOptionsMessage={({ inputValue }) => noFileOptionsMessage(inputValue)}
187171
isLoading={isLoadingNext}
188-
onMenuScrollToBottom={hasNext ? loadNextOptions : undefined}
172+
onMenuScrollToBottom={onLoadMore}
189173
onInputChange={(text) => setSearchText(text)}
190174
/>
191175
);

frontend/src/components/ReleaseSelect.tsx

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
* SPDX-License-Identifier: Apache-2.0
1919
*/
2020

21-
import _ from "lodash";
2221
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
2322
import type { FallbackProps } from "react-error-boundary";
2423
import { ErrorBoundary } from "react-error-boundary";
@@ -43,8 +42,9 @@ import {
4342
import Button from "@/components/Button";
4443
import Spinner from "@/components/Spinner";
4544
import Stack from "@/components/Stack";
46-
import { RECORDS_TO_LOAD_FIRST, RECORDS_TO_LOAD_NEXT } from "@/constants";
45+
import { RECORDS_TO_LOAD_FIRST } from "@/constants";
4746
import { ApplicationRecord } from "@/forms/CreateDeploymentCampaign";
47+
import useRelayConnectionPagination from "@/hooks/useRelayConnectionPagination";
4848

4949
const GET_APPLICATION_QUERY = graphql`
5050
query ReleaseSelect_getApplication_Query(
@@ -109,40 +109,24 @@ const ReleaseSelect = ({
109109

110110
const [searchText, setSearchText] = useState<string | null>(null);
111111

112-
const debounceRefetch = useMemo(
113-
() =>
114-
_.debounce((text: string) => {
115-
if (text === "") {
116-
refetch(
117-
{
118-
first: RECORDS_TO_LOAD_FIRST,
119-
},
120-
{ fetchPolicy: "network-only" },
121-
);
122-
} else {
123-
refetch(
124-
{
125-
first: RECORDS_TO_LOAD_FIRST,
126-
filter: { version: { ilike: `%${text}%` } },
127-
},
128-
{ fetchPolicy: "network-only" },
129-
);
130-
}
131-
}, 500),
132-
[refetch],
133-
);
134-
135-
useEffect(() => {
136-
if (searchText !== null) {
137-
debounceRefetch(searchText);
138-
}
139-
}, [debounceRefetch, searchText]);
112+
const { onLoadMore } = useRelayConnectionPagination({
113+
hasNext,
114+
isLoadingNext,
115+
loadNext,
116+
refetch,
117+
searchText,
118+
buildFilter: (text) => {
119+
if (text === "") {
120+
return undefined;
121+
}
140122

141-
const loadNextOptions = useCallback(() => {
142-
if (hasNext && !isLoadingNext) {
143-
loadNext(RECORDS_TO_LOAD_NEXT);
144-
}
145-
}, [hasNext, isLoadingNext, loadNext]);
123+
return {
124+
version: {
125+
ilike: `%${text}%`,
126+
},
127+
};
128+
},
129+
});
146130

147131
const releaseOptions = useMemo(() => {
148132
const releases =
@@ -192,7 +176,7 @@ const ReleaseSelect = ({
192176
getOptionValue={getReleaseValue}
193177
noOptionsMessage={({ inputValue }) => noReleaseOptionsMessage(inputValue)}
194178
isLoading={isLoadingNext}
195-
onMenuScrollToBottom={hasNext ? loadNextOptions : undefined}
179+
onMenuScrollToBottom={onLoadMore}
196180
onInputChange={(text) => setSearchText(text)}
197181
/>
198182
);

frontend/src/components/UpdateTargetsTabs.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* SPDX-License-Identifier: Apache-2.0
1919
*/
2020

21-
import { useCallback, useEffect, useMemo, useState } from "react";
21+
import { useEffect, useMemo, useState } from "react";
2222
import { FormattedMessage } from "react-intl";
2323
import { graphql, usePaginationFragment } from "react-relay/hooks";
2424

@@ -31,7 +31,8 @@ import CampaignTargetStatus, {
3131
} from "@/components/CampaignTargetStatus";
3232
import type { ColumnId } from "@/components/UpdateTargetsTable";
3333
import UpdateTargetsTable, { columnIds } from "@/components/UpdateTargetsTable";
34-
import { RECORDS_TO_LOAD_FIRST, RECORDS_TO_LOAD_NEXT } from "@/constants";
34+
import { RECORDS_TO_LOAD_FIRST } from "@/constants";
35+
import useRelayConnectionPagination from "@/hooks/useRelayConnectionPagination";
3536
import Nav from "react-bootstrap/Nav";
3637
import NavItem from "react-bootstrap/NavItem";
3738
import NavLink from "react-bootstrap/NavLink";
@@ -118,11 +119,11 @@ const UpdateTargetsTabs = ({ campaignRef }: Props) => {
118119
);
119120
}, [activeTab, committedTab, refetch]);
120121

121-
const loadNextUpdateTargets = useCallback(() => {
122-
if (hasNext && !isLoadingNext) {
123-
loadNext(RECORDS_TO_LOAD_NEXT);
124-
}
125-
}, [hasNext, isLoadingNext, loadNext]);
122+
const { onLoadMore } = useRelayConnectionPagination({
123+
hasNext,
124+
isLoadingNext,
125+
loadNext,
126+
});
126127

127128
const updateTargetsRef = data?.campaignTargets;
128129

@@ -167,7 +168,7 @@ const UpdateTargetsTabs = ({ campaignRef }: Props) => {
167168
campaignTargetsRef={updateTargetsRef}
168169
hiddenColumns={hiddenColumns}
169170
loading={isLoadingNext}
170-
onLoadMore={hasNext ? loadNextUpdateTargets : undefined}
171+
onLoadMore={onLoadMore}
171172
/>
172173
)}
173174
</div>

0 commit comments

Comments
 (0)