Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 20 additions & 100 deletions src/components/apis/api-products/react/runtime/ApiProductsRuntime.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import * as React from "react";
import { useEffect, useState } from "react";
import { Stack } from "@fluentui/react";
import { FluentProvider, Spinner } from "@fluentui/react-components";
import { FluentProvider } from "@fluentui/react-components";
import { Resolve } from "@paperbits/react/decorators";
import { Router } from "@paperbits/common/routing";
import { ApiService } from "../../../../../services/apiService";
import { SearchQuery } from "../../../../../contracts/searchQuery";
import { RouteHelper } from "../../../../../routing/routeHelper";
import { Pagination } from "../../../../utils/react/Pagination";
import { fuiTheme } from "../../../../../constants";
import { TLayout } from "../../../../utils/react/TableListInfo";
import { ProductsDropdown } from "../../../../products/product-list/react/runtime/ProductsDropdown";
import { ProductsTable } from "../../../../products/product-list/react/runtime/ProductsTable";
import { ProductsCards } from "../../../../products/product-list/react/runtime/ProductsCards";
import { TProductsData } from "../../../../products/product-list/react/runtime/utils";
import { TableListInfo, TLayout } from "../../../../utils/react/TableListInfo";
import { defaultPageSize, fuiTheme } from "../../../../../constants";
import { ProductsTableCards } from "../../../../products/product-list/react/runtime/ProductsTableCards";

