Skip to content

Commit

Permalink
feat: add documentation pages collapse and display in the breadcrumb (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
naouf4l-grav authored Jan 28, 2025
1 parent 8cb0631 commit ccf3363
Show file tree
Hide file tree
Showing 14 changed files with 297 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,18 @@
-->
@if (pages.length) {
<div class="api-tab-documentation__side-bar">
<app-page-tree class="api-tab-documentation__side-bar__tree" [activePage]="page" [pages]="pageNodes" (openFile)="showPage($event)" />
</div>

<div class="api-tab-documentation__page-content">
@if (selectedPageData$ | async; as selectedPageData) {
@if (selectedPageData.result) {
<app-page
class="api-tab-documentation__page-content__container"
[page]="selectedPageData.result"
[pages]="pages"
[apiId]="apiId"></app-page>
}
@if (selectedPageData.error) {
<div i18n="@@apiDocumentationError">An error has occurred.</div>
}
} @else {
<app-loader />
}
<div [ngClass]="isSidebarExpanded() ? 'api-tab-documentation__side-bar' : 'api-tab-documentation__side-bar--hidden'">
<app-drawer [isOpen]="isSidebarExpanded()" (collapse)="isSidebarExpanded.set($event)">
<app-page-tree
class="api-tab-documentation__side-bar__tree"
[activePage]="pageId()"
[pages]="pageNodes"
(openFile)="showPage($event)" />
</app-drawer>
</div>
@if (apiId && pageId(); as pageId) {
<app-api-documentation [apiId]="apiId" [pages]="pages" [pageId]="pageId"></app-api-documentation>
}
} @else {
<div i18n="@@apiDocumentationEmpty" class="api-tab-documentation__empty">Documentation for this API is missing.</div>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<SelectedPageData> = of();
pageId = signal<string | undefined>(undefined);
isSidebarExpanded: WritableSignal<boolean> = 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<SelectedPageData> {
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 });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!--
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.
-->
<div class="page-content">
@if (selectedPageData$ | async; as selectedPageData) {
@if (selectedPageData.result) {
<app-page class="page-content__container" [page]="selectedPageData.result" [pages]="pages()" [apiId]="apiId()"></app-page>
}
@if (selectedPageData.error) {
<div i18n="@@apiDocumentationError">An error has occurred.</div>
}
} @else {
<app-loader />
}
</div>
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<string> = input.required<string>();
apiId: InputSignal<string> = input.required<string>();
pages: InputSignal<Page[]> = input.required<Page[]>();
selectedPageData$: Observable<SelectedPageData> = 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<SelectedPageData> {
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 });
}),
);
}
}
10 changes: 9 additions & 1 deletion gravitee-apim-portal-webui-next/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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: [
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!--
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.
-->
<button mat-icon-button class="btn_container" (click)="close()">
<div class="btn_container__btn">
<mat-icon>
{{ isOpen ? 'keyboard_double_arrow_left' : 'menu' }}
</mat-icon>
</div>
</button>

<div [ngClass]="isOpen ? 'drawer-container__content' : 'drawer-container__content--hidden'">
<ng-content></ng-content>
</div>
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading

0 comments on commit ccf3363

Please sign in to comment.