mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Enhanced return details and search components with new features and improvements.
- ✨ **Feature**: Added InViewport directive for element visibility detection - ✨ **Feature**: Introduced new button for navigation in return details - 🛠️ **Refactor**: Improved scroll position restoration logic and removed deprecated files - 📚 **Docs**: Updated README with usage instructions for new directives Ref: #5034
This commit is contained in:
1
.github/review-instructions.md
vendored
1
.github/review-instructions.md
vendored
@@ -37,6 +37,7 @@ When conducting a code review, follow these steps to ensure a thorough and const
|
||||
## Additional Informations
|
||||
|
||||
- Treat missing tests and JSDocs as warnings
|
||||
- Tread missing unit test as warnings
|
||||
|
||||
### Review Template
|
||||
|
||||
|
||||
17
docs/templates/readme-library-template.md
vendored
Normal file
17
docs/templates/readme-library-template.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# [Library Name]
|
||||
|
||||
## Overview
|
||||
|
||||
<!-- Brief description of the library and its purpose -->
|
||||
|
||||
## Features
|
||||
|
||||
<!-- Key features or modules -->
|
||||
|
||||
## API
|
||||
|
||||
<!-- List Functions, Components, Services, etc. with their api and their purpose -->
|
||||
|
||||
## Usage
|
||||
|
||||
<!-- Sample code snippet or usage instructions -->
|
||||
@@ -1,5 +1,4 @@
|
||||
@let receipt = receiptResult().data;
|
||||
|
||||
<button
|
||||
class="fixed bottom-6 right-6"
|
||||
uiButton
|
||||
@@ -11,49 +10,64 @@
|
||||
Rückgabe starten
|
||||
</button>
|
||||
|
||||
@if (receipt) {
|
||||
<oms-feature-return-details-header [buyer]="receipt.buyer"></oms-feature-return-details-header>
|
||||
<div>
|
||||
<button
|
||||
uiButton
|
||||
color="tertiary"
|
||||
size="small"
|
||||
class="px-[0.875rem] py-1 min-w-0 bg-white gap-1"
|
||||
(click)="location.back()"
|
||||
>
|
||||
<ng-icon name="isaActionChevronLeft" size="1.5rem" class="-ml-2"></ng-icon>
|
||||
<span>zurück</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (showMore()) {
|
||||
<oms-feature-return-details-order-group-data
|
||||
[receipt]="receipt"
|
||||
></oms-feature-return-details-order-group-data>
|
||||
<button
|
||||
class="-ml-3"
|
||||
uiTextButton
|
||||
type="button"
|
||||
color="strong"
|
||||
size="small"
|
||||
(click)="showMore.set(false)"
|
||||
>
|
||||
<ng-icon name="isaActionMinus"></ng-icon>
|
||||
Weniger anzeigen
|
||||
</button>
|
||||
} @else {
|
||||
<oms-feature-return-details-data [receipt]="receipt"></oms-feature-return-details-data>
|
||||
<button
|
||||
class="-ml-3"
|
||||
uiTextButton
|
||||
type="button"
|
||||
color="strong"
|
||||
size="small"
|
||||
(click)="showMore.set(true)"
|
||||
>
|
||||
<ng-icon name="isaActionPlus"></ng-icon>
|
||||
Bestelldetails anzeigen
|
||||
</button>
|
||||
}
|
||||
<div></div>
|
||||
<oms-feature-return-details-order-group
|
||||
[items]="receiptItems()"
|
||||
[(selectedItems)]="selectedItems"
|
||||
></oms-feature-return-details-order-group>
|
||||
@for (item of receipt.items; track item.id; let last = $last) {
|
||||
<oms-feature-return-details-order-group-item
|
||||
class="border-b border-solid border-isa-neutral-300 last:border-none"
|
||||
[item]="item.data"
|
||||
(selectedChange)="selectItemById(item.id, $event)"
|
||||
[selected]="selectedItemIds().includes(item.id)"
|
||||
></oms-feature-return-details-order-group-item>
|
||||
}
|
||||
@if (receipt) {
|
||||
<div class="flex flex-col items-start justify-stretch gap-6 rounded-2xl bg-isa-white px-4 py-6">
|
||||
<oms-feature-return-details-header [buyer]="receipt.buyer"></oms-feature-return-details-header>
|
||||
|
||||
@if (showMore()) {
|
||||
<oms-feature-return-details-order-group-data
|
||||
[receipt]="receipt"
|
||||
></oms-feature-return-details-order-group-data>
|
||||
<button
|
||||
class="-ml-3"
|
||||
uiTextButton
|
||||
type="button"
|
||||
color="strong"
|
||||
size="small"
|
||||
(click)="showMore.set(false)"
|
||||
>
|
||||
<ng-icon name="isaActionMinus"></ng-icon>
|
||||
Weniger anzeigen
|
||||
</button>
|
||||
} @else {
|
||||
<oms-feature-return-details-data [receipt]="receipt"></oms-feature-return-details-data>
|
||||
<button
|
||||
class="-ml-3"
|
||||
uiTextButton
|
||||
type="button"
|
||||
color="strong"
|
||||
size="small"
|
||||
(click)="showMore.set(true)"
|
||||
>
|
||||
<ng-icon name="isaActionPlus"></ng-icon>
|
||||
Bestelldetails anzeigen
|
||||
</button>
|
||||
}
|
||||
<div></div>
|
||||
<oms-feature-return-details-order-group
|
||||
[items]="receiptItems()"
|
||||
[(selectedItems)]="selectedItems"
|
||||
></oms-feature-return-details-order-group>
|
||||
@for (item of receipt.items; track item.id; let last = $last) {
|
||||
<oms-feature-return-details-order-group-item
|
||||
class="border-b border-solid border-isa-neutral-300 last:border-none"
|
||||
[item]="item.data"
|
||||
(selectedChange)="selectItemById(item.id, $event)"
|
||||
[selected]="selectedItemIds().includes(item.id)"
|
||||
></oms-feature-return-details-order-group-item>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
:host {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
padding: 1rem 1.5rem;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1.5rem;
|
||||
align-self: stretch;
|
||||
|
||||
border-radius: 1rem;
|
||||
background: #fff;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionPlus, isaActionMinus } from '@isa/icons';
|
||||
import { isaActionPlus, isaActionMinus, isaActionChevronLeft } from '@isa/icons';
|
||||
import { ButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { ReceiptItem, ReturnDetailsStore, ReturnProcessStore } from '@isa/oms/data-access';
|
||||
import { injectActivatedProcessId } from '@isa/core/process';
|
||||
@@ -21,6 +21,7 @@ import { ReturnDetailsHeaderComponent } from './return-details-header/return-det
|
||||
import { ReturnDetailsOrderGroupComponent } from './return-details-order-group/return-details-order-group.component';
|
||||
import { ReturnDetailsOrderGroupItemComponent } from './return-details-order-group-item/return-details-order-group-item.component';
|
||||
import { ReturnDetailsOrderGroupDataComponent } from './return-details-order-group-data/return-details-order-group-data.component';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'oms-feature-return-details',
|
||||
@@ -37,7 +38,7 @@ import { ReturnDetailsOrderGroupDataComponent } from './return-details-order-gro
|
||||
ReturnDetailsOrderGroupItemComponent,
|
||||
ReturnDetailsOrderGroupDataComponent,
|
||||
],
|
||||
providers: [provideIcons({ isaActionPlus, isaActionMinus })],
|
||||
providers: [provideIcons({ isaActionPlus, isaActionMinus, isaActionChevronLeft })],
|
||||
})
|
||||
export class ReturnDetailsComponent {
|
||||
private processId = injectActivatedProcessId();
|
||||
@@ -46,6 +47,8 @@ export class ReturnDetailsComponent {
|
||||
|
||||
private _activatedRoute = inject(ActivatedRoute);
|
||||
|
||||
location = inject(Location);
|
||||
|
||||
params = toSignal(this._activatedRoute.params);
|
||||
|
||||
selectedItems = signal<ReceiptItem[]>([]);
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
|
||||
</div>
|
||||
} @else if (renderPageTrigger()) {
|
||||
<div (utilScrolledIntoViewport)="paging($event)"></div>
|
||||
<div (uiInViewport)="paging($event)"></div>
|
||||
}
|
||||
</div>
|
||||
} @else if (renderSearchLoader()) {
|
||||
|
||||
@@ -22,12 +22,9 @@ import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionSort } from '@isa/icons';
|
||||
import { ReceiptListItem, ReturnSearchStatus, ReturnSearchStore } from '@isa/oms/data-access';
|
||||
import { ReturnSearchResultItemComponent } from './return-search-result-item/return-search-result-item.component';
|
||||
import { BreakpointDirective } from '@isa/ui/layout';
|
||||
import { BreakpointDirective, InViewportDirective } from '@isa/ui/layout';
|
||||
import { CallbackResult, ListResponseArgs } from '@isa/common/result';
|
||||
import {
|
||||
injectRestoreScrollPosition,
|
||||
ScrolledIntoViewportDirective,
|
||||
} from '@isa/utils/scroll-position';
|
||||
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||
|
||||
@Component({
|
||||
selector: 'oms-feature-return-search-result',
|
||||
@@ -44,7 +41,7 @@ import {
|
||||
NgIconComponent,
|
||||
FilterMenuButtonComponent,
|
||||
BreakpointDirective,
|
||||
ScrolledIntoViewportDirective,
|
||||
InViewportDirective,
|
||||
],
|
||||
providers: [provideIcons({ isaActionSort })],
|
||||
})
|
||||
|
||||
@@ -33,6 +33,14 @@ The `uiBreakpoint` directive conditionally includes a template based on the view
|
||||
</ng-container>
|
||||
```
|
||||
|
||||
### InViewport Directive
|
||||
|
||||
Use this directive to emit an event whenever the host element enters or leaves the viewport.
|
||||
|
||||
```html
|
||||
<some-element uiInViewport (uiInViewport)="onInViewportChange($event)"> ... </some-element>
|
||||
```
|
||||
|
||||
## Breakpoints Table
|
||||
|
||||
| Breakpoint | CSS Media Query Selector |
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './lib/breakpoint.directive';
|
||||
export * from './lib/breakpoint';
|
||||
|
||||
export * from './lib/in-viewport.directive';
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
import { Directive, ElementRef, AfterViewInit, OnDestroy, output } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Directive that emits an event when its host element enters or leaves the viewport.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[utilScrolledIntoViewport]',
|
||||
selector: '[uiInViewport]',
|
||||
exportAs: 'uiInViewport',
|
||||
})
|
||||
export class ScrolledIntoViewportDirective implements AfterViewInit, OnDestroy {
|
||||
export class InViewportDirective implements AfterViewInit, OnDestroy {
|
||||
/**
|
||||
* Emits true when the element enters the viewport and false when it leaves.
|
||||
*/
|
||||
scrolledIntoViewport = output<boolean>({ alias: 'utilScrolledIntoViewport' });
|
||||
inViewport = output<boolean>({ alias: 'uiInViewport' });
|
||||
|
||||
/**
|
||||
* Emits when the element enters the viewport.
|
||||
*/
|
||||
enteredViewport = output<void>();
|
||||
|
||||
/**
|
||||
* Emits when the element leaves the viewport.
|
||||
*/
|
||||
leftViewport = output<void>();
|
||||
|
||||
private observer: IntersectionObserver | null = null;
|
||||
|
||||
@@ -18,9 +32,11 @@ export class ScrolledIntoViewportDirective implements AfterViewInit, OnDestroy {
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.scrolledIntoViewport.emit(true);
|
||||
this.inViewport.emit(true);
|
||||
this.enteredViewport.emit();
|
||||
} else {
|
||||
this.scrolledIntoViewport.emit(false);
|
||||
this.inViewport.emit(false);
|
||||
this.leftViewport.emit();
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1,7 +1,66 @@
|
||||
# utils-scroll-position
|
||||
# Scroll Position Library
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
## Overview
|
||||
|
||||
## Running unit tests
|
||||
Provides utilities for storing, restoring, and observing scroll position across navigation or component view changes.
|
||||
|
||||
Run `nx test utils-scroll-position` to execute the unit tests.
|
||||
## Features
|
||||
|
||||
- Store current scroll position in session storage.
|
||||
- Restore saved scroll position with an optional delay.
|
||||
- Observe when an element enters or leaves the viewport.
|
||||
|
||||
## Usage
|
||||
|
||||
### Storing Scroll Position
|
||||
|
||||
Call the function to save the current scroll position:
|
||||
|
||||
```typescript
|
||||
import { storeScrollPosition } from '@isa/utils/scroll-position';
|
||||
|
||||
storeScrollPosition();
|
||||
```
|
||||
|
||||
### Restoring Scroll Position
|
||||
|
||||
Inject the restore function and call it with an optional delay:
|
||||
|
||||
```typescript
|
||||
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||
|
||||
const restorePosition = injectRestoreScrollPosition();
|
||||
await restorePosition(200);
|
||||
```
|
||||
|
||||
### Automatic Restoration
|
||||
|
||||
Provide environment initializer in your app module to auto-save scroll positions:
|
||||
|
||||
```typescript
|
||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||
|
||||
provideScrollPositionRestoration();
|
||||
```
|
||||
|
||||
### Marking a Route for Auto Restoration
|
||||
|
||||
To enable automatic scroll restoration on a specific route, add the “scrollPositionRestoration” property in its data:
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: 'example',
|
||||
component: ExampleComponent,
|
||||
data: {
|
||||
scrollPositionRestoration: true
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
### Detecting Element Visibility
|
||||
|
||||
Apply the directive to a component template to emit true or false when entering or exiting the viewport:
|
||||
|
||||
```html
|
||||
<div utilScrolledIntoViewport (utilScrolledIntoViewport)="onVisibilityChange($event)">...</div>
|
||||
```
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './lib/scroll-position-restoration';
|
||||
export * from './lib/scrolled-into-viewport.directive';
|
||||
export * from './lib/inject-restore-scroll-position';
|
||||
export * from './lib/provide-scroll-position-restoration';
|
||||
export * from './lib/store-scroll-position';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ViewportScroller } from '@angular/common';
|
||||
import { inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { SessionStorageProvider } from '@isa/core/storage';
|
||||
|
||||
/**
|
||||
* Returns a function that restores the scroll position from session storage.
|
||||
*
|
||||
* The returned function waits for an optional delay to ensure the DOM is ready before scrolling.
|
||||
*
|
||||
* @returns A function accepting an optional delay (in milliseconds) and restoring scroll position.
|
||||
*/
|
||||
export function injectRestoreScrollPosition(): () => Promise<void> {
|
||||
const router = inject(Router);
|
||||
const viewportScroller = inject(ViewportScroller);
|
||||
const sessionStorage = inject(SessionStorageProvider);
|
||||
|
||||
return async (delay = 0) => {
|
||||
const url = router.url;
|
||||
const position = await sessionStorage.get(url);
|
||||
|
||||
if (position) {
|
||||
// wait for the next tick to ensure the DOM is ready
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
sessionStorage.clear(url);
|
||||
viewportScroller.scrollToPosition(position as [number, number]);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ViewportScroller } from '@angular/common';
|
||||
import { EnvironmentProviders, inject, provideEnvironmentInitializer } from '@angular/core';
|
||||
import { NavigationStart, Router } from '@angular/router';
|
||||
import { SessionStorageProvider } from '@isa/core/storage';
|
||||
import { getDeepestActivatedRoute } from './utils/get-deepest-activated-route';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
/**
|
||||
* Provides an environment initializer that restores scroll position upon navigation and page unload.
|
||||
*
|
||||
* Listens to router navigation events and the window's beforeunload event to store the current scroll position.
|
||||
*
|
||||
* @returns EnvironmentProviders for scroll position restoration.
|
||||
*/
|
||||
export function provideScrollPositionRestoration(): EnvironmentProviders {
|
||||
return provideEnvironmentInitializer(() => {
|
||||
const router = inject(Router);
|
||||
const viewportScroller = inject(ViewportScroller);
|
||||
const sessionStorage = inject(SessionStorageProvider);
|
||||
|
||||
function storeScrollPosition() {
|
||||
const url = router.url;
|
||||
const route = getDeepestActivatedRoute(router.routerState.root);
|
||||
|
||||
if (route.snapshot.data?.['scrollPositionRestoration']) {
|
||||
sessionStorage.set(url, viewportScroller.getScrollPosition());
|
||||
}
|
||||
}
|
||||
|
||||
if (window) {
|
||||
window.addEventListener('beforeunload', () => {
|
||||
storeScrollPosition();
|
||||
});
|
||||
}
|
||||
|
||||
router.events.pipe(takeUntilDestroyed()).subscribe((event) => {
|
||||
if (event instanceof NavigationStart) {
|
||||
storeScrollPosition();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { ViewportScroller } from '@angular/common';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { EnvironmentProviders, inject, provideEnvironmentInitializer } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
|
||||
import { SessionStorageProvider } from '@isa/core/storage';
|
||||
|
||||
// const route: Route = {
|
||||
// component: AnyComponent,
|
||||
// data: {
|
||||
// scrollPositionRestoration: true
|
||||
// }
|
||||
// }
|
||||
|
||||
const getDeepestActivatedRoute = (route: ActivatedRoute): ActivatedRoute => {
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
return route;
|
||||
};
|
||||
|
||||
export function provideScrollPositionRestoration(): EnvironmentProviders {
|
||||
return provideEnvironmentInitializer(() => {
|
||||
const router = inject(Router);
|
||||
const viewportScroller = inject(ViewportScroller);
|
||||
const sessionStorage = inject(SessionStorageProvider);
|
||||
|
||||
function storeScrollPosition() {
|
||||
const url = router.url;
|
||||
const route = getDeepestActivatedRoute(router.routerState.root);
|
||||
|
||||
if (route.snapshot.data?.['scrollPositionRestoration']) {
|
||||
sessionStorage.set(url, viewportScroller.getScrollPosition());
|
||||
}
|
||||
}
|
||||
|
||||
if (window) {
|
||||
window.addEventListener('beforeunload', () => {
|
||||
storeScrollPosition();
|
||||
});
|
||||
}
|
||||
|
||||
router.events.pipe(takeUntilDestroyed()).subscribe((event) => {
|
||||
if (event instanceof NavigationStart) {
|
||||
storeScrollPosition();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function storeScrollPosition() {
|
||||
const router = inject(Router);
|
||||
const viewportScroller = inject(ViewportScroller);
|
||||
const sessionStorage = inject(SessionStorageProvider);
|
||||
|
||||
const url = router.url;
|
||||
sessionStorage.set(url, viewportScroller.getScrollPosition());
|
||||
}
|
||||
|
||||
export function injectRestoreScrollPosition(): () => Promise<void> {
|
||||
const router = inject(Router);
|
||||
const viewportScroller = inject(ViewportScroller);
|
||||
const sessionStorage = inject(SessionStorageProvider);
|
||||
|
||||
return async (delay = 0) => {
|
||||
const url = router.url;
|
||||
const position = await sessionStorage.get(url);
|
||||
|
||||
if (position) {
|
||||
// wait for the next tick to ensure the DOM is ready
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
sessionStorage.clear(url);
|
||||
viewportScroller.scrollToPosition(position as [number, number]);
|
||||
}
|
||||
};
|
||||
}
|
||||
17
libs/utils/scroll-position/src/lib/store-scroll-position.ts
Normal file
17
libs/utils/scroll-position/src/lib/store-scroll-position.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ViewportScroller } from '@angular/common';
|
||||
import { inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { SessionStorageProvider } from '@isa/core/storage';
|
||||
|
||||
/**
|
||||
* Stores the current scroll position in session storage.
|
||||
* Uses the current router URL as the key.
|
||||
*/
|
||||
export async function storeScrollPosition() {
|
||||
const router = inject(Router);
|
||||
const viewportScroller = inject(ViewportScroller);
|
||||
const sessionStorage = inject(SessionStorageProvider);
|
||||
|
||||
const url = router.url;
|
||||
sessionStorage.set(url, viewportScroller.getScrollPosition());
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Recursively retrieves the deepest activated route.
|
||||
*
|
||||
* @param route - The starting ActivatedRoute instance.
|
||||
* @returns The deepest ActivatedRoute found.
|
||||
*/
|
||||
export const getDeepestActivatedRoute = (route: ActivatedRoute): ActivatedRoute => {
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
return route;
|
||||
};
|
||||
Reference in New Issue
Block a user