interface ApiProductsProps {
allowViewSwitching?: boolean;
Expand All @@ -25,90 +19,6 @@ interface ApiProductsState {
apiName: string
}

const loadProducts = async (apiService: ApiService, apiName: string, query: SearchQuery) => {
let products: TProductsData;

try {
products = await apiService.getApiProductsPage(apiName, query);
} catch (error) {
throw new Error(`Unable to load Products. Error: ${error.message}`);
}

return products;
}

export type TApiProductsRuntimeFCProps = Omit<ApiProductsProps, "detailsPageUrl"> & {
apiService: ApiService;
apiName: string;
getReferenceUrl: (productName: string) => string;
};

const ApiProductsRuntimeFC = ({ apiService, apiName, getReferenceUrl, layoutDefault, allowViewSwitching }: TApiProductsRuntimeFCProps) => {
const [working, setWorking] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [products, setProducts] = useState<TProductsData>();
const [layout, setLayout] = useState<TLayout>(layoutDefault ?? TLayout.table);
const [pattern, setPattern] = useState<string>();

/**
* Loads page of Products.
*/
useEffect(() => {
if (apiName) {
const query: SearchQuery = {
pattern,
skip: (pageNumber - 1) * defaultPageSize,
take: defaultPageSize,
};

setWorking(true);
loadProducts(apiService, apiName, query)
.then((products) => setProducts(products))
.finally(() => setWorking(false));
}
}, [apiService, apiName, pageNumber, pattern]);

return layout === TLayout.dropdown ? (
<ProductsDropdown
getReferenceUrl={getReferenceUrl}
working={working}
products={products}
statePageNumber={[pageNumber, setPageNumber]}
statePattern={[pattern, setPattern]}
/>
) : (
<Stack tokens={{ childrenGap: "1rem" }}>
<TableListInfo
layout={layout}
setLayout={setLayout}
pattern={pattern}
setPattern={setPattern}
allowViewSwitching={allowViewSwitching}
/>

{working || !products ? (
<div className="table-body">
<Spinner label="Loading products..." labelPosition="below" size="small" />
</div>
) : (
<>
<div style={{ marginTop: "2rem" }}>
{layout === TLayout.table ? (
<ProductsTable products={products} getReferenceUrl={getReferenceUrl} />
) : (
<ProductsCards products={products} getReferenceUrl={getReferenceUrl} />
)}
</div>

<div style={{ margin: "1rem auto" }}>
<Pagination pageNumber={pageNumber} setPageNumber={setPageNumber} pageMax={Math.ceil(products?.count / defaultPageSize)} />
</div>
</>
)}
</Stack>
);
};

export class ApiProductsRuntime extends React.Component<ApiProductsProps, ApiProductsState> {
@Resolve("apiService")
public apiService: ApiService;
Expand Down Expand Up @@ -151,12 +61,22 @@ export class ApiProductsRuntime extends React.Component<ApiProductsProps, ApiPro
render() {
return (
<FluentProvider theme={fuiTheme}>
<ApiProductsRuntimeFC
{...this.props}
apiService={this.apiService}
apiName={this.state.apiName}
getReferenceUrl={(productName) => this.getReferenceUrl(productName)}
/>
{this.props.layoutDefault == TLayout.dropdown
? <ProductsDropdown
{...this.props}
apiService={this.apiService}
apiName={this.state.apiName}
getReferenceUrl={(productName) => this.getReferenceUrl(productName)}
isApiProducts
/>
: <ProductsTableCards
{...this.props}
apiService={this.apiService}
apiName={this.state.apiName}
getReferenceUrl={(productName) => this.getReferenceUrl(productName)}
isApiProducts
/>
}
</FluentProvider>
);
}
Expand Down
179 changes: 97 additions & 82 deletions src/components/apis/list-of-apis/react/runtime/ApiListDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,26 @@
import * as React from "react";
import { useState } from "react";
import { useEffect, useState } from "react";
import {
Badge,
Combobox,
Option,
OptionGroup,
Spinner,
} from "@fluentui/react-components";
import { Resolve } from "@paperbits/react/decorators";
import { GroupByTag } from "../../../../utils/react/TableListInfo";
import { Pagination } from "../../../../utils/react/Pagination";
import * as Constants from "../../../../../constants";
import { Api } from "../../../../../models/api";
import {
isApisGrouped,
TagGroupToggleBtn,
TApisData,
toggleValueInSet,
} from "./utils";
import { RouteHelper } from "../../../../../routing/routeHelper";
import { ApiService } from "../../../../../services/apiService";
import { Tag } from "../../../../../models/tag";
import { TagGroup } from "../../../../../models/tagGroup";
import { Page } from "../../../../../models/page";
import { SearchQuery } from "../../../../../contracts/searchQuery";
import { TApiListRuntimeFCProps } from "./ApiListRuntime";
import { TagGroupToggleBtn, toggleValueInSet } from "./utils";

type TApiListDropdown = Omit<
TApiListRuntimeFCProps,
"apiService" | "layoutDefault" | "productName"
> & {
working: boolean;
apis: TApisData;
statePageNumber: ReturnType<typeof useState<number>>;
statePattern: ReturnType<typeof useState<string>>;
stateGroupByTag: ReturnType<typeof useState<boolean>>;
};
"tagService" | "layoutDefault" | "productName"
>;

const TagLabel = ({
tag,
Expand Down Expand Up @@ -76,22 +65,92 @@ const Options = ({
</>
);

const ApiListDropdownFC = ({
working,
apis,
export const ApiListDropdown = ({
apiService,
getReferenceUrl,
selectedApi,
statePageNumber: [pageNumber, setPageNumber],
statePattern: [_, setPattern],
stateGroupByTag: [groupByTag, setGroupByTag],
defaultGroupByTagToEnabled
}: TApiListDropdown & { selectedApi?: Api }) => {
const [expanded, setExpanded] = React.useState(new Set<string>());

const pageMax = Math.ceil(apis?.count / Constants.defaultPageSize);
const [working, setWorking] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [apis, setApis] = useState<Api[]>([]);
const [apisByTag, setApisByTag] = useState<TagGroup<Api>[]>([]);
const [pattern, setPattern] = useState<string>();
const [groupByTag, setGroupByTag] = useState(!!defaultGroupByTagToEnabled);
const [filters, setFilters] = useState<{ tags: Tag[] }>({ tags: [] });

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

useEffect(() => {
const query: SearchQuery = {
pattern,
tags: filters.tags,
skip: (pageNumber - 1) * Constants.defaultPageSize,
take: Constants.defaultPageSize
};

setWorking(true);
if (groupByTag) {
loadApisByTag(query)
.then(loadedApis => {
if (pageNumber > 1) {
// Check if the tag is already displayed. If yes, add to this tag
loadedApis.value.forEach(newApi => {
const existingTagIndex = apisByTag.findIndex(item => item.tag === newApi.tag);
if (existingTagIndex !== -1) {
apisByTag[existingTagIndex].items.push(...newApi.items);
} else {
apisByTag.push(newApi);
}
});
setApisByTag(apisByTag);
} else {
setApisByTag([...loadedApis.value]);
}
setHasNextPage(!!loadedApis.nextLink);
})
.finally(() => setWorking(false));
} else {
loadApis(query)
.then(loadedApis => {
if (pageNumber > 1) {
setApis([...apis, ...loadedApis.value]);
} else {
setApis([...loadedApis.value]);
}
setHasNextPage(!!loadedApis.nextLink);
})
.finally(() => setWorking(false));
}
}, [apiService, pageNumber, groupByTag, filters, pattern]);

const loadApis = async (query: SearchQuery) => {
let apis: Page<Api>;

try {
apis = await apiService.getApis(query);
} catch (error) {
throw new Error(`Unable to load APIs. Error: ${error.message}`);
}

return apis;
}

const loadApisByTag = async (query: SearchQuery) => {
let apis: Page<TagGroup<Api>>;

try {
apis = await apiService.getApisByTags(query);
} catch (error) {
throw new Error(`Unable to load APIs. Error: ${error.message}`);
}

return apis;
}

const content = !apis || !selectedApi ? (
<>Loading APIs</> // if data are not loaded yet ComboBox sometimes fails to initialize properly - edge case, in most cases almost instant from the cache
) : (
Expand Down Expand Up @@ -119,15 +178,19 @@ const ApiListDropdownFC = ({
disabled
value={"group by tag switch"}
text={"group by tag switch"}
style={{ columnGap: 0 }}
className="group-by-tag-switch"
>
<GroupByTag
groupByTag={groupByTag}
setGroupByTag={setGroupByTag}
setPageNumber={setPageNumber}
labelAfter
/>
</Option>

{isApisGrouped(apis) ? (
apis?.value.map(({ tag, items }) => (
{groupByTag ? (
apisByTag?.map(({ tag, items }) => (
<OptionGroup
key={tag}
label={
Expand All @@ -148,22 +211,20 @@ const ApiListDropdownFC = ({
))
) : (
<Options
apis={apis.value}
apis={apis}
getReferenceUrl={getReferenceUrl}
/>
)}

{pageMax > 1 && (
{hasNextPage && (
<Option
disabled
value={"pagination"}
text={"pagination"}
checkIcon={<></>}
style={{ columnGap: 0 }}
>
<Pagination
pageNumber={pageNumber}
setPageNumber={setPageNumber}
pageMax={pageMax}
/>
<button className={"button button-default show-more-options"} onClick={() => setPageNumber(prev => prev + 1)}>Show more</button>
</Option>
)}
</>
Expand All @@ -178,49 +239,3 @@ const ApiListDropdownFC = ({
</>
);
};

export class ApiListDropdown extends React.Component<
TApiListDropdown,
{ working: boolean; api?: Api }
> {
@Resolve("apiService")
public apiService: ApiService;

@Resolve("routeHelper")
public routeHelper: RouteHelper;

constructor(props: TApiListDropdown) {
super(props);

this.state = {
working: false,
api: undefined,
};
}

public componentDidMount() {
this.loadSelectedApi();
}

async loadSelectedApi() {
const apiName = this.routeHelper.getApiName();
if (!apiName) return;

this.setState({ working: true, api: undefined });

return this.apiService
.getApi(`apis/${apiName}`)
.then((api) => this.setState({ api }))
.finally(() => this.setState({ working: false }));
}

render() {
return (
<ApiListDropdownFC
{...this.props}
working={this.props.working || this.state.working}
selectedApi={this.state.api}
/>
);
}
}
Loading