Merged PR 260: #957 Show error message for failed submit requests on edit page / + Restore Scroll Position

Related work items: #957
This commit is contained in:
Sebastian Neumair
2020-09-07 11:28:02 +00:00
committed by Lorenz Hilpert
18 changed files with 322 additions and 39 deletions

View File

@@ -0,0 +1,9 @@
export interface ActionResult<T> {
error?: boolean;
errorReasons?: { [key: string]: string };
http?: {
code: number;
};
message?: string;
result?: T;
}

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { SessionStorageService } from './session-storage.service';
export interface ScrollPosition {
top?: number;
left?: number;
}
@Injectable({ providedIn: 'root' })
export class ScrollPositionService {
constructor(private storageService: SessionStorageService) {}
getPosition(key: string): ScrollPosition {
return this.storageService.get(key);
}
setPosition(key: string, position: ScrollPosition): void {
this.storageService.set(key, position);
}
deletePosition(key: string): void {
this.storageService.delete(key);
}
}

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { StorageService } from './storage.service';
@Injectable({ providedIn: 'root' })
export class SessionStorageService extends StorageService {
constructor() {
super();
}
get<T>(key: string): T {
let result: T;
try {
result = JSON.parse(sessionStorage.getItem(key));
} catch (error) {
console.error(`Error getting ${key}`);
}
return result;
}
set<T>(key: string, data: T): void {
try {
sessionStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error(`Error saving ${key}`);
}
}
delete(key: string): void {
try {
sessionStorage.removeItem(key);
} catch (error) {
console.error(`Error deleting ${key}`);
}
}
}

View File

@@ -0,0 +1,7 @@
export abstract class StorageService {
abstract get<T>(key: string): T;
abstract set<T>(key: string, data: T): void;
abstract delete(key: string): void;
}

View File

