Skip to content

Commit b04fbce

Browse files
authored
FUI - Pagination rework for API and Product lists (#2770)
1 parent acfe0e3 commit b04fbce

File tree

17 files changed

+506
-482
lines changed

17 files changed

+506
-482
lines changed

src/components/apis/api-products/react/runtime/ApiProductsRuntime.tsx

Lines changed: 20 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
import * as React from "react";
2-
import { useEffect, useState } from "react";
3-
import { Stack } from "@fluentui/react";
4-
import { FluentProvider, Spinner } from "@fluentui/react-components";
2+
import { FluentProvider } from "@fluentui/react-components";
53
import { Resolve } from "@paperbits/react/decorators";
64
import { Router } from "@paperbits/common/routing";
75
import { ApiService } from "../../../../../services/apiService";
8-
import { SearchQuery } from "../../../../../contracts/searchQuery";
96
import { RouteHelper } from "../../../../../routing/routeHelper";
10-
import { Pagination } from "../../../../utils/react/Pagination";
7+
import { fuiTheme } from "../../../../../constants";
8+
import { TLayout } from "../../../../utils/react/TableListInfo";
119
import { ProductsDropdown } from "../../../../products/product-list/react/runtime/ProductsDropdown";
12-
import { ProductsTable } from "../../../../products/product-list/react/runtime/ProductsTable";
13-
import { ProductsCards } from "../../../../products/product-list/react/runtime/ProductsCards";
14-
import { TProductsData } from "../../../../products/product-list/react/runtime/utils";
15-
import { TableListInfo, TLayout } from "../../../../utils/react/TableListInfo";
16-
import { defaultPageSize, fuiTheme } from "../../../../../constants";
10+
import { ProductsTableCards } from "../../../../products/product-list/react/runtime/ProductsTableCards";
1711

1812
interface ApiProductsProps {
1913
allowViewSwitching?: boolean;
@@ -25,90 +19,6 @@ interface ApiProductsState {
2519
apiName: string
2620
}
2721

28-
const loadProducts = async (apiService: ApiService, apiName: string, query: SearchQuery) => {
29-
let products: TProductsData;
30-
31-
try {
32-
products = await apiService.getApiProductsPage(apiName, query);
33-
} catch (error) {
34-
throw new Error(`Unable to load Products. Error: ${error.message}`);
35-
}
36-
37-
return products;
38-
}
39-
40-
export type TApiProductsRuntimeFCProps = Omit<ApiProductsProps, "detailsPageUrl"> & {
41-
apiService: ApiService;
42-
apiName: string;
43-
getReferenceUrl: (productName: string) => string;
44-
};
45-
46-
const ApiProductsRuntimeFC = ({ apiService, apiName, getReferenceUrl, layoutDefault, allowViewSwitching }: TApiProductsRuntimeFCProps) => {
47-
const [working, setWorking] = useState(false);
48-
const [pageNumber, setPageNumber] = useState(1);
49-
const [products, setProducts] = useState<TProductsData>();
50-
const [layout, setLayout] = useState<TLayout>(layoutDefault ?? TLayout.table);
51-
const [pattern, setPattern] = useState<string>();
52-
53-
/**
54-
* Loads page of Products.
55-
*/
56-
useEffect(() => {
57-
if (apiName) {
58-
const query: SearchQuery = {
59-
pattern,
60-
skip: (pageNumber - 1) * defaultPageSize,
61-
take: defaultPageSize,
62-
};
63-
64-
setWorking(true);
65-
loadProducts(apiService, apiName, query)
66-
.then((products) => setProducts(products))
67-
.finally(() => setWorking(false));
68-
}
69-
}, [apiService, apiName, pageNumber, pattern]);
70-
71-
return layout === TLayout.dropdown ? (
72-
<ProductsDropdown
73-
getReferenceUrl={getReferenceUrl}
74-
working={working}
75-
products={products}
76-
statePageNumber={[pageNumber, setPageNumber]}
77-
statePattern={[pattern, setPattern]}
78-
/>
79-
) : (
80-
<Stack tokens={{ childrenGap: "1rem" }}>
81-
<TableListInfo
82-
layout={layout}
83-
setLayout={setLayout}
84-
pattern={pattern}
85-
setPattern={setPattern}
86-
allowViewSwitching={allowViewSwitching}
87-
/>
88-
89-
{working || !products ? (
90-
<div className="table-body">
91-
<Spinner label="Loading products..." labelPosition="below" size="small" />
92-
</div>
93-
) : (
94-
<>
95-
<div style={{ marginTop: "2rem" }}>
96-
{layout === TLayout.table ? (
97-
<ProductsTable products={products} getReferenceUrl={getReferenceUrl} />
98-
) : (
99-
<ProductsCards products={products} getReferenceUrl={getReferenceUrl} />
100-
)}
101-
</div>
102-
103-
<div style={{ margin: "1rem auto" }}>
104-
<Pagination pageNumber={pageNumber} setPageNumber={setPageNumber} pageMax={Math.ceil(products?.count / defaultPageSize)} />
105-
</div>
106-
</>
107-
)}
108-
</Stack>
109-
);
110-
};
111-
11222
export class ApiProductsRuntime extends React.Component<ApiProductsProps, ApiProductsState> {
11323
@Resolve("apiService")
11424
public apiService: ApiService;
@@ -151,12 +61,22 @@ export class ApiProductsRuntime extends React.Component<ApiProductsProps, ApiPro
15161
render() {
15262
return (
15363
<FluentProvider theme={fuiTheme}>
154-
<ApiProductsRuntimeFC
155-
{...this.props}
156-
apiService={this.apiService}
157-
apiName={this.state.apiName}
158-
getReferenceUrl={(productName) => this.getReferenceUrl(productName)}
159-
/>
64+
{this.props.layoutDefault == TLayout.dropdown
65+
? <ProductsDropdown
66+
{...this.props}
67+
apiService={this.apiService}
68+
apiName={this.state.apiName}
69+
getReferenceUrl={(productName) => this.getReferenceUrl(productName)}
70+
isApiProducts
71+
/>
72+
: <ProductsTableCards
73+
{...this.props}
74+
apiService={this.apiService}
75+
apiName={this.state.apiName}
76+
getReferenceUrl={(productName) => this.getReferenceUrl(productName)}
77+
isApiProducts
78+
/>
79+
}
16080
</FluentProvider>
16181
);
16282
}

src/components/apis/list-of-apis/react/runtime/ApiListDropdown.tsx

Lines changed: 97 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,26 @@
11
import * as React from "react";
2-
import { useState } from "react";
2+
import { useEffect, useState } from "react";
33
import {
44
Badge,
55
Combobox,
66
Option,
77
OptionGroup,
88
Spinner,
99
} from "@fluentui/react-components";
10-
import { Resolve } from "@paperbits/react/decorators";
1110
import { GroupByTag } from "../../../../utils/react/TableListInfo";
12-
import { Pagination } from "../../../../utils/react/Pagination";
1311
import * as Constants from "../../../../../constants";
1412
import { Api } from "../../../../../models/api";
15-
import {
16-
isApisGrouped,
17-
TagGroupToggleBtn,
18-
TApisData,
19-
toggleValueInSet,
20-
} from "./utils";
21-
import { RouteHelper } from "../../../../../routing/routeHelper";
22-
import { ApiService } from "../../../../../services/apiService";
13+
import { Tag } from "../../../../../models/tag";
14+
import { TagGroup } from "../../../../../models/tagGroup";
15+
import { Page } from "../../../../../models/page";
16+
import { SearchQuery } from "../../../../../contracts/searchQuery";
2317
import { TApiListRuntimeFCProps } from "./ApiListRuntime";
18+
import { TagGroupToggleBtn, toggleValueInSet } from "./utils";
2419

2520
type TApiListDropdown = Omit<
2621
TApiListRuntimeFCProps,
27-
"apiService" | "layoutDefault" | "productName"
28-
> & {
29-
working: boolean;
30-
apis: TApisData;
31-
statePageNumber: ReturnType<typeof useState<number>>;
32-
statePattern: ReturnType<typeof useState<string>>;
33-
stateGroupByTag: ReturnType<typeof useState<boolean>>;
34-
};
22+
"tagService" | "layoutDefault" | "productName"
23+
>;
3524

3625
const TagLabel = ({
3726
tag,
@@ -76,22 +65,92 @@ const Options = ({
7665
</>
7766
);
7867

79-
const ApiListDropdownFC = ({
80-
working,
81-
apis,
68+
export const ApiListDropdown = ({
69+
apiService,
8270
getReferenceUrl,
8371
selectedApi,
84-
statePageNumber: [pageNumber, setPageNumber],
85-
statePattern: [_, setPattern],
86-
stateGroupByTag: [groupByTag, setGroupByTag],
72+
defaultGroupByTagToEnabled
8773
}: TApiListDropdown & { selectedApi?: Api }) => {
8874
const [expanded, setExpanded] = React.useState(new Set<string>());
89-
90-
const pageMax = Math.ceil(apis?.count / Constants.defaultPageSize);
75+
const [working, setWorking] = useState(false);
76+
const [pageNumber, setPageNumber] = useState(1);
77+
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
78+
const [apis, setApis] = useState<Api[]>([]);
79+
const [apisByTag, setApisByTag] = useState<TagGroup<Api>[]>([]);
80+
const [pattern, setPattern] = useState<string>();
81+
const [groupByTag, setGroupByTag] = useState(!!defaultGroupByTagToEnabled);
82+
const [filters, setFilters] = useState<{ tags: Tag[] }>({ tags: [] });
9183

9284
const toggleTag = (tag: string) =>
9385
setExpanded((old) => toggleValueInSet(old, tag));
9486

87+
useEffect(() => {
88+
const query: SearchQuery = {
89+
pattern,
90+
tags: filters.tags,
91+
skip: (pageNumber - 1) * Constants.defaultPageSize,
92+
take: Constants.defaultPageSize
93+
};
94+
95+
setWorking(true);
96+
if (groupByTag) {
97+
loadApisByTag(query)
98+
.then(loadedApis => {
99+
if (pageNumber > 1) {
100+
// Check if the tag is already displayed. If yes, add to this tag
101+
loadedApis.value.forEach(newApi => {
102+
const existingTagIndex = apisByTag.findIndex(item => item.tag === newApi.tag);
103+
if (existingTagIndex !== -1) {
104+
apisByTag[existingTagIndex].items.push(...newApi.items);
105+
} else {
106+
apisByTag.push(newApi);
107+
}
108+
});
109+
setApisByTag(apisByTag);
110+
} else {
111+
setApisByTag([...loadedApis.value]);
112+
}
113+
setHasNextPage(!!loadedApis.nextLink);
114+
})
115+
.finally(() => setWorking(false));
116+
} else {
117+
loadApis(query)
118+
.then(loadedApis => {
119+
if (pageNumber > 1) {
120+
setApis([...apis, ...loadedApis.value]);
121+
} else {
122+
setApis([...loadedApis.value]);
123+
}
124+
setHasNextPage(!!loadedApis.nextLink);
125+
})
126+
.finally(() => setWorking(false));
127+
}
128+
}, [apiService, pageNumber, groupByTag, filters, pattern]);
129+
130+
const loadApis = async (query: SearchQuery) => {
131+
let apis: Page<Api>;
132+
133+
try {
134+
apis = await apiService.getApis(query);
135+
} catch (error) {
136+
throw new Error(`Unable to load APIs. Error: ${error.message}`);
137+
}
138+
139+
return apis;
140+
}
141+
142+
const loadApisByTag = async (query: SearchQuery) => {
143+
let apis: Page<TagGroup<Api>>;
144+
145+
try {
146+
apis = await apiService.getApisByTags(query);
147+
} catch (error) {
148+
throw new Error(`Unable to load APIs. Error: ${error.message}`);
149+
}
150+
151+
return apis;
152+
}
153+
95154
const content = !apis || !selectedApi ? (
96155
<>Loading APIs</> // if data are not loaded yet ComboBox sometimes fails to initialize properly - edge case, in most cases almost instant from the cache
97156
) : (
@@ -119,15 +178,19 @@ const ApiListDropdownFC = ({
119178
disabled
120179
value={"group by tag switch"}
121180
text={"group by tag switch"}
181+
style={{ columnGap: 0 }}
182+
className="group-by-tag-switch"
122183
>
123184
<GroupByTag
124185
groupByTag={groupByTag}
125186
setGroupByTag={setGroupByTag}
187+
setPageNumber={setPageNumber}
188+
labelAfter
126189
/>
127190
</Option>
128191

129-
{isApisGrouped(apis) ? (
130-
apis?.value.map(({ tag, items }) => (
192+
{groupByTag ? (
193+
apisByTag?.map(({ tag, items }) => (
131194
<OptionGroup
132195
key={tag}
133196
label={
@@ -148,22 +211,20 @@ const ApiListDropdownFC = ({
148211
))
149212
) : (
150213
<Options
151-
apis={apis.value}
214+
apis={apis}
152215
getReferenceUrl={getReferenceUrl}
153216
/>
154217
)}
155218

156-
{pageMax > 1 && (
219+
{hasNextPage && (
157220
<Option
158221
disabled
159222
value={"pagination"}
160223
text={"pagination"}
224+
checkIcon={<></>}
225+
style={{ columnGap: 0 }}
161226
>
162-
<Pagination
163-
pageNumber={pageNumber}
164-
setPageNumber={setPageNumber}
165-
pageMax={pageMax}
166-
/>
227+
<button className={"button button-default show-more-options"} onClick={() => setPageNumber(prev => prev + 1)}>Show more</button>
167228
</Option>
168229
)}
169230
</>
@@ -178,49 +239,3 @@ const ApiListDropdownFC = ({
178239
</>
179240
);
180241
};
181-
182-
export class ApiListDropdown extends React.Component<
183-
TApiListDropdown,
184-
{ working: boolean; api?: Api }
185-
> {
186-
@Resolve("apiService")
187-
public apiService: ApiService;
188-
189-
@Resolve("routeHelper")
190-
public routeHelper: RouteHelper;
191-
192-
constructor(props: TApiListDropdown) {
193-
super(props);
194-
195-
this.state = {
196-
working: false,
197-
api: undefined,
198-
};
199-
}
200-
201-
public componentDidMount() {
202-
this.loadSelectedApi();
203-
}
204-
205-
async loadSelectedApi() {
206-
const apiName = this.routeHelper.getApiName();
207-
if (!apiName) return;
208-
209-
this.setState({ working: true, api: undefined });
210-
211-
return this.apiService
212-
.getApi(`apis/${apiName}`)
213-
.then((api) => this.setState({ api }))
214-
.finally(() => this.setState({ working: false }));
215-
}
216-
217-
render() {
218-
return (
219-
<ApiListDropdownFC
220-
{...this.props}
221-
working={this.props.working || this.state.working}
222-
selectedApi={this.state.api}
223-
/>
224-
);
225-
}
226-
}

0 commit comments

Comments
 (0)