Skip to content

Commit b241ec9

Browse files
committed
feat(console): add resizing of navigation items list
1 parent 57550bb commit b241ec9

File tree

4 files changed

+243
-5
lines changed

4 files changed

+243
-5
lines changed

gravitee-apim-console-webui/src/portal/navigation-items/portal-navigation-items.component.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<portal-header title="Manage your navigation" subtitle="Manage and edit your Developer Portal pages with Markdown" />
1919

2020
<main class="page-layout">
21-
<section class="panel sections-panel">
21+
<section class="panel sections-panel" [style.width.px]="panelWidth()">
2222
<header class="panel-header sections-panel__header">
2323
<h3>Navigation items</h3>
2424
<div class="panel-header__actions" *gioPermission="{ anyOf: ['environment-documentation-c'] }">
@@ -51,6 +51,11 @@ <h3>Navigation items</h3>
5151
}
5252
</section>
5353

54+
<div class="resize-handle-container" (mousedown)="onResizeStart($event)">
55+
<hr class="resize-handle" aria-label="Resize navigation panel" aria-orientation="vertical" />
56+
<mat-icon svgIcon="gio:drag-indicator"></mat-icon>
57+
</div>
58+
5459
<section class="panel editor-panel">
5560
<header class="panel-header editor-panel__header">
5661
@if (selectedNavigationItem()) {

gravitee-apim-console-webui/src/portal/navigation-items/portal-navigation-items.component.scss

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ $typography: map.get(gio.$mat-theme, typography);
3030

3131
.page-layout {
3232
display: grid;
33-
grid-template-columns: 2fr 7fr;
34-
gap: 2px;
33+
grid-template-columns: auto auto 1fr;
34+
gap: 0;
3535
flex-grow: 1;
3636
min-height: 0;
3737
}
@@ -77,6 +77,11 @@ $typography: map.get(gio.$mat-theme, typography);
7777

7878
.sections-panel {
7979
padding: 16px;
80+
width: 350px;
81+
min-width: 280px;
82+
max-width: 600px;
83+
resize: none;
84+
overflow: hidden;
8085

8186
&__header {
8287
padding-bottom: 16px;
@@ -90,7 +95,8 @@ $typography: map.get(gio.$mat-theme, typography);
9095
height: 100%;
9196
overflow-y: scroll;
9297
overflow-x: hidden;
93-
width: 350px;
98+
width: 100%;
99+
94100
portal-tree-component {
95101
height: 100%;
96102
}
@@ -126,3 +132,40 @@ $typography: map.get(gio.$mat-theme, typography);
126132
padding: 16px;
127133
height: 100%;
128134
}
135+
136+
.resize-handle-container {
137+
position: relative;
138+
width: 2px;
139+
flex-shrink: 0;
140+
display: flex;
141+
align-items: center;
142+
justify-content: center;
143+
144+
.resize-handle {
145+
width: 2px;
146+
height: 100%;
147+
margin: 0;
148+
padding: 0;
149+
border: none;
150+
background-color: mat.m2-get-color-from-palette(gio.$mat-space-palette, lighter80);
151+
cursor: col-resize;
152+
transition: background-color 0.2s ease;
153+
154+
&:hover {
155+
background-color: mat.m2-get-color-from-palette(gio.$mat-space-palette, lighter70);
156+
}
157+
}
158+
159+
mat-icon {
160+
position: absolute;
161+
color: mat.m2-get-color-from-palette(gio.$mat-space-palette, lighter60);
162+
width: 20px;
163+
height: 20px;
164+
background-color: white;
165+
cursor: grab;
166+
167+
&:active {
168+
cursor: grabbing;
169+
}
170+
}
171+
}

gravitee-apim-console-webui/src/portal/navigation-items/portal-navigation-items.component.spec.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,161 @@ describe('PortalNavigationItemsComponent', () => {
287287
});
288288
});
289289

