diff --git a/src/apim.runtime.module.ts b/src/apim.runtime.module.ts index 9188b368d..5af67e98d 100644 --- a/src/apim.runtime.module.ts +++ b/src/apim.runtime.module.ts @@ -46,6 +46,7 @@ import { ProductSubscribe } from "./components/products/product-subscribe/ko/run import { DefaultAuthenticator } from "./components/defaultAuthenticator"; import { Spinner } from "./components/spinner/spinner"; import { ProductApis } from "./components/products/product-apis/ko/runtime/product-apis"; +import { ProductApisTiles } from "./components/products/product-apis/ko/runtime/product-apis-tiles"; import { OperationList } from "./components/operations/operation-list/ko/runtime/operation-list"; import { ProductSubscriptions } from "./components/products/product-subscriptions/ko/runtime/product-subscriptions"; import { AadService } from "./services/aadService"; @@ -71,6 +72,7 @@ import { OAuthService } from "./services/oauthService"; import { DefaultSessionManager } from "./authentication/defaultSessionManager"; import { ApiProducts } from "./components/apis/api-products/ko/runtime/api-products"; import { ApiProductsTiles } from "./components/apis/api-products/ko/runtime/api-products-tiles"; +import { ProductListTiles } from "./components/products/product-list/ko/runtime/product-list-tiles"; export class ApimRuntimeModule implements IInjectorModule { public register(injector: IInjector): void { @@ -110,11 +112,13 @@ export class ApimRuntimeModule implements IInjectorModule { injector.bind("subscriptions", Subscriptions); injector.bind("productList", ProductList); injector.bind("productListDropdown", ProductListDropdown); + injector.bind("productListTiles", ProductListTiles); injector.bind("validationSummary", ValidationSummary); injector.bind("productDetails", ProductDetails); injector.bind("productSubscribe", ProductSubscribe); injector.bind("productSubscriptions", ProductSubscriptions); injector.bind("productApis", ProductApis); + injector.bind("productApisTiles", ProductApisTiles); injector.bind("operationList", OperationList); injector.bind("operationDetails", OperationDetails); injector.bind("usersService", UsersService); diff --git a/src/components/apis/api-products/apiProductsHandlers.ts b/src/components/apis/api-products/apiProductsHandlers.ts index 66fb408ee..338f09765 100644 --- a/src/components/apis/api-products/apiProductsHandlers.ts +++ b/src/components/apis/api-products/apiProductsHandlers.ts @@ -9,7 +9,7 @@ export class ApiProductsHandlers implements IWidgetHandler { displayName: "API: products", iconClass: "paperbits-cheque-3", requires: ["html"], - createModel: async () => new ApiProductsModel() + createModel: async () => new ApiProductsModel("list") }; return widgetOrder; diff --git a/src/components/apis/api-products/ko/apiProductsViewModelBinder.ts b/src/components/apis/api-products/ko/apiProductsViewModelBinder.ts index 639133760..2d81a65ae 100644 --- a/src/components/apis/api-products/ko/apiProductsViewModelBinder.ts +++ b/src/components/apis/api-products/ko/apiProductsViewModelBinder.ts @@ -21,7 +21,7 @@ export class ApiProductsViewModelBinder implements ViewModelBinder + + + + + diff --git a/src/components/products/product-apis/ko/productApisEditor.module.ts b/src/components/products/product-apis/ko/productApisEditor.module.ts index a63b8836a..551387cf4 100644 --- a/src/components/products/product-apis/ko/productApisEditor.module.ts +++ b/src/components/products/product-apis/ko/productApisEditor.module.ts @@ -1,10 +1,11 @@ import { IInjectorModule, IInjector } from "@paperbits/common/injection"; -import { ProductApisHandlers } from "../productApisHandlers"; +import { ProductApisHandlers, ProductApisTilesHandlers } from "../productApisHandlers"; import { ProductApisEditor } from "./productApisEditor"; export class ProductApisEditorModule implements IInjectorModule { public register(injector: IInjector): void { injector.bind("productApisEditor", ProductApisEditor); injector.bindToCollection("widgetHandlers", ProductApisHandlers, "productApisHandlers"); + injector.bindToCollection("widgetHandlers", ProductApisTilesHandlers, "productApisTilesHandlers"); } } \ No newline at end of file diff --git a/src/components/products/product-apis/ko/productApisViewModel.ts b/src/components/products/product-apis/ko/productApisViewModel.ts index d2ba83521..07cbe44e5 100644 --- a/src/components/products/product-apis/ko/productApisViewModel.ts +++ b/src/components/products/product-apis/ko/productApisViewModel.ts @@ -7,9 +7,11 @@ import { Component } from "@paperbits/common/ko/decorators/component.decorator"; template: template }) export class ProductApisViewModel { + public readonly layout: ko.Observable; public readonly runtimeConfig: ko.Observable; constructor() { + this.layout = ko.observable(); this.runtimeConfig = ko.observable(); } } \ No newline at end of file diff --git a/src/components/products/product-apis/ko/productApisViewModelBinder.ts b/src/components/products/product-apis/ko/productApisViewModelBinder.ts index da7dac198..97ab42e5c 100644 --- a/src/components/products/product-apis/ko/productApisViewModelBinder.ts +++ b/src/components/products/product-apis/ko/productApisViewModelBinder.ts @@ -12,6 +12,8 @@ export class ProductApisViewModelBinder implements ViewModelBinder +
+ + +
+ + +
+ +
+ +
+ + + +
+ + + +
+

