From ccf33639c37905a9bcf19259167a86993215b457 Mon Sep 17 00:00:00 2001 From: naouf4l-grav Date: Tue, 28 Jan 2025 11:20:14 +0100 Subject: [PATCH] feat: add documentation pages collapse and display in the breadcrumb (#10549) --- .../api-tab-documentation.component.html | 30 +++----- .../api-tab-documentation.component.scss | 13 ++-- .../api-tab-documentation.component.ts | 50 +++++--------- .../api-documentation.component.html | 29 ++++++++ .../api-documentation.component.scss | 23 +++++++ .../api-documentation.component.ts | 68 +++++++++++++++++++ .../src/app/app.routes.ts | 10 ++- .../src/app/guides/guides.component.spec.ts | 10 +-- .../components/drawer/drawer.component.html | 28 ++++++++ .../components/drawer/drawer.component.scss | 25 +++++++ .../drawer/drawer.component.spec.ts | 37 ++++++++++ .../src/components/drawer/drawer.component.ts | 35 ++++++++++ .../page-tree/page-tree.component.spec.ts | 8 +-- .../page-tree/page-tree.component.ts | 1 - 14 files changed, 297 insertions(+), 70 deletions(-) create mode 100644 gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.html create mode 100644 gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.scss create mode 100644 gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.ts create mode 100644 gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.html create mode 100644 gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.scss create mode 100644 gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.spec.ts create mode 100644 gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.ts diff --git a/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.html b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.html index 0e3246e897a..ff2dad9e8b6 100644 --- a/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.html +++ b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.html @@ -16,26 +16,18 @@ --> @if (pages.length) { -
- -
- -
- @if (selectedPageData$ | async; as selectedPageData) { - @if (selectedPageData.result) { - - } - @if (selectedPageData.error) { -
An error has occurred.
- } - } @else { - - } +
+ + +
+ @if (apiId && pageId(); as pageId) { + + } } @else {
Documentation for this API is missing.
} diff --git a/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.scss b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.scss index 5f03f7be3db..b1ac7509d91 100644 --- a/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.scss +++ b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.scss @@ -21,18 +21,15 @@ .api-tab-documentation { &__side-bar { min-width: 276px; + transition: margin-left 350ms ease-in-out; + + &--hidden { + min-width: unset; + } &__tree { position: sticky; top: 96px; } } - - &__page-content { - width: 100%; - - &__container { - min-width: 0; - } - } } diff --git a/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.ts b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.ts index 15f899c91b9..6b1d2cc1b7d 100644 --- a/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.ts +++ b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/api-tab-documentation.component.ts @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AsyncPipe } from '@angular/common'; -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { NgClass } from '@angular/common'; +import { Component, Input, OnInit, signal, WritableSignal } from '@angular/core'; +import { MatSidenavModule } from '@angular/material/sidenav'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { catchError, map, Observable, of } from 'rxjs'; +import { Observable, of } from 'rxjs'; -import { LoaderComponent } from '../../../../components/loader/loader.component'; -import { PageComponent } from '../../../../components/page/page.component'; +import { ApiDocumentationComponent } from './components/api-documentation/api-documentation.component'; +import { DrawerComponent } from '../../../../components/drawer/drawer.component'; import { PageTreeComponent, PageTreeNode } from '../../../../components/page-tree/page-tree.component'; import { Page } from '../../../../entities/page/page'; import { PageService } from '../../../../services/page.service'; @@ -33,49 +33,35 @@ interface SelectedPageData { @Component({ selector: 'app-api-tab-documentation', standalone: true, - imports: [PageTreeComponent, AsyncPipe, PageComponent, RouterModule, LoaderComponent], + imports: [PageTreeComponent, RouterModule, ApiDocumentationComponent, MatSidenavModule, DrawerComponent, NgClass], templateUrl: './api-tab-documentation.component.html', styleUrl: './api-tab-documentation.component.scss', }) -export class ApiTabDocumentationComponent implements OnInit, OnChanges { - @Input() - page!: string; +export class ApiTabDocumentationComponent implements OnInit { @Input() apiId!: string; @Input() pages!: Page[]; pageNodes: PageTreeNode[] = []; selectedPageData$: Observable = of(); + pageId = signal(undefined); + isSidebarExpanded: WritableSignal = signal(true); constructor( - private pageService: PageService, - private router: Router, - private activatedRoute: ActivatedRoute, + private readonly pageService: PageService, + private readonly router: Router, + private readonly activatedRoute: ActivatedRoute, ) {} ngOnInit() { this.pageNodes = this.pageService.mapToPageTreeNode(undefined, this.pages); - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes['page'] && !!changes['page'].currentValue) { - this.selectedPageData$ = this.getSelectedPage$(changes['page'].currentValue); + if (this.pageNodes.length == 1) { + this.isSidebarExpanded.set(false); } } - showPage(page: string) { - this.router.navigate(['.'], { queryParams: { page }, relativeTo: this.activatedRoute }); - } - - private getSelectedPage$(pageId: string): Observable { - return this.pageService.getByApiIdAndId(this.apiId, pageId, true).pipe( - map(result => ({ result })), - catchError((error: HttpErrorResponse) => { - if (error.status === 404) { - this.router.navigate(['404']); - } - return of({ error }); - }), - ); + showPage(pageId: string) { + this.pageId.set(pageId); + this.router.navigate(['.', pageId], { relativeTo: this.activatedRoute }); } } diff --git a/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.html b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.html new file mode 100644 index 00000000000..445175b6e06 --- /dev/null +++ b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.html @@ -0,0 +1,29 @@ + +
+ @if (selectedPageData$ | async; as selectedPageData) { + @if (selectedPageData.result) { + + } + @if (selectedPageData.error) { +
An error has occurred.
+ } + } @else { + + } +
diff --git a/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.scss b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.scss new file mode 100644 index 00000000000..42f08be808e --- /dev/null +++ b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.scss @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.page-content { + width: 100%; + + &__container { + min-width: 0; + } +} diff --git a/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.ts b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.ts new file mode 100644 index 00000000000..a9b165f70e5 --- /dev/null +++ b/gravitee-apim-portal-webui-next/src/app/api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component.ts @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AsyncPipe } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, DestroyRef, effect, inject, input, InputSignal } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; +import { catchError, map, Observable, of, tap } from 'rxjs'; +import { BreadcrumbService } from 'xng-breadcrumb'; + +import { LoaderComponent } from '../../../../../../components/loader/loader.component'; +import { PageComponent } from '../../../../../../components/page/page.component'; +import { Page } from '../../../../../../entities/page/page'; +import { PageService } from '../../../../../../services/page.service'; + +interface SelectedPageData { + result?: Page; + error?: unknown; +} + +@Component({ + selector: 'app-api-documentation', + standalone: true, + imports: [AsyncPipe, PageComponent, RouterModule, LoaderComponent], + templateUrl: './api-documentation.component.html', + styleUrl: './api-documentation.component.scss', +}) +export class ApiDocumentationComponent { + pageId: InputSignal = input.required(); + apiId: InputSignal = input.required(); + pages: InputSignal = input.required(); + selectedPageData$: Observable = of(); + destroyRef = inject(DestroyRef); + + constructor( + private readonly pageService: PageService, + private readonly router: Router, + private readonly breadcrumbService: BreadcrumbService, + ) { + effect(() => { + this.selectedPageData$ = this.getSelectedPage$(this.pageId()); + }); + } + + private getSelectedPage$(pageId: string): Observable { + return this.pageService.getByApiIdAndId(this.apiId(), pageId, true).pipe( + tap(page => { + this.breadcrumbService.set('@pageName', page.name); + }), + map(result => ({ result })), + catchError((error: HttpErrorResponse) => { + return of({ error }); + }), + ); + } +} diff --git a/gravitee-apim-portal-webui-next/src/app/app.routes.ts b/gravitee-apim-portal-webui-next/src/app/app.routes.ts index 39729bfbbac..1d2c48a0ef7 100644 --- a/gravitee-apim-portal-webui-next/src/app/app.routes.ts +++ b/gravitee-apim-portal-webui-next/src/app/app.routes.ts @@ -18,6 +18,7 @@ import { Routes } from '@angular/router'; import { ApiDetailsComponent } from './api/api-details/api-details.component'; import { ApiTabDetailsComponent } from './api/api-details/api-tab-details/api-tab-details.component'; import { ApiTabDocumentationComponent } from './api/api-details/api-tab-documentation/api-tab-documentation.component'; +import { ApiDocumentationComponent } from './api/api-details/api-tab-documentation/components/api-documentation/api-documentation.component'; import { ApiTabSubscriptionsComponent } from './api/api-details/api-tab-subscriptions/api-tab-subscriptions.component'; import { SubscriptionsDetailsComponent } from './api/api-details/api-tab-subscriptions/subscriptions-details/subscriptions-details.component'; import { SubscriptionsTableComponent } from './api/api-details/api-tab-subscriptions/subscriptions-table/subscriptions-table.component'; @@ -72,7 +73,14 @@ export const routes: Routes = [ { path: 'documentation', component: ApiTabDocumentationComponent, - data: { breadcrumb: { skip: true } }, + data: { breadcrumb: { label: 'Documentation', disable: true } }, + children: [ + { + path: ':pageId', + component: ApiDocumentationComponent, + data: { breadcrumb: { alias: 'pageName' } }, + }, + ], }, { path: 'subscriptions', diff --git a/gravitee-apim-portal-webui-next/src/app/guides/guides.component.spec.ts b/gravitee-apim-portal-webui-next/src/app/guides/guides.component.spec.ts index ceb9ed3663b..f207caa9e36 100644 --- a/gravitee-apim-portal-webui-next/src/app/guides/guides.component.spec.ts +++ b/gravitee-apim-portal-webui-next/src/app/guides/guides.component.spec.ts @@ -108,8 +108,8 @@ describe('GuidesComponent', () => { const tree = await harnessLoader.getHarness(PageTreeHarness); const displayedItems = await tree.displayedItems(); - expect(displayedItems).toHaveLength(2); - expect(displayedItems).toEqual(['a valid folder', 'valid page']); + expect(displayedItems).toHaveLength(1); + expect(displayedItems).toEqual(['a valid folder']); }); it('should not show page with parentId defined but invalid', async () => { expectGetPages( @@ -137,7 +137,7 @@ describe('GuidesComponent', () => { const markdown = await harnessLoader.getHarnessOrNull(PageMarkdownHarness); expect(markdown).toBeTruthy(); }); - it('should show folder + inner page', async () => { + it('should not show folder + inner page', async () => { expectGetPages( fakePagesResponse({ data: [ @@ -151,8 +151,8 @@ describe('GuidesComponent', () => { const tree = await harnessLoader.getHarness(PageTreeHarness); const displayedItems = await tree.displayedItems(); - expect(displayedItems).toHaveLength(2); - expect(displayedItems).toEqual(['valid folder', 'valid page']); + expect(displayedItems).toHaveLength(1); + expect(displayedItems).toEqual(['valid folder']); const markdown = await harnessLoader.getHarnessOrNull(PageMarkdownHarness); expect(markdown).toBeTruthy(); diff --git a/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.html b/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.html new file mode 100644 index 00000000000..9561c648c41 --- /dev/null +++ b/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.html @@ -0,0 +1,28 @@ + + + +
+ +
diff --git a/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.scss b/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.scss new file mode 100644 index 00000000000..61a571a1faf --- /dev/null +++ b/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.scss @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.drawer-container { + &__content { + transition: all 300ms; + + &--hidden { + display: none; + } + } +} diff --git a/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.spec.ts b/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.spec.ts new file mode 100644 index 00000000000..7f01e94f193 --- /dev/null +++ b/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.spec.ts @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DrawerComponent } from './drawer.component'; + +describe('DrawerComponent', () => { + let component: DrawerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DrawerComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DrawerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.ts b/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.ts new file mode 100644 index 00000000000..d31ce0e23cd --- /dev/null +++ b/gravitee-apim-portal-webui-next/src/components/drawer/drawer.component.ts @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { NgClass } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; + +@Component({ + selector: 'app-drawer', + standalone: true, + imports: [NgClass, MatIcon, MatButtonModule], + templateUrl: './drawer.component.html', + styleUrl: './drawer.component.scss', +}) +export class DrawerComponent { + @Input({ required: true }) isOpen: boolean = true; + @Output() collapse: EventEmitter = new EventEmitter(); + + close(): void { + this.collapse.emit(!this.isOpen); + } +} diff --git a/gravitee-apim-portal-webui-next/src/components/page-tree/page-tree.component.spec.ts b/gravitee-apim-portal-webui-next/src/components/page-tree/page-tree.component.spec.ts index c64b47153a0..8e17a0bf0b9 100644 --- a/gravitee-apim-portal-webui-next/src/components/page-tree/page-tree.component.spec.ts +++ b/gravitee-apim-portal-webui-next/src/components/page-tree/page-tree.component.spec.ts @@ -60,14 +60,14 @@ describe('PageTreeComponent', () => { fixture.detectChanges(); }); - it('should be expanded by default', async () => { + it('should not be expanded by default', async () => { const fileTree = await harnessLoader.getHarness(MatTreeHarness); - expect(await fileTree.getNodes().then(nodes => nodes[0].isExpanded())).toEqual(true); + expect(await fileTree.getNodes().then(nodes => nodes[0].isExpanded())).toEqual(false); }); it('should create a node for each page', async () => { const fileTree = await harnessLoader.getHarness(MatTreeHarness); - expect(await fileTree.getNodes().then(nodes => nodes.length)).toEqual(5); + expect(await fileTree.getNodes().then(nodes => nodes.length)).toEqual(2); }); it('should select file when page has no children', async () => { @@ -79,7 +79,7 @@ describe('PageTreeComponent', () => { const clickableNode = await fileTree.getNodes().then(nodes => nodes[1].host()); await clickableNode.click(); - expect(emitted).toEqual('child'); + expect(emitted).toEqual('lone-node'); }); it('should not select file when a page has children', async () => { diff --git a/gravitee-apim-portal-webui-next/src/components/page-tree/page-tree.component.ts b/gravitee-apim-portal-webui-next/src/components/page-tree/page-tree.component.ts index c223fcbbba5..89c58bde798 100644 --- a/gravitee-apim-portal-webui-next/src/components/page-tree/page-tree.component.ts +++ b/gravitee-apim-portal-webui-next/src/components/page-tree/page-tree.component.ts @@ -75,7 +75,6 @@ export class PageTreeComponent implements OnInit, OnChanges { ngOnInit() { this.dataSource.data = this.pages; - this.treeControl.expandAll(); } ngOnChanges(changes: SimpleChanges): void {