290+
describe('resizing the sections panel', () => {
291+
beforeEach(async () => {
292+
await expectGetNavigationItems(fakePortalNavigationItemsResponse({ items: [] }));
293+
});
294+
295+
it('should have default panel width of 350px', () => {
296+
const component = fixture.componentInstance;
297+
expect(component.panelWidth()).toBe(350);
298+
});
299+
300+
it('should increase panel width when dragging right', () => {
301+
const component = fixture.componentInstance;
302+
const resizeHandle = fixture.nativeElement.querySelector('.resize-handle');
303+
304+
const initialWidth = component.panelWidth();
305+
expect(initialWidth).toBe(350);
306+
307+
// Simulate mousedown
308+
const mousedownEvent = new MouseEvent('mousedown', {
309+
clientX: 100,
310+
bubbles: true,
311+
cancelable: true,
312+
});
313+
resizeHandle.dispatchEvent(mousedownEvent);
314+
fixture.detectChanges();
315+
316+
// Simulate mousemove (dragging right by 50px)
317+
const mousemoveEvent = new MouseEvent('mousemove', {
318+
clientX: 150,
319+
bubbles: true,
320+
cancelable: true,
321+
});
322+
document.dispatchEvent(mousemoveEvent);
323+
fixture.detectChanges();
324+
325+
expect(component.panelWidth()).toBe(400);
326+
327+
// Simulate mouseup
328+
const mouseupEvent = new MouseEvent('mouseup', {
329+
bubbles: true,
330+
cancelable: true,
331+
});
332+
document.dispatchEvent(mouseupEvent);
333+
fixture.detectChanges();
334+
});
335+
336+
it('should decrease panel width when dragging left', () => {
337+
const component = fixture.componentInstance;
338+
const resizeHandle = fixture.nativeElement.querySelector('.resize-handle');
339+
340+
const initialWidth = component.panelWidth();
341+
expect(initialWidth).toBe(350);
342+
343+
// Simulate mousedown
344+
const mousedownEvent = new MouseEvent('mousedown', {
345+
clientX: 100,
346+
bubbles: true,
347+
cancelable: true,
348+
});
349+
resizeHandle.dispatchEvent(mousedownEvent);
350+
fixture.detectChanges();
351+
352+
// Simulate mousemove (dragging left by 50px)
353+
const mousemoveEvent = new MouseEvent('mousemove', {
354+
clientX: 50,
355+
bubbles: true,
356+
cancelable: true,
357+
});
358+
document.dispatchEvent(mousemoveEvent);
359+
fixture.detectChanges();
360+
361+
expect(component.panelWidth()).toBe(300);
362+
363+
// Simulate mouseup
364+
const mouseupEvent = new MouseEvent('mouseup', {
365+
bubbles: true,
366+
cancelable: true,
367+
});
368+
document.dispatchEvent(mouseupEvent);
369+
fixture.detectChanges();
370+
});
371+
372+
it('should constrain panel width to minimum of 280px', () => {
373+
const component = fixture.componentInstance;
374+
const resizeHandle = fixture.nativeElement.querySelector('.resize-handle');
375+
376+
const initialWidth = component.panelWidth();
377+
expect(initialWidth).toBe(350);
378+
379+
// Simulate mousedown
380+
const mousedownEvent = new MouseEvent('mousedown', {
381+
clientX: 100,
382+
bubbles: true,
383+
cancelable: true,
384+
});
385+
resizeHandle.dispatchEvent(mousedownEvent);
386+
fixture.detectChanges();
387+
388+
// Simulate mousemove (dragging left by 200px, which would go below minimum)
389+
const mousemoveEvent = new MouseEvent('mousemove', {
390+
clientX: -100,
391+
bubbles: true,
392+
cancelable: true,
393+
});
394+
document.dispatchEvent(mousemoveEvent);
395+
fixture.detectChanges();
396+
397+
expect(component.panelWidth()).toBe(280);
398+
399+
// Simulate mouseup
400+
const mouseupEvent = new MouseEvent('mouseup', {
401+
bubbles: true,
402+
cancelable: true,
403+
});
404+
document.dispatchEvent(mouseupEvent);
405+
fixture.detectChanges();
406+
});
407+
408+
it('should constrain panel width to maximum of 600px', () => {
409+
const component = fixture.componentInstance;
410+
const resizeHandle = fixture.nativeElement.querySelector('.resize-handle');
411+
412+
const initialWidth = component.panelWidth();
413+
expect(initialWidth).toBe(350);
414+
415+
// Simulate mousedown
416+
const mousedownEvent = new MouseEvent('mousedown', {
417+
clientX: 100,
418+
bubbles: true,
419+
cancelable: true,
420+
});
421+
resizeHandle.dispatchEvent(mousedownEvent);
422+
fixture.detectChanges();
423+
424+
// Simulate mousemove (dragging right by 300px, which would exceed maximum)
425+
const mousemoveEvent = new MouseEvent('mousemove', {
426+
clientX: 400,
427+
bubbles: true,
428+
cancelable: true,
429+
});
430+
document.dispatchEvent(mousemoveEvent);
431+
fixture.detectChanges();
432+
433+
expect(component.panelWidth()).toBe(600);
434+
435+
// Simulate mouseup
436+
const mouseupEvent = new MouseEvent('mouseup', {
437+
bubbles: true,
438+
cancelable: true,
439+
});
440+
document.dispatchEvent(mouseupEvent);
441+
fixture.detectChanges();
442+
});
443+
});
444+
290445
async function expectGetNavigationItems(response: PortalNavigationItemsResponse = fakePortalNavigationItemsResponse()) {
291446
httpTestingController
292447
.expectOne({ method: 'GET', url: `${CONSTANTS_TESTING.env.v2BaseURL}/portal-navigation-items?area=TOP_NAVBAR` })

gravitee-apim-console-webui/src/portal/navigation-items/portal-navigation-items.component.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import { GraviteeMarkdownEditorModule } from '@gravitee/gravitee-markdown';
1717

1818
import { GIO_DIALOG_WIDTH, GioCardEmptyStateModule } from '@gravitee/ui-particles-angular';
19-
import { Component, computed, DestroyRef, inject, Signal } from '@angular/core';
19+
import { Component, computed, DestroyRef, inject, NgZone, Signal, signal } from '@angular/core';
2020
import { MatButton } from '@angular/material/button';
2121
import { FormControl, ReactiveFormsModule } from '@angular/forms';
2222
import { toSignal, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
@@ -117,6 +117,12 @@ export class PortalNavigationItemsComponent {
117117
return this.mapSelectedNavItemToNode(navId, menuLinks);
118118
});
119119

120+
// --- Resize Configuration ---
121+
private readonly ngZone = inject(NgZone);
122+
private readonly MIN_PANEL_WIDTH = 280;
123+
private readonly MAX_PANEL_WIDTH = 600;
124+
panelWidth = signal(350);
125+
120126
constructor(
121127
private readonly snackBarService: SnackBarService,
122128
private readonly router: Router,
@@ -136,6 +142,35 @@ export class PortalNavigationItemsComponent {
136142
this.manageSection(sectionType, 'create', 'TOP_NAVBAR');
137143
}
138144

145+
onResizeStart(event: MouseEvent): void {
146+
event.preventDefault();
147+
148+
const startX = event.clientX;
149+
const startWidth = this.panelWidth();
150+
151+
document.body.style.cursor = 'col-resize';
152+
document.body.style.userSelect = 'none';
153+
154+
this.ngZone.runOutsideAngular(() => {
155+
const onMove = (e: MouseEvent) => {
156+
const deltaX = e.clientX - startX;
157+
const newWidth = Math.max(this.MIN_PANEL_WIDTH, Math.min(this.MAX_PANEL_WIDTH, startWidth + deltaX));
158+
159+
this.ngZone.run(() => this.panelWidth.set(newWidth));
160+
};
161+
162+
const onUp = () => {
163+
document.removeEventListener('mousemove', onMove);
164+
document.removeEventListener('mouseup', onUp);
165+
document.body.style.cursor = '';
166+
document.body.style.userSelect = '';
167+
};
168+
169+
document.addEventListener('mousemove', onMove);
170+
document.addEventListener('mouseup', onUp);
171+
});
172+
}
173+
139174
private setupPageContentSubscription(): void {
140175
toObservable(this.selectedNavigationItem)
141176
.pipe(

0 commit comments

Comments
 (0)