Skip to content

Commit

Permalink
Merge branch 'main' into feature/ux-improvements
Browse files Browse the repository at this point in the history
* main:
  feat: IIIf image edit component (#1534)
  chore(main): release 11.7.5 (#1536)
  fix: initialize pendo on app start (#1535)
  • Loading branch information
irmastnt committed Mar 27, 2024
2 parents f4db8bf + bc4c95c commit 38d5c2c
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 7 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [11.7.5](https://github.com/dasch-swiss/dsp-das/compare/v11.7.4...v11.7.5) (2024-03-27)


### Bug Fixes

* initialize pendo on app start ([#1535](https://github.com/dasch-swiss/dsp-das/issues/1535)) ([76e5356](https://github.com/dasch-swiss/dsp-das/commit/76e5356b3fd48f1359c51b7fc28bb329fb8c93ad))

## [11.7.4](https://github.com/dasch-swiss/dsp-das/compare/v11.7.3...v11.7.4) (2024-03-22)


Expand Down
4 changes: 3 additions & 1 deletion apps/dsp-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { GrafanaFaroService } from '@dasch-swiss/vre/shared/app-analytics';
import { GrafanaFaroService, PendoAnalyticsService } from '@dasch-swiss/vre/shared/app-analytics';
import { RouteConstants } from '@dasch-swiss/vre/shared/app-config';
import { AutoLoginService, LocalStorageWatcherService } from '@dasch-swiss/vre/shared/app-session';

Expand All @@ -18,10 +18,12 @@ export class AppComponent implements OnInit {
private _router: Router,
private _titleService: Title,
private _autoLoginService: AutoLoginService,
private _pendo: PendoAnalyticsService,
private _localStorageWatcher: LocalStorageWatcherService,
private _grafana: GrafanaFaroService
) {
this._grafana.setup();
this._pendo.setup();
this._autoLoginService.setup();
this._localStorageWatcher.watchAccessToken();
this._titleService.setTitle('DaSCH Service Platform');
Expand Down
2 changes: 2 additions & 0 deletions apps/dsp-app/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ import { SublistValueComponent } from './workspace/resource/values/list-value/su
import { TextValueAsHtmlComponent } from './workspace/resource/values/text-value/text-value-as-html/text-value-as-html.component';
import { TextValueAsStringComponent } from './workspace/resource/values/text-value/text-value-as-string/text-value-as-string.component';
import { TextValueAsXMLComponent } from './workspace/resource/values/text-value/text-value-as-xml/text-value-as-xml.component';
import { ThirdPartyIiifComponent } from './workspace/resource/values/third-party-iiif/third-party-iiif.component';
import { TimeInputComponent } from './workspace/resource/values/time-value/time-input/time-input.component';
import { TimeValueComponent } from './workspace/resource/values/time-value/time-value.component';
import { UriValueComponent } from './workspace/resource/values/uri-value/uri-value.component';
Expand Down Expand Up @@ -307,6 +308,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
TextValueAsStringComponent,
TextValueAsXMLComponent,
TextValueHtmlLinkDirective,
ThirdPartyIiifComponent,
TimeInputComponent,
TimePipe,
TimeValueComponent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { AbstractControl, AsyncValidatorFn, ValidationErrors, ValidatorFn } from '@angular/forms';
import { from, Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { IIIFUrl } from './third-party-iiif';

export function iiifUrlValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!IIIFUrl.createUrl(control.value)?.isValidIiifUrl) {
return { invalidIiifUrl: true };
}
return null;
};
}

function fetchUrl(url: string): Promise<void> {
return fetch(url, { method: 'HEAD' }).then(response => {
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
});
}

export function infoJsonUrlValidatorAsync(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
const iiifUrl = IIIFUrl.createUrl(control.value);
if (!iiifUrl) {
return of(null);
}

return from(fetchUrl(iiifUrl.infoJsonUrl)).pipe(
map(() => null),
catchError(() => of({ infoJsonError: true }))
);
};
}

export function previewImageUrlValidatorAsync(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
const iiifUrl = IIIFUrl.createUrl(control.value);
if (!iiifUrl) {
return of(null);
}

return from(fetchUrl(iiifUrl?.previewImageUrl)).pipe(
map(() => null),
catchError(() => of({ previewImageError: true }))
);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<div class="third-party-iiif-preview">
<div
*ngIf="(!previewImageUrl || !iiifUrlControl.valid) && formStatus === 'IDLE'"
class="third-party-iiif-preview-placeholder">
<mat-icon class="iiif-preview-icon">preview</mat-icon>
<div>[ preview ]</div>
<div class="iiif-url-explanation mat-subtitle-2">
<div>The url must point to be a valid iiif image (jpg, tif, jp2, png).</div>
<div>Example: "{{exampleString}}"</div>
</div>
</div>
<dasch-swiss-app-progress-indicator *ngIf="formStatus !=='IDLE'"></dasch-swiss-app-progress-indicator>
<img
*ngIf="previewImageUrl && iiifUrlControl.valid"
[src]="previewImageUrl"
(loadstart)="formStatus='LOADING'"
(loadeddata)="formStatus='IDLE'"
(error)="formStatus='IDLE'"
alt="IIIF Preview"
height="240" />
</div>
<mat-form-field class="third-party-iiif-field">
<mat-label>IIIF Image URL</mat-label>
<input matInput [formControl]="iiifUrlControl" placeholder="Enter IIIF image URL" />
<mat-error *ngIf="iiifUrlControl.errors as errors"> {{ errors | humanReadableError: validatorErrors }} </mat-error>
<button
*ngIf="iiifUrlControl.value"
[disabled]="iiifUrlControl.valid"
(click)="resetIfInvalid()"
mat-icon-button
matSuffix
type="button"
color="primary">
<mat-icon>{{ iiifUrlControl.valid ? 'check' : 'close'}}</mat-icon>
</button>
<button
type="button"
color="primary"
mat-icon-button
matSuffix
matTooltip="Paste from clipboard"
(click)="pasteFromClipboard()">
<mat-icon>content_paste</mat-icon>
</button>
</mat-form-field>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.third-party-iiif-preview {
width: 100%;
min-height: 248px;
border: 2px solid #000;
border-radius: 2px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: #f2f2f2;
}

.third-party-iiif-preview-placeholder {
height: 100%;
width: 100%;
justify-content: space-between;
display: flex;
flex-direction: column;
}

.iiif-preview-icon {
margin-top: 1em;
height:auto;
width: auto;
font-size: 48px;
}

.iiif-url-explanation > div {
margin-top: 1em;
}

.third-party-iiif-field {
width: 100%;
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { FormBuilder, FormControl, Validators } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { iiifUrlValidator, infoJsonUrlValidatorAsync, previewImageUrlValidatorAsync } from './iiif-url-validator';
import { IIIFUrl } from './third-party-iiif';

@Component({
selector: 'app-third-part-iiif',
templateUrl: './third-party-iiif.component.html',
styleUrls: ['./third-party-iiif.component.scss'],
})
export class ThirdPartyIiifComponent implements OnInit, OnDestroy {
@Input() iiifUrlValue = '';
@Output() afterControlInit = new EventEmitter<FormControl<string>>();

iiifUrlControl: FormControl<string>;

previewImageUrl: string | null;
formStatus: 'VALIDATING' | 'LOADING' | 'IDLE' = 'IDLE';

private _destroy$ = new Subject<void>();

readonly validatorErrors = [
{ errorKey: 'invalidIiifUrl', message: 'The provided URL is not a valid IIIF image URL' },
{ errorKey: 'previewImageError', message: 'The image cannot be loaded from the third party server' },
{ errorKey: 'infoJsonError', message: 'The iiif info json cannot be loaded from the third party server' },
];

readonly templateString = '{scheme}://{server}{/prefix}/{identifier}/{region}/{size}/{rotation}/{quality}.{format}';
readonly exampleString = 'https://example.org/image-service/abcd1234/full/max/0/default.jpg';

constructor(
private _cdr: ChangeDetectorRef,
private _fb: FormBuilder
) {}

ngOnInit() {
this.iiifUrlControl = new FormControl(this.iiifUrlValue, {
validators: [Validators.required, iiifUrlValidator()],
asyncValidators: [previewImageUrlValidatorAsync(), infoJsonUrlValidatorAsync()],
});

this.iiifUrlControl.valueChanges.pipe(takeUntil(this._destroy$)).subscribe(urlStr => {
const iiifUrl = IIIFUrl.createUrl(urlStr);
this.previewImageUrl = iiifUrl?.previewImageUrl;
});

this.iiifUrlControl.statusChanges.pipe(takeUntil(this._destroy$)).subscribe(state => {
this.formStatus = state === 'PENDING' ? 'VALIDATING' : 'IDLE';
this._cdr.detectChanges();
});

this.afterControlInit.emit(this.iiifUrlControl);
}

pasteFromClipboard() {
if (navigator.clipboard && navigator.clipboard.readText) {
navigator.clipboard.readText().then(text => {
this.iiifUrlControl.setValue(text);
this.iiifUrlControl.markAsTouched();
this.iiifUrlControl.updateValueAndValidity();
});
}
}

resetIfInvalid() {
if (this.iiifUrlControl.invalid) {
this.iiifUrlControl.reset();
}
}

ngOnDestroy(): void {
this._destroy$.next();
this._destroy$.complete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
export class IIIFUrl {
private _url: URL;

private readonly qualform_ex = /^(color|gray|bitonal|default)\.(jpg|tif|png|jp2)$/;
private readonly rotation_ex = /^[-+]?[0-9]*\.?[0-9]+$|^![-+]?[0-9]*\.?[0-9]+$/;
private readonly size_ex = /^(\^?max)|(\^?pct:[0-9]*\.?[0-9]*)|(\^?[0-9]*,)|(\^?,[0-9]*)|(\^?!?[0-9]*,[0-9]*)$/;
private readonly region_ex =
/^(full)|(square)|([0-9]+,[0-9]+,[0-9]+,[0-9]+)|(pct:[0-9]*\.?[0-9]*,[0-9]*\.?[0-9]*,[0-9]*\.?[0-9]*,[0-9]*\.?[0-9]*)$/;

private constructor(url: string) {
this._url = new URL(url);
}

static createUrl(url: string): IIIFUrl | null {
try {
return new IIIFUrl(url);
} catch (error) {
return null;
}
}

private get _baseUrl(): string {
return `${this._url.protocol}//${this._url.host}`;
}

private get _segments(): string[] {
return this._url.pathname.split('/').slice(1);
}

private get _qualitySegment(): string {
return this._segments[this._segments.length - 1];
}

private get _quality(): string {
return this._qualitySegment.split('.')[0];
}

private get _rotationSegment(): string {
return this._segments[this._segments.length - 2];
}

private get _sizeSegment(): string {
return this._segments[this._segments.length - 3];
}

private get _regionSegment(): string {
return this._segments[this._segments.length - 4];
}

get isValidIiifUrl(): boolean {
if (this._segments.length < 5) {
return false;
}

if (!this.qualform_ex.test(this._qualitySegment)) {
return false;
}

if (!this.rotation_ex.test(this._rotationSegment)) {
return false;
}

if (!this.size_ex.test(this._sizeSegment)) {
return false;
}

if (!this.region_ex.test(this._regionSegment)) {
return false;
}

return true;
}

get iiifImageInstanceUrl(): string {
return `${this._baseUrl}/${this._segments.slice(0, this._segments.length - 4).join('/')}`;
}

get infoJsonUrl() {
return `${this.iiifImageInstanceUrl}/info.json`;
}

get previewImageUrl() {
return `${this.iiifImageInstanceUrl}/${this._regionSegment}/${this._sizeSegment}/${this._rotationSegment}/${this._quality}.jpg`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class PendoAnalyticsService {
private _accessTokenService: AccessTokenService = inject(AccessTokenService);
private environment: string = this.config.environment;

constructor() {
setup() {
this.authService
.isCredentialsValid$()
.pipe(takeUntilDestroyed())
Expand All @@ -32,7 +32,6 @@ export class PendoAnalyticsService {
* set active user
*/
setActiveUser(id: string): void {
console.log('setActiveUser', id);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
pendo.initialize({
Expand Down Expand Up @@ -67,7 +66,6 @@ export class PendoAnalyticsService {
* remove active user
*/
removeActiveUser(): void {
console.log('removeActiveUser');
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
pendo.initialize({
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dsp-app",
"version": "11.7.4",
"version": "11.7.5",
"repository": {
"type": "git",
"url": "https://github.com/dasch-swiss/dsp-app.git"
Expand Down

0 comments on commit 38d5c2c

Please sign in to comment.