Merge branch 'dev'

This commit is contained in:
Dan Percic 2023-03-15 17:49:49 +02:00
commit 4210811e70
18 changed files with 1155 additions and 690 deletions

View File

@ -29,7 +29,10 @@
<iqser-help-button *deny="roles.getRss" [iqserHelpMode]="'help_mode'" id="help-mode-button"></iqser-help-button>
<redaction-notifications [iqserHelpMode]="'open_notifications'"></redaction-notifications>
<redaction-notifications
*ngIf="!currentUser.isAdmin && !currentUser.isUserAdmin"
[iqserHelpMode]="'open_notifications'"
></redaction-notifications>
</div>
<iqser-user-button [iqserHelpMode]="'open_usermenu'" [matMenuTriggerFor]="userMenu" id="userMenu"></iqser-user-button>

View File

@ -1,4 +1,9 @@
<iqser-circle-button [matMenuTriggerFor]="menu" [showDot]="hasUnreadNotifications$ | async" icon="red:notification"></iqser-circle-button>
<iqser-circle-button
buttonId="notification-button"
[matMenuTriggerFor]="menu"
[showDot]="hasUnreadNotifications$ | async"
icon="red:notification"
></iqser-circle-button>
<mat-menu #menu="matMenu" backdropClass="notifications-backdrop" class="notifications-menu" xPosition="before">
<ng-template matMenuContent>
@ -13,14 +18,20 @@
<div *ngFor="let group of groups; let first = first">
<div class="all-caps-label flex-align-items-center">
<div>{{ group.date }}</div>
<div (click)="markRead($event)" *ngIf="(hasUnreadNotifications$ | async) && first" class="view-all">
<div
(click)="markRead($event)"
*ngIf="(hasUnreadNotifications$ | async) && first"
id="notifications-mark-all-as-read-btn"
class="view-all"
>
{{ 'notifications.mark-all-as-read' | translate }}
</div>
</div>
<div
(click)="markRead($event, [notification], true)"
*ngFor="let notification of group.notifications"
*ngFor="let notification of group.notifications; trackBy: trackBy"
[id]="'notifications-mark-as-read-' + notification.id + '-btn'"
[class.unread]="!notification.readDate"
class="notification"
mat-menu-item
@ -36,6 +47,7 @@
class="dot"
matTooltip="{{ 'notifications.mark-as' | translate : { type: notification.readDate ? 'unread' : 'read' } }}"
matTooltipPosition="before"
[id]="'notifications-mark-' + notification.id"
></div>
</div>
</div>

View File

@ -4,7 +4,7 @@ import { NotificationsService } from '@services/notifications.service';
import { Notification } from '@red/domain';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { isToday, shareLast } from '@iqser/common-ui';
import { isToday, shareLast, trackByFactory } from '@iqser/common-ui';
import dayjs, { Dayjs } from 'dayjs';
import { TranslateService } from '@ngx-translate/core';
@ -29,6 +29,7 @@ function chronologically(first: string, second: string) {
export class NotificationsComponent {
readonly hasUnreadNotifications$: Observable<boolean>;
readonly groupedNotifications$: Observable<NotificationsGroup[]>;
readonly trackBy = trackByFactory();
constructor(
private readonly _notificationsService: NotificationsService,

View File

@ -0,0 +1,11 @@
<div class="pagination noselect">
<div id="portraitPage" class="page-button" (click)="changePage.emit(1)">
<mat-icon class="chevron-icon" svgIcon="red:nav-prev"></mat-icon>
Portrait
</div>
<div class="separator">/</div>
<div id="landscapePage" class="page-button" (click)="changePage.emit(2)">
Landscape
<mat-icon class="chevron-icon" svgIcon="red:nav-next"></mat-icon>
</div>
</div>

View File

@ -0,0 +1,42 @@
.pagination {
z-index: 1;
position: absolute;
bottom: 20px;
right: calc(50% - (var(--viewer-width) / 2));
transform: translate(-50%);
background: var(--iqser-background);
color: var(--iqser-grey-7);
border: 1px solid var(--iqser-grey-7);
border-radius: 8px;
padding: 6px 2px;
display: flex;
justify-content: center;
align-items: center;
> div {
height: 16px;
cursor: default;
}
.separator {
padding-left: 2px;
padding-right: 2px;
}
.page-button {
cursor: pointer;
align-items: center;
display: inline-flex;
&:hover {
color: var(--iqser-text);
}
}
.chevron-icon {
height: 16px;
transform: rotate(-90deg);
padding-left: 4px;
padding-right: 4px;
}
}

View File

@ -0,0 +1,10 @@
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'redaction-paginator',
templateUrl: './paginator.component.html',
styleUrls: ['./paginator.component.scss'],
})
export class PaginatorComponent {
@Output() readonly changePage = new EventEmitter<number>();
}

View File

@ -1,7 +1,9 @@
<div class="content-container">
<div class="viewer" id="viewer"></div>
<div #viewer class="viewer" id="viewer"></div>
<div *ngIf="changed && currentUser.isAdmin" class="changes-box">
<redaction-paginator (changePage)="navigateTo($event)" *ngIf="loaded$ | async"></redaction-paginator>
<div *ngIf="!!instance && changed && currentUser.isAdmin" class="changes-box">
<iqser-icon-button
(action)="save()"
[disabled]="!valid"
@ -9,6 +11,7 @@
[type]="iconButtonTypes.primary"
icon="iqser:check"
></iqser-icon-button>
<div (click)="revert()" [translate]="'watermark-screen.action.revert'" class="all-caps-label cancel"></div>
</div>
</div>

View File

@ -5,6 +5,7 @@
flex-direction: row !important;
flex-grow: 1;
overflow: hidden;
--viewer-width: 380px;
}
.content-container {
@ -12,6 +13,7 @@
.viewer {
height: 100%;
width: 100%;
}
}

