Skip to content

Commit

Permalink
fix: tacton improvements (#368)
Browse files Browse the repository at this point in the history
* fix: click on image of image-text-button triggers change
* i18n: rename No to Product ID in tacton BOM
* i18n: rename submit to request proposal
* fix: display extended message on successful submission of firm request
* i18n: shorten reset configuration to just reset
* fix: text-buttons as radio button group
* fix: hide more components if product is configurable
* fix: styling improvements for step buttons
* fix: mark current sub group as active with intersection observer
  • Loading branch information
dhhyi authored Aug 27, 2020
1 parent 2a60f9d commit 84ed633
Show file tree
Hide file tree
Showing 31 changed files with 404 additions and 198 deletions.
5 changes: 3 additions & 2 deletions src/app/core/directives.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { NgModule } from '@angular/core';

import { ClickOutsideDirective } from './directives/click-outside.directive';
import { IntersectionObserverDirective } from './directives/intersection-observer.directive';
import { ServerHtmlDirective } from './directives/server-html.directive';

@NgModule({
declarations: [ClickOutsideDirective, ServerHtmlDirective],
exports: [ClickOutsideDirective, ServerHtmlDirective],
declarations: [ClickOutsideDirective, IntersectionObserverDirective, ServerHtmlDirective],
exports: [ClickOutsideDirective, IntersectionObserverDirective, ServerHtmlDirective],
})
export class DirectivesModule {}
114 changes: 114 additions & 0 deletions src/app/core/directives/intersection-observer.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { isPlatformBrowser } from '@angular/common';
import {
Directive,
ElementRef,
EventEmitter,
Inject,
Input,
OnDestroy,
OnInit,
Output,
PLATFORM_ID,
} from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { debounceTime, filter, takeUntil } from 'rxjs/operators';

/**
* detect visibility status of components via IntersectionObserver
*
* taken from: https://blog.bitsrc.io/angular-maximizing-performance-with-the-intersection-observer-api-23d81312f178
*/
@Directive({
selector: '[ishIntersectionObserver]',
})
export class IntersectionObserverDirective implements OnInit, OnDestroy {
@Input() intersectionDebounce = 0;
@Input() intersectionRootMargin = '0px';
@Input() intersectionRoot: HTMLElement;
@Input() intersectionThreshold: number | number[];

@Output() visibilityChange = new EventEmitter<IntersectionStatus>();

private destroy$ = new Subject();

constructor(private element: ElementRef, @Inject(PLATFORM_ID) private platformId: string) {}

ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
const element = this.element.nativeElement;
const config = {
root: this.intersectionRoot,
rootMargin: this.intersectionRootMargin,
threshold: this.intersectionThreshold,
};

fromIntersectionObserver(element, config, this.intersectionDebounce)
.pipe(takeUntil(this.destroy$))
.subscribe(status => {
this.visibilityChange.emit(status);
});
}
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

export type IntersectionStatus = 'Visible' | 'Pending' | 'NotVisible';

const fromIntersectionObserver = (element: HTMLElement, config: IntersectionObserverInit, debounce = 0) =>
new Observable<IntersectionStatus>(subscriber => {
const subject$ = new Subject<{
entry: IntersectionObserverEntry;
observer: IntersectionObserver;
}>();

const intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (isIntersecting(entry)) {
subject$.next({ entry, observer });
}
});
}, config);

subject$.subscribe(() => {
subscriber.next('Pending');
});

subject$.pipe(debounceTime(debounce), filter(Boolean)).subscribe(async ({ entry }) => {
const isEntryVisible = await isVisible(entry.target as HTMLElement);

if (isEntryVisible) {
subscriber.next('Visible');
} else {
subscriber.next('NotVisible');
}
});

intersectionObserver.observe(element);

return {
unsubscribe() {
intersectionObserver.disconnect();
// tslint:disable-next-line rxjs-no-subject-unsubscribe ban
subject$.unsubscribe();
},
};
});