+ + + SOAP + + + - + +

+
+

+
+
+
+ + + +

This product doesn't have APIs.

+ + +
+ + + +
\ No newline at end of file diff --git a/src/components/products/product-apis/ko/runtime/product-apis-tiles.ts b/src/components/products/product-apis/ko/runtime/product-apis-tiles.ts new file mode 100644 index 000000000..7cb31117a --- /dev/null +++ b/src/components/products/product-apis/ko/runtime/product-apis-tiles.ts @@ -0,0 +1,120 @@ +import * as ko from "knockout"; +import * as Constants from "../../../../../constants"; +import template from "./product-apis-tiles.html"; +import { Component, RuntimeComponent, OnMounted, OnDestroyed, Param } from "@paperbits/common/ko/decorators"; +import { Router } from "@paperbits/common/routing"; +import { ApiService } from "../../../../../services/apiService"; +import { Api } from "../../../../../models/api"; +import { SearchQuery } from "../../../../../contracts/searchQuery"; +import { RouteHelper } from "../../../../../routing/routeHelper"; + + +@RuntimeComponent({ + selector: "product-apis-tiles-runtime" +}) +@Component({ + selector: "product-apis-tiles-runtime", + template: template +}) +export class ProductApisTiles { + public readonly apis: ko.ObservableArray; + public readonly working: ko.Observable; + public readonly pattern: ko.Observable; + public readonly page: ko.Observable; + public readonly hasPager: ko.Computed; + public readonly hasPrevPage: ko.Observable; + public readonly hasNextPage: ko.Observable; + + constructor( + private readonly apiService: ApiService, + private readonly router: Router, + private readonly routeHelper: RouteHelper + ) { + this.detailsPageUrl = ko.observable(); + this.apis = ko.observableArray([]); + this.working = ko.observable(); + this.pattern = ko.observable(); + this.page = ko.observable(1); + this.hasPrevPage = ko.observable(); + this.hasNextPage = ko.observable(); + this.hasPager = ko.computed(() => this.hasPrevPage() || this.hasNextPage()); + } + + @Param() + public detailsPageUrl: ko.Observable; + + @OnMounted() + public async initialize(): Promise { + await this.searchApis(); + + this.router.addRouteChangeListener(this.searchApis); + + this.pattern + .extend({ rateLimit: { timeout: Constants.defaultInputDelayMs, method: "notifyWhenChangesStop" } }) + .subscribe(this.searchApis); + } + + /** + * Initiates searching APIs. + */ + public async searchApis(): Promise { + this.page(1); + this.loadPageOfApis(); + } + + /** + * Loads page of APIs. + */ + public async loadPageOfApis(): Promise { + const productName = this.routeHelper.getProductName(); + + if (!productName) { + return; + } + + try { + this.working(true); + + const pageNumber = this.page() - 1; + + const query: SearchQuery = { + pattern: this.pattern(), + skip: pageNumber * Constants.defaultPageSize, + take: Constants.defaultPageSize + }; + + const pageOfApis = await this.apiService.getProductApis(`products/${productName}`, query); + this.apis(pageOfApis.value); + + const nextLink = pageOfApis.nextLink; + + this.hasPrevPage(pageNumber > 0); + this.hasNextPage(!!nextLink); + } + catch (error) { + throw new Error(`Unable to load APIs. Error: ${error.message}`); + } + finally { + this.working(false); + } + } + + public getReferenceUrl(api: Api): string { + return this.routeHelper.getApiReferenceUrl(api.name, this.detailsPageUrl()); + } + + public prevPage(): void { + this.page(this.page() - 1); + this.loadPageOfApis(); + } + + public nextPage(): void { + this.page(this.page() + 1); + this.loadPageOfApis(); + } + + @OnDestroyed() + public dispose(): void { + this.router.removeRouteChangeListener(this.searchApis); + } +} \ No newline at end of file diff --git a/src/components/products/product-apis/productApisContract.ts b/src/components/products/product-apis/productApisContract.ts index a7b5b653a..3dcdd1d6f 100644 --- a/src/components/products/product-apis/productApisContract.ts +++ b/src/components/products/product-apis/productApisContract.ts @@ -5,6 +5,11 @@ import { HyperlinkContract } from "@paperbits/common/editing"; * Product API list widget configuration. */ export interface ProductApisContract extends Contract { + /** + * List layout. "list" or "tiles" + */ + itemStyleView?: string; + /** * Link to a page that contains operation details. */ diff --git a/src/components/products/product-apis/productApisHandlers.ts b/src/components/products/product-apis/productApisHandlers.ts index 253b6e1a0..19e3b0730 100644 --- a/src/components/products/product-apis/productApisHandlers.ts +++ b/src/components/products/product-apis/productApisHandlers.ts @@ -9,7 +9,22 @@ export class ProductApisHandlers implements IWidgetHandler { displayName: "Product: APIs", iconClass: "paperbits-cheque-3", requires: ["html"], - createModel: async () => new ProductApisModel() + createModel: async () => new ProductApisModel("list") + }; + + return widgetOrder; + } +} + +export class ProductApisTilesHandlers implements IWidgetHandler { + public async getWidgetOrder(): Promise { + const widgetOrder: IWidgetOrder = { + name: "product-apis-tiles", + category: "Products", + displayName: "Product: APIs (tiles)", + iconClass: "paperbits-cheque-3", + requires: ["html"], + createModel: async () => new ProductApisModel("tiles") }; return widgetOrder; diff --git a/src/components/products/product-apis/productApisModel.ts b/src/components/products/product-apis/productApisModel.ts index a223e74f3..bf0536e7f 100644 --- a/src/components/products/product-apis/productApisModel.ts +++ b/src/components/products/product-apis/productApisModel.ts @@ -4,8 +4,17 @@ import { HyperlinkModel } from "@paperbits/common/permalinks"; * Product API list widget configuration. */ export class ProductApisModel { + /** + * List layout. "list" or "tiles" + */ + public layout?: string; + /** * Link to a page that contains API details. */ public detailsPageHyperlink: HyperlinkModel; + + constructor(layout?: "list"|"tiles") { + this.layout = layout; + } } diff --git a/src/components/products/product-apis/productApisModelBinder.ts b/src/components/products/product-apis/productApisModelBinder.ts index ad8d6ba40..4b87d9dc6 100644 --- a/src/components/products/product-apis/productApisModelBinder.ts +++ b/src/components/products/product-apis/productApisModelBinder.ts @@ -17,6 +17,7 @@ export class ProductApisModelBinder implements IModelBinder { public async contractToModel(contract: ProductApisContract): Promise { const model = new ProductApisModel(); + model.layout = contract.itemStyleView || "list"; if (contract.detailsPageHyperlink) { model.detailsPageHyperlink = await this.permalinkResolver.getHyperlinkFromContract(contract.detailsPageHyperlink); @@ -28,6 +29,7 @@ export class ProductApisModelBinder implements IModelBinder { public modelToContract(model: ProductApisModel): ProductApisContract { const contract: ProductApisContract = { type: "product-apis", + itemStyleView: model.layout, detailsPageHyperlink: model.detailsPageHyperlink ? { target: model.detailsPageHyperlink.target, diff --git a/src/components/products/product-list/ko/productList.html b/src/components/products/product-list/ko/productList.html index f2dad11d0..f95770670 100644 --- a/src/components/products/product-list/ko/productList.html +++ b/src/components/products/product-list/ko/productList.html @@ -5,3 +5,7 @@ + + + + diff --git a/src/components/products/product-list/ko/productListEditor.module.ts b/src/components/products/product-list/ko/productListEditor.module.ts index ebee84f76..c3542987c 100644 --- a/src/components/products/product-list/ko/productListEditor.module.ts +++ b/src/components/products/product-list/ko/productListEditor.module.ts @@ -1,5 +1,5 @@ import { IInjectorModule, IInjector } from "@paperbits/common/injection"; -import { ProductListHandlers, ProductListDropdownHandlers } from "../productListHandlers"; +import { ProductListHandlers, ProductListDropdownHandlers, ProductListTilesHandlers } from "../productListHandlers"; import { ProductListEditor } from "./productListEditor"; export class ProductListEditorModule implements IInjectorModule { @@ -7,5 +7,6 @@ export class ProductListEditorModule implements IInjectorModule { injector.bind("productListEditor", ProductListEditor); injector.bindToCollection("widgetHandlers", ProductListHandlers, "productListHandlers"); injector.bindToCollection("widgetHandlers", ProductListDropdownHandlers, "productListDropdownHandlers"); + injector.bindToCollection("widgetHandlers", ProductListTilesHandlers, "productListTilesHandlers"); } } \ No newline at end of file diff --git a/src/components/products/product-list/ko/productListViewModelBinder.ts b/src/components/products/product-list/ko/productListViewModelBinder.ts index ad799408a..d1016609d 100644 --- a/src/components/products/product-list/ko/productListViewModelBinder.ts +++ b/src/components/products/product-list/ko/productListViewModelBinder.ts @@ -24,7 +24,7 @@ export class ProductListViewModelBinder implements ViewModelBinder +
+ + +
+ + +
+ +
+ +
+ + + +
+ + + +
+

+ +

+
+

+
+
+
+ + + +

No products found

+ + +
+ + + +
\ No newline at end of file diff --git a/src/components/products/product-list/ko/runtime/product-list-tiles.ts b/src/components/products/product-list/ko/runtime/product-list-tiles.ts new file mode 100644 index 000000000..0e9f2d6d0 --- /dev/null +++ b/src/components/products/product-list/ko/runtime/product-list-tiles.ts @@ -0,0 +1,114 @@ +import * as ko from "knockout"; +import * as Constants from "../../../../../constants"; +import template from "./product-list-tiles.html"; +import { Component, RuntimeComponent, OnMounted, OnDestroyed, Param } from "@paperbits/common/ko/decorators"; +import { Router } from "@paperbits/common/routing"; +import { SearchQuery } from "../../../../../contracts/searchQuery"; +import { RouteHelper } from "../../../../../routing/routeHelper"; +import { Product } from "../../../../../models/product"; +import { ProductService } from "../../../../../services/productService"; + +@RuntimeComponent({ + selector: "product-list-tiles-runtime" +}) +@Component({ + selector: "product-list-tiles-runtime", + template: template +}) +export class ProductListTiles { + public readonly products: ko.ObservableArray; + public readonly working: ko.Observable; + public readonly pattern: ko.Observable; + public readonly page: ko.Observable; + public readonly hasPager: ko.Computed; + public readonly hasPrevPage: ko.Observable; + public readonly hasNextPage: ko.Observable; + + constructor( + private readonly productService: ProductService, + private readonly router: Router, + private readonly routeHelper: RouteHelper + ) { + this.detailsPageUrl = ko.observable(); + this.products = ko.observableArray([]); + this.working = ko.observable(); + this.pattern = ko.observable(); + this.page = ko.observable(1); + this.hasPrevPage = ko.observable(); + this.hasNextPage = ko.observable(); + this.hasPager = ko.computed(() => this.hasPrevPage() || this.hasNextPage()); + } + + @Param() + public detailsPageUrl: ko.Observable; + + @OnMounted() + public async initialize(): Promise { + await this.resetSearch(); + + this.pattern + .extend({ rateLimit: { timeout: Constants.defaultInputDelayMs, method: "notifyWhenChangesStop" } }) + .subscribe(this.searchProducts); + } + + /** + * Initiates searching products. + */ + public async searchProducts(): Promise { + this.page(1); + await this.loadData(); + } + + /** + * Loads page of products. + */ + public async loadData(): Promise { + const pageNumber = this.page() - 1; + + const query: SearchQuery = { + pattern: this.pattern(), + skip: pageNumber * Constants.defaultPageSize, + take: Constants.defaultPageSize + }; + + try { + this.working(true); + + const itemsPage = await this.productService.getProductsPage(query); + this.hasPrevPage(pageNumber > 0); + this.hasNextPage(!!itemsPage.nextLink); + + this.products(itemsPage.value); + } + catch (error) { + throw new Error(`Unable to load API products. Error: ${error.message}`); + } + finally { + this.working(false); + } + } + + public getProductUrl(product: Product): string { + return this.routeHelper.getProductReferenceUrl(product.name, this.detailsPageUrl()); + } + + public prevPage(): void { + this.page(this.page() - 1); + this.loadData(); + } + + public nextPage(): void { + this.page(this.page() + 1); + this.loadData(); + } + + public async resetSearch(): Promise { + this.page(1); + this.loadData(); + } + + @OnDestroyed() + public dispose(): void { + this.router.removeRouteChangeListener(this.searchProducts); + } +} \ No newline at end of file diff --git a/src/components/products/product-list/productListHandlers.ts b/src/components/products/product-list/productListHandlers.ts index c904e5d22..52c927b35 100644 --- a/src/components/products/product-list/productListHandlers.ts +++ b/src/components/products/product-list/productListHandlers.ts @@ -27,6 +27,21 @@ export class ProductListDropdownHandlers implements IWidgetHandler { createModel: async () => new ProductListModel("dropdown") }; + return widgetOrder; + } +} + +export class ProductListTilesHandlers implements IWidgetHandler { + public async getWidgetOrder(): Promise { + const widgetOrder: IWidgetOrder = { + name: "productListTiles", + category: "Products", + displayName: "List of products (tiles)", + iconClass: "paperbits-cheque-3", + requires: ["html"], + createModel: async () => new ProductListModel("tiles") + }; + return widgetOrder; } } \ No newline at end of file diff --git a/src/components/products/product-list/productListModel.ts b/src/components/products/product-list/productListModel.ts index c8b94cda2..f2946491c 100644 --- a/src/components/products/product-list/productListModel.ts +++ b/src/components/products/product-list/productListModel.ts @@ -5,7 +5,7 @@ import { HyperlinkModel } from "@paperbits/common/permalinks"; */ export class ProductListModel { /** - * Product list layout, e.g. "list", "dropdown". + * Product list layout, e.g. "list", "dropdown", "tiles". */ public layout?: string; diff --git a/src/components/products/product-list/productListModelBinder.ts b/src/components/products/product-list/productListModelBinder.ts index e85fc0117..7168f399f 100644 --- a/src/components/products/product-list/productListModelBinder.ts +++ b/src/components/products/product-list/productListModelBinder.ts @@ -20,7 +20,7 @@ export class ProductListModelBinder implements IModelBinder { public async contractToModel(contract: ProductListContract): Promise { const model = new ProductListModel(); - model.layout = contract.itemStyleView; + model.layout = contract.itemStyleView || "list"; model.allowSelection = contract.allowSelection;