@@ -25,17 +25,31 @@ export class ProcessSelectors {
}
@Selector([AppState, ProcessState])
static getCurrentProcess(state: AppStateModel, processState: ProcessStateModel): Process {
static getCurrentProcess(
state: AppStateModel,
processState: ProcessStateModel
): Process {
return processState.processes[state.currentProcesssId];
}
@Selector([AppState, ProcessState])
static getCurrentProcessId(
state: AppStateModel,
processState: ProcessStateModel
): number {
return processState.processes[state.currentProcesssId].id;
}
@Selector([ProcessState])
static getRecentProducts(state: ProcessStateModel) {
return state.recentArticles;
}
@Selector([AppState, ProcessState])
static getSelectedProduct(state: AppStateModel, processState: ProcessStateModel) {
static getSelectedProduct(
state: AppStateModel,
processState: ProcessStateModel
) {
const currentProcess = processState.processes[state.currentProcesssId];
if (currentProcess) {
return currentProcess.selectedItem;
@@ -43,7 +57,10 @@ export class ProcessSelectors {
}
@Selector([AppState, ProcessState])
static getCurrentProcessCustomerNotificationFlag(state: AppStateModel, processState: ProcessStateModel) {
static getCurrentProcessCustomerNotificationFlag(
state: AppStateModel,
processState: ProcessStateModel
) {
const currentProcess = processState.processes[state.currentProcesssId];
if (currentProcess) {
return currentProcess.customerNotificationFlag;
@@ -51,7 +68,10 @@ export class ProcessSelectors {
}
@Selector([AppState, ProcessState])
static getScrollPositionForProduct(state: AppStateModel, processState: ProcessStateModel) {
static getScrollPositionForProduct(
state: AppStateModel,
processState: ProcessStateModel
) {
const currentProcess = processState.processes[state.currentProcesssId];
if (currentProcess) {
return currentProcess.productScrollTo;
@@ -59,7 +79,10 @@ export class ProcessSelectors {
}
@Selector([AppState, ProcessState])
static getActiveUserId(state: AppStateModel, processState: ProcessStateModel) {
static getActiveUserId(
state: AppStateModel,
processState: ProcessStateModel
) {
const currentProcess = processState.processes[state.currentProcesssId];
if (currentProcess) {
return currentProcess.activeCustomer;
@@ -67,15 +90,25 @@ export class ProcessSelectors {
}
@Selector([AppState, ProcessState])
static getDetailsUserId(state: AppStateModel, processState: ProcessStateModel) {
static getDetailsUserId(
state: AppStateModel,
processState: ProcessStateModel
) {
const currentProcess = processState.processes[state.currentProcesssId];
if (currentProcess && (!currentProcess.onlineCustomerCreationError || currentProcess.onlineCustomerCreationError.error === false)) {
if (
currentProcess &&
(!currentProcess.onlineCustomerCreationError ||
currentProcess.onlineCustomerCreationError.error === false)
) {
return currentProcess.detailsCustomer;
}
}
@Selector([AppState, ProcessState])
static getUniqueIdentifier(state: AppStateModel, processState: ProcessStateModel) {
static getUniqueIdentifier(
state: AppStateModel,
processState: ProcessStateModel
) {
const currentProcess = processState.processes[state.currentProcesssId];
if (currentProcess) {
return currentProcess.uniqueIdentifier;
@@ -83,7 +116,10 @@ export class ProcessSelectors {
}
@Selector([AppState, ProcessState])
static getCustomerSearch(state: AppStateModel, processState: ProcessStateModel) {
static getCustomerSearch(
state: AppStateModel,
processState: ProcessStateModel
) {
return processState.processes[state.currentProcesssId].customerSearch;
}
@@ -98,27 +134,46 @@ export class ProcessSelectors {
}
@Selector([AppState, ProcessState])
static getFinishedOrders(state: AppStateModel, processState: ProcessStateModel) {
static getFinishedOrders(
state: AppStateModel,
processState: ProcessStateModel
) {
return processState.processes[state.currentProcesssId].finishedOrder;
}
@Selector([AppState, ProcessState])
static getArticleSearchErrorStatus(state: AppStateModel, processState: ProcessStateModel) {
return processState.processes[state.currentProcesssId].articleSearchErrorStatus;
static getArticleSearchErrorStatus(
state: AppStateModel,
processState: ProcessStateModel
) {
return processState.processes[state.currentProcesssId]
.articleSearchErrorStatus;
}
@Selector([AppState, ProcessState])
static getPreviousRoute(state: AppStateModel, processState: ProcessStateModel) {
static getPreviousRoute(
state: AppStateModel,
processState: ProcessStateModel
) {
return processState.processes[state.currentProcesssId].previousRoute;
}
@Selector([AppState, ProcessState])
static getCurrentRoute(state: AppStateModel, processState: ProcessStateModel) {
return getProperty(processState, `processes.${state.currentProcesssId}.currentRoute`);
static getCurrentRoute(
state: AppStateModel,
processState: ProcessStateModel
) {
return getProperty(
processState,
`processes.${state.currentProcesssId}.currentRoute`
);
}
@Selector([AppState, ProcessState])
static getProductFilters(state: AppStateModel, processState: ProcessStateModel) {
static getProductFilters(
state: AppStateModel,
processState: ProcessStateModel
) {
return processState.processes[state.currentProcesssId].productSearchFilters;
}

View File

@@ -5,7 +5,7 @@ import {
ChangeDetectorRef,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable, from } from 'rxjs';
import { Observable } from 'rxjs';
import {
filter,
map,
@@ -26,6 +26,7 @@ import {
import { DatePipe } from '@angular/common';
import { ShelfNavigationService } from '../../shared/services';
import { OrderItemProcessingStatusValue } from '@swagger/oms/lib';
import { ErrorService } from 'apps/sales/src/app/core/error/component/error.service';
@Component({
selector: 'app-shelf-edit-compartment',
@@ -49,6 +50,7 @@ export class ShelfEditCompartmentComponent implements OnInit {
private activatedRoute: ActivatedRoute,
protected formService: ShelfEditFormService,
private shelfNavigationService: ShelfNavigationService,
private errorService: ErrorService,
private cdr: ChangeDetectorRef
) {}
@@ -99,8 +101,12 @@ export class ShelfEditCompartmentComponent implements OnInit {
processingStatus
);
let newValues;
if (submitResult) {
let newValues: {
orderNumber?: string;
compartmentCode?: string;
processingStatus?: number;
};
if (submitResult.result) {
newValues = {
compartmentCode:
this.form.get('compartmentCode').value || compartmentCode,
@@ -113,6 +119,14 @@ export class ShelfEditCompartmentComponent implements OnInit {
processingStatus,
});
}
if (!submitResult.result) {
this.showSubmitError({
message: submitResult.message,
code: submitResult.http ? submitResult.http.code : 500,
invalidProperties: submitResult.errorReasons,
});
}
}
onAbortEdit() {
@@ -138,4 +152,15 @@ export class ShelfEditCompartmentComponent implements OnInit {
this.customerName = this.formService.getCustomerName(compartmentCode);
this.cdr.detectChanges();
}
private showSubmitError(error: {
message: string;
code: number;
invalidProperties: { [key: string]: string };
}) {
const invalidProperties =
Object.values(error.invalidProperties).join(', ') || undefined;
this.errorService.addErrors(error.code, error.message, invalidProperties);
}
}

View File

@@ -25,6 +25,7 @@ import {
ProcessingStatusPipe,
PickUpDateOptionsToDisplayValuesPipe,
} from '../../pipes';
import { ErrorService } from 'apps/sales/src/app/core/error/component/error.service';
@Component({
selector: 'app-shelf-edit-order',
templateUrl: './shelf-edit-order.component.html',
@@ -47,6 +48,7 @@ export class ShelfEditOrderComponent implements OnInit {
private activatedRoute: ActivatedRoute,
protected formService: ShelfEditFormService,
private shelfNavigationService: ShelfNavigationService,
private errorService: ErrorService,
private cdr: ChangeDetectorRef
) {}
@@ -97,13 +99,21 @@ export class ShelfEditOrderComponent implements OnInit {
processingStatus
);
if (submitResult) {
if (submitResult.result) {
this.shelfNavigationService.navigateBackToDetails({
orderNumber,
processingStatus:
this.form.get('processingStatus').value || processingStatus,
});
}
if (!submitResult.result) {
this.showSubmitError({
message: submitResult.message,
code: submitResult.http ? submitResult.http.code : 500,
invalidProperties: submitResult.errorReasons,
});
}
}
onAbortEdit() {
@@ -126,10 +136,20 @@ export class ShelfEditOrderComponent implements OnInit {
orderNumber,
processingStatus
);
console.log({ form: this.form });
this.items = this.formService.getItemsForm(orderNumber);
this.customerName = this.formService.getCustomerName(orderNumber);
this.cdr.detectChanges();
}
private showSubmitError(error: {
message: string;
code: number;
invalidProperties: { [key: string]: string };
}) {
const invalidProperties =
Object.values(error.invalidProperties).join(', ') || undefined;
this.errorService.addErrors(error.code, error.message, invalidProperties);
}
}

View File

@@ -0,0 +1,6 @@
// start:ng42.barrel
export * from './order-details-prefix';
export * from './shelf-search-results.component';
export * from './shelf-search-results.module';
// end:ng42.barrel

View File

@@ -0,0 +1 @@
export const ORDER_DETAILS_PREFIX = 'order-details_';

View File

@@ -1,4 +1,9 @@
<div class="result-container" #scroll>
<div
class="result-container"
#scroll
rememberScrollPosition
[key]="scrollStorageKey + (currentProcessId$ | async)"
>
<app-search-result-group
class="isa-mb-10"
*ngFor="let group of grouped$ | async; let i = index"

View File

@@ -1,4 +1,4 @@
import { Subject, fromEvent, combineLatest } from 'rxjs';
import { Subject, fromEvent, combineLatest, Observable } from 'rxjs';
import {
Component,
@@ -13,6 +13,9 @@ import { first, takeUntil, map } from 'rxjs/operators';
import { groupBy } from 'apps/sales/src/app/utils';
import { OrderItemListItemDTO } from '@swagger/oms';
import { ShelfNavigationService } from '../../shared/services';
import { Store, Select } from '@ngxs/store';
import { ProcessSelectors } from 'apps/sales/src/app/core/store/selectors/process.selectors';
import { ORDER_DETAILS_PREFIX } from './order-details-prefix';
@Component({
selector: 'app-shelf-search-results',
@@ -24,6 +27,10 @@ export class ShelfSearchResultsComponent implements OnInit, OnDestroy {
@ViewChild('scroll', { static: true })
scrollContainer: ElementRef;
@Select(ProcessSelectors.getCurrentProcessId) currentProcessId$: Observable<
number
>;
destroy$ = new Subject();
grouped$ = this.searchStateFacade.result$.pipe(
@@ -32,9 +39,12 @@ export class ShelfSearchResultsComponent implements OnInit, OnDestroy {
fetching$ = this.searchStateFacade.fetching$;
scrollStorageKey = ORDER_DETAILS_PREFIX;
constructor(
private searchStateFacade: SearchStateFacade,
private shelfNavigationService: ShelfNavigationService
private shelfNavigationService: ShelfNavigationService,
private store: Store
) {}
ngOnInit() {

View File

@@ -1,12 +1,12 @@
import { NgModule } from '@angular/core';
import { ShelfSearchResultsComponent } from './shelf-search-results.component';
import { CommonModule } from '@angular/common';
import { LoadingModule } from '@libs/ui';
import { SearchResultGroupModule } from '../../components/search-result-group';
import { SharedModule } from 'apps/sales/src/app/shared/shared.module';
@NgModule({
imports: [CommonModule, LoadingModule, SearchResultGroupModule],
imports: [CommonModule, LoadingModule, SearchResultGroupModule, SharedModule],
exports: [ShelfSearchResultsComponent],
declarations: [ShelfSearchResultsComponent],
providers: [],

View File

@@ -40,6 +40,10 @@ import { SearchStateFacade } from '@shelf-store';
import { CollectingShelfScannerScanditComponent } from 'shared/public_api';
import { AutocompleteOptions } from '../../../defs';
import { isNullOrUndefined } from 'util';
import { ProcessSelectors } from 'apps/sales/src/app/core/store/selectors/process.selectors';
import { Select } from '@ngxs/store';
import { ScrollPositionService } from 'apps/sales/src/app/core/services/scroll-position.service';
import { ORDER_DETAILS_PREFIX } from '../../shelf-search-results/order-details-prefix';
@Component({
selector: 'app-shelf-search-input',
@@ -61,6 +65,10 @@ export class ShelfSearchInputComponent
@ViewChild(CollectingShelfScannerScanditComponent, { static: false })
scanner: CollectingShelfScannerScanditComponent;
@Select(ProcessSelectors.getCurrentProcessId) currentProcessId$: Observable<
number
>;
destroy$ = new Subject();
isFetching$: Observable<boolean>;
@@ -84,7 +92,8 @@ export class ShelfSearchInputComponent
private shelfSearchService: ShelfSearchFacadeService,
private searchStateFacade: SearchStateFacade,
private shelfNavigationService: ShelfNavigationService,
private filterService: ShelfFilterService
private filterService: ShelfFilterService,
private scrollPositionService: ScrollPositionService
) {}
ngOnInit() {
@@ -275,16 +284,20 @@ export class ShelfSearchInputComponent
withLatestFrom(
this.searchStateFacade.hits$,
this.searchStateFacade.input$,
this.searchStateFacade.result$
this.searchStateFacade.result$,
this.currentProcessId$
),
take(1)
)
.subscribe(([_, numberOfHits, searchQuery, result]) => {
.subscribe(([_, numberOfHits, searchQuery, result, processId]) => {
this.shelfNavigationService.updateResultPageBreadcrumb({
numberOfHits,
searchQuery,
});
const key = ORDER_DETAILS_PREFIX + processId;
this.resetScrollPosition(key);
if (this.shouldNavigateToResultList(numberOfHits)) {
this.resetSessionStorage();
@@ -318,6 +331,10 @@ export class ShelfSearchInputComponent
.subscribe(() => this.searchStateFacade.clearError());
}
private resetScrollPosition(key: string) {
this.scrollPositionService.deletePosition(key);
}
private setUpInputFocus() {
this.filterService.overlayClosed$
.pipe(takeUntil(this.destroy$))

View File

@@ -1,4 +1,4 @@
import { Injectable, ɵbypassSanitizationTrustResourceUrl } from '@angular/core';
import { Injectable } from '@angular/core';
import {
FormBuilder,
FormGroup,
@@ -6,7 +6,7 @@ import {
FormArray,
AbstractControl,
} from '@angular/forms';
import { Observable, of, from } from 'rxjs';
import { Observable } from 'rxjs';
import {
OrderItemListItemDTO,
VATDTO,
@@ -26,6 +26,7 @@ import { ProcessingStatusNameMap } from '../constants';
import { Select } from '@ngxs/store';
import { VatState } from '../../../core/store/state/vat.state';
import { toDecimalPlaces } from '../../../core/utils/price.util';
import { ActionResult } from '../../../core/models/action-result.model';
@Injectable({ providedIn: 'root' })
export class ShelfEditFormService {
@@ -307,11 +308,9 @@ export class ShelfEditFormService {
async submit(
form: FormGroup,
processingStatus: OrderItemProcessingStatusValue
): Promise<boolean> {
): Promise<ActionResult<boolean>> {
const formData = this.getDataFromGeneralForm(form);
console.log({ formData });
const orderItemsToPatch = this.prepareOrderItems(form);
const orderItemSubsetsToPatch = this.prepareOrderItemSubsets(form);
@@ -338,10 +337,21 @@ export class ShelfEditFormService {
}
} catch (error) {
console.log('%cSubmit Error: ', 'color: red; font-weight: bold', error);
return false;
return {
error: true,
errorReasons: error.invalidProperties || {},
http: {
code: error.status || 500,
},
message: 'Speichern fehlgeschlagen',
result: false,
};
}
return true;
return {
error: false,
result: true,
};
}
private prepareOrderItems(form: FormGroup) {
@@ -371,12 +381,13 @@ export class ShelfEditFormService {
) as FormArray).controls.map((control) =>
this.getOrderItemSubsetFromItemForm(control, form)
);
const { pickUpDeadline } = this.getDataFromGeneralForm(form);
return subsetItems.map((item) => ({
orderId: form.get('orderId').value,
orderItemId: item.orderItemId,
orderItemSubsetId: item.id,
orderItemSubset: { ...item },
orderItemSubset: { ...item, compartmentStop: pickUpDeadline },
}));
}

View File

@@ -1,9 +1,11 @@
// start:ng42.barrel
export * from './add-class.directive';
export * from './center-bottom-transition.directive';
export * from './click-outside.directive';
export * from './header-clicked.directive';
export * from './input-resize.directive';
export * from './scroll-position.directive';
export * from './tooltip.directive';
export * from './var.directive';
export * from './center-bottom-transition.directive';
// end:ng42.barrel

View File

@@ -0,0 +1,49 @@
import {
Directive,
OnDestroy,
AfterViewInit,
Input,
ElementRef,
} from '@angular/core';
import {
ScrollPosition,
ScrollPositionService,
} from '../../core/services/scroll-position.service';
// tslint:disable-next-line: directive-selector
@Directive({ selector: '[rememberScrollPosition]' })
export class ScrollPositionDirective implements AfterViewInit, OnDestroy {
@Input() key: string;
private get ScrollPosition(): ScrollPosition {
return {
top: this.nativeElement.scrollTop,
left: this.nativeElement.scrollLeft,
};
}
private get nativeElement(): HTMLElement {
return this.elementRef.nativeElement;
}
constructor(
private elementRef: ElementRef<HTMLElement>,
private scrollPositionService: ScrollPositionService
) {}
ngAfterViewInit() {
const scrollPosition = this.scrollPositionService.getPosition(this.key);
if (scrollPosition) {
this.restoreScroll(scrollPosition);
}
}
ngOnDestroy() {
this.scrollPositionService.setPosition(this.key, this.ScrollPosition);
}
private restoreScroll({ top, left }: ScrollPosition): void {
this.nativeElement.scroll({ top, left });
}
}

View File

@@ -15,7 +15,13 @@ import { PackageNumberParserPipe } from '../pipes/package-number-parser.pipe';
import { AddClassDirective } from './directives/add-class.directive';
import { ShippingDocumentNumberFormatterPipe } from '../pipes/shipping-document-number-formatter.pipe';
import { RemissionSourcesDisplayPipe } from '../pipes/remission-sources-display.pipe';
import { ClickOutsideDirective, HeaderClickedDirective, InputResizeDirective, CenterBottomDuringTransitionDirective } from './directives';
import {
ClickOutsideDirective,
HeaderClickedDirective,
CenterBottomDuringTransitionDirective,
ScrollPositionDirective,
InputResizeDirective,
} from './directives';
const components = [BackArrowComponent, ModalDialogComponent];
const directives = [
@@ -26,6 +32,7 @@ const directives = [
HeaderClickedDirective,
InputResizeDirective,
CenterBottomDuringTransitionDirective,
ScrollPositionDirective,
];
const pipes = [
SafeHtmlPipe,