async function isVisible(element: HTMLElement) {
return new Promise(resolve => {
const observer = new IntersectionObserver(([entry]) => {
resolve(entry.isIntersecting);
observer.disconnect();
});

observer.observe(element);
});
}

function isIntersecting(entry: IntersectionObserverEntry) {
return entry.isIntersecting || entry.intersectionRatio > 0;
}
1 change: 1 addition & 0 deletions src/app/core/store/core/messages/messages.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class MessagesEffects {
progressBar: false,
closeButton: false,
positionClass: 'toast-top-right',
enableHtml: true,
// defaults
// toastClass: 'ngx-toastr',
// titleClass: 'toast-title',
Expand Down
10 changes: 8 additions & 2 deletions src/app/extensions/tacton/facades/tacton.facade.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators';
import { Observable, ReplaySubject } from 'rxjs';
import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';

import { whenTruthy } from 'ish-core/utils/operators';

Expand Down Expand Up @@ -76,4 +76,10 @@ export class TactonFacade {
submitConfiguration() {
this.store.dispatch(submitTactonConfiguration());
}

private internalCurrentGroup$ = new ReplaySubject<string>(1);
currentGroup$ = this.internalCurrentGroup$.pipe(distinctUntilChanged());
setCurrentGroup(name: string) {
this.internalCurrentGroup$.next(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,9 @@
<ish-product-image [product]="product" imageType="M" class="product-image"></ish-product-image>
<ish-tacton-bom [bom]="state.bom"></ish-tacton-bom>
</div>
<div class="tacton-main-panel col-md-6">
<ish-tacton-group [group]="step$ | async"></ish-tacton-group>
<div class="tacton-buttonbar">
<ish-tacton-step-buttons></ish-tacton-step-buttons>
</div>
<div class="tacton-main-panel col-lg-6" *ngIf="step$ | async as step">
<ish-tacton-group [group]="step"></ish-tacton-group>
<ish-tacton-step-buttons></ish-tacton-step-buttons>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
<ul class="list-unstyled pl-3">
<ng-container *ngFor="let subItem of mainItem.children">
<li *ngIf="subItem.hasVisibleParameters" class="pt-1 pb-1">
<a (click)="scrollIntoView(subItem.name)">{{ subItem.description }}</a>
<a
(click)="scrollIntoView(subItem.name)"
[ngClass]="{ 'font-weight-bold': isActive$(subItem.name) | async }"
>{{ subItem.description }}</a
>
</li>
</ng-container>
</ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('Tacton Configure Navigation Component', () => {
});
it('should render for group-level navigation', () => {
when(tactonFacade.configurationTree$).thenReturn(of(tree));
when(tactonFacade.currentGroup$).thenReturn(of('step12'));

fixture.detectChanges();

Expand All @@ -58,7 +59,7 @@ describe('Tacton Configure Navigation Component', () => {
<a class="font-weight-bold">step 1 description</a>
<ul class="list-unstyled pl-3">
<li class="pt-1 pb-1"><a>step 1.1 description</a></li>
<li class="pt-1 pb-1"><a>step 1.2 description</a></li>
<li class="pt-1 pb-1"><a class="font-weight-bold">step 1.2 description</a></li>
</ul>
</li>
<li class="pt-1 pb-1"><a>step 2 description</a></li>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { TactonFacade } from '../../../facades/tacton.facade';
import { TactonNavigationTree } from '../../../models/tacton-navigation-tree/tacton-navigation-tree.model';
Expand Down Expand Up @@ -29,4 +30,8 @@ export class TactonConfigureNavigationComponent implements OnInit {
scrollIntoView(id: string) {
document.querySelector(`#anchor-${id}`)?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
}

isActive$(name: string) {
return this.tactonFacade.currentGroup$.pipe(map(current => current === name));
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<ng-container *ngIf="group && group.hasVisibleParameters">
<h2 [id]="'anchor-' + group.name" *ngIf="!isSubGroup; else subGroup">{{ group?.description }}</h2>
<h2 [id]="'anchor-' + group.name" *ngIf="!level; else subGroup">{{ group?.description }}</h2>
<ng-template #subGroup>
<span class="anchor" [id]="'anchor-' + group.name"></span>
<h3>{{ group.description }}</h3>
<h3 ishIntersectionObserver (visibilityChange)="onIntersection(group.name, $event)">{{ group.description }}</h3>
</ng-template>
<img *ngIf="group.properties?.tc_group_picture" [src]="getImageUrl(group.properties?.tc_group_picture) | async" />

<ng-container *ngFor="let item of group?.members">
<ish-tacton-group *ngIf="isGroup(item); else isParameter" [group]="item" [isSubGroup]="true"></ish-tacton-group>
<ng-container *ngFor="let item of group?.members; let last = last">
<ish-tacton-group *ngIf="isGroup(item); else isParameter" [group]="item" [level]="level + 1"></ish-tacton-group>
<ng-template #isParameter>
<ish-tacton-parameter [item]="item"></ish-tacton-parameter>
</ng-template>
<span *ngIf="!last" ishIntersectionObserver (visibilityChange)="onIntersection(group.name, $event)"></span>
</ng-container>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -88,24 +88,27 @@ describe('Tacton Group Component', () => {

expect(element).toMatchInlineSnapshot(`
<h2 id="anchor-root">g1</h2>
<ish-tacton-parameter></ish-tacton-parameter
><ish-tacton-group ng-reflect-is-sub-group="true"
<ish-tacton-parameter></ish-tacton-parameter><span ishintersectionobserver=""></span
><ish-tacton-group ng-reflect-level="1"
><span class="anchor" id="anchor-G11"></span>
<h3>g11</h3>
<h3 ishintersectionobserver="">g11</h3>
<ish-tacton-parameter></ish-tacton-parameter></ish-tacton-group
><ish-tacton-group ng-reflect-is-sub-group="true"></ish-tacton-group
><ish-tacton-parameter></ish-tacton-parameter
><ish-tacton-group ng-reflect-is-sub-group="true"
><span ishintersectionobserver=""></span><ish-tacton-group ng-reflect-level="1"></ish-tacton-group
><span ishintersectionobserver=""></span><ish-tacton-parameter></ish-tacton-parameter
><span ishintersectionobserver=""></span
><ish-tacton-group ng-reflect-level="1"
><span class="anchor" id="anchor-G13"></span>
<h3>g13</h3>
<h3 ishintersectionobserver="">g13</h3>
<ish-tacton-parameter></ish-tacton-parameter></ish-tacton-group
><ish-tacton-group ng-reflect-is-sub-group="true"
><span ishintersectionobserver=""></span
><ish-tacton-group ng-reflect-level="1"
><span class="anchor" id="anchor-G14"></span>
<h3>g14</h3>
<h3 ishintersectionobserver="">g14</h3>
<ish-tacton-parameter></ish-tacton-parameter></ish-tacton-group
><ish-tacton-group ng-reflect-is-sub-group="true"
><span ishintersectionobserver=""></span
><ish-tacton-group ng-reflect-level="1"
><span class="anchor" id="anchor-G15"></span>
<h3>g15</h3>
<h3 ishintersectionobserver="">g15</h3>
<ish-tacton-parameter></ish-tacton-parameter
></ish-tacton-group>
`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Observable } from 'rxjs';

import { IntersectionStatus } from 'ish-core/directives/intersection-observer.directive';

import { TactonFacade } from '../../../facades/tacton.facade';
import { TactonProductConfigurationHelper } from '../../../models/tacton-product-configuration/tacton-product-configuration.helper';
import { TactonProductConfigurationGroup } from '../../../models/tacton-product-configuration/tacton-product-configuration.model';
Expand All @@ -12,7 +14,7 @@ import { TactonProductConfigurationGroup } from '../../../models/tacton-product-
})
export class TactonGroupComponent {
@Input() group: TactonProductConfigurationGroup;
@Input() isSubGroup = false;
@Input() level = 0;

constructor(private facade: TactonFacade) {}

Expand All @@ -21,4 +23,10 @@ export class TactonGroupComponent {
getImageUrl(picture: string): Observable<string> {
return this.facade.getImageUrl$(picture);
}

onIntersection(name: string, event: IntersectionStatus) {
if (this.level === 1 && event === 'Visible') {
this.facade.setCurrentGroup(name);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div class="d-flex flex-wrap align-items-end pb-2">
<div *ngFor="let opt of parameter.domain?.elements" class="mr-3">
<div class="border" [ngClass]="{ 'border-primary': opt.selected }">
<img [src]="getImageUrl(opt.properties.tc_component_picture) | async" />
<img [src]="getImageUrl(opt.properties.tc_component_picture) | async" (click)="change(opt.name)" />
</div>
<label>
<input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,19 @@ describe('Tacton Image Text Buttons Component', () => {
fixture.detectChanges();

const input = fixture.debugElement.query(By.css('input')).nativeElement;
input.value = '3';
input.value = component.parameter.domain.elements[1].name;
input.dispatchEvent(new Event('change'));

verify(tactonFacade.commitValue(anything(), anything())).once();
expect(capture(tactonFacade.commitValue).last()[1]).toMatchInlineSnapshot(`"3"`);
expect(capture(tactonFacade.commitValue).last()[1]).toMatchInlineSnapshot(`"i2"`);
});

it('should trigger value commit if clicked on image', () => {
fixture.detectChanges();

fixture.debugElement.queryAll(By.css('img'))[1].triggerEventHandler('click', undefined);

verify(tactonFacade.commitValue(anything(), anything())).once();
expect(capture(tactonFacade.commitValue).last()[1]).toMatchInlineSnapshot(`"i2"`);
});
});
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
<div *ngIf="stepConfig$ | async as config" class="d-flex flex-nowrap justify-content-between">
<button class="btn btn-secondary no-wrap" (click)="reset()" data-testing-id="reset-configuration-button">
{{ 'tacton.step_buttons.reset.label' | translate }}
</button>
<div>
<div *ngIf="config.previousStep" class="d-inline-block">
<button class="btn btn-secondary" (click)="changeStep(config.previousStep)" data-testing-id="previous-button">
{{ 'tacton.step_buttons.previous.label' | translate }}
</button>
</div>
<div *ngIf="config.nextStep" class="d-inline-block ml-2">
<button class="btn btn-primary" (click)="changeStep(config.nextStep)" data-testing-id="next-button">
{{ 'tacton.step_buttons.next.label' | translate }}
</button>
</div>
<div *ngIf="!config.nextStep" class="d-inline-block ml-2">
<button class="btn btn-primary" (click)="submit()" data-testing-id="submit-button">
{{ 'tacton.step_buttons.submit.label' | translate }}
</button>
</div>
<div *ngIf="stepConfig$ | async as config" class="d-flex flex-nowrap justify-content-between tacton-buttonbar">
<div class="d-inline-block mr-auto">
<button
class="btn btn-secondary no-wrap text-nowrap"
(click)="reset()"
data-testing-id="reset-configuration-button"
>
{{ 'tacton.step_buttons.reset.label' | translate }}
</button>
</div>
<div *ngIf="config.previousStep" class="d-inline-block ml-2">
<button
class="btn btn-secondary text-nowrap"
(click)="changeStep(config.previousStep)"
data-testing-id="previous-button"
>
{{ 'tacton.step_buttons.previous.label' | translate }}
</button>
</div>
<div *ngIf="config.nextStep" class="d-inline-block ml-2">
<button class="btn btn-primary text-nowrap" (click)="changeStep(config.nextStep)" data-testing-id="next-button">
{{ 'tacton.step_buttons.next.label' | translate }}
</button>
</div>
<div *ngIf="!config.nextStep" class="d-inline-block ml-2">
<button class="btn btn-primary text-nowrap" (click)="submit()" data-testing-id="submit-button">
{{ 'tacton.step_buttons.submit.label' | translate }}
</button>
</div>
</div>
Loading

0 comments on commit 84ed633

Please sign in to comment.