View File

@ -1,4 +1,4 @@
import { Component, Inject } from '@angular/core';
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import WebViewer, { WebViewerInstance } from '@pdftron/webviewer';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup } from '@angular/forms';
@ -18,14 +18,14 @@ import { DOSSIER_TEMPLATE_ID, type IWatermark, type User, WATERMARK_ID, Watermar
import { stampPDFPage } from '@utils/page-stamper';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { WatermarkService } from '@services/entity-services/watermark.service';
import { firstValueFrom, Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { BehaviorSubject, firstValueFrom, Observable, of } from 'rxjs';
import { LicenseService } from '@services/license.service';
import { UserPreferenceService } from '@users/user-preference.service';
import { Router } from '@angular/router';
import { WatermarksMapService } from '@services/entity-services/watermarks-map.service';
import { ROLES } from '@users/roles';
import { environment } from '@environments/environment';
import { tap } from 'rxjs/operators';
export const DEFAULT_WATERMARK: Partial<IWatermark> = {
text: 'Watermark',
@ -51,10 +51,10 @@ interface WatermarkForm {
templateUrl: './watermark-screen.component.html',
styleUrls: ['./watermark-screen.component.scss'],
})
export class WatermarkScreenComponent {
export class WatermarkScreenComponent implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
readonly currentUser = getCurrentUser<User>();
readonly form = this._getForm();
readonly form = this.#form;
readonly watermark$: Observable<Partial<IWatermark>>;
readonly fontOptions = [
{ value: 'times-new-roman', display: 'Times' },
@ -62,10 +62,12 @@ export class WatermarkScreenComponent {
{ value: 'courier', display: 'Courier' },
];
readonly orientationOptions = ['DIAGONAL', 'HORIZONTAL', 'VERTICAL'];
instance: WebViewerInstance;
readonly loaded$ = new BehaviorSubject(false);
@ViewChild('viewer', { static: true }) private readonly _viewer: ElementRef<HTMLDivElement>;
readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
readonly #watermarkId = Number(getParam(WATERMARK_ID));
private _instance: WebViewerInstance;
private _watermark: Partial<IWatermark> = {};
#watermark: Partial<IWatermark> = {};
constructor(
private readonly _http: HttpClient,
@ -78,17 +80,16 @@ export class WatermarkScreenComponent {
private readonly _watermarkService: WatermarkService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _router: Router,
private readonly _watermarksMapService: WatermarksMapService,
watermarksMapService: WatermarksMapService,
) {
const obs$: Observable<Partial<IWatermark>> = this.#watermarkId
? _watermarksMapService.watch$(this.#dossierTemplateId, this.#watermarkId)
: of(DEFAULT_WATERMARK);
this.watermark$ = obs$.pipe(tap(wm => this._initForm(wm)));
const watermark$ = watermarksMapService.watch$(this.#dossierTemplateId, this.#watermarkId);
const obs$: Observable<Partial<IWatermark>> = this.#watermarkId ? watermark$ : of(DEFAULT_WATERMARK);
this.watermark$ = obs$.pipe(tap(watermark => this.#initForm(watermark)));
}
get changed(): boolean {
for (const key of Object.keys(this.form.getRawValue())) {
if (this._watermark[key] !== this.form.get(key)?.value) {
if (this.#watermark[key] !== this.form.get(key)?.value) {
return true;
}
}
@ -102,113 +103,7 @@ export class WatermarkScreenComponent {
return this.form.valid;
}
@Debounce()
async configChanged() {
await this._drawWatermark();
}
async save(): Promise<void> {
const watermark: IWatermark = {
id: this._watermark.id,
enabled: this._watermark.id ? this._watermark.enabled : true,
dossierTemplateId: this.#dossierTemplateId,
...this.form.getRawValue(),
};
this._loadingService.start();
try {
const updatedWatermark = await this._watermarkService.saveWatermark(watermark);
this._toaster.success(
watermark.id ? _('watermark-screen.action.change-success') : _('watermark-screen.action.created-success'),
);
if (!watermark.id) {
await this._router.navigate([`/main/admin/dossier-templates/${this.#dossierTemplateId}/watermarks/${updatedWatermark.id}`]);
}
} catch (error) {
this._toaster.error(_('watermark-screen.action.error'));
}
this._loadingService.stop();
}
async revert() {
this.form.patchValue({ ...this._watermark });
await this.configChanged();
}
async setValue(type: 'fontType' | 'orientation' | 'hexColor', value: any) {
if (!this.form.get(type).disabled) {
this.form.get(type).setValue(value);
await this.configChanged();
}
}
private async _initForm(watermark: Partial<IWatermark>) {
this._watermark = { ...watermark, dossierTemplateId: this.#dossierTemplateId };
this.form.patchValue({ ...watermark });
await this._loadViewer();
}
private async _loadViewer() {
if (this._instance) {
return;
}
this._instance = await WebViewer(
{
licenseKey: this._licenseService.activeLicenseKey,
path: this._convertPath('/assets/wv-resources'),
css: this._convertPath('/assets/pdftron/stylesheet.css'),
fullAPI: true,
isReadOnly: true,
backendType: 'ems',
},
document.getElementById('viewer'),
);
this._instance.UI.setTheme(this._userPreferenceService.getTheme());
this._instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
this._loadingService.stop();
await this._drawWatermark();
});
if (environment.production) {
this._instance.Core.setCustomFontURL('https://' + window.location.host + this._convertPath('/assets/pdftron'));
}
this._disableElements();
const request = this._http.get('/assets/pdftron/blank.pdf', {
responseType: 'blob',
});
const blobData = await firstValueFrom(request);
this._instance.UI.loadDocument(blobData, { filename: 'blank.pdf' });
}
private _disableElements() {
this._instance.UI.disableElements(['header', 'toolsHeader', 'pageNavOverlay', 'textPopup']);
}
private async _drawWatermark() {
const pdfNet = this._instance.Core.PDFNet;
const document = await this._instance.Core.documentViewer.getDocument().getPDFDoc();
await stampPDFPage(
document,
pdfNet,
this.form.controls.text.value || '',
this.form.controls.fontSize.value,
this.form.controls.fontType.value,
this.form.controls.orientation.value,
this.form.controls.opacity.value,
this.form.controls.hexColor.value,
[1],
this._licenseService.activeLicenseKey,
);
this._instance.Core.documentViewer.refreshAll();
this._instance.Core.documentViewer.updateView([0], 0);
}
private _getForm() {
get #form() {
const form: FormGroup<AsControl<WatermarkForm>> = this._formBuilder.group({
name: [null],
text: [null],
@ -225,4 +120,122 @@ export class WatermarkScreenComponent {
return form;
}
async ngOnInit() {
await this.#loadViewer();
}
@Debounce()
async configChanged() {
await this.#drawWatermark();
}
async save(): Promise<void> {
const watermark: IWatermark = {
id: this.#watermark.id,
enabled: this.#watermark.id ? this.#watermark.enabled : true,
dossierTemplateId: this.#dossierTemplateId,
...this.form.getRawValue(),
};
this._loadingService.start();
try {
const updatedWatermark = await this._watermarkService.saveWatermark(watermark);
this._toaster.success(
watermark.id ? _('watermark-screen.action.change-success') : _('watermark-screen.action.created-success'),
);
if (!watermark.id) {
await this._router.navigate([`/main/admin/dossier-templates/${this.#dossierTemplateId}/watermarks/${updatedWatermark.id}`]);
}
} catch (error) {
this._toaster.error(_('watermark-screen.action.error'));
}
this._loadingService.stop();
}
async revert() {
this.form.patchValue({ ...this.#watermark });
await this.configChanged();
}
async setValue(type: 'fontType' | 'orientation' | 'hexColor', value: any) {
if (!this.form.get(type).disabled) {
this.form.get(type).setValue(value);
await this.configChanged();
}
}
navigateTo($event: number) {
this.instance.Core.documentViewer.displayPageLocation($event, 0, 0);
}
async #initForm(watermark: Partial<IWatermark>) {
this.#watermark = { ...watermark, dossierTemplateId: this.#dossierTemplateId };
this.form.patchValue({ ...watermark });
}
async #loadViewer() {
this.instance = await WebViewer(
{
licenseKey: this._licenseService.activeLicenseKey,
path: this._convertPath('/assets/wv-resources'),
css: this._convertPath('/assets/pdftron/stylesheet.css'),
fullAPI: true,
isReadOnly: true,
backendType: 'ems',
},
// use nativeElement instead of document.getElementById('viwer')
// because WebViewer works better with this approach
this._viewer.nativeElement,
);
this.instance.UI.setTheme(this._userPreferenceService.getTheme());
this.instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
this.loaded$.next(true);
this._loadingService.stop();
await this.#drawWatermark();
});
if (environment.production) {
this.instance.Core.setCustomFontURL('https://' + window.location.host + this._convertPath('/assets/pdftron'));
}
this.#disableElements();
await this.#loadDocument();
}
async #loadDocument() {
const request = this._http.get('/assets/pdftron/blank.pdf', {
responseType: 'blob',
});
const blobData = await firstValueFrom(request);
this.instance.UI.loadDocument(blobData, { filename: 'blank.pdf' });
}
#disableElements() {
this.instance.UI.disableElements(['header', 'toolsHeader', 'pageNavOverlay', 'textPopup']);
}
async #drawWatermark() {
const pdfNet = this.instance.Core.PDFNet;
const document = await this.instance.Core.documentViewer.getDocument().getPDFDoc();
await stampPDFPage(
document,
pdfNet,
this.form.controls.text.value || '',
this.form.controls.fontSize.value,
this.form.controls.fontType.value,
this.form.controls.orientation.value,
this.form.controls.opacity.value,
this.form.controls.hexColor.value,
[1, 2],
this._licenseService.activeLicenseKey,
);
this.instance.Core.documentViewer.refreshAll();
this.instance.Core.documentViewer.updateView([0, 1], 0);
}
}

View File

@ -18,6 +18,7 @@ import { RedRoleGuard } from '@users/red-role.guard';
import { WATERMARK_ID } from '@red/domain';
import { WatermarkExistsGuard } from '@guards/watermark-exists.guard';
import { TranslateModule } from '@ngx-translate/core';
import { PaginatorComponent } from './paginator/paginator.component';
const routes = [
{
@ -47,7 +48,7 @@ const routes = [
];
@NgModule({
declarations: [WatermarkScreenComponent, WatermarksListingScreenComponent],
declarations: [WatermarkScreenComponent, WatermarksListingScreenComponent, PaginatorComponent],
imports: [
RouterModule.forChild(routes),
CommonModule,

View File

@ -4,12 +4,14 @@ import { ViewedPagesService } from '@services/files/viewed-pages.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { PageRotationService } from '../../../pdf-viewer/services/page-rotation.service';
import { ContextComponent, getConfig } from '@iqser/common-ui';
import { tap } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { AppConfig, ViewedPage } from '@red/domain';
import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service';
import { pairwise } from 'rxjs';
interface PageIndicatorContext {
isRotated: boolean;
assigneeChanged: boolean;
}
@Component({
@ -47,7 +49,12 @@ export class PageIndicatorComponent extends ContextComponent<PageIndicatorContex
this._changeDetectorRef.detectChanges();
}),
);
super._initContext({ isRotated: isRotated$ });
const assigneeChanged$ = this._state.file$.pipe(
pairwise(),
map(([prevFile, currFile]) => prevFile.assignee !== currFile.assignee),
tap(assigneeChanged => assigneeChanged && this.handlePageRead()),
);
super._initContext({ isRotated: isRotated$, assigneeChanged: assigneeChanged$ });
}
ngOnChanges() {

View File

@ -140,6 +140,12 @@ export class FilePreviewScreenComponent
this.fullScreen = false;
}
});
this.pdf.instance.UI.hotkeys.on('command+f, ctrl+f', e => {
e.preventDefault();
this.pdf.focusSearch();
this.pdf.activateSearch();
});
}
get changed() {
@ -360,7 +366,6 @@ export class FilePreviewScreenComponent
this.closeFullScreen();
this.pdf.deactivateSearch();
this._changeRef.markForCheck();
window.focus();
}
if (['f', 'F'].includes($event.key)) {
@ -368,12 +373,6 @@ export class FilePreviewScreenComponent
if ($event.target instanceof HTMLInputElement || $event.target instanceof HTMLTextAreaElement) {
return;
}
if ($event.ctrlKey) {
this.pdf.focusSearch();
this.pdf.activateSearch();
return;
}
this.toggleFullScreen();
return;
}

View File

@ -67,13 +67,7 @@ export class REDDocumentViewer {
}
return ($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input';
}),
filter(
$event =>
$event.key.startsWith('Arrow') ||
$event.key === 'f' ||
['h', 'H'].includes($event.key) ||
['Escape'].includes($event.key),
),
filter($event => $event.key.startsWith('Arrow') || ['f', 'h', 'H', 'Escape'].includes($event.key)),
tap<KeyboardEvent>(stopAndPrevent),
log('[PDF] Keyboard shortcut'),
);

View File

@ -68,6 +68,7 @@ export const DISABLED_HOTKEYS = [
'CTRL+P',
'COMMAND+P',
'CTRL+F',
'COMMAND+F',
'SPACE',
'UP',
'DOWN',

@ -1 +1 @@
Subproject commit d09078e44c8c294c78c44080d0bb8403d0ec6c34
Subproject commit 223080763f280c58c50f739d316bfd7f50bdcab1

View File

@ -35,11 +35,11 @@
"@angular/service-worker": "15.1.2",
"@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
"@materia-ui/ngx-monaco-editor": "^6.0.0",
"@messageformat/core": "^3.0.1",
"@messageformat/core": "^3.1.0",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@nrwl/angular": "15.6.3",
"@pdftron/webviewer": "8.11.0",
"@pdftron/webviewer": "8.12.0",
"@swimlane/ngx-charts": "^20.0.1",
"dayjs": "^1.11.5",
"file-saver": "^2.0.5",
@ -47,25 +47,25 @@
"keycloak-angular": "13.0.0",
"keycloak-js": "20.0.3",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.34.0",
"ngx-color-picker": "^13.0.0",
"monaco-editor": "^0.36.1",
"ngx-color-picker": "^14.0.0",
"ngx-logger": "^5.0.11",
"ngx-toastr": "^16.0.2",
"ngx-toastr": "^16.1.0",
"ngx-translate-messageformat-compiler": "^6.2.0",
"object-hash": "^3.0.0",
"papaparse": "^5.3.2",
"papaparse": "^5.4.0",
"rxjs": "7.8.0",
"sass": "^1.58.0",
"scroll-into-view-if-needed": "^3.0.4",
"sass": "^1.59.2",
"scroll-into-view-if-needed": "^3.0.6",
"streamsaver": "^2.0.5",
"tslib": "^2.5.0",
"zone.js": "0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "15.1.4",
"@angular-eslint/eslint-plugin": "15.2.0",
"@angular-eslint/eslint-plugin-template": "15.2.0",
"@angular-eslint/template-parser": "15.2.0",
"@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "~15.1.0",
"@angular/compiler-cli": "15.1.2",
"@angular/language-service": "15.1.2",
@ -76,34 +76,34 @@
"@nrwl/workspace": "15.6.3",
"@types/jest": "^29.4.0",
"@types/lodash-es": "^4.17.6",
"@types/node": "18.11.18",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"axios": "^1.3.1",
"@types/node": "18.15.1",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"axios": "^1.3.4",
"dotenv": "16.0.3",
"eslint": "8.33.0",
"eslint-config-prettier": "8.6.0",
"eslint": "8.36.0",
"eslint-config-prettier": "8.7.0",
"eslint-plugin-prettier": "^4.0.0",
"google-translate-api-browser": "^3.0.0",
"google-translate-api-browser": "^4.0.6",
"husky": "^8.0.3",
"jest": "^28.1.3",
"jest-environment-jsdom": "^29.4.1",
"jest-extended": "^3.2.3",
"jest-preset-angular": "12.2.6",
"lint-staged": "^13.1.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"jest-extended": "^3.2.4",
"jest-preset-angular": "13.0.0",
"lint-staged": "^13.2.0",
"nx": "15.6.3",
"postcss": "^8.4.21",
"postcss-import": "15.1.0",
"postcss-preset-env": "~8.0.1",
"postcss-url": "10.1.3",
"prettier": "2.8.3",
"sonarqube-scanner": "^3.0.0",
"prettier": "2.8.4",
"sonarqube-scanner": "^3.0.1",
"superagent": "^8.0.9",
"superagent-promise": "^1.1.0",
"ts-node": "10.9.1",
"typescript": "4.9.5",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.7.0",
"webpack": "^5.76.1",
"webpack-bundle-analyzer": "^4.8.0",
"xliff": "^6.1.0"
},
"jest": {

1402
yarn.lock

File diff suppressed because it is too large Load Diff