mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
add pagination in all components in default state
This commit is contained in:
@@ -1,29 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import BlocksList from "./BlocksList";
|
||||
import { Block } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import React from "react";
|
||||
import PaginatedBlocksContent from "./PaginatedBlocksContent";
|
||||
|
||||
const ActionBlocksContent: React.FC = () => {
|
||||
const [blocks, setBlocks] = useState<Block[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const api = useBackendAPI();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBlocks = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.getBuilderBlocks({ type: "action" });
|
||||
setBlocks(response.blocks);
|
||||
} catch (error) {
|
||||
console.error("Error fetching blocks:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBlocks();
|
||||
}, [api]);
|
||||
return <BlocksList blocks={blocks} loading={loading} />;
|
||||
return <PaginatedBlocksContent blockRequest={{ type: "action" }} />;
|
||||
};
|
||||
|
||||
export default ActionBlocksContent;
|
||||
export default ActionBlocksContent;
|
||||
@@ -43,8 +43,6 @@ const AllBlocksContent: React.FC = () => {
|
||||
return cat;
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
setCategories(updatedCategories);
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch blocks for category ${category}:`, error);
|
||||
@@ -75,7 +73,6 @@ const AllBlocksContent: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
// BLOCK TODO : NEED to add the laoding skeleton when clicking see all
|
||||
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
|
||||
<div className="w-full space-y-3 px-4 pb-4">
|
||||
{categories.map((category, index) => (
|
||||
@@ -113,6 +110,16 @@ const AllBlocksContent: React.FC = () => {
|
||||
/>
|
||||
))}
|
||||
|
||||
{loadingCategories.has(category.name) && (
|
||||
<>
|
||||
{[0, 1, 2, 3, 4].map((skeletonIndex) => (
|
||||
<Block.Skeleton
|
||||
key={`skeleton-${category.name}-${skeletonIndex}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{category.total_blocks > category.blocks.length && (
|
||||
<Button
|
||||
variant={"link"}
|
||||
|
||||
@@ -11,28 +11,26 @@ interface BlocksListProps {
|
||||
const BlocksList: React.FC<BlocksListProps> = ({ blocks, loading = false }) => {
|
||||
const { addNode } = useBlockMenuContext();
|
||||
return (
|
||||
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
|
||||
<div className="w-full space-y-3 px-4 pb-4">
|
||||
{loading
|
||||
? Array.from({ length: 7 }).map((_, index) => (
|
||||
<Block.Skeleton key={index} />
|
||||
))
|
||||
: blocks.map((block) => (
|
||||
<Block
|
||||
key={block.id}
|
||||
title={block.name}
|
||||
description={block.description}
|
||||
onClick={() => {
|
||||
addNode(
|
||||
block.id,
|
||||
block.name,
|
||||
block.hardcodedValues || {},
|
||||
block,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full space-y-3 px-4 pb-4">
|
||||
{loading
|
||||
? Array.from({ length: 7 }).map((_, index) => (
|
||||
<Block.Skeleton key={index} />
|
||||
))
|
||||
: blocks.map((block) => (
|
||||
<Block
|
||||
key={block.id}
|
||||
title={block.name}
|
||||
description={block.description}
|
||||
onClick={() => {
|
||||
addNode(
|
||||
block.id,
|
||||
block.name,
|
||||
block.hardcodedValues || {},
|
||||
block,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import BlocksList from "./BlocksList";
|
||||
import { Block } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import React from "react";
|
||||
import PaginatedBlocksContent from "./PaginatedBlocksContent";
|
||||
|
||||
const InputBlocksContent: React.FC = () => {
|
||||
const [blocks, setBlocks] = useState<Block[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const api = useBackendAPI();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBlocks = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.getBuilderBlocks({ type: "input" });
|
||||
setBlocks(response.blocks);
|
||||
} catch (error) {
|
||||
console.error("Error fetching blocks:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBlocks();
|
||||
}, [api]);
|
||||
return <BlocksList blocks={blocks} loading={loading} />;
|
||||
return <PaginatedBlocksContent blockRequest={{ type: "input" }} />;
|
||||
};
|
||||
|
||||
export default InputBlocksContent;
|
||||
export default InputBlocksContent;
|
||||
@@ -1,59 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Integration from "../Integration";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { Provider } from "@/lib/autogpt-server-api";
|
||||
|
||||
const IntegrationList: React.FC = ({}) => {
|
||||
const { setIntegration } = useBlockMenuContext();
|
||||
const [integrations, setIntegrations] = useState<Provider[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
const api = useBackendAPI();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchIntegrations = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Some integrations are missing, like twitter or todoist or more
|
||||
const providers = await api.getProviders();
|
||||
setIntegrations(providers.providers);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch integrations:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchIntegrations();
|
||||
}, [api]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{Array(5)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<Integration.Skeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{integrations.map((integration, index) => (
|
||||
<Integration
|
||||
key={index}
|
||||
title={integration.name}
|
||||
icon_url={`/integrations/${integration.name}.png`}
|
||||
description={integration.description}
|
||||
number_of_blocks={integration.integration_count}
|
||||
onClick={() => setIntegration(integration.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntegrationList;
|
||||
@@ -1,14 +1,19 @@
|
||||
import React from "react";
|
||||
import IntegrationList from "./IntegrationList";
|
||||
import PaginatedIntegrationList from "./PaginatedIntegrationList";
|
||||
import IntegrationBlocks from "./IntegrationBlocks";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
|
||||
const IntegrationsContent: React.FC = () => {
|
||||
const { integration } = useBlockMenuContext();
|
||||
|
||||
if (!integration) {
|
||||
return <PaginatedIntegrationList />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
|
||||
<div className="w-full px-4 pb-4">
|
||||
{!integration ? <IntegrationList /> : <IntegrationBlocks />}
|
||||
<IntegrationBlocks />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,54 +1,57 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
import MarketplaceAgentBlock from "../MarketplaceAgentBlock";
|
||||
import { marketplaceAgentData } from "../../testing_data";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { StoreAgent } from "@/lib/autogpt-server-api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePagination } from "@/hooks/usePagination";
|
||||
|
||||
const MarketplaceAgentsContent: React.FC = () => {
|
||||
const [agents, setAgents] = useState<StoreAgent[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const api = useBackendAPI();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAgents = async () => {
|
||||
try {
|
||||
const response = await api.getStoreAgents();
|
||||
// BLOCK MENU TODO : figure out how to add agent in flow and add pagination as well
|
||||
setAgents(response.agents);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAgents();
|
||||
}, [api]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full space-y-3 p-4">
|
||||
{Array(5)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<MarketplaceAgentBlock.Skeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { data: agents, loading, loadingMore, hasMore, error, scrollRef, refresh } = usePagination({
|
||||
request: { apiType: 'store-agents' },
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200"
|
||||
>
|
||||
<div className="w-full space-y-3 px-4 pb-4">
|
||||
{agents.map((agent) => (
|
||||
<MarketplaceAgentBlock
|
||||
key={agent.slug}
|
||||
title={agent.agent_name}
|
||||
image_url={agent.agent_image}
|
||||
creator_name={agent.creator}
|
||||
number_of_runs={agent.runs}
|
||||
/>
|
||||
))}
|
||||
{loading
|
||||
? Array(5)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<MarketplaceAgentBlock.Skeleton key={index} />
|
||||
))
|
||||
: agents.map((agent) => (
|
||||
<MarketplaceAgentBlock
|
||||
key={agent.slug}
|
||||
title={agent.agent_name}
|
||||
image_url={agent.agent_image}
|
||||
creator_name={agent.creator}
|
||||
number_of_runs={agent.runs}
|
||||
/>
|
||||
))}
|
||||
{error && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||
<p className="text-sm text-red-600 mb-2">
|
||||
Error loading marketplace agents: {error}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refresh}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{loadingMore && hasMore && (
|
||||
<>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<MarketplaceAgentBlock.Skeleton key={`loading-${index}`} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,53 +1,57 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
import UGCAgentBlock from "../UGCAgentBlock";
|
||||
import { myAgentData } from "../../testing_data";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { LibraryAgent } from "@/lib/autogpt-server-api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePagination } from "@/hooks/usePagination";
|
||||
|
||||
const MyAgentsContent: React.FC = () => {
|
||||
const [agents, setAgents] = useState<LibraryAgent[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const api = useBackendAPI();
|
||||
// TEMPORARY FETCHING
|
||||
useEffect(() => {
|
||||
const fetchAgents = async () => {
|
||||
try {
|
||||
// BLOCK MENU TODO : figure out how to add agent in flow and add pagination as well
|
||||
const response = await api.listLibraryAgents();
|
||||
setAgents(response.agents);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAgents();
|
||||
}, [api]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full space-y-3 p-4">
|
||||
{Array(5)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<UGCAgentBlock.Skeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { data: agents, loading, loadingMore, hasMore, error, scrollRef, refresh } = usePagination({
|
||||
request: { apiType: 'library-agents' },
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200"
|
||||
>
|
||||
<div className="w-full space-y-3 px-4 pb-4">
|
||||
{agents.map((agent) => (
|
||||
<UGCAgentBlock
|
||||
key={agent.id}
|
||||
title={agent.name}
|
||||
edited_time={agent.updated_at}
|
||||
version={agent.graph_version}
|
||||
image_url={agent.image_url}
|
||||
/>
|
||||
))}
|
||||
{loading
|
||||
? Array(5)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<UGCAgentBlock.Skeleton key={index} />
|
||||
))
|
||||
: agents.map((agent) => (
|
||||
<UGCAgentBlock
|
||||
key={agent.id}
|
||||
title={agent.name}
|
||||
edited_time={agent.updated_at}
|
||||
version={agent.graph_version}
|
||||
image_url={agent.image_url}
|
||||
/>
|
||||
))}
|
||||
{error && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||
<p className="text-sm text-red-600 mb-2">
|
||||
Error loading library agents: {error}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refresh}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{loadingMore && hasMore && (
|
||||
<>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<UGCAgentBlock.Skeleton key={`loading-${index}`} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import BlocksList from "./BlocksList";
|
||||
import { Block } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import React from "react";
|
||||
import PaginatedBlocksContent from "./PaginatedBlocksContent";
|
||||
|
||||
const OutputBlocksContent: React.FC = () => {
|
||||
const [blocks, setBlocks] = useState<Block[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const api = useBackendAPI();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBlocks = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.getBuilderBlocks({ type: "output" });
|
||||
setBlocks(response.blocks);
|
||||
} catch (error) {
|
||||
console.error("Error fetching blocks:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBlocks();
|
||||
}, [api]);
|
||||
return <BlocksList blocks={blocks} loading={loading} />;
|
||||
return <PaginatedBlocksContent blockRequest={{ type: "output" }} />;
|
||||
};
|
||||
|
||||
export default OutputBlocksContent;
|
||||
export default OutputBlocksContent;
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import BlocksList from "./BlocksList";
|
||||
import Block from "../Block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { BlockRequest } from "@/lib/autogpt-server-api";
|
||||
import { usePagination } from "@/hooks/usePagination";
|
||||
|
||||
interface PaginatedBlocksContentProps {
|
||||
blockRequest: BlockRequest;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
const PaginatedBlocksContent: React.FC<PaginatedBlocksContentProps> = ({
|
||||
blockRequest,
|
||||
pageSize = 10,
|
||||
}) => {
|
||||
const { data: blocks, loading, loadingMore, hasMore, error, scrollRef, refresh } = usePagination({
|
||||
request: { apiType: 'blocks', ...blockRequest },
|
||||
pageSize,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200"
|
||||
>
|
||||
<BlocksList blocks={blocks} loading={loading} />
|
||||
{error && (
|
||||
<div className="w-full px-4 pb-4">
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||
<p className="text-sm text-red-600 mb-2">
|
||||
Error loading blocks: {error}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refresh}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loadingMore && hasMore && (
|
||||
<div className="w-full space-y-3 px-4 pb-4">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Block.Skeleton key={`loading-${index}`} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaginatedBlocksContent;
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import Integration from "../Integration";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useBlockMenuContext } from "../block-menu-provider";
|
||||
import { usePagination } from "@/hooks/usePagination";
|
||||
|
||||
const PaginatedIntegrationList: React.FC = () => {
|
||||
const { setIntegration } = useBlockMenuContext();
|
||||
const { data: providers, loading, loadingMore, hasMore, error, scrollRef, refresh } = usePagination({
|
||||
request: { apiType: 'providers' },
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="scrollbar-thumb-rounded h-full overflow-y-auto pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-200"
|
||||
>
|
||||
<div className="w-full px-4 pb-4">
|
||||
<div className="space-y-3">
|
||||
{loading
|
||||
? Array(5)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<Integration.Skeleton key={index} />
|
||||
))
|
||||
: providers.map((integration, index) => (
|
||||
<Integration
|
||||
key={index}
|
||||
title={integration.name}
|
||||
icon_url={`/integrations/${integration.name}.png`}
|
||||
description={integration.description}
|
||||
number_of_blocks={integration.integration_count}
|
||||
onClick={() => setIntegration(integration.name)}
|
||||
/>
|
||||
))}
|
||||
{error && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||
<p className="text-sm text-red-600 mb-2">
|
||||
Error loading integrations: {error}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refresh}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{loadingMore && hasMore && (
|
||||
<>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Integration.Skeleton key={`loading-${index}`} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaginatedIntegrationList;
|
||||
1
autogpt_platform/frontend/src/hooks/index.ts
Normal file
1
autogpt_platform/frontend/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { usePagination } from './usePagination';
|
||||
234
autogpt_platform/frontend/src/hooks/usePagination.ts
Normal file
234
autogpt_platform/frontend/src/hooks/usePagination.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import {
|
||||
Block,
|
||||
BlockRequest,
|
||||
Provider,
|
||||
StoreAgent,
|
||||
LibraryAgent,
|
||||
LibraryAgentSortEnum,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
|
||||
type BlocksPaginationRequest = { apiType: "blocks" } & BlockRequest;
|
||||
type ProvidersPaginationRequest = { apiType: "providers" } & {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
};
|
||||
type StoreAgentsPaginationRequest = { apiType: "store-agents" } & {
|
||||
featured?: boolean;
|
||||
creator?: string;
|
||||
sorted_by?: string;
|
||||
search_query?: string;
|
||||
category?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
};
|
||||
type LibraryAgentsPaginationRequest = { apiType: "library-agents" } & {
|
||||
search_term?: string;
|
||||
sort_by?: LibraryAgentSortEnum;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
};
|
||||
|
||||
type PaginationRequest =
|
||||
| BlocksPaginationRequest
|
||||
| ProvidersPaginationRequest
|
||||
| StoreAgentsPaginationRequest
|
||||
| LibraryAgentsPaginationRequest;
|
||||
|
||||
interface UsePaginationOptions<T extends PaginationRequest> {
|
||||
request: T;
|
||||
pageSize?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface UsePaginationReturn<T> {
|
||||
data: T[];
|
||||
loading: boolean;
|
||||
loadingMore: boolean;
|
||||
hasMore: boolean;
|
||||
error: string | null;
|
||||
scrollRef: React.RefObject<HTMLDivElement>;
|
||||
refresh: () => void;
|
||||
loadMore: () => void;
|
||||
}
|
||||
|
||||
type GetReturnType<T> = T extends BlocksPaginationRequest
|
||||
? Block
|
||||
: T extends ProvidersPaginationRequest
|
||||
? Provider
|
||||
: T extends StoreAgentsPaginationRequest
|
||||
? StoreAgent
|
||||
: T extends LibraryAgentsPaginationRequest
|
||||
? LibraryAgent
|
||||
: never;
|
||||
|
||||
export const usePagination = <T extends PaginationRequest>({
|
||||
request,
|
||||
pageSize = 10,
|
||||
enabled = true, // to allow pagination or nor
|
||||
}: UsePaginationOptions<T>): UsePaginationReturn<GetReturnType<T>> => {
|
||||
const [data, setData] = useState<GetReturnType<T>[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const isLoadingRef = useRef(false);
|
||||
const requestRef = useRef(request);
|
||||
const api = useBackendAPI();
|
||||
|
||||
// because we are using this pagination for multiple components
|
||||
requestRef.current = request;
|
||||
|
||||
const fetchData = useCallback(
|
||||
async (page: number, isLoadMore = false) => {
|
||||
if (isLoadingRef.current || !enabled) return;
|
||||
|
||||
isLoadingRef.current = true;
|
||||
|
||||
if (isLoadMore) {
|
||||
setLoadingMore(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
let response;
|
||||
let newData: GetReturnType<T>[];
|
||||
let pagination;
|
||||
|
||||
const currentRequest = requestRef.current;
|
||||
const requestWithPagination = {
|
||||
...currentRequest,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
};
|
||||
|
||||
switch (currentRequest.apiType) {
|
||||
case "blocks":
|
||||
const { apiType: _, ...blockRequest } = requestWithPagination;
|
||||
response = await api.getBuilderBlocks(blockRequest);
|
||||
newData = response.blocks as GetReturnType<T>[];
|
||||
pagination = response.pagination;
|
||||
break;
|
||||
|
||||
case "providers":
|
||||
const { apiType: __, ...providerRequest } = requestWithPagination;
|
||||
response = await api.getProviders(providerRequest);
|
||||
newData = response.providers as GetReturnType<T>[];
|
||||
pagination = response.pagination;
|
||||
break;
|
||||
|
||||
case "store-agents":
|
||||
const { apiType: ___, ...storeAgentRequest } =
|
||||
requestWithPagination;
|
||||
response = await api.getStoreAgents(storeAgentRequest);
|
||||
newData = response.agents as GetReturnType<T>[];
|
||||
pagination = response.pagination;
|
||||
break;
|
||||
|
||||
case "library-agents":
|
||||
const { apiType: ____, ...libraryAgentRequest } =
|
||||
requestWithPagination;
|
||||
response = await api.listLibraryAgents(libraryAgentRequest);
|
||||
newData = response.agents as GetReturnType<T>[];
|
||||
pagination = response.pagination;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown request type: ${(currentRequest as any).apiType}`,
|
||||
);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
if (isLoadMore) {
|
||||
setData((prev) => [...prev, ...newData]);
|
||||
} else {
|
||||
setData(newData);
|
||||
}
|
||||
|
||||
setHasMore(page < pagination.total_pages);
|
||||
setCurrentPage(page);
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Failed to fetch data";
|
||||
setError(errorMessage);
|
||||
console.error("Error fetching data:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoadingMore(false);
|
||||
isLoadingRef.current = false;
|
||||
}
|
||||
},
|
||||
[api, pageSize, enabled],
|
||||
);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
const scrollElement = scrollRef.current;
|
||||
if (
|
||||
!scrollElement ||
|
||||
loadingMore ||
|
||||
!hasMore ||
|
||||
isLoadingRef.current ||
|
||||
!enabled
|
||||
)
|
||||
return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollElement;
|
||||
const threshold = 100;
|
||||
|
||||
if (scrollTop + clientHeight >= scrollHeight - threshold) {
|
||||
fetchData(currentPage + 1, true);
|
||||
}
|
||||
}, [fetchData, currentPage, loadingMore, hasMore, enabled]);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setCurrentPage(1);
|
||||
setHasMore(true);
|
||||
setError(null);
|
||||
fetchData(1);
|
||||
}, [fetchData]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!loadingMore && hasMore && !isLoadingRef.current && enabled) {
|
||||
fetchData(currentPage + 1, true);
|
||||
}
|
||||
}, [fetchData, currentPage, loadingMore, hasMore, enabled]);
|
||||
|
||||
const requestString = JSON.stringify(request);
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setCurrentPage(1);
|
||||
setHasMore(true);
|
||||
setError(null);
|
||||
setData([]);
|
||||
fetchData(1);
|
||||
}
|
||||
}, [requestString, enabled, fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollRef.current;
|
||||
if (scrollElement && enabled) {
|
||||
scrollElement.addEventListener("scroll", handleScroll);
|
||||
return () => scrollElement.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
}, [handleScroll, enabled]);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
loadingMore,
|
||||
hasMore,
|
||||
error,
|
||||
scrollRef,
|
||||
refresh,
|
||||
loadMore,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user