Skip to content

FUI - APIs list Dropdown #2501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 10, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isRedesignEnabledSetting } from "../../../../constants";
export class DetailsOfApiViewModelBinder implements ViewModelBinder<DetailsOfApiModel, DetailsOfApiViewModel> {
constructor(
private readonly styleCompiler: StyleCompiler,
private readonly siteService: ISiteService
private readonly siteService: ISiteService,
) { }

public stateToInstance(state: WidgetState, componentInstance: DetailsOfApiViewModel): void {
Expand Down
20 changes: 13 additions & 7 deletions src/components/apis/list-of-apis/ko/listOfApis.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
<!-- ko if: layout() === 'list' -->
<!-- ko if: isRedesignEnabled -->
<fui-api-list-runtime data-bind="attr: { props: runtimeConfig }"></fui-api-list-runtime>
<!-- /ko -->

<!-- ko if: layout() === 'dropdown' -->
<api-list-dropdown data-bind="attr: { params: runtimeConfig }"></api-list-dropdown>
<!-- /ko -->
<!-- ko ifnot: isRedesignEnabled -->
<!-- ko if: layout() === 'list' -->
<api-list data-bind="attr: { params: runtimeConfig }"></api-list>
<!-- /ko -->

<!-- ko if: layout() === 'tiles' -->
<fui-api-list-runtime data-bind="attr: { props: runtimeConfig }"></fui-api-list-runtime>
<!-- /ko -->
<!-- ko if: layout() === 'dropdown' -->
<api-list-dropdown data-bind="attr: { params: runtimeConfig }"></api-list-dropdown>
<!-- /ko -->

<!-- ko if: layout() === 'tiles' -->
<api-list-tiles data-bind="attr: { params: runtimeConfig }"></api-list-tiles>
<!-- /ko -->
<!-- /ko -->
2 changes: 2 additions & 0 deletions src/components/apis/list-of-apis/ko/listOfApisViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ export class ListOfApisViewModel {
public readonly layout: ko.Observable<string>;
public readonly runtimeConfig: ko.Observable<string>;
public readonly styles: ko.Observable<StyleModel>;
public readonly isRedesignEnabled: ko.Observable<boolean>;

constructor() {
this.layout = ko.observable();
this.runtimeConfig = ko.observable();
this.styles = ko.observable<StyleModel>();
this.isRedesignEnabled = ko.observable<boolean>();
}
}
14 changes: 11 additions & 3 deletions src/components/apis/list-of-apis/ko/listOfApisViewModelBinder.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { ViewModelBinder, WidgetState } from "@paperbits/common/widgets";
import { ListOfApisViewModel } from "./listOfApisViewModel";
import { ListOfApisModel } from "../listOfApisModel";
import { StyleCompiler } from "@paperbits/common/styles";
import { ISiteService } from "@paperbits/common/sites";
import { layoutsMap } from "../../../utils/react/TableListInfo";
import { isRedesignEnabledSetting } from "../../../../constants";
import { ListOfApisModel } from "../listOfApisModel";
import { ListOfApisViewModel } from "./listOfApisViewModel";

export class ListOfApisViewModelBinder implements ViewModelBinder<ListOfApisModel, ListOfApisViewModel> {
constructor(private readonly styleCompiler: StyleCompiler) { }
constructor(
private readonly styleCompiler: StyleCompiler,
private readonly siteService: ISiteService,
) { }

public stateToInstance(state: WidgetState, componentInstance: ListOfApisViewModel): void {
componentInstance.styles(state.styles);
componentInstance.layout(state.layout);
componentInstance.isRedesignEnabled(state.isRedesignEnabled);

componentInstance.runtimeConfig(JSON.stringify({
allowSelection: state.allowSelection,
Expand Down Expand Up @@ -40,5 +46,7 @@ export class ListOfApisViewModelBinder implements ViewModelBinder<ListOfApisMode
if (model.styles) {
state.styles = await this.styleCompiler.getStyleModelAsync(model.styles);
}

state.isRedesignEnabled = !!(await this.siteService.getSetting(isRedesignEnabledSetting));
}
}
214 changes: 214 additions & 0 deletions src/components/apis/list-of-apis/react/runtime/ApiListDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import * as React from "react";
import { useState } from "react";
import {
Body1Strong,
Combobox,
Link,
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 { TApiListRuntimeFC } from "./ApiListRuntime";
import {
isApisGrouped,
TagGroupToggleBtn,
TApisData,
toggleValueInSet,
} from "./utils";
import { RouteHelper } from "../../../../../routing/routeHelper";
import { ApiService } from "../../../../../services/apiService";

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

const TagLabel = ({
tag,
expanded,
onClick,
}: {
tag: string;
expanded: Set<string>;
onClick(): void;
}) => (
<button onClick={onClick} className={"no-border"}>
<TagGroupToggleBtn expanded={expanded.has(tag)} />
<span style={{ marginLeft: ".575rem" }}>{tag}</span>
</button>
);

const Options = ({
apis,
getReferenceUrl,
}: {
apis: Api[];
getReferenceUrl: TApiListRuntimeFC["getReferenceUrl"];
}) => (
<>
{apis.map((api) => (
<Option key={api.id} value={api.name} text={api.displayName}>
<Link href={getReferenceUrl(api.name)}>{api.displayName}</Link>
</Option>
))}
</>
);

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

const pageMax = Math.ceil(apis?.count / Constants.defaultPageSize);

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

const content = !apis ? (
<>Loading APIs</> // if data are not loaded yet ComboBox sometimes fails to initialize properly - edge case, in most cases almost instant from the cache
) : (
<Combobox
style={{ width: "100%" }}
onInput={(event) => setPattern(event.target?.["value"])}
defaultValue={selectedApi?.displayName}
defaultSelectedOptions={[selectedApi?.name]}
onOptionSelect={(_, { optionValue }) => {
if (!optionValue) return;
window.location.hash = getReferenceUrl(optionValue);
}}
>
{working ? (
<Spinner
label={"Loading APIs"}
labelPosition={"above"}
size={"small"}
style={{ padding: "1rem" }}
/>
) : (
<>
<Option
disabled
value={"group by tag switch"}
text={"group by tag switch"}
>
<GroupByTag
groupByTag={groupByTag}
setGroupByTag={setGroupByTag}
/>
</Option>

{isApisGrouped(apis) ? (
apis?.value.map(({ tag, items }) => (
<OptionGroup
key={tag}
label={
<TagLabel
onClick={() => toggleTag(tag)}
tag={tag}
expanded={expanded}
/>
}
>
{expanded.has(tag) && (
<Options
apis={items}
getReferenceUrl={getReferenceUrl}
/>
)}
</OptionGroup>
))
) : (
<Options
apis={apis.value}
getReferenceUrl={getReferenceUrl}
/>
)}

{pageMax > 1 && (
<Option
disabled
value={"pagination"}
text={"pagination"}
>
<Pagination
pageNumber={pageNumber}
setPageNumber={setPageNumber}
pageMax={pageMax}
/>
</Option>
)}
</>
)}
</Combobox>
);

return (
<>
<Body1Strong block>APIs</Body1Strong>
<div style={{ padding: ".25rem 0 1rem" }}>{content}</div>
</>
);
};

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