mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 14:32:10 +01:00
Merged PR 2059: feature(ui-modal): add QR code display for URLs in dialog modals
feature(ui-modal): add QR code display for URLs in dialog modals Add automatic URL detection and QR code rendering in dialog modals: - Parse dialog content to extract URLs (http/https) - Display extracted URLs as QR codes using angularx-qrcode library - Split content around URL to show text before and after the QR code - Auto-detect URLs by default, with optional showUrlAsQrCode override - Add comprehensive unit tests for URL parsing helpers Ref: #5511
This commit is contained in:
committed by
Lorenz Hilpert
parent
0aeef0592b
commit
7884e1af32
@@ -1,16 +1,43 @@
|
||||
@if (modalRef.data.subtitle; as subtitle) {
|
||||
<h2 class="subtitle">{{ subtitle }}</h2>
|
||||
}
|
||||
@if (modalRef.data.content; as content) {
|
||||
<p class="content">
|
||||
{{ content }}
|
||||
</p>
|
||||
|
||||
<!-- QR Code Display Mode -->
|
||||
@if (shouldShowQrCode(); as showQr) {
|
||||
@if (parsedContent(); as parsed) {
|
||||
@if (parsed.textBefore) {
|
||||
<p class="content">{{ parsed.textBefore }}</p>
|
||||
}
|
||||
|
||||
<div class="qr-code-container">
|
||||
<qrcode
|
||||
[qrdata]="parsed.url!"
|
||||
[width]="200"
|
||||
[errorCorrectionLevel]="'M'"
|
||||
[margin]="2"
|
||||
></qrcode>
|
||||
</div>
|
||||
|
||||
@if (parsed.textAfter) {
|
||||
<p class="content">{{ parsed.textAfter }}</p>
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<!-- Default Text Display Mode -->
|
||||
@if (modalRef.data.content; as content) {
|
||||
<p class="content">
|
||||
{{ content }}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
@if (modalRef.data.actions; as actions) {
|
||||
<div class="actions">
|
||||
@for (action of actions; track action) {
|
||||
<button [class.selected]="action.selected" (click)="handleCommand(action.command)">
|
||||
<button
|
||||
[class.selected]="action.selected"
|
||||
(click)="handleCommand(action.command)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -15,11 +15,15 @@
|
||||
@apply text-lg text-center whitespace-pre-wrap mb-8 px-16;
|
||||
}
|
||||
|
||||
.qr-code-container {
|
||||
@apply flex flex-col items-center justify-center mb-8;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@apply text-center mb-8;
|
||||
|
||||
button {
|
||||
@apply border-2 border-solid border-brand bg-white text-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap ml-4;
|
||||
@apply border-2 border-solid border-brand bg-white text-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap;
|
||||
|
||||
&.selected {
|
||||
@apply bg-brand text-white;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, computed, OnInit } from '@angular/core';
|
||||
import { CommandService } from '@core/command';
|
||||
import { UiModalRef } from '../defs/modal-ref';
|
||||
import { DialogModel } from './dialog.model';
|
||||
import { parseDialogContentForUrl } from './dialog.helper';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-dialog-modal',
|
||||
@@ -10,6 +11,26 @@ import { DialogModel } from './dialog.model';
|
||||
standalone: false,
|
||||
})
|
||||
export class UiDialogModalComponent implements OnInit {
|
||||
/**
|
||||
* Parsed content with URL extracted for QR code display.
|
||||
* Only relevant when showUrlAsQrCode is true.
|
||||
*/
|
||||
readonly parsedContent = computed(() => {
|
||||
const data = this.modalRef.data;
|
||||
if (!data.showUrlAsQrCode) {
|
||||
return null;
|
||||
}
|
||||
return parseDialogContentForUrl(data.content);
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether to show the QR code instead of the URL text.
|
||||
*/
|
||||
readonly shouldShowQrCode = computed(() => {
|
||||
const parsed = this.parsedContent();
|
||||
return parsed !== null && parsed.url !== null;
|
||||
});
|
||||
|
||||
constructor(
|
||||
public modalRef: UiModalRef<any, DialogModel<any>>,
|
||||
private _command: CommandService,
|
||||
|
||||
48
apps/isa-app/src/ui/modal/dialog/dialog.helper.ts
Normal file
48
apps/isa-app/src/ui/modal/dialog/dialog.helper.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ParsedDialogContent } from './dialog.model';
|
||||
|
||||
/**
|
||||
* Regular expression to match URLs in text.
|
||||
* Matches http:// and https:// URLs.
|
||||
*/
|
||||
const URL_REGEX = /https?:\/\/[^\s]+/i;
|
||||
|
||||
/**
|
||||
* Parses the dialog content and extracts the first URL.
|
||||
* Splits the content into text before the URL, the URL itself, and text after.
|
||||
*
|
||||
* @param content - The dialog content string to parse
|
||||
* @returns ParsedDialogContent with the split content
|
||||
*/
|
||||
export const parseDialogContentForUrl = (
|
||||
content: string | undefined,
|
||||
): ParsedDialogContent => {
|
||||
if (!content) {
|
||||
return { textBefore: '', url: null, textAfter: '' };
|
||||
}
|
||||
|
||||
const match = content.match(URL_REGEX);
|
||||
|
||||
if (!match || match.index === undefined) {
|
||||
return { textBefore: content, url: null, textAfter: '' };
|
||||
}
|
||||
|
||||
const url = match[0];
|
||||
const urlIndex = match.index;
|
||||
const textBefore = content.substring(0, urlIndex).trim();
|
||||
const textAfter = content.substring(urlIndex + url.length).trim();
|
||||
|
||||
return { textBefore, url, textAfter };
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given content contains a URL.
|
||||
*
|
||||
* @param content - The content string to check
|
||||
* @returns true if a URL is found, false otherwise
|
||||
*/
|
||||
export const contentHasUrl = (content: string | undefined): boolean => {
|
||||
if (!content) {
|
||||
return false;
|
||||
}
|
||||
return URL_REGEX.test(content);
|
||||
};
|
||||
152
apps/isa-app/src/ui/modal/dialog/dialog.model.spec.ts
Normal file
152
apps/isa-app/src/ui/modal/dialog/dialog.model.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { contentHasUrl, parseDialogContentForUrl } from './dialog.helper';
|
||||
import { ParsedDialogContent } from './dialog.model';
|
||||
|
||||
describe('parseDialogContentForUrl', () => {
|
||||
it('should return empty result for undefined content', () => {
|
||||
const result = parseDialogContentForUrl(undefined);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: '',
|
||||
url: null,
|
||||
textAfter: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty result for empty string', () => {
|
||||
const result = parseDialogContentForUrl('');
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: '',
|
||||
url: null,
|
||||
textAfter: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return content as textBefore when no URL is found', () => {
|
||||
const content = 'This is some text without a URL';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: content,
|
||||
url: null,
|
||||
textAfter: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract https URL from content', () => {
|
||||
const content = 'Text before https://example.com text after';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: 'Text before',
|
||||
url: 'https://example.com',
|
||||
textAfter: 'text after',
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract http URL from content', () => {
|
||||
const content = 'Text before http://example.com text after';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: 'Text before',
|
||||
url: 'http://example.com',
|
||||
textAfter: 'text after',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URL at the beginning of content', () => {
|
||||
const content = 'https://example.com/path text after';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: '',
|
||||
url: 'https://example.com/path',
|
||||
textAfter: 'text after',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URL at the end of content', () => {
|
||||
const content = 'Text before https://example.com/path';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: 'Text before',
|
||||
url: 'https://example.com/path',
|
||||
textAfter: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle real-world content with newlines', () => {
|
||||
const content = `Punkte: 80500
|
||||
Um alle Vorteile der Kundenkarte nutzen zu können, ist eine Verknüpfung zu einem Online-Konto notwendig. Kund:innen können sich über den QR-Code selbstständig anmelden oder die Kundenkarte dem bestehendem Konto hinzufügen. Bereits gesammelte Punkte werden übernommen.
|
||||
https://h-k.me/QOHNTFVA`;
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result.url).toBe('https://h-k.me/QOHNTFVA');
|
||||
expect(result.textBefore).toContain('Punkte: 80500');
|
||||
expect(result.textBefore).toContain(
|
||||
'Bereits gesammelte Punkte werden übernommen.',
|
||||
);
|
||||
expect(result.textAfter).toBe('');
|
||||
});
|
||||
|
||||
it('should extract only the first URL when multiple URLs are present', () => {
|
||||
const content = 'First https://first.com then https://second.com';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: 'First',
|
||||
url: 'https://first.com',
|
||||
textAfter: 'then https://second.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URLs with paths and query parameters', () => {
|
||||
const content =
|
||||
'Visit https://example.com/path?query=value&foo=bar for more';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result.url).toBe('https://example.com/path?query=value&foo=bar');
|
||||
expect(result.textBefore).toBe('Visit');
|
||||
expect(result.textAfter).toBe('for more');
|
||||
});
|
||||
});
|
||||
|
||||
describe('contentHasUrl', () => {
|
||||
it('should return false for undefined content', () => {
|
||||
expect(contentHasUrl(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty string', () => {
|
||||
expect(contentHasUrl('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for content without URL', () => {
|
||||
expect(contentHasUrl('This is text without a URL')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for content with https URL', () => {
|
||||
expect(contentHasUrl('Check out https://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for content with http URL', () => {
|
||||
expect(contentHasUrl('Check out http://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for real-world content', () => {
|
||||
const content = `Punkte: 80500
|
||||
Um alle Vorteile der Kundenkarte nutzen zu können...
|
||||
https://h-k.me/QOHNTFVA`;
|
||||
|
||||
expect(contentHasUrl(content)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,7 @@
|
||||
import { DialogSettings, KeyValueDTOOfStringAndString } from '@generated/swagger/crm-api';
|
||||
import {
|
||||
DialogSettings,
|
||||
KeyValueDTOOfStringAndString,
|
||||
} from '@generated/swagger/crm-api';
|
||||
|
||||
export interface DialogModel<T = any> {
|
||||
actions?: Array<KeyValueDTOOfStringAndString>;
|
||||
@@ -14,4 +17,21 @@ export interface DialogModel<T = any> {
|
||||
* default: true
|
||||
*/
|
||||
handleCommand?: boolean;
|
||||
/**
|
||||
* If true, URLs in the content will be displayed as QR codes.
|
||||
* default: false
|
||||
*/
|
||||
showUrlAsQrCode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of parsing content for URLs
|
||||
*/
|
||||
export interface ParsedDialogContent {
|
||||
/** Text before the URL */
|
||||
textBefore: string;
|
||||
/** The extracted URL (if any) */
|
||||
url: string | null;
|
||||
/** Text after the URL */
|
||||
textAfter: string;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { DialogModel } from './dialog.model';
|
||||
import { ToasterService } from '@shared/shell';
|
||||
import { contentHasUrl } from './dialog.helper';
|
||||
|
||||
@Injectable()
|
||||
export class OpenDialogInterceptor implements HttpInterceptor {
|
||||
@@ -21,7 +22,10 @@ export class OpenDialogInterceptor implements HttpInterceptor {
|
||||
private _toast: ToasterService,
|
||||
) {}
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
intercept(
|
||||
req: HttpRequest<any>,
|
||||
next: HttpHandler,
|
||||
): Observable<HttpEvent<any>> {
|
||||
return next.handle(req).pipe(
|
||||
tap((response) => {
|
||||
if (response instanceof HttpResponse) {
|
||||
@@ -59,9 +63,17 @@ export class OpenDialogInterceptor implements HttpInterceptor {
|
||||
}
|
||||
|
||||
openDialog(model: DialogModel<any>) {
|
||||
// Auto-detect URLs and enable QR code display if URL is found
|
||||
// Can be overridden by explicitly setting showUrlAsQrCode in the model
|
||||
const showUrlAsQrCode =
|
||||
model.showUrlAsQrCode ?? contentHasUrl(model.content);
|
||||
|
||||
this._modal.open({
|
||||
content: UiDialogModalComponent,
|
||||
data: model,
|
||||
data: {
|
||||
...model,
|
||||
showUrlAsQrCode,
|
||||
},
|
||||
title: model.title,
|
||||
config: {
|
||||
canClose: (model.settings & 1) === 1,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { UiModalComponent } from './modal.component';
|
||||
import { UiModalService } from './modal.service';
|
||||
import { UiDebugModalComponent } from './debug-modal/debug-modal.component';
|
||||
import { UiMessageModalComponent } from './message-modal.component';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
@@ -10,9 +9,10 @@ import { UiDialogModalComponent } from './dialog/dialog-modal.component';
|
||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { OpenDialogInterceptor } from './dialog/open-dialog.interceptor';
|
||||
import { UiPromptModalComponent } from './prompt-modal';
|
||||
import { QRCodeComponent } from 'angularx-qrcode';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, OverlayModule, UiIconModule],
|
||||
imports: [CommonModule, OverlayModule, UiIconModule, QRCodeComponent],
|
||||
declarations: [
|
||||
UiModalComponent,
|
||||
UiDebugModalComponent,
|
||||
|
||||
254
package-lock.json
generated
254
package-lock.json
generated
@@ -32,6 +32,7 @@
|
||||
"@ngrx/store-devtools": "^20.0.0",
|
||||
"angular-oauth2-oidc": "^20.0.2",
|
||||
"angular-oauth2-oidc-jwks": "^20.0.0",
|
||||
"angularx-qrcode": "^20.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"jsbarcode": "^3.12.1",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -15055,6 +15056,19 @@
|
||||
"tslib": "^2.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/angularx-qrcode": {
|
||||
"version": "20.0.0",
|
||||
"resolved": "https://registry.npmjs.org/angularx-qrcode/-/angularx-qrcode-20.0.0.tgz",
|
||||
"integrity": "sha512-WZolRZztQsQxOXqodNSDicxPWNO79t/AT4wts+DxwYdtdXb1RELfZjtax9oGMQQ6mEZ6bwk5GqBGEDB3Y+cSqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"qrcode": "1.5.4",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "^20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
@@ -15098,7 +15112,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -15108,7 +15121,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -15794,37 +15806,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
|
||||
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"type-is": "^2.0.0"
|
||||
"raw-body": "^3.0.1",
|
||||
"type-is": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/bonjour-service": {
|
||||
@@ -16189,7 +16192,6 @@
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -16630,7 +16632,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -16643,7 +16644,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colord": {
|
||||
@@ -18307,6 +18307,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
@@ -18595,6 +18604,12 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -29306,9 +29321,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz",
|
||||
"integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==",
|
||||
"dev": true,
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
@@ -30412,7 +30427,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -30673,7 +30687,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -30943,6 +30956,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
"version": "1.0.38",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
|
||||
@@ -32100,6 +32122,156 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qrcode/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
@@ -32583,7 +32755,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -32599,6 +32770,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
@@ -33812,6 +33989,12 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
@@ -34599,7 +34782,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -37831,6 +38013,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
@@ -37876,7 +38064,6 @@
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -37942,14 +38129,12 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -37959,7 +38144,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"@ngrx/store-devtools": "^20.0.0",
|
||||
"angular-oauth2-oidc": "^20.0.2",
|
||||
"angular-oauth2-oidc-jwks": "^20.0.0",
|
||||
"angularx-qrcode": "^20.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"jsbarcode": "^3.12.1",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
Reference in New Issue
Block a user