From be662f3df16e6d7d92c80ddb0f994b400775997f Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Mon, 9 Mar 2026 13:50:46 +0000 Subject: [PATCH 1/7] signals: table display components --- .../base-table-display-field.component.ts | 17 +- .../binary-data-caption.component.html | 2 +- .../boolean/boolean.component.html | 6 +- .../boolean/boolean.component.ts | 35 ++--- .../code/code.component.html | 4 +- .../color/color.component.html | 6 +- .../color/color.component.ts | 77 +++++----- .../country/country.component.ts | 78 +++++----- .../date-time/date-time.component.ts | 114 +++++++------- .../date/date.component.ts | 114 +++++++------- .../file/file.component.ts | 36 ++--- .../foreign-key/foreign-key.component.html | 10 +- .../foreign-key/foreign-key.component.ts | 38 ++--- .../table-display-fields/id/id.component.html | 4 +- .../table-display-fields/id/id.component.ts | 17 +- .../image/image.component.html | 2 +- .../image/image.component.ts | 40 ++--- .../json-editor/json-editor.component.html | 4 +- .../json-editor/json-editor.component.ts | 28 ++-- .../long-text/long-text.component.html | 4 +- .../long-text/long-text.component.ts | 18 ++- .../markdown/markdown.component.html | 4 +- .../markdown/markdown.component.ts | 124 +++++++-------- .../money/money.component.ts | 99 ++++++------ .../number/number.component.html | 2 +- .../number/number.component.ts | 89 +++++------ .../phone/phone.component.html | 8 +- .../phone/phone.component.ts | 90 +++++------ .../point/point.component.ts | 65 ++++---- .../range/range.component.ts | 107 ++++++------- .../table-display-fields/s3/s3.component.html | 8 +- .../table-display-fields/s3/s3.component.ts | 20 +-- .../select/select.component.ts | 61 ++++---- .../static-text/static-text.component.html | 4 +- .../text/text.component.html | 4 +- .../text/text.component.ts | 145 +++++++++--------- .../time-interval/time-interval.component.ts | 52 ++++--- .../time/time.component.ts | 55 +++---- .../timezone/timezone.component.html | 2 +- .../timezone/timezone.component.ts | 85 +++++----- .../url/url.component.html | 6 +- .../table-display-fields/url/url.component.ts | 44 +++--- .../uuid/uuid.component.html | 6 +- .../uuid/uuid.component.ts | 16 +- 44 files changed, 880 insertions(+), 870 deletions(-) diff --git a/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.ts b/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.ts index f8d2802dd..a9190a544 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, input, output } from '@angular/core'; import { TableField, WidgetStructure } from 'src/app/models/table'; @Component({ @@ -9,13 +9,12 @@ import { TableField, WidgetStructure } from 'src/app/models/table'; imports: [CommonModule], }) export class BaseTableDisplayFieldComponent { - @Input() key: string; - @Input() value: any; - @Input() structure: TableField; - @Input() widgetStructure: WidgetStructure; - @Input() rowData: Record; - @Input() primaryKeys: { column_name: string }[]; - // @Input() relations: TableForeignKey; + readonly key = input(); + readonly value = input(); + readonly structure = input(); + readonly widgetStructure = input(); + readonly rowData = input>(); + readonly primaryKeys = input<{ column_name: string }[]>(); - @Output() onCopyToClipboard = new EventEmitter(); + readonly onCopyToClipboard = output(); } diff --git a/frontend/src/app/components/ui-components/table-display-fields/binary-data-caption/binary-data-caption.component.html b/frontend/src/app/components/ui-components/table-display-fields/binary-data-caption/binary-data-caption.component.html index ea5a639c0..0e88e629b 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/binary-data-caption/binary-data-caption.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/binary-data-caption/binary-data-caption.component.html @@ -1,3 +1,3 @@
- {{value || '—'}} + {{value() || '—'}}
diff --git a/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.html b/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.html index dd7c2f1ab..b5a18a2a6 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.html @@ -1,13 +1,13 @@
- check - close - +
diff --git a/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.ts b/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.ts index 34e51ac4b..f1dd65cdf 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.ts @@ -1,29 +1,28 @@ +import { Component } from '@angular/core'; + +import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; -import { Component, Injectable } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component'; -@Injectable() @Component({ - selector: 'app-display-boolean', - templateUrl: './boolean.component.html', - styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './boolean.component.css'], - imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule, CommonModule], + selector: 'app-display-boolean', + templateUrl: './boolean.component.html', + styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './boolean.component.css'], + imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule, CommonModule] }) export class BooleanDisplayComponent extends BaseTableDisplayFieldComponent { - get invertColors(): boolean { - // Parse widget parameters if available - if (this.widgetStructure?.widget_params) { - const params = - typeof this.widgetStructure.widget_params === 'string' - ? JSON.parse(this.widgetStructure.widget_params) - : this.widgetStructure.widget_params; + get invertColors(): boolean { + // Parse widget parameters if available + if (this.widgetStructure()?.widget_params) { + const params = typeof this.widgetStructure().widget_params === 'string' + ? JSON.parse(this.widgetStructure().widget_params as unknown as string) + : this.widgetStructure().widget_params; - return params?.invert_colors === true; - } - return false; - } + return params?.invert_colors === true; + } + return false; + } } diff --git a/frontend/src/app/components/ui-components/table-display-fields/code/code.component.html b/frontend/src/app/components/ui-components/table-display-fields/code/code.component.html index acf48b2d5..479ddf07a 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/code/code.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/code/code.component.html @@ -1,9 +1,9 @@
- {{value || '—'}} + {{value() || '—'}}
- {{ value || '—' }} + {{ value() || '—' }} - \ No newline at end of file + diff --git a/frontend/src/app/components/ui-components/table-display-fields/color/color.component.ts b/frontend/src/app/components/ui-components/table-display-fields/color/color.component.ts index 408ac0f6f..7f83e1416 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/color/color.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/color/color.component.ts @@ -5,14 +5,13 @@ import { ClipboardModule } from '@angular/cdk/clipboard'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { NgIf } from '@angular/common'; import colorString from 'color-string'; @Component({ selector: 'app-display-color', templateUrl: './color.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './color.component.css'], - imports: [NgIf, ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] + imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] }) export class ColorDisplayComponent extends BaseTableDisplayFieldComponent { get isValidColor(): boolean { diff --git a/frontend/src/app/components/ui-components/table-display-fields/country/country.component.html b/frontend/src/app/components/ui-components/table-display-fields/country/country.component.html index 54639a23b..0be43269a 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/country/country.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/country/country.component.html @@ -1,6 +1,8 @@
- {{ countryFlag }} + @if (showFlag && countryFlag) { + {{ countryFlag }} + } {{ countryName }} - {{ value() || '—' }} + @if (relations() && value()) { + + } @else { + {{ value() || '—' }} + }
- +@if (value()) { +
+
{{ formattedJson }}
+ +
+} diff --git a/frontend/src/app/components/ui-components/table-display-fields/json-editor/json-editor.component.ts b/frontend/src/app/components/ui-components/table-display-fields/json-editor/json-editor.component.ts index c7bfa3bb1..bee4bc5fa 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/json-editor/json-editor.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/json-editor/json-editor.component.ts @@ -1,6 +1,5 @@ import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @@ -10,7 +9,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; selector: 'app-json-editor-display', templateUrl: './json-editor.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './json-editor.component.css'], - imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule, CommonModule] + imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] }) export class JsonEditorDisplayComponent extends BaseTableDisplayFieldComponent { get formattedJson(): string { diff --git a/frontend/src/app/components/ui-components/table-display-fields/language/language.component.ts b/frontend/src/app/components/ui-components/table-display-fields/language/language.component.ts index 2d09732cd..078e541f3 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/language/language.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/language/language.component.ts @@ -1,5 +1,4 @@ import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { Component, computed, input, output } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @@ -11,7 +10,7 @@ import { getLanguageFlag, LANGUAGES } from '../../../../consts/languages'; selector: 'app-language-display', templateUrl: './language.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './language.component.css'], - imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule, CommonModule], + imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule], }) export class LanguageDisplayComponent { static type = 'language'; diff --git a/frontend/src/app/components/ui-components/table-display-fields/long-text/long-text.component.ts b/frontend/src/app/components/ui-components/table-display-fields/long-text/long-text.component.ts index ef0150e5c..c2f9847cc 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/long-text/long-text.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/long-text/long-text.component.ts @@ -2,7 +2,6 @@ import { Component } from '@angular/core'; import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -11,7 +10,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; selector: 'app-display-long-text', templateUrl: './long-text.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './long-text.component.css'], - imports: [CommonModule, ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] + imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] }) export class LongTextDisplayComponent extends BaseTableDisplayFieldComponent { diff --git a/frontend/src/app/components/ui-components/table-display-fields/markdown/markdown.component.ts b/frontend/src/app/components/ui-components/table-display-fields/markdown/markdown.component.ts index c9a3d138c..e328e3454 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/markdown/markdown.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/markdown/markdown.component.ts @@ -1,7 +1,6 @@ import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; import { Component, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { MarkdownModule } from 'ngx-markdown'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @@ -12,7 +11,7 @@ import { marked } from 'marked'; selector: 'app-markdown-display', templateUrl: './markdown.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './markdown.component.css'], - imports: [CommonModule, MarkdownModule, ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] + imports: [MarkdownModule, ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] }) export class MarkdownDisplayComponent extends BaseTableDisplayFieldComponent implements OnInit { public renderedMarkdown: string = ''; diff --git a/frontend/src/app/components/ui-components/table-display-fields/money/money.component.ts b/frontend/src/app/components/ui-components/table-display-fields/money/money.component.ts index f1e2c33e1..c1ec43f2d 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/money/money.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/money/money.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core'; import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -12,7 +11,7 @@ import { getCurrencyByCode } from 'src/app/consts/currencies'; selector: 'app-money-display', templateUrl: './money.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './money.component.css'], - imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule, CommonModule] + imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] }) export class MoneyDisplayComponent extends BaseTableDisplayFieldComponent implements OnInit { public displayCurrency: string = ''; diff --git a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html index ce1614e1b..3905c10bf 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.html @@ -1,8 +1,12 @@
{{displayValue}} - south - north + @if (isOutOfThreshold === 'down') { + south + } + @if (isOutOfThreshold === 'up') { + north + } + @if (value()) { + + }
diff --git a/frontend/src/app/components/ui-components/table-display-fields/s3/s3.component.ts b/frontend/src/app/components/ui-components/table-display-fields/s3/s3.component.ts index fb172cfbd..95630e55d 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/s3/s3.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/s3/s3.component.ts @@ -1,5 +1,4 @@ import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @@ -23,7 +22,7 @@ interface S3WidgetParams { selector: 'app-s3-display', templateUrl: './s3.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './s3.component.css'], - imports: [CommonModule, ClipboardModule, MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatTooltipModule], + imports: [ClipboardModule, MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatTooltipModule], }) export class S3DisplayComponent extends BaseTableDisplayFieldComponent implements OnInit { public params: S3WidgetParams; diff --git a/frontend/src/app/components/ui-components/table-display-fields/select/select.component.ts b/frontend/src/app/components/ui-components/table-display-fields/select/select.component.ts index 9a43b2250..461dea151 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/select/select.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/select/select.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core'; import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -11,7 +10,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; selector: 'app-select-display', templateUrl: './select.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './select.component.css'], - imports: [CommonModule, ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] + imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] }) export class SelectDisplayComponent extends BaseTableDisplayFieldComponent implements OnInit { public displayValue: string; diff --git a/frontend/src/app/components/ui-components/table-display-fields/time-interval/time-interval.component.ts b/frontend/src/app/components/ui-components/table-display-fields/time-interval/time-interval.component.ts index 43b8425a4..d2c2146a8 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/time-interval/time-interval.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/time-interval/time-interval.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core'; import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -11,7 +10,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; selector: 'app-time-interval-display', templateUrl: './time-interval.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './time-interval.component.css'], - imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule, CommonModule] + imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] }) export class TimeIntervalDisplayComponent extends BaseTableDisplayFieldComponent implements OnInit { diff --git a/frontend/src/app/components/ui-components/table-display-fields/url/url.component.ts b/frontend/src/app/components/ui-components/table-display-fields/url/url.component.ts index 78f62e7c3..a5fc0cdf8 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/url/url.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/url/url.component.ts @@ -1,6 +1,5 @@ import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @@ -10,7 +9,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; selector: 'app-url-display', templateUrl: './url.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './url.component.css'], - imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule, CommonModule] + imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] }) export class UrlDisplayComponent extends BaseTableDisplayFieldComponent { static type = 'url'; diff --git a/frontend/src/app/components/ui-components/table-display-fields/uuid/uuid.component.ts b/frontend/src/app/components/ui-components/table-display-fields/uuid/uuid.component.ts index 2cdab1843..92b22f1c8 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/uuid/uuid.component.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/uuid/uuid.component.ts @@ -1,7 +1,6 @@ import { Component } from '@angular/core'; import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -10,7 +9,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; selector: 'app-display-uuid', templateUrl: './uuid.component.html', styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './uuid.component.css'], - imports: [CommonModule, ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] + imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule] }) export class UuidDisplayComponent extends BaseTableDisplayFieldComponent { } \ No newline at end of file From e29e8536367043f9367df4963e1d1cbf577475cb Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Mon, 9 Mar 2026 14:47:25 +0000 Subject: [PATCH 3/7] unit tests: add for record view components --- .../binary-data-caption.component.spec.ts | 23 +++++++ .../boolean/boolean.component.spec.ts | 33 +++++++++ .../code/code.component.spec.ts | 52 ++++++++++++++ .../color/color.component.spec.ts | 38 +++++++++++ .../country/country.component.spec.ts | 43 ++++++++++++ .../date-time/date-time.component.spec.ts | 36 ++++++++++ .../date/date.component.spec.ts | 42 ++++++++++++ .../file/file.component.spec.ts | 48 +++++++++++++ .../foreign-key/foreign-key.component.spec.ts | 30 ++++++++ .../id/id.component.spec.ts | 23 +++++++ .../image/image.component.spec.ts | 47 +++++++++++++ .../json-editor/json-editor.component.spec.ts | 37 ++++++++++ .../long-text/long-text.component.spec.ts | 23 +++++++ .../money/money.component.spec.ts | 50 ++++++++++++++ .../number/number.component.spec.ts | 51 ++++++++++++++ .../password/password.component.spec.ts | 23 +++++++ .../phone/phone.component.spec.ts | 41 +++++++++++ .../point/point.component.spec.ts | 40 +++++++++++ .../range/range.component.spec.ts | 49 +++++++++++++ .../s3/s3.component.spec.ts | 68 +++++++++++++++++++ .../select/select.component.spec.ts | 61 +++++++++++++++++ .../static-text/static-text.component.spec.ts | 23 +++++++ .../text/text.component.spec.ts | 57 ++++++++++++++++ .../time-interval.component.spec.ts | 42 ++++++++++++ .../time/time.component.spec.ts | 36 ++++++++++ .../url/url.component.spec.ts | 38 +++++++++++ .../uuid/uuid.component.spec.ts | 23 +++++++ .../markdown/markdown.component.spec.ts | 24 +++---- .../timezone/timezone.component.spec.ts | 10 +-- 29 files changed, 1094 insertions(+), 17 deletions(-) create mode 100644 frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/code/code.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/color/color.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/country/country.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/date/date.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/file/file.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/id/id.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/image/image.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/money/money.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/number/number.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/password/password.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/point/point.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/range/range.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/select/select.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/text/text.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/time/time.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/url/url.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.spec.ts diff --git a/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.spec.ts new file mode 100644 index 000000000..427999347 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BinaryDataCaptionRecordViewComponent } from './binary-data-caption.component'; + +describe('BinaryDataCaptionRecordViewComponent', () => { + let component: BinaryDataCaptionRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BinaryDataCaptionRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BinaryDataCaptionRecordViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.spec.ts new file mode 100644 index 000000000..2d5eb97c8 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BooleanRecordViewComponent } from './boolean.component'; + +describe('BooleanRecordViewComponent', () => { + let component: BooleanRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BooleanRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BooleanRecordViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should return false for invertColors when no widget params', () => { + component.widgetStructure = undefined; + expect(component.invertColors).toBe(false); + }); + + it('should return true for invertColors when widget_params.invertColors is true', () => { + component.widgetStructure = { widget_params: { invertColors: true } } as any; + expect(component.invertColors).toBe(true); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/code/code.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/code/code.component.spec.ts new file mode 100644 index 000000000..4084ea99b --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/code/code.component.spec.ts @@ -0,0 +1,52 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CodeRecordViewComponent } from './code.component'; +import { UiSettingsService } from 'src/app/services/ui-settings.service'; + +describe('CodeRecordViewComponent', () => { + let component: CodeRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CodeRecordViewComponent], + providers: [ + { provide: UiSettingsService, useValue: { isDarkMode: false } }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CodeRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set code model on init', () => { + component.value = 'console.log()'; + component.widgetStructure = { + widget_params: { language: 'javascript' }, + field_name: 'code', + } as any; + component.key = 'test'; + component.ngOnInit(); + expect(component.codeModel).toEqual({ + language: 'javascript', + uri: 'test.json', + value: 'console.log()', + }); + }); + + it('should use light theme when not dark mode', () => { + component.value = 'code'; + component.widgetStructure = { + widget_params: { language: 'javascript' }, + field_name: 'code', + } as any; + component.key = 'test'; + component.ngOnInit(); + expect(component.codeEditorTheme).toBe('vs'); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/color/color.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/color/color.component.spec.ts new file mode 100644 index 000000000..e958707df --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/color/color.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ColorRecordViewComponent } from './color.component'; + +describe('ColorRecordViewComponent', () => { + let component: ColorRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ColorRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ColorRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should validate hex color', () => { + component.value = '#ff0000'; + expect(component.isValidColor).toBe(true); + }); + + it('should return false for invalid color', () => { + component.value = 'notacolor'; + expect(component.isValidColor).toBe(false); + }); + + it('should normalize color to hex format', () => { + component.value = '#ff0000'; + expect(component.normalizedColorForDisplay).toBe('#ff0000'); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/country/country.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/country/country.component.spec.ts new file mode 100644 index 000000000..ed807b9ce --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/country/country.component.spec.ts @@ -0,0 +1,43 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CountryRecordViewComponent } from './country.component'; + +describe('CountryRecordViewComponent', () => { + let component: CountryRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CountryRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CountryRecordViewComponent); + component = fixture.componentInstance; + // Don't call fixture.detectChanges() here - let individual tests set inputs first + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should display country name from code', () => { + component.value = 'US'; + component.ngOnInit(); + expect(component.countryName).toContain('United States'); + }); + + it('should display em dash for null value', () => { + component.value = null; + component.ngOnInit(); + expect(component.countryName).toBe('—'); + }); + + it('should respect show_flag widget param', () => { + component.value = 'US'; + component.widgetStructure = { widget_params: { show_flag: false } } as any; + component.ngOnInit(); + expect(component.showFlag).toBe(false); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.spec.ts new file mode 100644 index 000000000..ed54784ef --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DateTimeRecordViewComponent } from './date-time.component'; + +describe('DateTimeRecordViewComponent', () => { + let component: DateTimeRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DateTimeRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DateTimeRecordViewComponent); + component = fixture.componentInstance; + // Don't call fixture.detectChanges() here - let individual tests set inputs first + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should format valid datetime', () => { + component.value = '2023-04-29T14:30:00'; + component.ngOnInit(); + expect(component.formattedDateTime).toBeDefined(); + }); + + it('should handle invalid datetime', () => { + component.value = 'invalid'; + component.ngOnInit(); + expect(component.formattedDateTime).toBe('invalid'); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/date/date.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/date/date.component.spec.ts new file mode 100644 index 000000000..918c9ac1d --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/date/date.component.spec.ts @@ -0,0 +1,42 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DateRecordViewComponent } from './date.component'; + +describe('DateRecordViewComponent', () => { + let component: DateRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DateRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DateRecordViewComponent); + component = fixture.componentInstance; + // Don't call fixture.detectChanges() here - let individual tests set inputs first + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should format valid date', () => { + component.value = '2023-04-29'; + component.ngOnInit(); + expect(component.formattedDate).toBeDefined(); + }); + + it('should handle invalid date', () => { + component.value = 'invalid'; + component.ngOnInit(); + expect(component.formattedDate).toBe('invalid'); + }); + + it('should handle null value', () => { + component.value = null; + component.ngOnInit(); + expect(component.formattedDate).toBeUndefined(); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/file/file.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/file/file.component.spec.ts new file mode 100644 index 000000000..f41919a6f --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/file/file.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FileRecordViewComponent } from './file.component'; + +describe('FileRecordViewComponent', () => { + let component: FileRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FileRecordViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should identify blob objects', () => { + component.value = { type: 'Buffer', data: [1, 2] }; + expect(component.isBlob).toBe(true); + }); + + it('should return Binary Data for blobs', () => { + component.value = { type: 'Buffer', data: [1, 2] }; + expect(component.displayText).toBe('Binary Data'); + }); + + it('should return Binary Data for long strings', () => { + component.value = 'a]bcdefghijklmnopqrstu'; + expect(component.displayText).toBe('Binary Data'); + }); + + it('should return the value for short strings', () => { + component.value = 'short.txt'; + expect(component.displayText).toBe('short.txt'); + }); + + it('should return dash for null value', () => { + component.value = null; + expect(component.displayText).toBe('—'); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts new file mode 100644 index 000000000..426e4f919 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { ForeignKeyRecordViewComponent } from './foreign-key.component'; + +describe('ForeignKeyRecordViewComponent', () => { + let component: ForeignKeyRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ForeignKeyRecordViewComponent], + providers: [provideRouter([])], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ForeignKeyRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set foreignKeyURLParams on init', () => { + component.primaryKeysParams = { id: 1 }; + component.ngOnInit(); + expect(component.foreignKeyURLParams).toEqual({ id: 1, mode: 'view' }); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/id/id.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/id/id.component.spec.ts new file mode 100644 index 000000000..249d8d8b2 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/id/id.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { IdRecordViewComponent } from './id.component'; + +describe('IdRecordViewComponent', () => { + let component: IdRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IdRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(IdRecordViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/image/image.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/image/image.component.spec.ts new file mode 100644 index 000000000..924adac4d --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/image/image.component.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ImageRecordViewComponent } from './image.component'; + +describe('ImageRecordViewComponent', () => { + let component: ImageRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ImageRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ImageRecordViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should return value as srcValue when no prefix', () => { + component.value = 'image.png'; + component.widgetStructure = undefined; + expect(component.srcValue).toBe('image.png'); + }); + + it('should prepend prefix to srcValue from widget params', () => { + component.value = 'image.png'; + component.widgetStructure = { widget_params: { prefix: 'https://cdn.example.com/' } } as any; + expect(component.srcValue).toBe('https://cdn.example.com/image.png'); + }); + + it('should return true for isUrl when valid URL', () => { + component.value = 'image.png'; + component.widgetStructure = { widget_params: { prefix: 'https://cdn.example.com/' } } as any; + expect(component.isUrl).toBe(true); + }); + + it('should return false for isUrl when invalid URL', () => { + component.value = 'image.png'; + component.widgetStructure = undefined; + expect(component.isUrl).toBe(false); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.spec.ts new file mode 100644 index 000000000..7a82ce011 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { JsonEditorRecordViewComponent } from './json-editor.component'; +import { UiSettingsService } from 'src/app/services/ui-settings.service'; + +describe('JsonEditorRecordViewComponent', () => { + let component: JsonEditorRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [JsonEditorRecordViewComponent], + providers: [ + { provide: UiSettingsService, useValue: { isDarkMode: false } }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(JsonEditorRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set JSON code model on init', () => { + component.value = '{"key":"value"}'; + component.key = 'test'; + component.ngOnInit(); + expect(component.codeModel).toEqual({ + language: 'json', + uri: 'test.json', + value: '{"key":"value"}', + }); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.spec.ts new file mode 100644 index 000000000..442e573d1 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { LongTextRecordViewComponent } from './long-text.component'; + +describe('LongTextRecordViewComponent', () => { + let component: LongTextRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LongTextRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LongTextRecordViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/money/money.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/money/money.component.spec.ts new file mode 100644 index 000000000..de226c036 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/money/money.component.spec.ts @@ -0,0 +1,50 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MoneyRecordViewComponent } from './money.component'; + +describe('MoneyRecordViewComponent', () => { + let component: MoneyRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MoneyRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MoneyRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should format value with currency symbol', () => { + component.widgetStructure = { + widget_params: { default_currency: 'USD' }, + } as any; + component.value = 42.5; + component.ngOnInit(); + expect(component.formattedValue).toContain('$'); + expect(component.formattedValue).toContain('42.50'); + }); + + it('should return empty string for null value', () => { + component.widgetStructure = { + widget_params: { default_currency: 'USD' }, + } as any; + component.value = null; + component.ngOnInit(); + expect(component.formattedValue).toBe(''); + }); + + it('should handle object value with amount and currency', () => { + component.widgetStructure = { + widget_params: { default_currency: 'USD' }, + } as any; + component.value = { amount: 100, currency: 'EUR' }; + component.ngOnInit(); + expect(component.formattedValue).toContain('100.00'); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.spec.ts new file mode 100644 index 000000000..460a7d56b --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NumberRecordViewComponent } from './number.component'; + +describe('NumberRecordViewComponent', () => { + let component: NumberRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NumberRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NumberRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should display em dash for null value', () => { + component.value = null; + expect(component.displayValue).toBe('—'); + }); + + it('should display number as string when no unit', () => { + component.value = 42; + expect(component.displayValue).toBe('42'); + }); + + it('should return false for isOutOfThreshold when no thresholds', () => { + component.value = '50'; + component.widgetStructure = { widget_params: {} } as any; + expect(component.isOutOfThreshold).toBe(false); + }); + + it('should return down when value below threshold_min', () => { + component.value = '5'; + component.widgetStructure = { widget_params: { threshold_min: 10 } } as any; + expect(component.isOutOfThreshold).toBe('down'); + }); + + it('should return up when value above threshold_max', () => { + component.value = '150'; + component.widgetStructure = { widget_params: { threshold_max: 100 } } as any; + expect(component.isOutOfThreshold).toBe('up'); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/password/password.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/password/password.component.spec.ts new file mode 100644 index 000000000..d4daee738 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/password/password.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PasswordRecordViewComponent } from './password.component'; + +describe('PasswordRecordViewComponent', () => { + let component: PasswordRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PasswordRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordRecordViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.spec.ts new file mode 100644 index 000000000..bd7c853fa --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PhoneRecordViewComponent } from './phone.component'; + +describe('PhoneRecordViewComponent', () => { + let component: PhoneRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PhoneRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PhoneRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should parse valid phone number', () => { + component.value = '+12025551234'; + component.ngOnInit(); + expect(component.formattedNumber).toBeDefined(); + expect(component.formattedNumber).not.toBe(''); + }); + + it('should handle invalid phone number gracefully', () => { + component.value = 'not-a-phone'; + component.ngOnInit(); + expect(component.formattedNumber).toBe('not-a-phone'); + }); + + it('should handle null value', () => { + component.value = null; + component.ngOnInit(); + expect(component.formattedNumber).toBe(''); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/point/point.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/point/point.component.spec.ts new file mode 100644 index 000000000..d4566e697 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/point/point.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PointRecordViewComponent } from './point.component'; + +describe('PointRecordViewComponent', () => { + let component: PointRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PointRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PointRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should format string point', () => { + component.value = '(1,2)'; + component.ngOnInit(); + expect(component.formattedPoint).toBe('(1, 2)'); + }); + + it('should format object point', () => { + component.value = { x: 3, y: 4 }; + component.ngOnInit(); + expect(component.formattedPoint).toBe('(3, 4)'); + }); + + it('should handle null value', () => { + component.value = null; + component.ngOnInit(); + expect(component.formattedPoint).toBe(''); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.spec.ts new file mode 100644 index 000000000..703a38e88 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.spec.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RangeRecordViewComponent } from './range.component'; + +describe('RangeRecordViewComponent', () => { + let component: RangeRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RangeRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RangeRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value with default max', () => { + component.value = 50; + component.ngOnInit(); + expect(component.displayValue).toBe('50 / 100'); + }); + + it('should parse widget params for min/max', () => { + component.widgetStructure = { + widget_params: { min: 0, max: 200 }, + } as any; + component.value = 100; + component.ngOnInit(); + expect(component.displayValue).toBe('100 / 200'); + }); + + it('should calculate progress value', () => { + component.value = 50; + component.ngOnInit(); + expect(component.getProgressValue()).toBe(50); + }); + + it('should clamp progress value', () => { + component.value = 150; + component.ngOnInit(); + expect(component.getProgressValue()).toBe(100); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.spec.ts new file mode 100644 index 000000000..00178f65c --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.spec.ts @@ -0,0 +1,68 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { S3RecordViewComponent } from './s3.component'; +import { S3Service } from 'src/app/services/s3.service'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { TablesService } from 'src/app/services/tables.service'; + +describe('S3RecordViewComponent', () => { + let component: S3RecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [S3RecordViewComponent], + providers: [ + { + provide: S3Service, + useValue: { + getFileUrl: () => Promise.resolve({ url: 'https://s3.example.com/file.jpg' }), + }, + }, + { + provide: ConnectionsService, + useValue: { currentConnectionID: 'conn-1' }, + }, + { + provide: TablesService, + useValue: { currentTableName: 'test_table' }, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(S3RecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should parse widget params', () => { + component.widgetStructure = { + widget_params: { + bucket: 'my-bucket', + type: 'file', + aws_access_key_id_secret_name: 'key', + aws_secret_access_key_secret_name: 'secret', + }, + } as any; + component.ngOnInit(); + expect(component.params).toBeDefined(); + expect(component.params.bucket).toBe('my-bucket'); + }); + + it('should identify image type', () => { + component.widgetStructure = { + widget_params: { + bucket: 'my-bucket', + type: 'image', + aws_access_key_id_secret_name: 'key', + aws_secret_access_key_secret_name: 'secret', + }, + } as any; + component.ngOnInit(); + expect(component.isImageType).toBe(true); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/select/select.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/select/select.component.spec.ts new file mode 100644 index 000000000..44ce71f39 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/select/select.component.spec.ts @@ -0,0 +1,61 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SelectRecordViewComponent } from './select.component'; + +describe('SelectRecordViewComponent', () => { + let component: SelectRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SelectRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display dash for null value', () => { + component.value = null; + component.ngOnInit(); + expect(component.displayValue).toBe('—'); + }); + + it('should display option label when matching widget option', () => { + component.widgetStructure = { + widget_params: { + options: [{ value: 'a', label: 'Alpha' }], + }, + } as any; + component.value = 'a'; + component.ngOnInit(); + expect(component.displayValue).toBe('Alpha'); + }); + + it('should display raw value when no matching option', () => { + component.widgetStructure = { + widget_params: { + options: [{ value: 'b', label: 'Beta' }], + }, + } as any; + component.value = 'unknown'; + component.ngOnInit(); + expect(component.displayValue).toBe('unknown'); + }); + + it('should set backgroundColor from matching option', () => { + component.widgetStructure = { + widget_params: { + options: [{ value: 'a', label: 'Alpha', background_color: '#ff0000' }], + }, + } as any; + component.value = 'a'; + component.ngOnInit(); + expect(component.backgroundColor).toBe('#ff0000'); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.spec.ts new file mode 100644 index 000000000..76f9ebc14 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { StaticTextRecordViewComponent } from './static-text.component'; + +describe('StaticTextRecordViewComponent', () => { + let component: StaticTextRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StaticTextRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(StaticTextRecordViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.spec.ts new file mode 100644 index 000000000..6c4e7127b --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TextRecordViewComponent } from './text.component'; + +describe('TextRecordViewComponent', () => { + let component: TextRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TextRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TextRecordViewComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should return false for isInvalid when value is empty', () => { + component.value = ''; + expect(component.isInvalid).toBe(false); + }); + + it('should return false for isInvalid when no validate widget param', () => { + component.value = 'sometext'; + component.widgetStructure = { widget_params: {} } as any; + expect(component.isInvalid).toBe(false); + }); + + it('should return true for isInvalid when email validation fails', () => { + component.value = 'notanemail'; + component.widgetStructure = { widget_params: { validate: 'isEmail' } } as any; + expect(component.isInvalid).toBe(true); + }); + + it('should return false for isInvalid when email validation passes', () => { + component.value = 'test@test.com'; + component.widgetStructure = { widget_params: { validate: 'isEmail' } } as any; + expect(component.isInvalid).toBe(false); + }); + + it('should return correct validationErrorMessage for isEmail', () => { + component.widgetStructure = { widget_params: { validate: 'isEmail' } } as any; + expect(component.validationErrorMessage).toBe('Invalid email address'); + }); + + it('should validate regex pattern', () => { + component.value = 'abc'; + component.widgetStructure = { widget_params: { validate: 'regex', regex: '^[0-9]+$' } } as any; + expect(component.isInvalid).toBe(true); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.spec.ts new file mode 100644 index 000000000..a6733d4fe --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.spec.ts @@ -0,0 +1,42 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TimeIntervalRecordViewComponent } from './time-interval.component'; + +describe('TimeIntervalRecordViewComponent', () => { + let component: TimeIntervalRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TimeIntervalRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeIntervalRecordViewComponent); + component = fixture.componentInstance; + // Don't call fixture.detectChanges() here - let individual tests set inputs first + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should format interval object', () => { + component.value = { hours: 2, minutes: 30 }; + component.ngOnInit(); + expect(component.formattedInterval).toBe('2h 30m'); + }); + + it('should display em dash for null value', () => { + component.value = null; + component.ngOnInit(); + expect(component.formattedInterval).toBe('—'); + }); + + it('should handle string JSON interval', () => { + component.value = '{"hours":1,"minutes":15}'; + component.ngOnInit(); + expect(component.formattedInterval).toBe('1h 15m'); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/time/time.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/time/time.component.spec.ts new file mode 100644 index 000000000..d78cb4584 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/time/time.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TimeRecordViewComponent } from './time.component'; + +describe('TimeRecordViewComponent', () => { + let component: TimeRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TimeRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeRecordViewComponent); + component = fixture.componentInstance; + // Don't call fixture.detectChanges() here - let individual tests set inputs first + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should preserve time string format', () => { + component.value = '14:30:00'; + component.ngOnInit(); + expect(component.formattedTime).toBe('14:30:00'); + }); + + it('should handle null value', () => { + component.value = null; + component.ngOnInit(); + expect(component.formattedTime).toBeUndefined(); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/url/url.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/url/url.component.spec.ts new file mode 100644 index 000000000..0f8040e2c --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/url/url.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UrlRecordViewComponent } from './url.component'; + +describe('UrlRecordViewComponent', () => { + let component: UrlRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UrlRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UrlRecordViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should return true for valid URL', () => { + component.value = 'https://example.com'; + expect(component.isValidUrl).toBe(true); + }); + + it('should return false for invalid URL', () => { + component.value = 'not-a-url'; + expect(component.isValidUrl).toBe(false); + }); + + it('should return false for empty value', () => { + component.value = ''; + expect(component.isValidUrl).toBe(false); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.spec.ts new file mode 100644 index 000000000..3650282fd --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UuidRecordViewComponent } from './uuid.component'; + +describe('UuidRecordViewComponent', () => { + let component: UuidRecordViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UuidRecordViewComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UuidRecordViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/markdown/markdown.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/markdown/markdown.component.spec.ts index 26ee34e10..746617c7b 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/markdown/markdown.component.spec.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/markdown/markdown.component.spec.ts @@ -16,7 +16,7 @@ describe('MarkdownDisplayComponent', () => { fixture = TestBed.createComponent(MarkdownDisplayComponent); component = fixture.componentInstance; - component.value = '# Test Markdown'; + fixture.componentRef.setInput('value', '# Test Markdown'); fixture.detectChanges(); }); @@ -26,68 +26,68 @@ describe('MarkdownDisplayComponent', () => { describe('Title extraction', () => { it('should extract H1 heading as title', () => { - component.value = '# Main Title\n\nSome paragraph text here.'; + fixture.componentRef.setInput('value', '# Main Title\n\nSome paragraph text here.'); component.ngOnInit(); expect(component.displayTitle).toBe('Main Title'); }); it('should extract H2 heading as title', () => { - component.value = '## Secondary Title\n\nContent follows.'; + fixture.componentRef.setInput('value', '## Secondary Title\n\nContent follows.'); component.ngOnInit(); expect(component.displayTitle).toBe('Secondary Title'); }); it('should prioritize heading over paragraph', () => { - component.value = 'Some intro text\n\n# Actual Title\n\nParagraph content.'; + fixture.componentRef.setInput('value', 'Some intro text\n\n# Actual Title\n\nParagraph content.'); component.ngOnInit(); expect(component.displayTitle).toBe('Actual Title'); }); it('should use first paragraph if no heading exists', () => { - component.value = 'This is the first paragraph.\n\nThis is the second paragraph.'; + fixture.componentRef.setInput('value', 'This is the first paragraph.\n\nThis is the second paragraph.'); component.ngOnInit(); expect(component.displayTitle).toBe('This is the first paragraph.'); }); it('should truncate long titles at 100 characters', () => { const longTitle = 'a'.repeat(150); - component.value = `# ${longTitle}`; + fixture.componentRef.setInput('value', `# ${longTitle}`); component.ngOnInit(); expect(component.displayTitle).toBe('a'.repeat(100) + '...'); }); it('should handle markdown with bold and italic formatting', () => { - component.value = '# **Bold** and *italic* title'; + fixture.componentRef.setInput('value', '# **Bold** and *italic* title'); component.ngOnInit(); expect(component.displayTitle).toBe('**Bold** and *italic* title'); }); it('should handle empty markdown', () => { - component.value = ''; + fixture.componentRef.setInput('value', ''); component.ngOnInit(); expect(component.displayTitle).toBe('—'); }); it('should handle null/undefined markdown', () => { - component.value = null as any; + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.displayTitle).toBe('—'); }); it('should handle markdown with only whitespace', () => { - component.value = ' \n\n '; + fixture.componentRef.setInput('value', ' \n\n '); component.ngOnInit(); expect(component.displayTitle).toBe('—'); }); it('should extract title from markdown with code blocks', () => { - component.value = '# API Documentation\n\n```javascript\nconst x = 5;\n```'; + fixture.componentRef.setInput('value', '# API Documentation\n\n```javascript\nconst x = 5;\n```'); component.ngOnInit(); expect(component.displayTitle).toBe('API Documentation'); }); it('should extract title from markdown with lists', () => { - component.value = '# Shopping List\n\n- Item 1\n- Item 2'; + fixture.componentRef.setInput('value', '# Shopping List\n\n- Item 1\n- Item 2'); component.ngOnInit(); expect(component.displayTitle).toBe('Shopping List'); }); diff --git a/frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.spec.ts index b2fd2598e..490009280 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.spec.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.spec.ts @@ -22,19 +22,19 @@ describe('TimezoneDisplayComponent', () => { }); it('should display formatted timezone with UTC offset', () => { - component.value = 'America/New_York'; + fixture.componentRef.setInput('value', 'America/New_York'); expect(component.formattedTimezone).toContain('America/New_York'); expect(component.formattedTimezone).toContain('UTC'); }); it('should display dash for null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); expect(component.formattedTimezone).toBe('—'); }); - it('should emit copy event on button click', () => { - vi.spyOn(component.onCopyToClipboard, 'emit'); - component.value = 'Europe/London'; + it('should render copy button', () => { + fixture.componentRef.setInput('value', 'Europe/London'); + fixture.detectChanges(); const compiled = fixture.nativeElement; const button = compiled.querySelector('button'); expect(button).toBeTruthy(); From f55595228795df2a70c81f54af960b1e3d0c6fab Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Mon, 9 Mar 2026 15:15:51 +0000 Subject: [PATCH 4/7] signals: record view fields --- .../base-record-view-field.component.ts | 19 ++++++------- .../binary-data-caption.component.html | 2 +- .../binary-data-caption.component.ts | 6 ++-- .../boolean/boolean.component.html | 12 ++++++-- .../boolean/boolean.component.spec.ts | 4 +-- .../boolean/boolean.component.ts | 5 ++-- .../code/code.component.spec.ts | 16 +++++------ .../record-view-fields/code/code.component.ts | 9 +++--- .../color/color.component.html | 14 ++++++---- .../color/color.component.spec.ts | 6 ++-- .../color/color.component.ts | 12 ++++---- .../country/country.component.html | 4 ++- .../country/country.component.spec.ts | 8 +++--- .../country/country.component.ts | 22 +++++++-------- .../date-time/date-time.component.spec.ts | 4 +-- .../date-time/date-time.component.ts | 11 ++++---- .../date/date.component.spec.ts | 6 ++-- .../record-view-fields/date/date.component.ts | 11 ++++---- .../file/file.component.spec.ts | 10 +++---- .../record-view-fields/file/file.component.ts | 9 +++--- .../foreign-key/foreign-key.component.html | 6 ++-- .../foreign-key/foreign-key.component.spec.ts | 2 +- .../foreign-key/foreign-key.component.ts | 16 +++++------ .../record-view-fields/id/id.component.html | 2 +- .../record-view-fields/id/id.component.ts | 3 +- .../image/image.component.html | 14 ++++++---- .../image/image.component.spec.ts | 16 +++++------ .../image/image.component.ts | 14 ++++------ .../json-editor/json-editor.component.spec.ts | 4 +-- .../json-editor/json-editor.component.ts | 7 ++--- .../long-text/long-text.component.html | 2 +- .../long-text/long-text.component.ts | 3 +- .../markdown/markdown.component.spec.ts | 2 +- .../markdown/markdown.component.ts | 8 ++---- .../money/money.component.spec.ts | 18 ++++++------ .../money/money.component.ts | 21 +++++++------- .../number/number.component.html | 8 ++++-- .../number/number.component.spec.ts | 16 +++++------ .../number/number.component.ts | 24 ++++++++-------- .../password/password.component.ts | 3 +- .../phone/phone.component.html | 12 +++++--- .../phone/phone.component.spec.ts | 6 ++-- .../phone/phone.component.ts | 16 +++++------ .../point/point.component.html | 12 ++++---- .../point/point.component.spec.ts | 6 ++-- .../point/point.component.ts | 17 ++++++----- .../range/range.component.spec.ts | 12 ++++---- .../range/range.component.ts | 18 ++++++------ .../record-view-fields/s3/s3.component.html | 28 +++++++++++-------- .../s3/s3.component.spec.ts | 8 +++--- .../record-view-fields/s3/s3.component.ts | 22 +++++++-------- .../select/select.component.spec.ts | 20 ++++++------- .../select/select.component.ts | 19 ++++++------- .../static-text/static-text.component.html | 2 +- .../static-text/static-text.component.ts | 3 +- .../text/text.component.html | 2 +- .../text/text.component.spec.ts | 20 ++++++------- .../record-view-fields/text/text.component.ts | 13 ++++----- .../time-interval.component.spec.ts | 6 ++-- .../time-interval/time-interval.component.ts | 9 +++--- .../time/time.component.spec.ts | 4 +-- .../record-view-fields/time/time.component.ts | 15 +++++----- .../timezone/timezone.component.spec.ts | 4 +-- .../timezone/timezone.component.ts | 13 ++++----- .../record-view-fields/url/url.component.html | 11 +++++--- .../url/url.component.spec.ts | 6 ++-- .../record-view-fields/url/url.component.ts | 10 +++---- .../uuid/uuid.component.html | 2 +- .../record-view-fields/uuid/uuid.component.ts | 3 +- 69 files changed, 343 insertions(+), 355 deletions(-) diff --git a/frontend/src/app/components/ui-components/record-view-fields/base-record-view-field/base-record-view-field.component.ts b/frontend/src/app/components/ui-components/record-view-fields/base-record-view-field/base-record-view-field.component.ts index b18cd91f5..e594d11ae 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/base-record-view-field/base-record-view-field.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/base-record-view-field/base-record-view-field.component.ts @@ -1,21 +1,18 @@ -import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, input, output } from '@angular/core'; import { TableField, WidgetStructure } from 'src/app/models/table'; @Component({ selector: 'app-base-record-view-field', templateUrl: './base-record-view-field.component.html', styleUrls: ['./base-record-view-field.component.css'], - imports: [CommonModule], }) export class BaseRecordViewFieldComponent { - @Input() key: string; - @Input() value: any; - @Input() structure: TableField; - @Input() widgetStructure: WidgetStructure; - @Input() rowData: Record; - @Input() primaryKeys: Record; - // @Input() relations: TableForeignKey; + readonly key = input(); + readonly value = input(); + readonly structure = input(); + readonly widgetStructure = input(); + readonly rowData = input>(); + readonly primaryKeys = input>(); - @Output() onCopyToClipboard = new EventEmitter(); + readonly onCopyToClipboard = output(); } diff --git a/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.html b/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.html index ea5a639c0..0e88e629b 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.html @@ -1,3 +1,3 @@
- {{value || '—'}} + {{value() || '—'}}
diff --git a/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.ts b/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.ts index ae72360c3..e5faa8310 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/binary-data-caption/binary-data-caption.component.ts @@ -1,15 +1,13 @@ -import { CommonModule } from '@angular/common'; -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-binary-data-caption-record-view', templateUrl: './binary-data-caption.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './binary-data-caption.component.css'], - imports: [CommonModule, MatIconModule, MatButtonModule, MatTooltipModule], + imports: [MatIconModule, MatButtonModule, MatTooltipModule], }) export class BinaryDataCaptionRecordViewComponent extends BaseRecordViewFieldComponent {} diff --git a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.html b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.html index 0064d88fd..6b24d5b26 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.html @@ -1,9 +1,15 @@ - check - close - +} +@if (value() !== true && value() !== false) { + +} diff --git a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.spec.ts index 2d5eb97c8..b222ebdab 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.spec.ts @@ -22,12 +22,12 @@ describe('BooleanRecordViewComponent', () => { }); it('should return false for invertColors when no widget params', () => { - component.widgetStructure = undefined; + fixture.componentRef.setInput('widgetStructure', undefined); expect(component.invertColors).toBe(false); }); it('should return true for invertColors when widget_params.invertColors is true', () => { - component.widgetStructure = { widget_params: { invertColors: true } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { invertColors: true } } as any); expect(component.invertColors).toBe(true); }); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.ts b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.ts index 7abc99b3a..629cef471 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/boolean/boolean.component.ts @@ -1,9 +1,8 @@ import { CommonModule } from '@angular/common'; -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-boolean-record-view', templateUrl: './boolean.component.html', @@ -12,6 +11,6 @@ import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-rec }) export class BooleanRecordViewComponent extends BaseRecordViewFieldComponent { get invertColors(): boolean { - return this.widgetStructure?.widget_params?.invertColors === true; + return this.widgetStructure()?.widget_params?.invertColors === true; } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/code/code.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/code/code.component.spec.ts index 4084ea99b..7c1df4898 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/code/code.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/code/code.component.spec.ts @@ -25,12 +25,12 @@ describe('CodeRecordViewComponent', () => { }); it('should set code model on init', () => { - component.value = 'console.log()'; - component.widgetStructure = { + fixture.componentRef.setInput('value', 'console.log()'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { language: 'javascript' }, field_name: 'code', - } as any; - component.key = 'test'; + }); + fixture.componentRef.setInput('key', 'test'); component.ngOnInit(); expect(component.codeModel).toEqual({ language: 'javascript', @@ -40,12 +40,12 @@ describe('CodeRecordViewComponent', () => { }); it('should use light theme when not dark mode', () => { - component.value = 'code'; - component.widgetStructure = { + fixture.componentRef.setInput('value', 'code'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { language: 'javascript' }, field_name: 'code', - } as any; - component.key = 'test'; + }); + fixture.componentRef.setInput('key', 'test'); component.ngOnInit(); expect(component.codeEditorTheme).toBe('vs'); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/code/code.component.ts b/frontend/src/app/components/ui-components/record-view-fields/code/code.component.ts index edc7c67bd..740f6a105 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/code/code.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/code/code.component.ts @@ -1,9 +1,8 @@ -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { CodeEditorModule } from '@ngstack/code-editor'; import { UiSettingsService } from 'src/app/services/ui-settings.service'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-code-record-view', templateUrl: './code.component.html', @@ -27,9 +26,9 @@ export class CodeRecordViewComponent extends BaseRecordViewFieldComponent { ngOnInit(): void { this.codeModel = { - language: `${this.widgetStructure.widget_params.language}`, - uri: `${this.key}.json`, - value: this.value, + language: `${this.widgetStructure().widget_params.language}`, + uri: `${this.key()}.json`, + value: this.value(), }; this.codeEditorTheme = this._uiSettings.isDarkMode ? 'vs-dark' : 'vs'; diff --git a/frontend/src/app/components/ui-components/record-view-fields/color/color.component.html b/frontend/src/app/components/ui-components/record-view-fields/color/color.component.html index 6e2da6998..38d77bb57 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/color/color.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/color/color.component.html @@ -1,8 +1,10 @@
-
- {{value || '—'}} + @if (isValidColor) { +
+ } + {{value() || '—'}}
\ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-view-fields/color/color.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/color/color.component.spec.ts index e958707df..dcb68057f 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/color/color.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/color/color.component.spec.ts @@ -22,17 +22,17 @@ describe('ColorRecordViewComponent', () => { }); it('should validate hex color', () => { - component.value = '#ff0000'; + fixture.componentRef.setInput('value', '#ff0000'); expect(component.isValidColor).toBe(true); }); it('should return false for invalid color', () => { - component.value = 'notacolor'; + fixture.componentRef.setInput('value', 'notacolor'); expect(component.isValidColor).toBe(false); }); it('should normalize color to hex format', () => { - component.value = '#ff0000'; + fixture.componentRef.setInput('value', '#ff0000'); expect(component.normalizedColorForDisplay).toBe('#ff0000'); }); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/color/color.component.ts b/frontend/src/app/components/ui-components/record-view-fields/color/color.component.ts index 4553fc2c0..fa2531fb4 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/color/color.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/color/color.component.ts @@ -1,23 +1,21 @@ -import { CommonModule } from '@angular/common'; -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import colorString from 'color-string'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-color-record-view', templateUrl: './color.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './color.component.css'], - imports: [CommonModule], + imports: [], }) export class ColorRecordViewComponent extends BaseRecordViewFieldComponent { get isValidColor(): boolean { - if (!this.value) return false; - return this.parseColor(this.value) !== null; + if (!this.value()) return false; + return this.parseColor(this.value()) !== null; } get normalizedColorForDisplay(): string { - const parsed = this.parseColor(this.value); + const parsed = this.parseColor(this.value()); if (parsed) { const [r, g, b] = parsed.value; return `#${this.toHex(r)}${this.toHex(g)}${this.toHex(b)}`; diff --git a/frontend/src/app/components/ui-components/record-view-fields/country/country.component.html b/frontend/src/app/components/ui-components/record-view-fields/country/country.component.html index 27895c83a..4ec31fad7 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/country/country.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/country/country.component.html @@ -1,4 +1,6 @@ - {{ countryFlag }} + @if (showFlag && countryFlag) { + {{ countryFlag }} + } {{ countryName }} diff --git a/frontend/src/app/components/ui-components/record-view-fields/country/country.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/country/country.component.spec.ts index ed807b9ce..553b08a8a 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/country/country.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/country/country.component.spec.ts @@ -23,20 +23,20 @@ describe('CountryRecordViewComponent', () => { }); it('should display country name from code', () => { - component.value = 'US'; + fixture.componentRef.setInput('value', 'US'); component.ngOnInit(); expect(component.countryName).toContain('United States'); }); it('should display em dash for null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.countryName).toBe('—'); }); it('should respect show_flag widget param', () => { - component.value = 'US'; - component.widgetStructure = { widget_params: { show_flag: false } } as any; + fixture.componentRef.setInput('value', 'US'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { show_flag: false } } as any); component.ngOnInit(); expect(component.showFlag).toBe(false); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/country/country.component.ts b/frontend/src/app/components/ui-components/record-view-fields/country/country.component.ts index 2d2b98a5f..bf6ba1654 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/country/country.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/country/country.component.ts @@ -1,14 +1,12 @@ -import { CommonModule } from '@angular/common'; -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { COUNTRIES, getCountryFlag } from '../../../../consts/countries'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-country-record-view', templateUrl: './country.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './country.component.css'], - imports: [CommonModule], + imports: [], }) export class CountryRecordViewComponent extends BaseRecordViewFieldComponent implements OnInit { static type = 'country'; @@ -20,10 +18,10 @@ export class CountryRecordViewComponent extends BaseRecordViewFieldComponent imp ngOnInit(): void { this.parseWidgetParams(); - if (this.value) { - const country = COUNTRIES.find((c) => c.code === this.value); - this.countryName = country ? country.name : this.value; - this.countryFlag = getCountryFlag(this.value); + if (this.value()) { + const country = COUNTRIES.find((c) => c.code === this.value()); + this.countryName = country ? country.name : this.value(); + this.countryFlag = getCountryFlag(this.value()); } else { this.countryName = '—'; this.countryFlag = ''; @@ -31,12 +29,12 @@ export class CountryRecordViewComponent extends BaseRecordViewFieldComponent imp } private parseWidgetParams(): void { - if (this.widgetStructure?.widget_params) { + if (this.widgetStructure()?.widget_params) { try { const params = - typeof this.widgetStructure.widget_params === 'string' - ? JSON.parse(this.widgetStructure.widget_params) - : this.widgetStructure.widget_params; + typeof this.widgetStructure().widget_params === 'string' + ? JSON.parse(this.widgetStructure().widget_params as unknown as string) + : this.widgetStructure().widget_params; if (params.show_flag !== undefined) { this.showFlag = params.show_flag; diff --git a/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.spec.ts index ed54784ef..1a65f53f1 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.spec.ts @@ -23,13 +23,13 @@ describe('DateTimeRecordViewComponent', () => { }); it('should format valid datetime', () => { - component.value = '2023-04-29T14:30:00'; + fixture.componentRef.setInput('value', '2023-04-29T14:30:00'); component.ngOnInit(); expect(component.formattedDateTime).toBeDefined(); }); it('should handle invalid datetime', () => { - component.value = 'invalid'; + fixture.componentRef.setInput('value', 'invalid'); component.ngOnInit(); expect(component.formattedDateTime).toBe('invalid'); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.ts b/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.ts index 0d734bd9f..1b7a38078 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/date-time/date-time.component.ts @@ -1,8 +1,7 @@ -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { format } from 'date-fns'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-date-time-record-view', templateUrl: './date-time.component.html', @@ -15,16 +14,16 @@ export class DateTimeRecordViewComponent extends BaseRecordViewFieldComponent im public formattedDateTime: string; ngOnInit(): void { - if (this.value) { + if (this.value()) { try { - const date = new Date(this.value); + const date = new Date(this.value()); if (!Number.isNaN(date.getTime())) { this.formattedDateTime = format(date, 'P p'); } else { - this.formattedDateTime = this.value; + this.formattedDateTime = this.value(); } } catch (_error) { - this.formattedDateTime = this.value; + this.formattedDateTime = this.value(); } } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/date/date.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/date/date.component.spec.ts index 918c9ac1d..84ec9af92 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/date/date.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/date/date.component.spec.ts @@ -23,19 +23,19 @@ describe('DateRecordViewComponent', () => { }); it('should format valid date', () => { - component.value = '2023-04-29'; + fixture.componentRef.setInput('value', '2023-04-29'); component.ngOnInit(); expect(component.formattedDate).toBeDefined(); }); it('should handle invalid date', () => { - component.value = 'invalid'; + fixture.componentRef.setInput('value', 'invalid'); component.ngOnInit(); expect(component.formattedDate).toBe('invalid'); }); it('should handle null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.formattedDate).toBeUndefined(); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/date/date.component.ts b/frontend/src/app/components/ui-components/record-view-fields/date/date.component.ts index 72a7b0290..ec1cd1e28 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/date/date.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/date/date.component.ts @@ -1,9 +1,8 @@ -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { format } from 'date-fns'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-date-record-view', templateUrl: './date.component.html', @@ -16,16 +15,16 @@ export class DateRecordViewComponent extends BaseRecordViewFieldComponent implem public formattedDate: string; ngOnInit(): void { - if (this.value) { + if (this.value()) { try { - const date = new Date(this.value); + const date = new Date(this.value()); if (!Number.isNaN(date.getTime())) { this.formattedDate = format(date, 'P'); } else { - this.formattedDate = this.value; + this.formattedDate = this.value(); } } catch (_error) { - this.formattedDate = this.value; + this.formattedDate = this.value(); } } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/file/file.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/file/file.component.spec.ts index f41919a6f..710a61256 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/file/file.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/file/file.component.spec.ts @@ -22,27 +22,27 @@ describe('FileRecordViewComponent', () => { }); it('should identify blob objects', () => { - component.value = { type: 'Buffer', data: [1, 2] }; + fixture.componentRef.setInput('value', { type: 'Buffer', data: [1, 2] }); expect(component.isBlob).toBe(true); }); it('should return Binary Data for blobs', () => { - component.value = { type: 'Buffer', data: [1, 2] }; + fixture.componentRef.setInput('value', { type: 'Buffer', data: [1, 2] }); expect(component.displayText).toBe('Binary Data'); }); it('should return Binary Data for long strings', () => { - component.value = 'a]bcdefghijklmnopqrstu'; + fixture.componentRef.setInput('value', 'a]bcdefghijklmnopqrstu'); expect(component.displayText).toBe('Binary Data'); }); it('should return the value for short strings', () => { - component.value = 'short.txt'; + fixture.componentRef.setInput('value', 'short.txt'); expect(component.displayText).toBe('short.txt'); }); it('should return dash for null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); expect(component.displayText).toBe('—'); }); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/file/file.component.ts b/frontend/src/app/components/ui-components/record-view-fields/file/file.component.ts index 87ca8ee33..51d4a2001 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/file/file.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/file/file.component.ts @@ -1,4 +1,4 @@ -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; interface Blob { @@ -6,7 +6,6 @@ interface Blob { data: any[]; } -@Injectable() @Component({ selector: 'app-file-record-view', templateUrl: './file.component.html', @@ -15,15 +14,15 @@ interface Blob { }) export class FileRecordViewComponent extends BaseRecordViewFieldComponent { get isBlob(): boolean { - return typeof this.value === 'object' && this.value !== null && 'type' in this.value && 'data' in this.value; + return typeof this.value() === 'object' && this.value() !== null && 'type' in this.value() && 'data' in this.value(); } get displayText(): string { if (this.isBlob) { return 'Binary Data'; - } else if (typeof this.value === 'string' && this.value.length > 20) { + } else if (typeof this.value() === 'string' && this.value().length > 20) { return 'Binary Data'; } - return this.value ? String(this.value) : '—'; + return this.value() ? String(this.value()) : '—'; } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.html b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.html index 99a6f0a99..a5b804a17 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.html @@ -1,8 +1,8 @@ - {{displayValue}} + (click)="onForeignKeyClick.emit({ foreignKey: primaryKeysParams(), value: displayValue() })"> + {{displayValue()}} visibility diff --git a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts index 426e4f919..ede913808 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts @@ -23,7 +23,7 @@ describe('ForeignKeyRecordViewComponent', () => { }); it('should set foreignKeyURLParams on init', () => { - component.primaryKeysParams = { id: 1 }; + fixture.componentRef.setInput('primaryKeysParams', { id: 1 }); component.ngOnInit(); expect(component.foreignKeyURLParams).toEqual({ id: 1, mode: 'view' }); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.ts b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.ts index 4e1be41fd..d35fa1384 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.ts @@ -1,26 +1,24 @@ -import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Injectable, Input, OnInit, Output } from '@angular/core'; +import { Component, input, OnInit, output } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { RouterModule } from '@angular/router'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-foreign-key-record-view', templateUrl: './foreign-key.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './foreign-key.component.css'], - imports: [MatIconModule, RouterModule, CommonModule], + imports: [MatIconModule, RouterModule], }) export class ForeignKeyRecordViewComponent extends BaseRecordViewFieldComponent implements OnInit { - @Input() link: string; - @Input() primaryKeysParams: any; - @Input() displayValue: string; + readonly link = input(); + readonly primaryKeysParams = input(); + readonly displayValue = input(); - @Output() onForeignKeyClick = new EventEmitter<{ foreignKey: any; value: string }>(); + readonly onForeignKeyClick = output<{ foreignKey: any; value: string }>(); public foreignKeyURLParams: any; ngOnInit() { - this.foreignKeyURLParams = { ...this.primaryKeysParams, mode: 'view' }; + this.foreignKeyURLParams = { ...this.primaryKeysParams(), mode: 'view' }; } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/id/id.component.html b/frontend/src/app/components/ui-components/record-view-fields/id/id.component.html index a7259d1fb..2e72a7973 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/id/id.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/id/id.component.html @@ -1 +1 @@ -{{value || '—'}} +{{value() || '—'}} diff --git a/frontend/src/app/components/ui-components/record-view-fields/id/id.component.ts b/frontend/src/app/components/ui-components/record-view-fields/id/id.component.ts index bb01857af..ca33f2197 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/id/id.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/id/id.component.ts @@ -1,8 +1,7 @@ -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-id-record-view', templateUrl: './id.component.html', diff --git a/frontend/src/app/components/ui-components/record-view-fields/image/image.component.html b/frontend/src/app/components/ui-components/record-view-fields/image/image.component.html index 421d0f49e..375785e8d 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/image/image.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/image/image.component.html @@ -1,5 +1,9 @@ -[Image URL] -Image -{{value || '—'}} +@if (!isUrl) { + [Image URL] +} +@if (isUrl) { + Image +} +{{value() || '—'}} diff --git a/frontend/src/app/components/ui-components/record-view-fields/image/image.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/image/image.component.spec.ts index 924adac4d..980a45d4a 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/image/image.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/image/image.component.spec.ts @@ -22,26 +22,26 @@ describe('ImageRecordViewComponent', () => { }); it('should return value as srcValue when no prefix', () => { - component.value = 'image.png'; - component.widgetStructure = undefined; + fixture.componentRef.setInput('value', 'image.png'); + fixture.componentRef.setInput('widgetStructure', undefined); expect(component.srcValue).toBe('image.png'); }); it('should prepend prefix to srcValue from widget params', () => { - component.value = 'image.png'; - component.widgetStructure = { widget_params: { prefix: 'https://cdn.example.com/' } } as any; + fixture.componentRef.setInput('value', 'image.png'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { prefix: 'https://cdn.example.com/' } } as any); expect(component.srcValue).toBe('https://cdn.example.com/image.png'); }); it('should return true for isUrl when valid URL', () => { - component.value = 'image.png'; - component.widgetStructure = { widget_params: { prefix: 'https://cdn.example.com/' } } as any; + fixture.componentRef.setInput('value', 'image.png'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { prefix: 'https://cdn.example.com/' } } as any); expect(component.isUrl).toBe(true); }); it('should return false for isUrl when invalid URL', () => { - component.value = 'image.png'; - component.widgetStructure = undefined; + fixture.componentRef.setInput('value', 'image.png'); + fixture.componentRef.setInput('widgetStructure', undefined); expect(component.isUrl).toBe(false); }); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/image/image.component.ts b/frontend/src/app/components/ui-components/record-view-fields/image/image.component.ts index 36eb14eb9..b6ac25e49 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/image/image.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/image/image.component.ts @@ -1,23 +1,21 @@ -import { CommonModule } from '@angular/common'; -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-image-record-view', templateUrl: './image.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './image.component.css'], - imports: [CommonModule], + imports: [], }) export class ImageRecordViewComponent extends BaseRecordViewFieldComponent { get srcValue(): string { - if (!this.value) return ''; - const prefix = this.widgetStructure?.widget_params?.prefix || ''; - return prefix + this.value; + if (!this.value()) return ''; + const prefix = this.widgetStructure()?.widget_params?.prefix || ''; + return prefix + this.value(); } get isUrl(): boolean { - if (!this.value) return false; + if (!this.value()) return false; try { // Check if the prefixed URL is valid new URL(this.srcValue); diff --git a/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.spec.ts index 7a82ce011..2ab2029e9 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.spec.ts @@ -25,8 +25,8 @@ describe('JsonEditorRecordViewComponent', () => { }); it('should set JSON code model on init', () => { - component.value = '{"key":"value"}'; - component.key = 'test'; + fixture.componentRef.setInput('value', '{"key":"value"}'); + fixture.componentRef.setInput('key', 'test'); component.ngOnInit(); expect(component.codeModel).toEqual({ language: 'json', diff --git a/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.ts b/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.ts index 98fb2ee25..205810467 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/json-editor/json-editor.component.ts @@ -1,9 +1,8 @@ -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { CodeEditorModule } from '@ngstack/code-editor'; import { UiSettingsService } from 'src/app/services/ui-settings.service'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-json-editor-record-view', templateUrl: './json-editor.component.html', @@ -28,8 +27,8 @@ export class JsonEditorRecordViewComponent extends BaseRecordViewFieldComponent ngOnInit(): void { this.codeModel = { language: 'json', - uri: `${this.key}.json`, - value: this.value, + uri: `${this.key()}.json`, + value: this.value(), }; this.codeEditorTheme = this._uiSettings.isDarkMode ? 'vs-dark' : 'vs'; diff --git a/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.html b/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.html index 7dfcb3f4b..a071ebc00 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.html @@ -1 +1 @@ -{{value || '—'}} +{{value() || '—'}} diff --git a/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.ts b/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.ts index 991bc8272..88e693289 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/long-text/long-text.component.ts @@ -1,8 +1,7 @@ -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-long-text-record-view', templateUrl: './long-text.component.html', diff --git a/frontend/src/app/components/ui-components/record-view-fields/markdown/markdown.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/markdown/markdown.component.spec.ts index d7a1af29d..113d9c9fc 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/markdown/markdown.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/markdown/markdown.component.spec.ts @@ -16,7 +16,7 @@ describe('MarkdownRecordViewComponent', () => { fixture = TestBed.createComponent(MarkdownRecordViewComponent); component = fixture.componentInstance; - component.value = '# Test Markdown'; + fixture.componentRef.setInput('value', '# Test Markdown'); fixture.detectChanges(); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/markdown/markdown.component.ts b/frontend/src/app/components/ui-components/record-view-fields/markdown/markdown.component.ts index 09dda79c5..bec5e93de 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/markdown/markdown.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/markdown/markdown.component.ts @@ -1,19 +1,17 @@ -import { CommonModule } from '@angular/common'; -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MarkdownModule } from 'ngx-markdown'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-markdown-record-view', templateUrl: './markdown.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './markdown.component.css'], - imports: [CommonModule, MarkdownModule], + imports: [MarkdownModule], }) export class MarkdownRecordViewComponent extends BaseRecordViewFieldComponent implements OnInit { public renderedMarkdown: string = ''; ngOnInit(): void { - this.renderedMarkdown = this.value || ''; + this.renderedMarkdown = this.value() || ''; } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/money/money.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/money/money.component.spec.ts index de226c036..536575c3d 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/money/money.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/money/money.component.spec.ts @@ -21,29 +21,29 @@ describe('MoneyRecordViewComponent', () => { }); it('should format value with currency symbol', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { default_currency: 'USD' }, - } as any; - component.value = 42.5; + }); + fixture.componentRef.setInput('value', 42.5); component.ngOnInit(); expect(component.formattedValue).toContain('$'); expect(component.formattedValue).toContain('42.50'); }); it('should return empty string for null value', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { default_currency: 'USD' }, - } as any; - component.value = null; + }); + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.formattedValue).toBe(''); }); it('should handle object value with amount and currency', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { default_currency: 'USD' }, - } as any; - component.value = { amount: 100, currency: 'EUR' }; + }); + fixture.componentRef.setInput('value', { amount: 100, currency: 'EUR' }); component.ngOnInit(); expect(component.formattedValue).toContain('100.00'); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/money/money.component.ts b/frontend/src/app/components/ui-components/record-view-fields/money/money.component.ts index 155d917c5..fcf71c41d 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/money/money.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/money/money.component.ts @@ -1,8 +1,7 @@ -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { getCurrencyByCode } from 'src/app/consts/currencies'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-money-record-view', templateUrl: './money.component.html', @@ -16,30 +15,30 @@ export class MoneyRecordViewComponent extends BaseRecordViewFieldComponent imple ngOnInit(): void { // Get currency from widget params this.displayCurrency = ''; - if (this.widgetStructure?.widget_params?.default_currency) { - this.displayCurrency = this.widgetStructure.widget_params.default_currency; + if (this.widgetStructure()?.widget_params?.default_currency) { + this.displayCurrency = this.widgetStructure().widget_params.default_currency; const currency = getCurrencyByCode(this.displayCurrency); this.currencySymbol = currency ? currency.symbol : ''; } } get formattedValue(): string { - if (!this.value) { + if (!this.value()) { return ''; } let amount: number | string; let currency: string = this.displayCurrency; - if (typeof this.value === 'object' && this.value.amount !== undefined) { - amount = this.value.amount; - if (this.value.currency) { - currency = this.value.currency; + if (typeof this.value() === 'object' && this.value().amount !== undefined) { + amount = this.value().amount; + if (this.value().currency) { + currency = this.value().currency; const currencyObj = getCurrencyByCode(currency); this.currencySymbol = currencyObj ? currencyObj.symbol : ''; } } else { - amount = this.value; + amount = this.value(); } if (typeof amount === 'string') { @@ -50,7 +49,7 @@ export class MoneyRecordViewComponent extends BaseRecordViewFieldComponent imple return ''; } - const decimalPlaces = this.widgetStructure?.widget_params?.decimal_places ?? 2; + const decimalPlaces = this.widgetStructure()?.widget_params?.decimal_places ?? 2; return `${this.currencySymbol}${(amount as number).toFixed(decimalPlaces)}`; } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html index 55d954de1..d2dfc17a3 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.html @@ -1,5 +1,9 @@ {{displayValue}} - north - south + @if (isOutOfThreshold === 'down') { + north + } + @if (isOutOfThreshold === 'up') { + south + } diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.spec.ts index 460a7d56b..779fe5c04 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.spec.ts @@ -22,30 +22,30 @@ describe('NumberRecordViewComponent', () => { }); it('should display em dash for null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); expect(component.displayValue).toBe('—'); }); it('should display number as string when no unit', () => { - component.value = 42; + fixture.componentRef.setInput('value', 42); expect(component.displayValue).toBe('42'); }); it('should return false for isOutOfThreshold when no thresholds', () => { - component.value = '50'; - component.widgetStructure = { widget_params: {} } as any; + fixture.componentRef.setInput('value', '50'); + fixture.componentRef.setInput('widgetStructure', { widget_params: {} } as any); expect(component.isOutOfThreshold).toBe(false); }); it('should return down when value below threshold_min', () => { - component.value = '5'; - component.widgetStructure = { widget_params: { threshold_min: 10 } } as any; + fixture.componentRef.setInput('value', '5'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { threshold_min: 10 } } as any); expect(component.isOutOfThreshold).toBe('down'); }); it('should return up when value above threshold_max', () => { - component.value = '150'; - component.widgetStructure = { widget_params: { threshold_max: 100 } } as any; + fixture.componentRef.setInput('value', '150'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { threshold_max: 100 } } as any); expect(component.isOutOfThreshold).toBe('up'); }); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts index 30b9fdc64..79993151b 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.ts @@ -1,47 +1,45 @@ -import { CommonModule } from '@angular/common'; -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import convert from 'convert'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-number-record-view', templateUrl: './number.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './number.component.css'], - imports: [CommonModule, MatIconModule], + imports: [MatIconModule], }) export class NumberRecordViewComponent extends BaseRecordViewFieldComponent { get displayValue(): string { - if (this.value == null || this.value === '') { + if (this.value() == null || this.value() === '') { return '—'; } - const unit = this.widgetStructure?.widget_params?.unit; + const unit = this.widgetStructure()?.widget_params?.unit; if (!unit) { - return this.value.toString(); + return this.value().toString(); } try { - const convertedValue = convert(parseFloat(this.value), unit).to('best'); + const convertedValue = convert(parseFloat(this.value()), unit).to('best'); // Format number to max 2 decimal places without trailing zeros const formattedQuantity = parseFloat(convertedValue.quantity.toFixed(2)).toString(); return `${formattedQuantity} ${convertedValue.unit}`; } catch (error) { console.warn('Unit conversion failed:', error); - return this.value.toString(); + return this.value().toString(); } } get isOutOfThreshold(): 'up' | 'down' | false { - if (this.value == null || this.value === '') { + if (this.value() == null || this.value() === '') { return false; } - const thresholdMin = this.widgetStructure?.widget_params?.threshold_min; - const thresholdMax = this.widgetStructure?.widget_params?.threshold_max; - const numValue = parseFloat(this.value); + const thresholdMin = this.widgetStructure()?.widget_params?.threshold_min; + const thresholdMax = this.widgetStructure()?.widget_params?.threshold_max; + const numValue = parseFloat(this.value()); if (thresholdMin !== undefined && numValue < thresholdMin) { return 'down'; diff --git a/frontend/src/app/components/ui-components/record-view-fields/password/password.component.ts b/frontend/src/app/components/ui-components/record-view-fields/password/password.component.ts index eb283a909..7eb00cf86 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/password/password.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/password/password.component.ts @@ -1,7 +1,6 @@ -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-password-record-view', templateUrl: './password.component.html', diff --git a/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.html b/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.html index c7aad71e8..15fbf3e5f 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.html @@ -1,4 +1,8 @@ - - {{ countryFlag }} - {{ formattedNumber || value }} - \ No newline at end of file +@if (value()) { + + @if (countryFlag) { + {{ countryFlag }} + } + {{ formattedNumber || value() }} + +} \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.spec.ts index bd7c853fa..57f70fb50 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.spec.ts @@ -21,20 +21,20 @@ describe('PhoneRecordViewComponent', () => { }); it('should parse valid phone number', () => { - component.value = '+12025551234'; + fixture.componentRef.setInput('value', '+12025551234'); component.ngOnInit(); expect(component.formattedNumber).toBeDefined(); expect(component.formattedNumber).not.toBe(''); }); it('should handle invalid phone number gracefully', () => { - component.value = 'not-a-phone'; + fixture.componentRef.setInput('value', 'not-a-phone'); component.ngOnInit(); expect(component.formattedNumber).toBe('not-a-phone'); }); it('should handle null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.formattedNumber).toBe(''); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.ts b/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.ts index 3c3b9c56e..04db89a27 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/phone/phone.component.ts @@ -1,16 +1,14 @@ -import { CommonModule } from '@angular/common'; -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MatTooltipModule } from '@angular/material/tooltip'; import { parsePhoneNumber } from 'libphonenumber-js'; import { COUNTRIES, getCountryFlag } from '../../../../consts/countries'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-phone-record-view', templateUrl: './phone.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './phone.component.css'], - imports: [MatTooltipModule, CommonModule], + imports: [MatTooltipModule], }) export class PhoneRecordViewComponent extends BaseRecordViewFieldComponent implements OnInit { public countryFlag: string = ''; @@ -22,7 +20,7 @@ export class PhoneRecordViewComponent extends BaseRecordViewFieldComponent imple } private parsePhoneNumber(): void { - if (!this.value || typeof this.value !== 'string') { + if (!this.value() || typeof this.value() !== 'string') { this.countryFlag = ''; this.countryName = ''; this.formattedNumber = ''; @@ -30,7 +28,7 @@ export class PhoneRecordViewComponent extends BaseRecordViewFieldComponent imple } try { - const phoneNumber = parsePhoneNumber(this.value); + const phoneNumber = parsePhoneNumber(this.value()); if (phoneNumber?.country) { const country = COUNTRIES.find((c) => c.code === phoneNumber.country); @@ -42,17 +40,17 @@ export class PhoneRecordViewComponent extends BaseRecordViewFieldComponent imple } else { this.countryFlag = ''; this.countryName = ''; - this.formattedNumber = this.value; + this.formattedNumber = this.value(); } } else { this.countryFlag = ''; this.countryName = ''; - this.formattedNumber = this.value; + this.formattedNumber = this.value(); } } catch (_error) { this.countryFlag = ''; this.countryName = ''; - this.formattedNumber = this.value; + this.formattedNumber = this.value(); } } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/point/point.component.html b/frontend/src/app/components/ui-components/record-view-fields/point/point.component.html index 5ea1fcb37..54eb81845 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/point/point.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/point/point.component.html @@ -1,5 +1,7 @@ - - {{ formattedPoint }} - +@if (formattedPoint) { + + {{ formattedPoint }} + +} diff --git a/frontend/src/app/components/ui-components/record-view-fields/point/point.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/point/point.component.spec.ts index d4566e697..6598a0de0 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/point/point.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/point/point.component.spec.ts @@ -21,19 +21,19 @@ describe('PointRecordViewComponent', () => { }); it('should format string point', () => { - component.value = '(1,2)'; + fixture.componentRef.setInput('value', '(1,2)'); component.ngOnInit(); expect(component.formattedPoint).toBe('(1, 2)'); }); it('should format object point', () => { - component.value = { x: 3, y: 4 }; + fixture.componentRef.setInput('value', { x: 3, y: 4 }); component.ngOnInit(); expect(component.formattedPoint).toBe('(3, 4)'); }); it('should handle null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.formattedPoint).toBe(''); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/point/point.component.ts b/frontend/src/app/components/ui-components/record-view-fields/point/point.component.ts index e017915f2..d1d70dce7 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/point/point.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/point/point.component.ts @@ -1,8 +1,7 @@ -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MatTooltipModule } from '@angular/material/tooltip'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-point-record-view', templateUrl: './point.component.html', @@ -19,25 +18,25 @@ export class PointRecordViewComponent extends BaseRecordViewFieldComponent imple } private formatPoint() { - if (!this.value) { + if (!this.value()) { this.formattedPoint = ''; return; } try { - if (typeof this.value === 'string') { + if (typeof this.value() === 'string') { // Handle string format like "(x,y)" or "x,y" - const pointStr = this.value.trim().replace(/[()]/g, ''); + const pointStr = this.value().trim().replace(/[()]/g, ''); const [x, y] = pointStr.split(',').map((coord) => parseFloat(coord.trim())); this.formattedPoint = `(${x}, ${y})`; - } else if (typeof this.value === 'object') { + } else if (typeof this.value() === 'object') { // Handle object format like {x: 1, y: 2} - const x = this.value.x || this.value[0]; - const y = this.value.y || this.value[1]; + const x = this.value().x || this.value()[0]; + const y = this.value().y || this.value()[1]; this.formattedPoint = `(${x}, ${y})`; } } catch (_e) { - this.formattedPoint = String(this.value); + this.formattedPoint = String(this.value()); } } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.spec.ts index 703a38e88..15900122d 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.spec.ts @@ -21,28 +21,28 @@ describe('RangeRecordViewComponent', () => { }); it('should display value with default max', () => { - component.value = 50; + fixture.componentRef.setInput('value', 50); component.ngOnInit(); expect(component.displayValue).toBe('50 / 100'); }); it('should parse widget params for min/max', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { min: 0, max: 200 }, - } as any; - component.value = 100; + }); + fixture.componentRef.setInput('value', 100); component.ngOnInit(); expect(component.displayValue).toBe('100 / 200'); }); it('should calculate progress value', () => { - component.value = 50; + fixture.componentRef.setInput('value', 50); component.ngOnInit(); expect(component.getProgressValue()).toBe(50); }); it('should clamp progress value', () => { - component.value = 150; + fixture.componentRef.setInput('value', 150); component.ngOnInit(); expect(component.getProgressValue()).toBe(100); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts index 560906e18..da49ab3f7 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts @@ -1,6 +1,4 @@ -import { CommonModule } from '@angular/common'; - -import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Component, input, OnChanges, OnInit } from '@angular/core'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; @@ -9,10 +7,10 @@ import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-rec standalone: true, templateUrl: './range.component.html', styleUrls: ['./range.component.css'], - imports: [CommonModule, MatProgressBarModule], + imports: [MatProgressBarModule], }) export class RangeRecordViewComponent extends BaseRecordViewFieldComponent implements OnInit, OnChanges { - @Input() declare value: number; + override readonly value = input(); static type = 'range'; public min: number = 0; @@ -31,7 +29,7 @@ export class RangeRecordViewComponent extends BaseRecordViewFieldComponent imple } public getProgressValue(): number { - const numValue = Number(this.value) || 0; + const numValue = Number(this.value()) || 0; const range = this.max - this.min; if (range === 0) return 0; const progress = ((numValue - this.min) / range) * 100; @@ -40,10 +38,10 @@ export class RangeRecordViewComponent extends BaseRecordViewFieldComponent imple } private _parseWidgetParams(): void { - console.log('Parsing widget params:', this.widgetStructure?.widget_params); - if (this.widgetStructure?.widget_params) { + console.log('Parsing widget params:', this.widgetStructure()?.widget_params); + if (this.widgetStructure()?.widget_params) { try { - const params = this.widgetStructure.widget_params; + const params = this.widgetStructure().widget_params; if (params.min !== undefined) { this.min = Number(params.min) || 0; } @@ -61,7 +59,7 @@ export class RangeRecordViewComponent extends BaseRecordViewFieldComponent imple } private _updateDisplayValue(): void { - const numValue = Number(this.value) || 0; + const numValue = Number(this.value()) || 0; this.displayValue = `${numValue} / ${this.max}`; } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.html b/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.html index 86e19c6a6..c2b400c42 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.html @@ -1,12 +1,16 @@ - - - S3 Image - [S3 Image] - - - - {{value || '—'}} - +@if (isImageType) { + @if (isLoading) { + + } + @if (previewUrl && !isLoading) { + S3 Image + } + @if (!previewUrl && !isLoading && value()) { + [S3 Image] + } +} @else { + {{value() || '—'}} +} diff --git a/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.spec.ts index 00178f65c..089b3ccb6 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.spec.ts @@ -40,28 +40,28 @@ describe('S3RecordViewComponent', () => { }); it('should parse widget params', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { bucket: 'my-bucket', type: 'file', aws_access_key_id_secret_name: 'key', aws_secret_access_key_secret_name: 'secret', }, - } as any; + }); component.ngOnInit(); expect(component.params).toBeDefined(); expect(component.params.bucket).toBe('my-bucket'); }); it('should identify image type', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { bucket: 'my-bucket', type: 'image', aws_access_key_id_secret_name: 'key', aws_secret_access_key_secret_name: 'secret', }, - } as any; + }); component.ngOnInit(); expect(component.isImageType).toBe(true); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.ts b/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.ts index 3c034b1d3..9682f30f6 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/s3/s3.component.ts @@ -1,5 +1,4 @@ -import { CommonModule } from '@angular/common'; -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ConnectionsService } from 'src/app/services/connections.service'; import { S3Service } from 'src/app/services/s3.service'; @@ -15,12 +14,11 @@ interface S3WidgetParams { type?: 'file' | 'image'; } -@Injectable() @Component({ selector: 'app-s3-record-view', templateUrl: './s3.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './s3.component.css'], - imports: [CommonModule, MatProgressSpinnerModule], + imports: [MatProgressSpinnerModule], }) export class S3RecordViewComponent extends BaseRecordViewFieldComponent implements OnInit { public params: S3WidgetParams; @@ -43,7 +41,7 @@ export class S3RecordViewComponent extends BaseRecordViewFieldComponent implemen this.tableName = this.tablesService.currentTableName; this._parseWidgetParams(); - if (this.value && this.isImageType && this.primaryKeys) { + if (this.value() && this.isImageType && this.primaryKeys()) { this._loadPreview(); } } @@ -53,12 +51,12 @@ export class S3RecordViewComponent extends BaseRecordViewFieldComponent implemen } private _parseWidgetParams(): void { - if (this.widgetStructure?.widget_params) { + if (this.widgetStructure()?.widget_params) { try { this.params = - typeof this.widgetStructure.widget_params === 'string' - ? JSON.parse(this.widgetStructure.widget_params) - : this.widgetStructure.widget_params; + typeof this.widgetStructure().widget_params === 'string' + ? JSON.parse(this.widgetStructure().widget_params as unknown as string) + : this.widgetStructure().widget_params; } catch (e) { console.error('Error parsing S3 widget params:', e); } @@ -66,15 +64,15 @@ export class S3RecordViewComponent extends BaseRecordViewFieldComponent implemen } private async _loadPreview(): Promise { - if (!this.value || !this.connectionId || !this.tableName || !this.primaryKeys) return; + if (!this.value() || !this.connectionId || !this.tableName || !this.primaryKeys()) return; this.isLoading = true; const response = await this.s3Service.getFileUrl( this.connectionId, this.tableName, - this.widgetStructure.field_name, - this.primaryKeys, + this.widgetStructure().field_name, + this.primaryKeys(), ); if (response) { diff --git a/frontend/src/app/components/ui-components/record-view-fields/select/select.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/select/select.component.spec.ts index 44ce71f39..92f2d2ba5 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/select/select.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/select/select.component.spec.ts @@ -21,40 +21,40 @@ describe('SelectRecordViewComponent', () => { }); it('should display dash for null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.displayValue).toBe('—'); }); it('should display option label when matching widget option', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { options: [{ value: 'a', label: 'Alpha' }], }, - } as any; - component.value = 'a'; + }); + fixture.componentRef.setInput('value', 'a'); component.ngOnInit(); expect(component.displayValue).toBe('Alpha'); }); it('should display raw value when no matching option', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { options: [{ value: 'b', label: 'Beta' }], }, - } as any; - component.value = 'unknown'; + }); + fixture.componentRef.setInput('value', 'unknown'); component.ngOnInit(); expect(component.displayValue).toBe('unknown'); }); it('should set backgroundColor from matching option', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { options: [{ value: 'a', label: 'Alpha', background_color: '#ff0000' }], }, - } as any; - component.value = 'a'; + }); + fixture.componentRef.setInput('value', 'a'); component.ngOnInit(); expect(component.backgroundColor).toBe('#ff0000'); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/select/select.component.ts b/frontend/src/app/components/ui-components/record-view-fields/select/select.component.ts index 2d4349071..b87d76f39 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/select/select.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/select/select.component.ts @@ -1,8 +1,7 @@ -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-select-record-view', templateUrl: './select.component.html', @@ -18,23 +17,23 @@ export class SelectRecordViewComponent extends BaseRecordViewFieldComponent impl } private setDisplayValue(): void { - if (!this.value) { + if (!this.value()) { this.displayValue = '—'; return; } - if (this.widgetStructure?.widget_params?.options) { + if (this.widgetStructure()?.widget_params?.options) { // Find the matching option based on value and use its label - const option = this.widgetStructure.widget_params.options.find( - (opt: { value: any; label: string }) => opt.value === this.value, + const option = this.widgetStructure().widget_params.options.find( + (opt: { value: any; label: string }) => opt.value === this.value(), ); - this.displayValue = option ? option.label : this.value; + this.displayValue = option ? option.label : this.value(); this.backgroundColor = option?.background_color ? option.background_color : 'transparent'; - } else if (this.structure?.data_type_params) { + } else if (this.structure()?.data_type_params) { // If no widget structure but we have data_type_params, just use the value - this.displayValue = this.value; + this.displayValue = this.value(); } else { - this.displayValue = this.value; + this.displayValue = this.value(); } } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.html b/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.html index 7dfcb3f4b..a071ebc00 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.html @@ -1 +1 @@ -{{value || '—'}} +{{value() || '—'}} diff --git a/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.ts b/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.ts index 70c4b4bdc..b8f777ce3 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/static-text/static-text.component.ts @@ -1,7 +1,6 @@ -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-static-text-record-view', templateUrl: './static-text.component.html', diff --git a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.html b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.html index d6b7e12f0..4f8475678 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.html @@ -1 +1 @@ -{{value || '—'}} +{{value() || '—'}} diff --git a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.spec.ts index 6c4e7127b..64a0c7917 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.spec.ts @@ -22,36 +22,36 @@ describe('TextRecordViewComponent', () => { }); it('should return false for isInvalid when value is empty', () => { - component.value = ''; + fixture.componentRef.setInput('value', ''); expect(component.isInvalid).toBe(false); }); it('should return false for isInvalid when no validate widget param', () => { - component.value = 'sometext'; - component.widgetStructure = { widget_params: {} } as any; + fixture.componentRef.setInput('value', 'sometext'); + fixture.componentRef.setInput('widgetStructure', { widget_params: {} } as any); expect(component.isInvalid).toBe(false); }); it('should return true for isInvalid when email validation fails', () => { - component.value = 'notanemail'; - component.widgetStructure = { widget_params: { validate: 'isEmail' } } as any; + fixture.componentRef.setInput('value', 'notanemail'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { validate: 'isEmail' } } as any); expect(component.isInvalid).toBe(true); }); it('should return false for isInvalid when email validation passes', () => { - component.value = 'test@test.com'; - component.widgetStructure = { widget_params: { validate: 'isEmail' } } as any; + fixture.componentRef.setInput('value', 'test@test.com'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { validate: 'isEmail' } } as any); expect(component.isInvalid).toBe(false); }); it('should return correct validationErrorMessage for isEmail', () => { - component.widgetStructure = { widget_params: { validate: 'isEmail' } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { validate: 'isEmail' } } as any); expect(component.validationErrorMessage).toBe('Invalid email address'); }); it('should validate regex pattern', () => { - component.value = 'abc'; - component.widgetStructure = { widget_params: { validate: 'regex', regex: '^[0-9]+$' } } as any; + fixture.componentRef.setInput('value', 'abc'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { validate: 'regex', regex: '^[0-9]+$' } } as any); expect(component.isInvalid).toBe(true); }); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.ts b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.ts index 09d1b3dd9..8d5f20ff7 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.ts @@ -1,8 +1,7 @@ -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import * as validator from 'validator'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-text-record-view', templateUrl: './text.component.html', @@ -11,20 +10,20 @@ import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-rec }) export class TextRecordViewComponent extends BaseRecordViewFieldComponent { get isInvalid(): boolean { - if (!this.value || this.value === '') { + if (!this.value() || this.value() === '') { return false; } - const validateType = this.widgetStructure?.widget_params?.validate; + const validateType = this.widgetStructure()?.widget_params?.validate; if (!validateType) { return false; } - const stringValue = String(this.value); + const stringValue = String(this.value()); // Special case for regex validation if (validateType === 'regex') { - const regexPattern = this.widgetStructure?.widget_params?.regex; + const regexPattern = this.widgetStructure()?.widget_params?.regex; if (!regexPattern) { return false; } @@ -55,7 +54,7 @@ export class TextRecordViewComponent extends BaseRecordViewFieldComponent { } get validationErrorMessage(): string { - const validateType = this.widgetStructure?.widget_params?.validate; + const validateType = this.widgetStructure()?.widget_params?.validate; if (!validateType) { return ''; } diff --git a/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.spec.ts index a6733d4fe..a105e0ebc 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.spec.ts @@ -23,19 +23,19 @@ describe('TimeIntervalRecordViewComponent', () => { }); it('should format interval object', () => { - component.value = { hours: 2, minutes: 30 }; + fixture.componentRef.setInput('value', { hours: 2, minutes: 30 }); component.ngOnInit(); expect(component.formattedInterval).toBe('2h 30m'); }); it('should display em dash for null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.formattedInterval).toBe('—'); }); it('should handle string JSON interval', () => { - component.value = '{"hours":1,"minutes":15}'; + fixture.componentRef.setInput('value', '{"hours":1,"minutes":15}'); component.ngOnInit(); expect(component.formattedInterval).toBe('1h 15m'); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.ts b/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.ts index 5375870b1..5d413de73 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/time-interval/time-interval.component.ts @@ -1,8 +1,7 @@ -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-time-interval-record-view', templateUrl: './time-interval.component.html', @@ -13,13 +12,13 @@ export class TimeIntervalRecordViewComponent extends BaseRecordViewFieldComponen formattedInterval: string; ngOnInit() { - if (!this.value) { + if (!this.value()) { this.formattedInterval = '—'; return; } try { - const interval = typeof this.value === 'string' ? JSON.parse(this.value) : this.value; + const interval = typeof this.value() === 'string' ? JSON.parse(this.value()) : this.value(); let parts = []; if (interval.days) parts.push(`${interval.days}d`); @@ -30,7 +29,7 @@ export class TimeIntervalRecordViewComponent extends BaseRecordViewFieldComponen this.formattedInterval = parts.length > 0 ? parts.join(' ') : '0'; } catch (_e) { - this.formattedInterval = String(this.value); + this.formattedInterval = String(this.value()); } } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/time/time.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/time/time.component.spec.ts index d78cb4584..4fd51af40 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/time/time.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/time/time.component.spec.ts @@ -23,13 +23,13 @@ describe('TimeRecordViewComponent', () => { }); it('should preserve time string format', () => { - component.value = '14:30:00'; + fixture.componentRef.setInput('value', '14:30:00'); component.ngOnInit(); expect(component.formattedTime).toBe('14:30:00'); }); it('should handle null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.formattedTime).toBeUndefined(); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/time/time.component.ts b/frontend/src/app/components/ui-components/record-view-fields/time/time.component.ts index 5a90e0280..1d8f85260 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/time/time.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/time/time.component.ts @@ -1,8 +1,7 @@ -import { Component, Injectable, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { format } from 'date-fns'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-time-record-view', templateUrl: './time.component.html', @@ -15,21 +14,21 @@ export class TimeRecordViewComponent extends BaseRecordViewFieldComponent implem public formattedTime: string; ngOnInit(): void { - if (this.value) { + if (this.value()) { try { - if (this.value.includes(':')) { + if (this.value().includes(':')) { // Handle time string format - this.formattedTime = this.value; + this.formattedTime = this.value(); } else { - const date = new Date(this.value); + const date = new Date(this.value()); if (!Number.isNaN(date.getTime())) { this.formattedTime = format(date, 'HH:mm:ss'); } else { - this.formattedTime = this.value; + this.formattedTime = this.value(); } } } catch (_error) { - this.formattedTime = this.value; + this.formattedTime = this.value(); } } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.spec.ts index e79620c77..5248c5dfb 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.spec.ts @@ -22,13 +22,13 @@ describe('TimezoneRecordViewComponent', () => { }); it('should display formatted timezone with UTC offset', () => { - component.value = 'America/New_York'; + fixture.componentRef.setInput('value', 'America/New_York'); expect(component.formattedTimezone).toContain('America/New_York'); expect(component.formattedTimezone).toContain('UTC'); }); it('should display dash for null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); expect(component.formattedTimezone).toBe('—'); }); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.ts b/frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.ts index 1f05b6802..23ca59272 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.ts @@ -1,7 +1,6 @@ -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-timezone-record-view', templateUrl: './timezone.component.html', @@ -10,16 +9,16 @@ import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-rec }) export class TimezoneRecordViewComponent extends BaseRecordViewFieldComponent { get formattedTimezone(): string { - console.log('timezone', this.value); - if (!this.value) { + console.log('timezone', this.value()); + if (!this.value()) { return '—'; } try { - const offset = this.getTimezoneOffset(this.value); - return `${this.value} (UTC${offset})`; + const offset = this.getTimezoneOffset(this.value()); + return `${this.value()} (UTC${offset})`; } catch (_error) { - return this.value; + return this.value(); } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/url/url.component.html b/frontend/src/app/components/ui-components/record-view-fields/url/url.component.html index 4327fcd07..284b29434 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/url/url.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/url/url.component.html @@ -1,8 +1,11 @@ - link - {{value || '—'}} + {{value() || '—'}} -{{value || '—'}} +} @else { +{{value() || '—'}} +} diff --git a/frontend/src/app/components/ui-components/record-view-fields/url/url.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/url/url.component.spec.ts index 0f8040e2c..6207ac321 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/url/url.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/url/url.component.spec.ts @@ -22,17 +22,17 @@ describe('UrlRecordViewComponent', () => { }); it('should return true for valid URL', () => { - component.value = 'https://example.com'; + fixture.componentRef.setInput('value', 'https://example.com'); expect(component.isValidUrl).toBe(true); }); it('should return false for invalid URL', () => { - component.value = 'not-a-url'; + fixture.componentRef.setInput('value', 'not-a-url'); expect(component.isValidUrl).toBe(false); }); it('should return false for empty value', () => { - component.value = ''; + fixture.componentRef.setInput('value', ''); expect(component.isValidUrl).toBe(false); }); }); diff --git a/frontend/src/app/components/ui-components/record-view-fields/url/url.component.ts b/frontend/src/app/components/ui-components/record-view-fields/url/url.component.ts index abbc1a6ab..79c967e7d 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/url/url.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/url/url.component.ts @@ -1,22 +1,20 @@ -import { CommonModule } from '@angular/common'; -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-url-record-view', templateUrl: './url.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './url.component.css'], - imports: [CommonModule, MatIconModule], + imports: [MatIconModule], }) export class UrlRecordViewComponent extends BaseRecordViewFieldComponent { static type = 'url'; get isValidUrl(): boolean { - if (!this.value) return false; + if (!this.value()) return false; try { - new URL(this.value); + new URL(this.value()); return true; } catch { return false; diff --git a/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.html b/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.html index 2034cc2c5..d4fd04722 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.html @@ -1 +1 @@ -{{ value || 'Null' }} \ No newline at end of file +{{ value() || 'Null' }} \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.ts b/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.ts index 8f15ec411..08ac526bf 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/uuid/uuid.component.ts @@ -1,7 +1,6 @@ -import { Component, Injectable } from '@angular/core'; +import { Component } from '@angular/core'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; -@Injectable() @Component({ selector: 'app-uuid-record-view', templateUrl: './uuid.component.html', From e4b077e1e514ca2eba414be10387e39683db486d Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Mon, 9 Mar 2026 16:48:44 +0000 Subject: [PATCH 5/7] unit tests: table display fields --- ...base-table-display-field.component.spec.ts | 35 ++++++++++++ .../binary-data-caption.component.spec.ts | 30 ++++++++++ .../boolean/boolean.component.spec.ts | 45 +++++++++++++++ .../code/code.component.spec.ts | 33 +++++++++++ .../color/color.component.spec.ts | 44 ++++++++++++++ .../country/country.component.spec.ts | 50 ++++++++++++++++ .../date-time/date-time.component.spec.ts | 42 ++++++++++++++ .../date/date.component.spec.ts | 55 ++++++++++++++++++ .../file/file.component.spec.ts | 46 +++++++++++++++ .../foreign-key/foreign-key.component.spec.ts | 42 ++++++++++++++ .../id/id.component.spec.ts | 37 ++++++++++++ .../image/image.component.spec.ts | 38 +++++++++++++ .../json-editor/json-editor.component.spec.ts | 33 +++++++++++ .../long-text/long-text.component.spec.ts | 36 ++++++++++++ .../money/money.component.spec.ts | 38 +++++++++++++ .../number/number.component.spec.ts | 54 ++++++++++++++++++ .../password/password.component.spec.ts | 31 ++++++++++ .../phone/phone.component.spec.ts | 40 +++++++++++++ .../point/point.component.spec.ts | 46 +++++++++++++++ .../range/range.component.spec.ts | 43 ++++++++++++++ .../s3/s3.component.spec.ts | 57 +++++++++++++++++++ .../select/select.component.spec.ts | 52 +++++++++++++++++ .../static-text/static-text.component.spec.ts | 36 ++++++++++++ .../text/text.component.spec.ts | 48 ++++++++++++++++ .../time-interval.component.spec.ts | 39 +++++++++++++ .../time/time.component.spec.ts | 39 +++++++++++++ .../url/url.component.spec.ts | 48 ++++++++++++++++ .../uuid/uuid.component.spec.ts | 38 +++++++++++++ 28 files changed, 1175 insertions(+) create mode 100644 frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/binary-data-caption/binary-data-caption.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/code/code.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/color/color.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/country/country.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/date-time/date-time.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/date/date.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/file/file.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/id/id.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/image/image.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/json-editor/json-editor.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/long-text/long-text.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/money/money.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/number/number.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/password/password.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/phone/phone.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/point/point.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/range/range.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/s3/s3.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/select/select.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/static-text/static-text.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/text/text.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/time-interval/time-interval.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/time/time.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/url/url.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/table-display-fields/uuid/uuid.component.spec.ts diff --git a/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.spec.ts new file mode 100644 index 000000000..e3c7aa953 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BaseTableDisplayFieldComponent } from './base-table-display-field.component'; + +describe('BaseTableDisplayFieldComponent', () => { + let component: BaseTableDisplayFieldComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BaseTableDisplayFieldComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BaseTableDisplayFieldComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should accept value input', () => { + fixture.componentRef.setInput('value', 'test-value'); + fixture.detectChanges(); + expect(component.value()).toBe('test-value'); + }); + + it('should accept key input', () => { + fixture.componentRef.setInput('key', 'test-key'); + fixture.detectChanges(); + expect(component.key()).toBe('test-key'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/binary-data-caption/binary-data-caption.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/binary-data-caption/binary-data-caption.component.spec.ts new file mode 100644 index 000000000..51fd74860 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/binary-data-caption/binary-data-caption.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BinaryDataCaptionDisplayComponent } from './binary-data-caption.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('BinaryDataCaptionDisplayComponent', () => { + let component: BinaryDataCaptionDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BinaryDataCaptionDisplayComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BinaryDataCaptionDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value', () => { + fixture.componentRef.setInput('value', 'binary-data-placeholder'); + fixture.detectChanges(); + expect(component.value()).toBe('binary-data-placeholder'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.spec.ts new file mode 100644 index 000000000..210882822 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/boolean/boolean.component.spec.ts @@ -0,0 +1,45 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BooleanDisplayComponent } from './boolean.component'; + +describe('BooleanDisplayComponent', () => { + let component: BooleanDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BooleanDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BooleanDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should handle true value', () => { + fixture.componentRef.setInput('value', true); + expect(component.value()).toBe(true); + }); + + it('should handle false value', () => { + fixture.componentRef.setInput('value', false); + expect(component.value()).toBe(false); + }); + + it('should handle null value', () => { + fixture.componentRef.setInput('value', null); + expect(component.value()).toBeNull(); + }); + + it('should invert colors when configured', () => { + fixture.componentRef.setInput('widgetStructure', { + widget_params: { invert_colors: true }, + }); + expect(component.invertColors).toBe(true); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/code/code.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/code/code.component.spec.ts new file mode 100644 index 000000000..f88d56618 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/code/code.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CodeDisplayComponent } from './code.component'; + +describe('CodeDisplayComponent', () => { + let component: CodeDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CodeDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CodeDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value', () => { + fixture.componentRef.setInput('value', 'console.log("hello")'); + expect(component.value()).toBe('console.log("hello")'); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + expect(component.value()).toBeNull(); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/color/color.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/color/color.component.spec.ts new file mode 100644 index 000000000..6026f618d --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/color/color.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ColorDisplayComponent } from './color.component'; + +describe('ColorDisplayComponent', () => { + let component: ColorDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ColorDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ColorDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should detect valid color', () => { + fixture.componentRef.setInput('value', '#ff0000'); + fixture.detectChanges(); + + expect(component.isValidColor).toBe(true); + }); + + it('should detect invalid color', () => { + fixture.componentRef.setInput('value', 'not-a-color'); + fixture.detectChanges(); + + expect(component.isValidColor).toBe(false); + }); + + it('should normalize hex color', () => { + fixture.componentRef.setInput('value', 'ff0000'); + fixture.detectChanges(); + + expect(component.normalizedColorForDisplay).toBe('#ff0000'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/country/country.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/country/country.component.spec.ts new file mode 100644 index 000000000..68044fa3a --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/country/country.component.spec.ts @@ -0,0 +1,50 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CountryDisplayComponent } from './country.component'; + +describe('CountryDisplayComponent', () => { + let component: CountryDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CountryDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CountryDisplayComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should display country name from code', () => { + fixture.componentRef.setInput('value', 'US'); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.countryName).toBe('United States'); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.countryName).toBe('\u2014'); + }); + + it('should respect show_flag widget param', () => { + fixture.componentRef.setInput('value', 'US'); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { show_flag: false }, + }); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.showFlag).toBe(false); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/date-time/date-time.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/date-time/date-time.component.spec.ts new file mode 100644 index 000000000..e984f97f6 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/date-time/date-time.component.spec.ts @@ -0,0 +1,42 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DateTimeDisplayComponent } from './date-time.component'; +import { format } from 'date-fns'; + +describe('DateTimeDisplayComponent', () => { + let component: DateTimeDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DateTimeDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DateTimeDisplayComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should format datetime value', () => { + const dateStr = '2023-04-29T10:30:00Z'; + fixture.componentRef.setInput('value', dateStr); + component.ngOnInit(); + fixture.detectChanges(); + + const expected = format(new Date(dateStr), 'P p'); + expect(component.formattedDateTime).toBe(expected); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.formattedDateTime).toBeUndefined(); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/date/date.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/date/date.component.spec.ts new file mode 100644 index 000000000..7afac7a26 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/date/date.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DateDisplayComponent } from './date.component'; +import { format } from 'date-fns'; + +describe('DateDisplayComponent', () => { + let component: DateDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DateDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DateDisplayComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should format date value', () => { + const dateStr = '2023-04-29'; + fixture.componentRef.setInput('value', dateStr); + component.ngOnInit(); + fixture.detectChanges(); + + const expected = format(new Date(dateStr), 'P'); + expect(component.formattedDate).toBe(expected); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.formattedDate).toBeUndefined(); + }); + + it('should show relative date when formatDistanceWithinHours configured', () => { + const now = new Date(); + const recentDate = new Date(now.getTime() - 1000 * 60 * 60); // 1 hour ago + fixture.componentRef.setInput('value', recentDate.toISOString()); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { formatDistanceWithinHours: 48 }, + }); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.formattedDate).toContain('ago'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/file/file.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/file/file.component.spec.ts new file mode 100644 index 000000000..8b680dab3 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/file/file.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FileDisplayComponent } from './file.component'; + +describe('FileDisplayComponent', () => { + let component: FileDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FileDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should detect blob data', () => { + fixture.componentRef.setInput('value', { type: 'Buffer', data: [1, 2, 3] }); + fixture.detectChanges(); + + expect(component.isBlob).toBe(true); + expect(component.displayText).toBe('Binary Data'); + }); + + it('should display short text value', () => { + fixture.componentRef.setInput('value', 'short.txt'); + fixture.detectChanges(); + + expect(component.isBlob).toBe(false); + expect(component.displayText).toBe('short.txt'); + }); + + it('should display Binary Data for long strings', () => { + fixture.componentRef.setInput('value', 'a'.repeat(25)); + fixture.detectChanges(); + + expect(component.displayText).toBe('Binary Data'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.spec.ts new file mode 100644 index 000000000..b4930a1b6 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.spec.ts @@ -0,0 +1,42 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ForeignKeyDisplayComponent } from './foreign-key.component'; + +describe('ForeignKeyDisplayComponent', () => { + let component: ForeignKeyDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ForeignKeyDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ForeignKeyDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value', () => { + fixture.componentRef.setInput('value', '42'); + expect(component.value()).toBe('42'); + }); + + it('should show foreign key button when relations exist', () => { + fixture.componentRef.setInput('value', '42'); + fixture.componentRef.setInput('relations', { + column_name: 'user_id', + constraint_name: 'fk_user', + referenced_column_name: 'id', + referenced_table_name: 'users', + }); + fixture.detectChanges(); + const compiled = fixture.nativeElement; + const button = compiled.querySelector('button'); + expect(button).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/id/id.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/id/id.component.spec.ts new file mode 100644 index 000000000..61d06a0e8 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/id/id.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { IdDisplayComponent } from './id.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('IdDisplayComponent', () => { + let component: IdDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IdDisplayComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(IdDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value', () => { + fixture.componentRef.setInput('value', 42); + fixture.detectChanges(); + expect(component.value()).toBe(42); + }); + + it('should display dash for null value', () => { + fixture.componentRef.setInput('value', null); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.field-value-id')?.textContent?.trim()).toBe('—'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/image/image.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/image/image.component.spec.ts new file mode 100644 index 000000000..d4cca4d5a --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/image/image.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ImageDisplayComponent } from './image.component'; + +describe('ImageDisplayComponent', () => { + let component: ImageDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ImageDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ImageDisplayComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('widgetStructure', { widget_params: { height: 50 } }); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should detect valid URL', () => { + fixture.componentRef.setInput('value', 'https://example.com/image.png'); + fixture.detectChanges(); + + expect(component.isUrl).toBe(true); + }); + + it('should detect invalid URL', () => { + fixture.componentRef.setInput('value', 'not-a-url'); + fixture.detectChanges(); + + expect(component.isUrl).toBe(false); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/json-editor/json-editor.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/json-editor/json-editor.component.spec.ts new file mode 100644 index 000000000..57e6bac60 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/json-editor/json-editor.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { JsonEditorDisplayComponent } from './json-editor.component'; + +describe('JsonEditorDisplayComponent', () => { + let component: JsonEditorDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [JsonEditorDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(JsonEditorDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should format JSON value', () => { + fixture.componentRef.setInput('value', '{"name":"test","count":3}'); + expect(component.formattedJson).toBe(JSON.stringify({ name: 'test', count: 3 }, null, 2)); + }); + + it('should handle invalid JSON', () => { + fixture.componentRef.setInput('value', 'not valid json'); + expect(component.formattedJson).toBe('not valid json'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/long-text/long-text.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/long-text/long-text.component.spec.ts new file mode 100644 index 000000000..f7b66403b --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/long-text/long-text.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { LongTextDisplayComponent } from './long-text.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('LongTextDisplayComponent', () => { + let component: LongTextDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LongTextDisplayComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LongTextDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value', () => { + fixture.componentRef.setInput('value', 'This is a long text value for testing'); + fixture.detectChanges(); + expect(component.value()).toBe('This is a long text value for testing'); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + fixture.detectChanges(); + expect(component.value()).toBeNull(); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/money/money.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/money/money.component.spec.ts new file mode 100644 index 000000000..0a713b83d --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/money/money.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MoneyDisplayComponent } from './money.component'; + +describe('MoneyDisplayComponent', () => { + let component: MoneyDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MoneyDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MoneyDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display formatted value with currency', () => { + fixture.componentRef.setInput('value', 42.5); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { default_currency: 'USD' }, + }); + component.ngOnInit(); + fixture.detectChanges(); + expect(component.formattedValue).toBe('$42.50'); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + expect(component.formattedValue).toBe(''); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.spec.ts new file mode 100644 index 000000000..ee7a2c353 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NumberDisplayComponent } from './number.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('NumberDisplayComponent', () => { + let component: NumberDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NumberDisplayComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NumberDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value', () => { + fixture.componentRef.setInput('value', 42); + fixture.detectChanges(); + expect(component.displayValue).toBe('42'); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + fixture.detectChanges(); + expect(component.displayValue).toBe('—'); + }); + + it('should detect out-of-threshold up', () => { + fixture.componentRef.setInput('value', 150); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { threshold_min: 0, threshold_max: 100 }, + }); + fixture.detectChanges(); + expect(component.isOutOfThreshold).toBe('up'); + }); + + it('should detect out-of-threshold down', () => { + fixture.componentRef.setInput('value', -5); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { threshold_min: 0, threshold_max: 100 }, + }); + fixture.detectChanges(); + expect(component.isOutOfThreshold).toBe('down'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/password/password.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/password/password.component.spec.ts new file mode 100644 index 000000000..1d3bda323 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/password/password.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PasswordDisplayComponent } from './password.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('PasswordDisplayComponent', () => { + let component: PasswordDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PasswordDisplayComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display masked dots', () => { + fixture.componentRef.setInput('value', 'secret-password'); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.field-value')?.textContent?.trim()).toBe('••••••••'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/phone/phone.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/phone/phone.component.spec.ts new file mode 100644 index 000000000..303de79e7 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/phone/phone.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PhoneDisplayComponent } from './phone.component'; + +describe('PhoneDisplayComponent', () => { + let component: PhoneDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PhoneDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PhoneDisplayComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should format phone number', () => { + fixture.componentRef.setInput('value', '+14155552671'); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.formattedNumber).toBe('+1 415 555 2671'); + expect(component.countryName).toBe('United States'); + }); + + it('should display raw value for invalid number', () => { + fixture.componentRef.setInput('value', '12345'); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.formattedNumber).toBe('12345'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/point/point.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/point/point.component.spec.ts new file mode 100644 index 000000000..65d2df7f4 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/point/point.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { PointDisplayComponent } from './point.component'; + +describe('PointDisplayComponent', () => { + let component: PointDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PointDisplayComponent], + }) + .overrideComponent(PointDisplayComponent, { + add: { imports: [CommonModule] }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PointDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should format point string', () => { + fixture.componentRef.setInput('value', '(3.5, 7.2)'); + component.ngOnInit(); + expect(component.formattedPoint).toBe('(3.5, 7.2)'); + }); + + it('should format point object', () => { + fixture.componentRef.setInput('value', { x: 10, y: 20 }); + component.ngOnInit(); + expect(component.formattedPoint).toBe('(10, 20)'); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + component.ngOnInit(); + expect(component.formattedPoint).toBe(''); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/range/range.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/range/range.component.spec.ts new file mode 100644 index 000000000..be9b0a81f --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/range/range.component.spec.ts @@ -0,0 +1,43 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RangeDisplayComponent } from './range.component'; + +describe('RangeDisplayComponent', () => { + let component: RangeDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RangeDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RangeDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value with max', () => { + fixture.componentRef.setInput('value', 50); + component.ngOnInit(); + fixture.detectChanges(); + expect(component.displayValue).toBe('50 / 100'); + }); + + it('should parse min/max from widget params', () => { + fixture.componentRef.setInput('value', 75); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { min: 0, max: 200, step: 5 }, + }); + component.ngOnInit(); + fixture.detectChanges(); + expect(component.min).toBe(0); + expect(component.max).toBe(200); + expect(component.step).toBe(5); + expect(component.displayValue).toBe('75 / 200'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/s3/s3.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/s3/s3.component.spec.ts new file mode 100644 index 000000000..4ebc1c424 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/s3/s3.component.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { S3DisplayComponent } from './s3.component'; +import { S3Service } from 'src/app/services/s3.service'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { TablesService } from 'src/app/services/tables.service'; +import { vi } from 'vitest'; + +describe('S3DisplayComponent', () => { + let component: S3DisplayComponent; + let fixture: ComponentFixture; + + const mockS3Service: Partial = { + getFileUrl: vi.fn(), + }; + + const mockConnectionsService: Partial = { + currentConnectionID: 'test-conn', + }; + + const mockTablesService: Partial = { + currentTableName: 'test-table', + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [S3DisplayComponent], + providers: [ + { provide: S3Service, useValue: mockS3Service }, + { provide: ConnectionsService, useValue: mockConnectionsService }, + { provide: TablesService, useValue: mockTablesService }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(S3DisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should detect image type from widget params', () => { + fixture.componentRef.setInput('widgetStructure', { + widget_params: { + bucket: 'my-bucket', + aws_access_key_id_secret_name: 'key', + aws_secret_access_key_secret_name: 'secret', + type: 'image', + }, + }); + component.ngOnInit(); + expect(component.isImageType).toBe(true); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/select/select.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/select/select.component.spec.ts new file mode 100644 index 000000000..b926c3793 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/select/select.component.spec.ts @@ -0,0 +1,52 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SelectDisplayComponent } from './select.component'; + +describe('SelectDisplayComponent', () => { + let component: SelectDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SelectDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display option label from widget params', () => { + fixture.componentRef.setInput('value', 'opt1'); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { + options: [ + { value: 'opt1', label: 'Option One' }, + { value: 'opt2', label: 'Option Two' }, + ], + }, + }); + component.ngOnInit(); + fixture.detectChanges(); + expect(component.displayValue).toBe('Option One'); + }); + + it('should display raw value when no options match', () => { + fixture.componentRef.setInput('value', 'unknown'); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { + options: [ + { value: 'opt1', label: 'Option One' }, + ], + }, + }); + component.ngOnInit(); + fixture.detectChanges(); + expect(component.displayValue).toBe('unknown'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/static-text/static-text.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/static-text/static-text.component.spec.ts new file mode 100644 index 000000000..cd02fd957 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/static-text/static-text.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { StaticTextDisplayComponent } from './static-text.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('StaticTextDisplayComponent', () => { + let component: StaticTextDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StaticTextDisplayComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(StaticTextDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value', () => { + fixture.componentRef.setInput('value', 'Hello World'); + fixture.detectChanges(); + expect(component.value()).toBe('Hello World'); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + fixture.detectChanges(); + expect(component.value()).toBeNull(); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/text/text.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/text/text.component.spec.ts new file mode 100644 index 000000000..18573e992 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/text/text.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TextDisplayComponent } from './text.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('TextDisplayComponent', () => { + let component: TextDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TextDisplayComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TextDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value', () => { + fixture.componentRef.setInput('value', 'Sample text'); + fixture.detectChanges(); + expect(component.value()).toBe('Sample text'); + }); + + it('should detect invalid email when email validation configured', () => { + fixture.componentRef.setInput('value', 'not-an-email'); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { validate: 'isEmail' }, + }); + fixture.detectChanges(); + expect(component.isInvalid).toBe(true); + }); + + it('should report valid for correct email', () => { + fixture.componentRef.setInput('value', 'user@example.com'); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { validate: 'isEmail' }, + }); + fixture.detectChanges(); + expect(component.isInvalid).toBe(false); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/time-interval/time-interval.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/time-interval/time-interval.component.spec.ts new file mode 100644 index 000000000..2aef98197 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/time-interval/time-interval.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TimeIntervalDisplayComponent } from './time-interval.component'; + +describe('TimeIntervalDisplayComponent', () => { + let component: TimeIntervalDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TimeIntervalDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeIntervalDisplayComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should format interval object', () => { + fixture.componentRef.setInput('value', { days: 2, hours: 5, minutes: 30 }); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.formattedInterval).toBe('2d 5h 30m'); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.formattedInterval).toBe('\u2014'); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/time/time.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/time/time.component.spec.ts new file mode 100644 index 000000000..752a6f506 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/time/time.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TimeDisplayComponent } from './time.component'; + +describe('TimeDisplayComponent', () => { + let component: TimeDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TimeDisplayComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeDisplayComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should format time string', () => { + fixture.componentRef.setInput('value', '14:30:00'); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.formattedTime).toBe('14:30:00'); + }); + + it('should display dash for null', () => { + fixture.componentRef.setInput('value', null); + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.formattedTime).toBeUndefined(); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/url/url.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/url/url.component.spec.ts new file mode 100644 index 000000000..e747d14da --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/url/url.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { UrlDisplayComponent } from './url.component'; + +describe('UrlDisplayComponent', () => { + let component: UrlDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UrlDisplayComponent], + }) + .overrideComponent(UrlDisplayComponent, { + add: { imports: [CommonModule] }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UrlDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should return valid URL', () => { + fixture.componentRef.setInput('value', 'https://example.com'); + expect(component.isValidUrl).toBe(true); + expect(component.hrefValue).toBe('https://example.com'); + }); + + it('should prepend prefix from widget params', () => { + fixture.componentRef.setInput('value', 'example.com/path'); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { prefix: 'https://' }, + }); + expect(component.hrefValue).toBe('https://example.com/path'); + expect(component.isValidUrl).toBe(true); + }); + + it('should detect invalid URL', () => { + fixture.componentRef.setInput('value', 'not a valid url'); + expect(component.isValidUrl).toBe(false); + }); +}); diff --git a/frontend/src/app/components/ui-components/table-display-fields/uuid/uuid.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/uuid/uuid.component.spec.ts new file mode 100644 index 000000000..0b1852ac4 --- /dev/null +++ b/frontend/src/app/components/ui-components/table-display-fields/uuid/uuid.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UuidDisplayComponent } from './uuid.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('UuidDisplayComponent', () => { + let component: UuidDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UuidDisplayComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UuidDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display UUID value', () => { + fixture.componentRef.setInput('value', '550e8400-e29b-41d4-a716-446655440000'); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.uuid-text')?.textContent?.trim()).toBe('550e8400-e29b-41d4-a716-446655440000'); + }); + + it('should show NULL for empty value', () => { + fixture.componentRef.setInput('value', null); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.null-value')?.textContent?.trim()).toBe('NULL'); + }); +}); From b1a68271e6f9fd2121e9779a1bb0446d398e65fe Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Mon, 9 Mar 2026 17:05:02 +0000 Subject: [PATCH 6/7] unit tests: add and extend unit tests for reocord edit fields --- .../base-row-field.component.spec.ts | 19 +- .../color/color.component.spec.ts | 120 +++++++++++++ .../country/country.component.spec.ts | 113 ++++++++++++ .../file/file.component.spec.ts | 58 ++++++- .../image/image.component.spec.ts | 51 +++++- .../long-text/long-text.component.spec.ts | 63 ++++++- .../range/range.component.spec.ts | 75 ++++++++ .../select/select.component.spec.ts | 58 ++++++- .../text/text.component.spec.ts | 61 ++++++- .../time-interval.component.spec.ts | 50 +++++- .../url/url.component.spec.ts | 41 ++++- .../uuid/uuid.component.spec.ts | 162 ++++++++++++++++++ 12 files changed, 862 insertions(+), 9 deletions(-) create mode 100644 frontend/src/app/components/ui-components/record-edit-fields/color/color.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-edit-fields/country/country.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-edit-fields/range/range.component.spec.ts create mode 100644 frontend/src/app/components/ui-components/record-edit-fields/uuid/uuid.component.spec.ts diff --git a/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.spec.ts index 245d71db8..30253d3a4 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.spec.ts @@ -1,5 +1,4 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { BaseEditFieldComponent } from './base-row-field.component'; describe('BaseEditFieldComponent', () => { @@ -13,10 +12,26 @@ describe('BaseEditFieldComponent', () => { fixture = TestBed.createComponent(BaseEditFieldComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should normalize label on init', () => { + component.label = 'user_first_name'; + component.ngOnInit(); + expect(component.normalizedLabel).toBeTruthy(); + }); + + it('should set normalizedLabel from label input', () => { + component.label = 'test_field'; + component.ngOnInit(); + expect(component.normalizedLabel).toBeDefined(); + expect(typeof component.normalizedLabel).toBe('string'); + }); + + it('should have onFieldChange event emitter', () => { + expect(component.onFieldChange).toBeDefined(); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.spec.ts new file mode 100644 index 000000000..e910028c3 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.spec.ts @@ -0,0 +1,120 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ColorEditComponent } from './color.component'; + +describe('ColorEditComponent', () => { + let component: ColorEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ColorEditComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ColorEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should return false for isValidColor when value is empty', () => { + component.value = ''; + expect(component.isValidColor).toBe(false); + }); + + it('should return false for isValidColor when value is null', () => { + component.value = null; + expect(component.isValidColor).toBe(false); + }); + + it('should return true for isValidColor when value is a valid hex color', () => { + component.value = '#ff0000'; + expect(component.isValidColor).toBe(true); + }); + + it('should return true for isValidColor when value is a hex color without hash', () => { + component.value = 'ff0000'; + expect(component.isValidColor).toBe(true); + }); + + it('should return true for isValidColor when value is a valid rgb color', () => { + component.value = 'rgb(255, 0, 0)'; + expect(component.isValidColor).toBe(true); + }); + + it('should return false for isValidColor when value is invalid', () => { + component.value = 'notacolor'; + expect(component.isValidColor).toBe(false); + }); + + it('should return normalized hex for color picker', () => { + component.value = '#ff0000'; + expect(component.normalizedColorForPicker.toLowerCase()).toBe('#ff0000'); + }); + + it('should return #000000 for invalid color in normalizedColorForPicker', () => { + component.value = 'invalid'; + expect(component.normalizedColorForPicker).toBe('#000000'); + }); + + it('should return hex_hash format by default in formattedColorValue', () => { + component.value = 'rgb(255, 0, 0)'; + const result = component.formattedColorValue; + expect(result).toMatch(/^#[A-Fa-f0-9]{6,8}$/); + }); + + it('should return hex without hash when format is hex', () => { + component.value = '#ff0000'; + component.widgetStructure = { widget_params: { format: 'hex' } } as any; + const result = component.formattedColorValue; + expect(result).not.toContain('#'); + }); + + it('should return rgb format when format is rgb', () => { + component.value = '#ff0000'; + component.widgetStructure = { widget_params: { format: 'rgb' } } as any; + const result = component.formattedColorValue; + expect(result).toMatch(/^rgb/); + }); + + it('should handle hsl format and fallback to hex when hsl conversion fails', () => { + component.value = '#ff0000'; + component.widgetStructure = { widget_params: { format: 'hsl' } } as any; + const result = component.formattedColorValue; + // colorString.get.hsl returns null for hex input, so it falls back to hex + expect(result).toMatch(/^#|^hsl/); + }); + + it('should return original value for formattedColorValue when value is invalid', () => { + component.value = 'invalid'; + expect(component.formattedColorValue).toBe('invalid'); + }); + + it('should emit onFieldChange when onTextInputChange is called', () => { + vi.spyOn(component.onFieldChange, 'emit'); + component.value = '#ff0000'; + component.onTextInputChange(); + expect(component.onFieldChange.emit).toHaveBeenCalledWith('#ff0000'); + }); + + it('should update value and emit onFieldChange when onColorPickerChange is called', () => { + vi.spyOn(component.onFieldChange, 'emit'); + const event = { target: { value: '#00ff00' } } as any; + component.onColorPickerChange(event); + expect(component.value).toBeTruthy(); + expect(component.onFieldChange.emit).toHaveBeenCalled(); + }); + + it('should set value directly when picker value cannot be parsed', () => { + vi.spyOn(component.onFieldChange, 'emit'); + const event = { target: { value: '' } } as any; + component.onColorPickerChange(event); + expect(component.value).toBe(''); + expect(component.onFieldChange.emit).toHaveBeenCalledWith(''); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.spec.ts new file mode 100644 index 000000000..17dcaaf23 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.spec.ts @@ -0,0 +1,113 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { CountryEditComponent } from './country.component'; + +describe('CountryEditComponent', () => { + let component: CountryEditComponent; + let fixture: ComponentFixture; + + const fakeStructure = { + column_name: 'country', + column_default: null, + data_type: 'varchar', + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: false, + character_maximum_length: 2, + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CountryEditComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CountryEditComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should load countries on init', () => { + component.structure = fakeStructure as any; + component.ngOnInit(); + expect(component.countries.length).toBeGreaterThan(0); + }); + + it('should sort countries alphabetically', () => { + component.structure = fakeStructure as any; + component.ngOnInit(); + const labels = component.countries.map((c) => c.label); + const sorted = [...labels].sort((a, b) => a.localeCompare(b)); + expect(labels).toEqual(sorted); + }); + + it('should prepend null option when allow_null is true', () => { + component.structure = { ...fakeStructure, allow_null: true } as any; + component.ngOnInit(); + expect(component.countries[0].value).toBeNull(); + expect(component.countries[0].label).toBe(''); + }); + + it('should not prepend null option when allow_null is false', () => { + component.structure = fakeStructure as any; + component.ngOnInit(); + expect(component.countries[0].value).not.toBeNull(); + }); + + it('should set initial value when value matches a country code', () => { + component.value = 'US'; + component.structure = fakeStructure as any; + component.ngOnInit(); + const controlValue = component.countryControl.value; + expect(controlValue).toBeTruthy(); + expect(typeof controlValue).toBe('object'); + expect((controlValue as any).value).toBe('US'); + }); + + it('should not set control value when value does not match any country', () => { + component.value = 'XX'; + component.structure = fakeStructure as any; + component.ngOnInit(); + const controlValue = component.countryControl.value; + expect(controlValue).toBe(''); + }); + + it('should emit onFieldChange when a country is selected', () => { + vi.spyOn(component.onFieldChange, 'emit'); + const country = { value: 'FR', label: 'France', flag: '🇫🇷' }; + component.onCountrySelected(country); + expect(component.value).toBe('FR'); + expect(component.onFieldChange.emit).toHaveBeenCalledWith('FR'); + }); + + it('should display label for country object in displayFn', () => { + const result = component.displayFn({ value: 'US', label: 'United States', flag: '🇺🇸' }); + expect(result).toBe('United States'); + }); + + it('should return string as-is in displayFn', () => { + const result = component.displayFn('some text'); + expect(result).toBe('some text'); + }); + + it('should return empty string for falsy input in displayFn', () => { + const result = component.displayFn(null); + expect(result).toBe(''); + }); + + it('should filter countries by label', () => { + component.structure = fakeStructure as any; + component.ngOnInit(); + component.countryControl.setValue('united'); + const filtered = component.filteredCountries(); + expect(filtered.length).toBeGreaterThan(0); + filtered.forEach((c) => { + expect(c.label.toLowerCase() + c.value?.toLowerCase()).toContain('united'); + }); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.spec.ts index 7371de04c..9e81f3eb0 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.spec.ts @@ -17,10 +17,66 @@ describe('FileEditComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(FileEditComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should default fileType to hex', () => { + expect(component.fileType).toBe('hex'); + }); + + it('should set hexData from value on init when no widgetStructure', () => { + component.value = '48656c6c6f' as any; + component.ngOnInit(); + expect(component.hexData).toBe('48656c6c6f'); + }); + + it('should set fileType from widgetStructure on init', () => { + component.value = '48656c6c6f' as any; + component.widgetStructure = { widget_params: { type: 'base64' } } as any; + component.ngOnInit(); + expect(component.fileType).toBe('base64'); + }); + + it('should convert hex to base64', () => { + component.hexData = '48656c6c6f'; + component.fromHexToBase64(); + expect(component.base64Data).toBe('SGVsbG8='); + }); + + it('should convert base64 to hex', () => { + component.base64Data = 'SGVsbG8='; + component.fromBase64ToHex(); + expect(component.hexData).toBe('48656c6c6f'); + }); + + it('should set isNotSwitcherActive to true on invalid base64', () => { + component.base64Data = '!!!invalid!!!'; + component.fromBase64ToHex(); + expect(component.isNotSwitcherActive).toBe(true); + }); + + it('should emit onFieldChange on hex change', () => { + vi.spyOn(component.onFieldChange, 'emit'); + component.hexData = '48656c6c6f'; + component.onHexChange(); + expect(component.onFieldChange.emit).toHaveBeenCalledWith('48656c6c6f'); + }); + + it('should convert base64 to hex and emit on base64 change', () => { + vi.spyOn(component.onFieldChange, 'emit'); + component.base64Data = 'SGVsbG8='; + component.onBase64Change(); + expect(component.hexData).toBe('48656c6c6f'); + expect(component.onFieldChange.emit).toHaveBeenCalledWith('48656c6c6f'); + }); + + it('should call convertValue for base64 when hexData exists', () => { + component.hexData = '48656c6c6f'; + component.convertValue('base64' as any); + expect(component.base64Data).toBe('SGVsbG8='); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.spec.ts index f07eebb22..fc40e2960 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.spec.ts @@ -14,10 +14,59 @@ describe('ImageComponent', () => { fixture = TestBed.createComponent(ImageEditComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have empty prefix by default', () => { + expect(component.prefix).toBe(''); + }); + + it('should parse prefix from widget params object', () => { + component.widgetStructure = { widget_params: { prefix: 'https://cdn.example.com/' } } as any; + component.ngOnInit(); + expect(component.prefix).toBe('https://cdn.example.com/'); + }); + + it('should parse prefix from widget params string', () => { + component.widgetStructure = { widget_params: JSON.stringify({ prefix: 'https://images.test/' }) } as any; + component.ngOnInit(); + expect(component.prefix).toBe('https://images.test/'); + }); + + it('should keep empty prefix when widget params have no prefix', () => { + component.widgetStructure = { widget_params: {} } as any; + component.ngOnInit(); + expect(component.prefix).toBe(''); + }); + + it('should update prefix on ngOnChanges', () => { + component.widgetStructure = { widget_params: { prefix: 'https://cdn.test/' } } as any; + component.ngOnChanges(); + expect(component.prefix).toBe('https://cdn.test/'); + }); + + it('should return empty string for imageUrl when value is empty', () => { + component.value = ''; + expect(component.imageUrl).toBe(''); + }); + + it('should return value without prefix when prefix is empty', () => { + component.value = 'photo.jpg'; + expect(component.imageUrl).toBe('photo.jpg'); + }); + + it('should return prefix + value for imageUrl', () => { + component.prefix = 'https://cdn.example.com/'; + component.value = 'photo.jpg'; + expect(component.imageUrl).toBe('https://cdn.example.com/photo.jpg'); + }); + + it('should handle invalid JSON in widget params gracefully', () => { + component.widgetStructure = { widget_params: 'invalid-json' } as any; + component.ngOnInit(); + expect(component.prefix).toBe(''); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.spec.ts index fa17154ce..722825d9a 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.spec.ts @@ -15,10 +15,71 @@ describe('LongTextEditComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(LongTextEditComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set maxLength from structure character_maximum_length', () => { + component.structure = { character_maximum_length: 1000 } as any; + component.ngOnInit(); + expect(component.maxLength).toBe(1000); + }); + + it('should keep maxLength null when structure has no character_maximum_length', () => { + component.structure = {} as any; + component.ngOnInit(); + expect(component.maxLength).toBeNull(); + }); + + it('should default rowsCount to 4 when no widget params', () => { + component.ngOnInit(); + expect(component.rowsCount).toBe('4'); + }); + + it('should parse rowsCount from widget params', () => { + component.widgetStructure = { widget_params: { rows: '10' } } as any; + component.ngOnInit(); + expect(component.rowsCount).toBe('10'); + }); + + it('should parse validateType from widget params object', () => { + component.widgetStructure = { widget_params: { validate: 'isJSON' } } as any; + component.ngOnInit(); + expect(component.validateType).toBe('isJSON'); + }); + + it('should parse validateType from widget params string', () => { + component.widgetStructure = { widget_params: JSON.stringify({ validate: 'isEmail', rows: '6' }) } as any; + component.ngOnInit(); + expect(component.validateType).toBe('isEmail'); + expect(component.rowsCount).toBe('6'); + }); + + it('should parse regexPattern from widget params', () => { + component.widgetStructure = { widget_params: { validate: 'regex', regex: '^\\d+$' } } as any; + component.ngOnInit(); + expect(component.regexPattern).toBe('^\\d+$'); + }); + + it('should return empty string for getValidationErrorMessage when no validateType', () => { + component.validateType = null; + expect(component.getValidationErrorMessage()).toBe(''); + }); + + it('should return regex message for regex validateType', () => { + component.validateType = 'regex'; + expect(component.getValidationErrorMessage()).toBe("Value doesn't match the required pattern"); + }); + + it('should return correct message for known validateType', () => { + component.validateType = 'isIP'; + expect(component.getValidationErrorMessage()).toBe('Invalid IP address'); + }); + + it('should return fallback message for unknown validateType', () => { + component.validateType = 'customRule'; + expect(component.getValidationErrorMessage()).toBe('Invalid customRule'); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.spec.ts new file mode 100644 index 000000000..c0703a8fc --- /dev/null +++ b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.spec.ts @@ -0,0 +1,75 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RangeEditComponent } from './range.component'; + +describe('RangeEditComponent', () => { + let component: RangeEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RangeEditComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RangeEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have default min, max, and step values', () => { + expect(component.min).toBe(0); + expect(component.max).toBe(100); + expect(component.step).toBe(1); + }); + + it('should parse widget params on init', () => { + component.widgetStructure = { + widget_params: { min: 10, max: 200, step: 5 }, + } as any; + component.ngOnInit(); + expect(component.min).toBe(10); + expect(component.max).toBe(200); + expect(component.step).toBe(5); + }); + + it('should parse widget params on changes', () => { + component.widgetStructure = { + widget_params: { min: 5, max: 50, step: 2 }, + } as any; + component.ngOnChanges(); + expect(component.min).toBe(5); + expect(component.max).toBe(50); + expect(component.step).toBe(2); + }); + + it('should keep defaults when widget_params is undefined', () => { + component.widgetStructure = undefined; + component.ngOnInit(); + expect(component.min).toBe(0); + expect(component.max).toBe(100); + expect(component.step).toBe(1); + }); + + it('should handle partial widget params', () => { + component.widgetStructure = { + widget_params: { min: 20 }, + } as any; + component.ngOnInit(); + expect(component.min).toBe(20); + expect(component.max).toBe(100); + expect(component.step).toBe(1); + }); + + it('should emit onFieldChange when value changes', () => { + vi.spyOn(component.onFieldChange, 'emit'); + component.onValueChange(42); + expect(component.value).toBe(42); + expect(component.onFieldChange.emit).toHaveBeenCalledWith(42); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.spec.ts index c22baf7ea..4822e605b 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.spec.ts @@ -15,10 +15,66 @@ describe('SelectEditComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(SelectEditComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have empty options by default', () => { + expect(component.options).toEqual([]); + }); + + it('should load options from widgetStructure widget_params', () => { + const options = [ + { value: 'opt1', label: 'Option 1' }, + { value: 'opt2', label: 'Option 2' }, + ]; + component.widgetStructure = { widget_params: { options } } as any; + component.ngOnInit(); + expect(component.options).toEqual(options); + }); + + it('should prepend null option when widgetStructure allow_null is true', () => { + const options = [{ value: 'opt1', label: 'Option 1' }]; + component.widgetStructure = { widget_params: { options, allow_null: true } } as any; + component.ngOnInit(); + expect(component.options[0]).toEqual({ value: null, label: '' }); + expect(component.options.length).toBe(2); + }); + + it('should not prepend null option when widgetStructure allow_null is false', () => { + const options = [{ value: 'opt1', label: 'Option 1' }]; + component.widgetStructure = { widget_params: { options, allow_null: false } } as any; + component.ngOnInit(); + expect(component.options.length).toBe(1); + expect(component.options[0].value).toBe('opt1'); + }); + + it('should load options from structure data_type_params when no widgetStructure', () => { + component.structure = { + data_type_params: ['active', 'inactive', 'pending'], + allow_null: false, + } as any; + component.ngOnInit(); + expect(component.options).toEqual([ + { value: 'active', label: 'active' }, + { value: 'inactive', label: 'inactive' }, + { value: 'pending', label: 'pending' }, + ]); + }); + + it('should prepend null option from structure when allow_null is true', () => { + component.structure = { + data_type_params: ['active', 'inactive'], + allow_null: true, + } as any; + component.ngOnInit(); + expect(component.options[0]).toEqual({ value: null, label: '' }); + expect(component.options.length).toBe(3); + }); + + it('should return 0 from originalOrder', () => { + expect(component.originalOrder()).toBe(0); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts index d8f7dc665..814901a56 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts @@ -15,10 +15,69 @@ describe('TextEditComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(TextEditComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set maxLength from structure character_maximum_length', () => { + component.structure = { character_maximum_length: 255 } as any; + component.ngOnInit(); + expect(component.maxLength).toBe(255); + }); + + it('should keep maxLength null when structure has no character_maximum_length', () => { + component.structure = {} as any; + component.ngOnInit(); + expect(component.maxLength).toBeNull(); + }); + + it('should parse validateType from widget params object', () => { + component.widgetStructure = { widget_params: { validate: 'isEmail' } } as any; + component.ngOnInit(); + expect(component.validateType).toBe('isEmail'); + }); + + it('should parse validateType from widget params string', () => { + component.widgetStructure = { widget_params: JSON.stringify({ validate: 'isURL' }) } as any; + component.ngOnInit(); + expect(component.validateType).toBe('isURL'); + }); + + it('should parse regexPattern from widget params', () => { + component.widgetStructure = { widget_params: { validate: 'regex', regex: '^[a-z]+$' } } as any; + component.ngOnInit(); + expect(component.regexPattern).toBe('^[a-z]+$'); + }); + + it('should return empty string for getValidationErrorMessage when no validateType', () => { + component.validateType = null; + expect(component.getValidationErrorMessage()).toBe(''); + }); + + it('should return regex message for regex validateType', () => { + component.validateType = 'regex'; + expect(component.getValidationErrorMessage()).toBe("Value doesn't match the required pattern"); + }); + + it('should return correct message for isEmail validateType', () => { + component.validateType = 'isEmail'; + expect(component.getValidationErrorMessage()).toBe('Invalid email address'); + }); + + it('should return correct message for isURL validateType', () => { + component.validateType = 'isURL'; + expect(component.getValidationErrorMessage()).toBe('Invalid URL'); + }); + + it('should return correct message for isJSON validateType', () => { + component.validateType = 'isJSON'; + expect(component.getValidationErrorMessage()).toBe('Invalid JSON'); + }); + + it('should return fallback message for unknown validateType', () => { + component.validateType = 'customValidator'; + expect(component.getValidationErrorMessage()).toBe('Invalid customValidator'); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.spec.ts index 2d1d53d27..c961a417d 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.spec.ts @@ -15,10 +15,58 @@ describe('TimeIntervalEditComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(TimeIntervalEditComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should have default empty interval values', () => { + expect(component.interval.years).toBe(''); + expect(component.interval.months).toBe(''); + expect(component.interval.days).toBe(''); + expect(component.interval.hours).toBe(''); + expect(component.interval.minutes).toBe(''); + expect(component.interval.seconds).toBe(''); + expect(component.interval.milliseconds).toBe(''); + }); + + it('should assign value to interval on init when value exists', () => { + const intervalValue = { + years: '1', + months: '2', + days: '3', + hours: '4', + minutes: '30', + seconds: '0', + milliseconds: '0', + }; + component.value = intervalValue; + component.ngOnInit(); + expect(component.interval).toBe(intervalValue); + }); + + it('should keep default interval when value is falsy on init', () => { + component.value = null; + component.ngOnInit(); + expect(component.interval.years).toBe(''); + }); + + it('should emit postgres interval string on input change', () => { + vi.spyOn(component.onFieldChange, 'emit'); + component.interval = { + years: '1', + months: '0', + days: '5', + hours: '0', + minutes: '0', + seconds: '0', + milliseconds: '0', + }; + component.onInputChange(); + expect(component.onFieldChange.emit).toHaveBeenCalled(); + const emittedValue = (component.onFieldChange.emit as any).mock.calls[0][0]; + expect(typeof emittedValue).toBe('string'); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.spec.ts index c52f40c1e..8a13469f0 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.spec.ts @@ -14,10 +14,49 @@ describe('UrlComponent', () => { fixture = TestBed.createComponent(UrlEditComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have empty prefix by default', () => { + expect(component.prefix).toBe(''); + }); + + it('should parse prefix from widget params object', () => { + component.widgetStructure = { widget_params: { prefix: 'https://api.example.com/' } } as any; + component.ngOnInit(); + expect(component.prefix).toBe('https://api.example.com/'); + }); + + it('should parse prefix from widget params string', () => { + component.widgetStructure = { widget_params: JSON.stringify({ prefix: 'https://test.com/' }) } as any; + component.ngOnInit(); + expect(component.prefix).toBe('https://test.com/'); + }); + + it('should keep empty prefix when widget params have no prefix', () => { + component.widgetStructure = { widget_params: {} } as any; + component.ngOnInit(); + expect(component.prefix).toBe(''); + }); + + it('should update prefix on ngOnChanges', () => { + component.widgetStructure = { widget_params: { prefix: 'https://updated.com/' } } as any; + component.ngOnChanges(); + expect(component.prefix).toBe('https://updated.com/'); + }); + + it('should handle invalid JSON in widget params gracefully', () => { + component.widgetStructure = { widget_params: 'invalid-json' } as any; + component.ngOnInit(); + expect(component.prefix).toBe(''); + }); + + it('should not change prefix when widgetStructure is undefined', () => { + component.widgetStructure = undefined; + component.ngOnInit(); + expect(component.prefix).toBe(''); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/uuid/uuid.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/uuid/uuid.component.spec.ts new file mode 100644 index 000000000..8265eb3ea --- /dev/null +++ b/frontend/src/app/components/ui-components/record-edit-fields/uuid/uuid.component.spec.ts @@ -0,0 +1,162 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { validate as uuidValidate } from 'uuid'; +import { UuidEditComponent } from './uuid.component'; + +describe('UuidEditComponent', () => { + let component: UuidEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UuidEditComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UuidEditComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should be in create mode when value is empty', () => { + component.value = ''; + component.ngOnInit(); + expect(component.isCreateMode).toBe(true); + }); + + it('should be in update mode when value is set', () => { + component.value = '550e8400-e29b-41d4-a716-446655440000'; + component.ngOnInit(); + expect(component.isCreateMode).toBe(false); + }); + + it('should generate a UUID on create mode', () => { + component.value = ''; + component.ngOnInit(); + expect(component.value).toBeTruthy(); + expect(uuidValidate(component.value)).toBe(true); + }); + + it('should not overwrite existing value on init', () => { + const existingUuid = '550e8400-e29b-41d4-a716-446655440000'; + component.value = existingUuid; + component.ngOnInit(); + expect(component.value).toBe(existingUuid); + }); + + it('should default to v4 UUID version', () => { + expect(component.uuidVersion).toBe('v4'); + }); + + it('should parse widget params for version', () => { + component.value = ''; + component.widgetStructure = { + widget_params: { version: 'v1' }, + } as any; + component.ngOnInit(); + expect(component.uuidVersion).toBe('v1'); + }); + + it('should parse widget params for namespace and name', () => { + component.value = ''; + component.widgetStructure = { + widget_params: { + version: 'v5', + namespace: '6ba7b811-9dad-11d1-80b4-00c04fd430c8', + name: 'test', + }, + } as any; + component.ngOnInit(); + expect(component.namespace).toBe('6ba7b811-9dad-11d1-80b4-00c04fd430c8'); + expect(component.name).toBe('test'); + }); + + it('should generate a valid v1 UUID', () => { + component.uuidVersion = 'v1'; + component.generateUuid(); + expect(uuidValidate(component.value)).toBe(true); + }); + + it('should generate a valid v4 UUID', () => { + component.uuidVersion = 'v4'; + component.generateUuid(); + expect(uuidValidate(component.value)).toBe(true); + }); + + it('should generate a valid v7 UUID', () => { + component.uuidVersion = 'v7'; + component.generateUuid(); + expect(uuidValidate(component.value)).toBe(true); + }); + + it('should generate a v3 UUID with name', () => { + component.uuidVersion = 'v3'; + component.name = 'test-name'; + component.generateUuid(); + expect(uuidValidate(component.value)).toBe(true); + }); + + it('should generate a v5 UUID with name', () => { + component.uuidVersion = 'v5'; + component.name = 'test-name'; + component.generateUuid(); + expect(uuidValidate(component.value)).toBe(true); + }); + + it('should generate deterministic v5 UUID for same inputs', () => { + component.uuidVersion = 'v5'; + component.name = 'test'; + component.namespace = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + component.generateUuid(); + const firstUuid = component.value; + + component.generateUuid(); + const secondUuid = component.value; + + expect(firstUuid).toBe(secondUuid); + }); + + it('should emit onFieldChange when UUID is generated', () => { + vi.spyOn(component.onFieldChange, 'emit'); + component.generateUuid(); + expect(component.onFieldChange.emit).toHaveBeenCalled(); + }); + + it('should fallback to v4 for unknown version', () => { + component.uuidVersion = 'v99' as any; + component.generateUuid(); + expect(uuidValidate(component.value)).toBe(true); + }); + + it('should validate a valid UUID', () => { + expect(component.validateUuid('550e8400-e29b-41d4-a716-446655440000')).toBe(true); + }); + + it('should invalidate a non-UUID string', () => { + expect(component.validateUuid('not-a-uuid')).toBe(false); + }); + + it('should return UUID version for valid UUID', () => { + const result = component.getUuidVersion('550e8400-e29b-41d4-a716-446655440000'); + expect(result).toBe(4); + }); + + it('should return false for invalid UUID in getUuidVersion', () => { + const result = component.getUuidVersion('invalid'); + expect(result).toBe(false); + }); + + it('should have available versions list', () => { + expect(component.availableVersions.length).toBe(5); + expect(component.availableVersions.map((v) => v.value)).toEqual(['v1', 'v3', 'v4', 'v5', 'v7']); + }); + + it('should have standard namespaces', () => { + expect(component.namespaces.length).toBe(4); + expect(component.namespaces.map((n) => n.label)).toEqual(['DNS', 'URL', 'OID', 'X500']); + }); +}); From 308ba9c3feb6746f16102003afb4f486f28e059f Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Mar 2026 10:59:34 +0000 Subject: [PATCH 7/7] signals: record edit fields --- .../base-row-field.component.spec.ts | 10 +- .../base-row-field.component.ts | 26 +- .../binary-data-caption.component.html | 4 +- .../boolean/boolean.component.html | 36 +- .../boolean/boolean.component.spec.ts | 28 +- .../boolean/boolean.component.ts | 41 +- .../code/code.component.html | 4 +- .../code/code.component.spec.ts | 20 +- .../record-edit-fields/code/code.component.ts | 16 +- .../color/color.component.html | 12 +- .../color/color.component.spec.ts | 38 +- .../color/color.component.ts | 88 ++- .../country/country.component.html | 8 +- .../country/country.component.spec.ts | 20 +- .../country/country.component.ts | 24 +- .../date-time/date-time.component.html | 16 +- .../date-time/date-time.component.spec.ts | 2 +- .../date-time/date-time.component.ts | 13 +- .../date/date.component.html | 8 +- .../date/date.component.spec.ts | 4 +- .../record-edit-fields/date/date.component.ts | 9 +- .../file/file.component.html | 99 ++-- .../file/file.component.spec.ts | 6 +- .../record-edit-fields/file/file.component.ts | 29 +- .../foreign-key/foreign-key.component.html | 16 +- .../foreign-key/foreign-key.component.spec.ts | 20 +- .../foreign-key/foreign-key.component.ts | 28 +- .../record-edit-fields/id/id.component.html | 12 +- .../record-edit-fields/id/id.component.ts | 4 +- .../image/image.component.html | 20 +- .../image/image.component.spec.ts | 16 +- .../image/image.component.ts | 23 +- .../json-editor/json-editor.component.html | 6 +- .../json-editor/json-editor.component.spec.ts | 14 +- .../json-editor/json-editor.component.ts | 14 +- .../long-text/long-text.component.html | 24 +- .../long-text/long-text.component.spec.ts | 12 +- .../long-text/long-text.component.ts | 20 +- .../markdown/markdown.component.html | 6 +- .../markdown/markdown.component.spec.ts | 18 +- .../markdown/markdown.component.ts | 14 +- .../money/money.component.html | 55 +- .../money/money.component.spec.ts | 20 +- .../money/money.component.ts | 36 +- .../number/number.component.html | 8 +- .../number/number.component.ts | 4 +- .../password/password.component.html | 8 +- .../password/password.component.spec.ts | 10 +- .../password/password.component.ts | 12 +- .../phone/phone.component.html | 14 +- .../phone/phone.component.spec.ts | 26 +- .../phone/phone.component.ts | 531 +++++------------- .../point/point.component.html | 22 +- .../point/point.component.ts | 4 +- .../range/range.component.html | 8 +- .../range/range.component.spec.ts | 16 +- .../range/range.component.ts | 13 +- .../select/select.component.html | 17 +- .../select/select.component.spec.ts | 14 +- .../select/select.component.ts | 19 +- .../static-text/static-text.component.html | 11 +- .../static-text/static-text.component.ts | 4 +- .../text/text.component.html | 24 +- .../text/text.component.spec.ts | 10 +- .../record-edit-fields/text/text.component.ts | 22 +- .../time-interval.component.html | 26 +- .../time-interval.component.spec.ts | 4 +- .../time-interval/time-interval.component.ts | 6 +- .../time/time.component.html | 8 +- .../record-edit-fields/time/time.component.ts | 6 +- .../timezone/timezone.component.html | 22 +- .../timezone/timezone.component.spec.ts | 6 +- .../timezone/timezone.component.ts | 9 +- .../record-edit-fields/url/url.component.html | 16 +- .../url/url.component.spec.ts | 12 +- .../record-edit-fields/url/url.component.ts | 12 +- .../uuid/uuid.component.html | 18 +- .../uuid/uuid.component.spec.ts | 42 +- .../record-edit-fields/uuid/uuid.component.ts | 25 +- 79 files changed, 858 insertions(+), 1090 deletions(-) diff --git a/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.spec.ts index 30253d3a4..5028614d9 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.spec.ts @@ -19,16 +19,16 @@ describe('BaseEditFieldComponent', () => { }); it('should normalize label on init', () => { - component.label = 'user_first_name'; + fixture.componentRef.setInput('label', 'user_first_name'); component.ngOnInit(); - expect(component.normalizedLabel).toBeTruthy(); + expect(component.normalizedLabel()).toBeTruthy(); }); it('should set normalizedLabel from label input', () => { - component.label = 'test_field'; + fixture.componentRef.setInput('label', 'test_field'); component.ngOnInit(); - expect(component.normalizedLabel).toBeDefined(); - expect(typeof component.normalizedLabel).toBe('string'); + expect(component.normalizedLabel()).toBeDefined(); + expect(typeof component.normalizedLabel()).toBe('string'); }); it('should have onFieldChange event emitter', () => { diff --git a/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.ts index 5eb10c9a6..57825999a 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/base-row-field/base-row-field.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, computed, input, OnInit, output } from '@angular/core'; import { TableField, TableForeignKey, WidgetStructure } from 'src/app/models/table'; import { normalizeFieldName } from '../../../../lib/normalize'; @@ -10,20 +10,18 @@ import { normalizeFieldName } from '../../../../lib/normalize'; imports: [CommonModule], }) export class BaseEditFieldComponent implements OnInit { - @Input() key: string; - @Input() label: string; - @Input() required: boolean; - @Input() readonly: boolean; - @Input() structure: TableField; - @Input() disabled: boolean; - @Input() widgetStructure: WidgetStructure; - @Input() relations: TableForeignKey; + readonly key = input(); + readonly label = input(); + readonly required = input(false); + readonly readonly = input(false); + readonly structure = input(); + readonly disabled = input(false); + readonly widgetStructure = input(); + readonly relations = input(); - @Output() onFieldChange = new EventEmitter(); + readonly onFieldChange = output(); - public normalizedLabel: string; + readonly normalizedLabel = computed(() => normalizeFieldName(this.label() || '')); - ngOnInit(): void { - this.normalizedLabel = normalizeFieldName(this.label); - } + ngOnInit(): void {} } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/binary-data-caption/binary-data-caption.component.html b/frontend/src/app/components/ui-components/record-edit-fields/binary-data-caption/binary-data-caption.component.html index 824ba0f1b..10de68e46 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/binary-data-caption/binary-data-caption.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/binary-data-caption/binary-data-caption.component.html @@ -1,7 +1,7 @@
- {{normalizedLabel}} + {{normalizedLabel()}} + attr.data-testid="record-{{label()}}-binary-data-caption"> binary data
\ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.html b/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.html index 375b23a88..208cbc33d 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.html @@ -1,26 +1,26 @@ -
- - - Yes - No - -
- - +@if (isRadiogroup) {
- - {{normalizedLabel()}} + + Yes + No + +
+} @else { +
+ + Yes No
-
+} diff --git a/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.spec.ts index bec93cda4..b17d63a5f 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.spec.ts @@ -37,40 +37,40 @@ describe('BooleanEditComponent', () => { }); it('should set value in true when input value contain anything', () => { - component.value = 'anything'; - component.structure = fakeStructure; + fixture.componentRef.setInput('value', 'anything'); + fixture.componentRef.setInput('structure', fakeStructure); component.ngOnInit(); - expect(component.value).toBeTruthy(); + expect(component.value()).toBeTruthy(); }); it('should set value in felse when input value is 0', () => { - component.value = 0; - component.structure = fakeStructure; + fixture.componentRef.setInput('value', 0); + fixture.componentRef.setInput('structure', fakeStructure); component.ngOnInit(); - expect(component.value).toBeFalsy(); + expect(component.value()).toBeFalsy(); }); it('should set value in null when input value is undefined', () => { - component.value = undefined; - component.structure = fakeStructure; + fixture.componentRef.setInput('value', undefined); + fixture.componentRef.setInput('structure', fakeStructure); component.ngOnInit(); - expect(component.value).toEqual(null); + expect(component.value()).toEqual(null); }); it('should set isRadiogroup in false if allow_null is false', () => { - component.value = undefined; - component.structure = fakeStructure; + fixture.componentRef.setInput('value', undefined); + fixture.componentRef.setInput('structure', fakeStructure); component.ngOnInit(); expect(component.isRadiogroup).toEqual(false); }); it('should set isRadiogroup in true if allow_null is true', () => { - component.value = undefined; - component.structure = { + fixture.componentRef.setInput('value', undefined); + fixture.componentRef.setInput('structure', { column_name: 'banned', column_default: '0', data_type: 'tinyint', @@ -79,7 +79,7 @@ describe('BooleanEditComponent', () => { auto_increment: false, allow_null: true, character_maximum_length: 1, - }; + }); component.ngOnInit(); expect(component.isRadiogroup).toEqual(true); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.ts index 178ff0c08..3445003da 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/boolean/boolean.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, inject, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { DBtype } from 'src/app/models/connection'; @@ -13,48 +13,43 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, FormsModule, MatButtonToggleModule], }) export class BooleanEditComponent extends BaseEditFieldComponent { - @Input() value: boolean | number | string | null; + readonly value = model(); public isRadiogroup: boolean; connectionType: DBtype; - constructor(private _connections: ConnectionsService) { - super(); - } + private _connections = inject(ConnectionsService); ngOnInit(): void { super.ngOnInit(); this.connectionType = this._connections.currentConnection.type; - if (this.value) { - this.value = true; - } else if (this.value === 0 || this.value === '' || this.value === false) { - this.value = false; + const val = this.value(); + if (val) { + this.value.set(true); + } else if (val === 0 || val === '' || val === false) { + this.value.set(false); } else { - this.value = null; + this.value.set(null); } - this.onFieldChange.emit(this.value); + this.onFieldChange.emit(this.value()); - // Parse widget parameters if available let parsedParams = null; - if (this.widgetStructure?.widget_params) { - parsedParams = - typeof this.widgetStructure.widget_params === 'string' - ? JSON.parse(this.widgetStructure.widget_params) - : this.widgetStructure.widget_params; + const ws = this.widgetStructure(); + if (ws?.widget_params) { + parsedParams = typeof ws.widget_params === 'string' ? JSON.parse(ws.widget_params) : ws.widget_params; } - // Check allow_null from either structure or widget params - this.isRadiogroup = this.structure?.allow_null || !!parsedParams?.allow_null; + this.isRadiogroup = this.structure()?.allow_null || !!parsedParams?.allow_null; } onToggleChange(optionValue: boolean): void { - if (this.value === optionValue) { - this.value = null; + if (this.value() === optionValue) { + this.value.set(null); } else { - this.value = optionValue; + this.value.set(optionValue); } - this.onFieldChange.emit(this.value); + this.onFieldChange.emit(this.value()); } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.html b/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.html index 11e4bac37..6dce1ee7a 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.html @@ -1,11 +1,11 @@ -{{ normalizedLabel }} {{ required ? '*' : '' }} +{{ normalizedLabel() }} {{ required() ? '*' : '' }}
\ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.spec.ts index bc6d68e9c..08f2fc367 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.spec.ts @@ -29,13 +29,13 @@ describe('CodeEditComponent', () => { fixture = TestBed.createComponent(CodeEditComponent); component = fixture.componentInstance; - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { language: 'css', }, - } as any; - component.label = 'styles'; - component.value = '.container { display: flex; }'; + } as any); + fixture.componentRef.setInput('label', 'styles'); + fixture.componentRef.setInput('value', '.container { display: flex; }'); fixture.detectChanges(); }); @@ -69,21 +69,21 @@ describe('CodeEditComponent', () => { }); it('should support different languages', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { language: 'javascript', }, - } as any; - component.label = 'script'; - component.value = 'console.log("hello");'; + } as any); + fixture.componentRef.setInput('label', 'script'); + fixture.componentRef.setInput('value', 'console.log("hello");'); component.ngOnInit(); expect((component.mutableCodeModel as any).language).toBe('javascript'); }); it('should normalize label from base class', () => { - component.label = 'custom_styles'; + fixture.componentRef.setInput('label', 'custom_styles'); component.ngOnInit(); - expect(component.normalizedLabel).toBeDefined(); + expect(component.normalizedLabel()).toBeDefined(); }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.ts index 461a1a600..692293453 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/code/code.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, inject, model } from '@angular/core'; import { CodeEditorModule } from '@ngstack/code-editor'; import { UiSettingsService } from 'src/app/services/ui-settings.service'; import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; @@ -11,7 +11,9 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, CodeEditorModule], }) export class CodeEditComponent extends BaseEditFieldComponent { - @Input() value; + readonly value = model(); + + private _uiSettings = inject(UiSettingsService); public mutableCodeModel: Object; public codeEditorOptions = { @@ -22,16 +24,12 @@ export class CodeEditComponent extends BaseEditFieldComponent { }; public codeEditorTheme = 'vs-dark'; - constructor(private _uiSettings: UiSettingsService) { - super(); - } - ngOnInit(): void { super.ngOnInit(); this.mutableCodeModel = { - language: `${this.widgetStructure.widget_params.language}`, - uri: `${this.label}.json`, - value: this.value, + language: `${this.widgetStructure().widget_params.language}`, + uri: `${this.label()}.json`, + value: this.value(), }; this.codeEditorTheme = this._uiSettings.isDarkMode ? 'vs-dark' : 'vs'; diff --git a/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.html b/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.html index e97f8054e..6f25f812b 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.html @@ -1,16 +1,16 @@
- {{normalizedLabel}} - {{normalizedLabel()}} +
-
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.spec.ts index e910028c3..282e302f7 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.spec.ts @@ -23,81 +23,81 @@ describe('ColorEditComponent', () => { }); it('should return false for isValidColor when value is empty', () => { - component.value = ''; + fixture.componentRef.setInput('value', ''); expect(component.isValidColor).toBe(false); }); it('should return false for isValidColor when value is null', () => { - component.value = null; + fixture.componentRef.setInput('value', null); expect(component.isValidColor).toBe(false); }); it('should return true for isValidColor when value is a valid hex color', () => { - component.value = '#ff0000'; + fixture.componentRef.setInput('value', '#ff0000'); expect(component.isValidColor).toBe(true); }); it('should return true for isValidColor when value is a hex color without hash', () => { - component.value = 'ff0000'; + fixture.componentRef.setInput('value', 'ff0000'); expect(component.isValidColor).toBe(true); }); it('should return true for isValidColor when value is a valid rgb color', () => { - component.value = 'rgb(255, 0, 0)'; + fixture.componentRef.setInput('value', 'rgb(255, 0, 0)'); expect(component.isValidColor).toBe(true); }); it('should return false for isValidColor when value is invalid', () => { - component.value = 'notacolor'; + fixture.componentRef.setInput('value', 'notacolor'); expect(component.isValidColor).toBe(false); }); it('should return normalized hex for color picker', () => { - component.value = '#ff0000'; + fixture.componentRef.setInput('value', '#ff0000'); expect(component.normalizedColorForPicker.toLowerCase()).toBe('#ff0000'); }); it('should return #000000 for invalid color in normalizedColorForPicker', () => { - component.value = 'invalid'; + fixture.componentRef.setInput('value', 'invalid'); expect(component.normalizedColorForPicker).toBe('#000000'); }); it('should return hex_hash format by default in formattedColorValue', () => { - component.value = 'rgb(255, 0, 0)'; + fixture.componentRef.setInput('value', 'rgb(255, 0, 0)'); const result = component.formattedColorValue; expect(result).toMatch(/^#[A-Fa-f0-9]{6,8}$/); }); it('should return hex without hash when format is hex', () => { - component.value = '#ff0000'; - component.widgetStructure = { widget_params: { format: 'hex' } } as any; + fixture.componentRef.setInput('value', '#ff0000'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { format: 'hex' } } as any); const result = component.formattedColorValue; expect(result).not.toContain('#'); }); it('should return rgb format when format is rgb', () => { - component.value = '#ff0000'; - component.widgetStructure = { widget_params: { format: 'rgb' } } as any; + fixture.componentRef.setInput('value', '#ff0000'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { format: 'rgb' } } as any); const result = component.formattedColorValue; expect(result).toMatch(/^rgb/); }); it('should handle hsl format and fallback to hex when hsl conversion fails', () => { - component.value = '#ff0000'; - component.widgetStructure = { widget_params: { format: 'hsl' } } as any; + fixture.componentRef.setInput('value', '#ff0000'); + fixture.componentRef.setInput('widgetStructure', { widget_params: { format: 'hsl' } } as any); const result = component.formattedColorValue; // colorString.get.hsl returns null for hex input, so it falls back to hex expect(result).toMatch(/^#|^hsl/); }); it('should return original value for formattedColorValue when value is invalid', () => { - component.value = 'invalid'; + fixture.componentRef.setInput('value', 'invalid'); expect(component.formattedColorValue).toBe('invalid'); }); it('should emit onFieldChange when onTextInputChange is called', () => { vi.spyOn(component.onFieldChange, 'emit'); - component.value = '#ff0000'; + fixture.componentRef.setInput('value', '#ff0000'); component.onTextInputChange(); expect(component.onFieldChange.emit).toHaveBeenCalledWith('#ff0000'); }); @@ -106,7 +106,7 @@ describe('ColorEditComponent', () => { vi.spyOn(component.onFieldChange, 'emit'); const event = { target: { value: '#00ff00' } } as any; component.onColorPickerChange(event); - expect(component.value).toBeTruthy(); + expect(component.value()).toBeTruthy(); expect(component.onFieldChange.emit).toHaveBeenCalled(); }); @@ -114,7 +114,7 @@ describe('ColorEditComponent', () => { vi.spyOn(component.onFieldChange, 'emit'); const event = { target: { value: '' } } as any; component.onColorPickerChange(event); - expect(component.value).toBe(''); + expect(component.value()).toBe(''); expect(component.onFieldChange.emit).toHaveBeenCalledWith(''); }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.ts index 2225c2fc3..35700e6de 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/color/color.component.ts @@ -1,12 +1,10 @@ -import { Component, Injectable, Input } from '@angular/core'; +import { Component, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import colorString from 'color-string'; import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; -@Injectable() - @Component({ selector: 'app-edit-color', templateUrl: './color.component.html', @@ -14,40 +12,41 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [MatFormFieldModule, MatInputModule, FormsModule], }) export class ColorEditComponent extends BaseEditFieldComponent { - @Input() value: string; + readonly value = model(); static type = 'color'; get isValidColor(): boolean { - if (!this.value) return false; - return this.parseColor(this.value) !== null; + const val = this.value(); + if (!val) return false; + return this._parseColor(val) !== null; } get normalizedColorForPicker(): string { - const parsed = this.parseColor(this.value); + const parsed = this._parseColor(this.value()); if (parsed) { const [r, g, b] = parsed.value; - return `#${this.toHex(r)}${this.toHex(g)}${this.toHex(b)}`; + return `#${this._toHex(r)}${this._toHex(g)}${this._toHex(b)}`; } return '#000000'; } get formattedColorValue(): string { - const parsed = this.parseColor(this.value); - if (!parsed) return this.value; + const val = this.value(); + const parsed = this._parseColor(val); + if (!parsed) return val; - const format = this.widgetStructure?.widget_params?.format || 'hex_hash'; + const format = this.widgetStructure()?.widget_params?.format || 'hex_hash'; const [r, g, b, a] = parsed.value; switch (format) { case 'hex': - return colorString.to.hex(r, g, b, a).slice(1); // Remove # prefix + return colorString.to.hex(r, g, b, a).slice(1); case 'hex_hash': return colorString.to.hex(r, g, b, a); case 'rgb': return colorString.to.rgb(r, g, b, a); case 'hsl': { - // Convert RGB to HSL using built-in conversion const hex = colorString.to.hex(r, g, b, a); const hslParsed = colorString.get.hsl(hex); if (hslParsed) { @@ -61,70 +60,65 @@ export class ColorEditComponent extends BaseEditFieldComponent { } } - private parseColor(color: string): any { - if (!color) return null; - - // Try parsing with color-string - const parsed = colorString.get(color); - if (parsed) return parsed; - - // Try hex without hash - if (/^[A-Fa-f0-9]{6}$|^[A-Fa-f0-9]{3}$/.test(color)) { - return colorString.get('#' + color); - } - - return null; - } - - private toHex(n: number): string { - const hex = n.toString(16); - return hex.length === 1 ? '0' + hex : hex; - } - onColorPickerChange(event: Event) { const target = event.target as HTMLInputElement; const pickerValue = target.value; - // Convert picker value to desired format - const parsed = this.parseColor(pickerValue); + const parsed = this._parseColor(pickerValue); if (parsed) { - const format = this.widgetStructure?.widget_params?.format || 'hex_hash'; - + const format = this.widgetStructure()?.widget_params?.format || 'hex_hash'; const [r, g, b, a] = parsed.value; switch (format) { case 'hex': - this.value = colorString.to.hex(r, g, b, a).slice(1); + this.value.set(colorString.to.hex(r, g, b, a).slice(1)); break; case 'hex_hash': - this.value = colorString.to.hex(r, g, b, a); + this.value.set(colorString.to.hex(r, g, b, a)); break; case 'rgb': - this.value = colorString.to.rgb(r, g, b, a); + this.value.set(colorString.to.rgb(r, g, b, a)); break; case 'hsl': { - // Convert RGB to HSL using built-in conversion const hex = colorString.to.hex(r, g, b, a); const hslParsed = colorString.get.hsl(hex); if (hslParsed) { const [h, s, l, alpha] = hslParsed; - this.value = colorString.to.hsl(h, s, l, alpha); + this.value.set(colorString.to.hsl(h, s, l, alpha)); } else { - this.value = hex; + this.value.set(hex); } break; } default: - this.value = colorString.to.hex(r, g, b, a); + this.value.set(colorString.to.hex(r, g, b, a)); } } else { - this.value = pickerValue; + this.value.set(pickerValue); } - this.onFieldChange.emit(this.value); + this.onFieldChange.emit(this.value()); } onTextInputChange() { - this.onFieldChange.emit(this.value); + this.onFieldChange.emit(this.value()); + } + + private _parseColor(color: string): any { + if (!color) return null; + + const parsed = colorString.get(color); + if (parsed) return parsed; + + if (/^[A-Fa-f0-9]{6}$|^[A-Fa-f0-9]{3}$/.test(color)) { + return colorString.get('#' + color); + } + + return null; + } + + private _toHex(n: number): string { + const hex = n.toString(16); + return hex.length === 1 ? '0' + hex : hex; } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.html b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.html index 229239a2e..596070843 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.html @@ -1,12 +1,12 @@ - {{normalizedLabel}} + {{normalizedLabel()}}
@if (selectedCountryFlag() && showFlag()) { {{selectedCountryFlag()}} } @@ -26,4 +26,4 @@ } - + \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.spec.ts index 17dcaaf23..e5d856407 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.spec.ts @@ -33,13 +33,13 @@ describe('CountryEditComponent', () => { }); it('should load countries on init', () => { - component.structure = fakeStructure as any; + fixture.componentRef.setInput('structure', fakeStructure as any); component.ngOnInit(); expect(component.countries.length).toBeGreaterThan(0); }); it('should sort countries alphabetically', () => { - component.structure = fakeStructure as any; + fixture.componentRef.setInput('structure', fakeStructure as any); component.ngOnInit(); const labels = component.countries.map((c) => c.label); const sorted = [...labels].sort((a, b) => a.localeCompare(b)); @@ -47,21 +47,21 @@ describe('CountryEditComponent', () => { }); it('should prepend null option when allow_null is true', () => { - component.structure = { ...fakeStructure, allow_null: true } as any; + fixture.componentRef.setInput('structure', { ...fakeStructure, allow_null: true } as any); component.ngOnInit(); expect(component.countries[0].value).toBeNull(); expect(component.countries[0].label).toBe(''); }); it('should not prepend null option when allow_null is false', () => { - component.structure = fakeStructure as any; + fixture.componentRef.setInput('structure', fakeStructure as any); component.ngOnInit(); expect(component.countries[0].value).not.toBeNull(); }); it('should set initial value when value matches a country code', () => { - component.value = 'US'; - component.structure = fakeStructure as any; + fixture.componentRef.setInput('value', 'US'); + fixture.componentRef.setInput('structure', fakeStructure as any); component.ngOnInit(); const controlValue = component.countryControl.value; expect(controlValue).toBeTruthy(); @@ -70,8 +70,8 @@ describe('CountryEditComponent', () => { }); it('should not set control value when value does not match any country', () => { - component.value = 'XX'; - component.structure = fakeStructure as any; + fixture.componentRef.setInput('value', 'XX'); + fixture.componentRef.setInput('structure', fakeStructure as any); component.ngOnInit(); const controlValue = component.countryControl.value; expect(controlValue).toBe(''); @@ -81,7 +81,7 @@ describe('CountryEditComponent', () => { vi.spyOn(component.onFieldChange, 'emit'); const country = { value: 'FR', label: 'France', flag: '🇫🇷' }; component.onCountrySelected(country); - expect(component.value).toBe('FR'); + expect(component.value()).toBe('FR'); expect(component.onFieldChange.emit).toHaveBeenCalledWith('FR'); }); @@ -101,7 +101,7 @@ describe('CountryEditComponent', () => { }); it('should filter countries by label', () => { - component.structure = fakeStructure as any; + fixture.componentRef.setInput('structure', fakeStructure as any); component.ngOnInit(); component.countryControl.setValue('united'); const filtered = component.filteredCountries(); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.ts index 6ba610a7b..1a1b62c53 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, CUSTOM_ELEMENTS_SCHEMA, computed, Input } from '@angular/core'; +import { Component, CUSTOM_ELEMENTS_SCHEMA, computed, model } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; @@ -22,7 +22,7 @@ interface CountryOption { schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class CountryEditComponent extends BaseEditFieldComponent { - @Input() value: string; + readonly value = model(); public countries: CountryOption[] = []; public countryControl = new FormControl(''); @@ -35,12 +35,13 @@ export class CountryEditComponent extends BaseEditFieldComponent { }); public showFlag = computed(() => { - if (this.widgetStructure?.widget_params) { + const ws = this.widgetStructure(); + if (ws?.widget_params) { try { const params = - typeof this.widgetStructure.widget_params === 'string' - ? JSON.parse(this.widgetStructure.widget_params) - : this.widgetStructure.widget_params; + typeof ws.widget_params === 'string' + ? JSON.parse(ws.widget_params) + : ws.widget_params; if (params.show_flag !== undefined) { return params.show_flag; @@ -72,8 +73,8 @@ export class CountryEditComponent extends BaseEditFieldComponent { } onCountrySelected(selectedCountry: CountryOption): void { - this.value = selectedCountry.value; - this.onFieldChange.emit(this.value); + this.value.set(selectedCountry.value); + this.onFieldChange.emit(this.value()); } displayFn(country: CountryOption | string): string { @@ -82,8 +83,8 @@ export class CountryEditComponent extends BaseEditFieldComponent { } private setInitialValue(): void { - if (this.value) { - const country = this.countries.find((c) => c.value === this.value); + if (this.value()) { + const country = this.countries.find((c) => c.value === this.value()); if (country) { this.countryControl.setValue(country); } @@ -105,7 +106,8 @@ export class CountryEditComponent extends BaseEditFieldComponent { flag: getCountryFlag(country.code), })).toSorted((a, b) => a.label.localeCompare(b.label)); - if (this.widgetStructure?.widget_params?.allow_null || this.structure?.allow_null) { + const ws = this.widgetStructure(); + if (ws?.widget_params?.allow_null || this.structure()?.allow_null) { this.countries = [{ value: null, label: '', flag: '' }, ...this.countries]; } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.html b/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.html index 7dac7b2ca..f6708d890 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.html @@ -1,17 +1,17 @@
- {{normalizedLabel}} (date) - {{normalizedLabel()}} (date) + - {{normalizedLabel}} (time) - {{normalizedLabel()}} (time) +
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.spec.ts index b0f4a6440..52f4ba963 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.spec.ts @@ -28,7 +28,7 @@ describe('DateTimeEditComponent', () => { }); it('should prepare date and time for date and time inputs', () => { - component.value = '2021-06-26T07:22:00.603'; + fixture.componentRef.setInput('value', '2021-06-26T07:22:00.603'); component.ngOnInit(); expect(component.date).toEqual('2021-06-26'); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.ts index aa13b4e52..39a9da4c2 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, inject, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -15,23 +15,22 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule], }) export class DateTimeEditComponent extends BaseEditFieldComponent { - @Input() value: string; + readonly value = model(); static type = 'datetime'; public date: string; public time: string; public connectionType: DBtype; - constructor(private _connections: ConnectionsService) { - super(); - } + private _connections = inject(ConnectionsService); ngOnInit(): void { super.ngOnInit(); this.connectionType = this._connections.currentConnection.type; - if (this.value) { - const datetime = new Date(this.value); + const val = this.value(); + if (val) { + const datetime = new Date(val); this.date = format(datetime, 'yyyy-MM-dd'); this.time = format(datetime, 'HH:mm:ss'); } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.html b/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.html index e672cef64..46faec623 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.html @@ -1,7 +1,7 @@ - {{normalizedLabel}} - {{normalizedLabel()}} + diff --git a/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.spec.ts index 75865a1bf..585ebc891 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.spec.ts @@ -23,14 +23,14 @@ describe('DateEditComponent', () => { }); it('should prepare date for date input', () => { - component.value = '2021-06-26T07:22:00.603Z'; + fixture.componentRef.setInput('value', '2021-06-26T07:22:00.603Z'); component.ngOnInit(); expect(component.date).toEqual('2021-06-26'); }); it('should remain date undefined if there is no value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.date).not.toBeDefined(); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.ts index b9313b9a0..bb9624a60 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -13,15 +13,16 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, MatFormFieldModule, MatInputModule, FormsModule], }) export class DateEditComponent extends BaseEditFieldComponent { - @Input() value: string; + readonly value = model(); static type = 'datetime'; public date: string; ngOnInit(): void { super.ngOnInit(); - if (this.value) { - const datetime = new Date(this.value); + const val = this.value(); + if (val) { + const datetime = new Date(val); this.date = format(datetime, 'yyyy-MM-dd'); } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.html b/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.html index 9463ddb32..1e85a3b34 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.html @@ -1,17 +1,16 @@
- {{ normalizedLabel }} + {{ normalizedLabel() }} -
- {{ initError }} - - {{ widgetStructure.widget_params.type }} - -
- - + @if (widgetStructure() && widgetStructure().widget_params.type) { + @if (initError) { + {{ initError }} + } @else { + {{ widgetStructure().widget_params.type }} + } + } @else { @@ -25,43 +24,55 @@ File - + }
- - - Hex - - Invalid hex. - + @if (!initError) { + @if (fileType === 'hex') { + + Hex + + @if (hexContent.errors?.isInvalidHex) { + Invalid hex. + } + + } - - Base64 - - Invalid base64. - + @if (fileType === 'base64') { + + Base64 + + @if (base64Content.errors?.isInvalidBase64 || isNotSwitcherActive) { + Invalid base64. + } + + } -
- + @if (fileType === 'file') { +
+ - {{ value ? 'File is uploaded.' : 'No file uploaded yet.' }} + {{ value() ? 'File is uploaded.' : 'No file uploaded yet.' }} - + - - Download file - -
- -
+ @if (hexData) { + + Download file + + } +
+ } + } +
\ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.spec.ts index 9e81f3eb0..2189df298 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.spec.ts @@ -29,14 +29,14 @@ describe('FileEditComponent', () => { }); it('should set hexData from value on init when no widgetStructure', () => { - component.value = '48656c6c6f' as any; + fixture.componentRef.setInput('value', '48656c6c6f' as any); component.ngOnInit(); expect(component.hexData).toBe('48656c6c6f'); }); it('should set fileType from widgetStructure on init', () => { - component.value = '48656c6c6f' as any; - component.widgetStructure = { widget_params: { type: 'base64' } } as any; + fixture.componentRef.setInput('value', '48656c6c6f' as any); + fixture.componentRef.setInput('widgetStructure', { widget_params: { type: 'base64' } } as any); component.ngOnInit(); expect(component.fileType).toBe('base64'); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.ts index e3d7a30e6..b99033392 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/file/file.component.ts @@ -1,5 +1,5 @@ -import { NgIf } from '@angular/common'; -import { Component, Input } from '@angular/core'; + +import { Component, inject, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -28,7 +28,6 @@ enum FileType { templateUrl: './file.component.html', styleUrls: ['./file.component.css'], imports: [ - NgIf, FormsModule, MatFormFieldModule, MatInputModule, @@ -39,7 +38,9 @@ enum FileType { ], }) export class FileEditComponent extends BaseEditFieldComponent { - @Input() value: Blob; + readonly value = model(); + + private sanitazer = inject(DomSanitizer); static type = 'file'; public isNotSwitcherActive; @@ -50,24 +51,21 @@ export class FileEditComponent extends BaseEditFieldComponent { public fileURL: SafeUrl; public initError: string | null = null; - constructor(private sanitazer: DomSanitizer) { - super(); - } - ngOnInit(): void { super.ngOnInit(); - if (this.widgetStructure && this.value) { - this.fileType = this.widgetStructure.widget_params.type; + const ws = this.widgetStructure(); + if (ws && this.value()) { + this.fileType = ws.widget_params.type; if (this.fileType === 'hex') { - this.hexData = this.value; + this.hexData = this.value(); //@ts-expect-error this.initError = hexValidation()({ value: this.hexData }); this.initError = 'Invalid hex format.'; } if (this.fileType === 'base64') { - this.base64Data = this.value; + this.base64Data = this.value(); //@ts-expect-error this.initError = base64Validation()({ value: this.hexData }); this.initError = 'Invalid base64 format.'; @@ -75,13 +73,13 @@ export class FileEditComponent extends BaseEditFieldComponent { if (this.fileType === 'file') { //@ts-expect-error - const blob = new Blob([this.value]); + const blob = new Blob([this.value()]); this.fileURL = this.sanitazer.bypassSecurityTrustUrl(URL.createObjectURL(blob)); } } - if (this.value) { - this.hexData = this.value; + if (this.value()) { + this.hexData = this.value(); } } @@ -155,7 +153,6 @@ export class FileEditComponent extends BaseEditFieldComponent { fromFileToHex(reader: FileReader) { let dataString = reader.result as ArrayBuffer; - // let dataStringArray = new Array(dataString.byteLength); this.hexData = [...new Uint8Array(dataString)].map((b) => b.toString(16).padStart(2, '0')).join(''); } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html index 601574eee..65519179d 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html @@ -1,13 +1,13 @@ - +
- {{normalizedLabel}} + {{normalizedLabel()}} @if (fetching()) { } @@ -21,10 +21,10 @@ } Improve search performance by configuring Searchable foreign key columns  - here + here - @@ -37,6 +37,6 @@
- - - + + + \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts index c8c106c56..0277a802d 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts @@ -136,7 +136,7 @@ describe('ForeignKeyEditComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ForeignKeyEditComponent); component = fixture.componentInstance; - component.relations = fakeRelations; + fixture.componentRef.setInput('relations', fakeRelations); tablesService = TestBed.inject(TablesService); fixture.detectChanges(); }); @@ -151,7 +151,7 @@ describe('ForeignKeyEditComponent', () => { vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetworkWithIdentityColumn)); component.connectionID = '12345678'; - component.value = ''; + fixture.componentRef.setInput('value', ''); await component.ngOnInit(); fixture.detectChanges(); @@ -184,7 +184,7 @@ describe('ForeignKeyEditComponent', () => { component.connectionID = '12345678'; - component.value = ''; + fixture.componentRef.setInput('value', ''); await component.ngOnInit(); fixture.detectChanges(); @@ -216,15 +216,15 @@ describe('ForeignKeyEditComponent', () => { vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork)); component.connectionID = '12345678'; - component.relations = { + fixture.componentRef.setInput('relations', { autocomplete_columns: [], column_name: 'userId', constraint_name: '', referenced_column_name: 'id', referenced_table_name: 'users', column_default: '', - }; - component.value = ''; + }); + fixture.componentRef.setInput('value', ''); await component.ngOnInit(); fixture.detectChanges(); @@ -301,7 +301,7 @@ describe('ForeignKeyEditComponent', () => { vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(searchSuggestionsNetwork)); - component.relations = fakeRelations; + fixture.componentRef.setInput('relations', fakeRelations); component.suggestions.set([ { @@ -393,7 +393,7 @@ describe('ForeignKeyEditComponent', () => { const fakeFetchTable = vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(searchSuggestionsNetwork)); component.connectionID = '12345678'; - component.relations = fakeRelations; + fixture.componentRef.setInput('relations', fakeRelations); component.suggestions.set([ { @@ -417,12 +417,12 @@ describe('ForeignKeyEditComponent', () => { expect(fakeFetchTable).toHaveBeenCalledWith({ connectionID: '12345678', - tableName: component.relations.referenced_table_name, + tableName: component.relations().referenced_table_name, requstedPage: 1, chunkSize: 20, foreignKeyRowName: 'autocomplete', foreignKeyRowValue: component.currentDisplayedString, - referencedColumn: component.relations.referenced_column_name, + referencedColumn: component.relations().referenced_column_name, }); expect(component.suggestions()).toEqual([ diff --git a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts index 9ac292260..0ba4e4d31 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input, signal } from '@angular/core'; +import { Component, inject, model, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -43,7 +43,10 @@ interface Suggestion { ], }) export class ForeignKeyEditComponent extends BaseEditFieldComponent { - @Input() value; + readonly value = model(); + + private _tables = inject(TablesService); + private _connections = inject(ConnectionsService); public connectionID: string; public currentDisplayedString: string; @@ -55,23 +58,18 @@ export class ForeignKeyEditComponent extends BaseEditFieldComponent { public primaeyKeys: { data_type: string; column_name: string }[]; public fkRelations: TableForeignKey = null; - private _debounceTimer: ReturnType; - constructor( - private _tables: TablesService, - private _connections: ConnectionsService, - ) { - super(); - } + private _debounceTimer: ReturnType; async ngOnInit(): Promise { super.ngOnInit(); this.connectionID = this._connections.currentConnectionID; - if (this.widgetStructure?.widget_params) { - this.fkRelations = this.widgetStructure.widget_params as TableForeignKey; - } else if (this.relations) { - this.fkRelations = this.relations; + const ws = this.widgetStructure(); + if (ws?.widget_params) { + this.fkRelations = ws.widget_params as TableForeignKey; + } else if (this.relations()) { + this.fkRelations = this.relations(); } if (this.fkRelations) { @@ -83,14 +81,14 @@ export class ForeignKeyEditComponent extends BaseEditFieldComponent { requstedPage: 1, chunkSize: 10, foreignKeyRowName: this.fkRelations.referenced_column_name, - foreignKeyRowValue: this.value, + foreignKeyRowValue: this.value(), }), )) as FetchTableResponse; if (res.rows.length) { this.identityColumn = res.identity_column; const modifiedRow = this.getModifiedRow(res.rows[0]); - if (this.value) { + if (this.value()) { this.currentDisplayedString = this.identityColumn ? `${res.rows[0][this.identityColumn]} (${Object.values(modifiedRow) .filter((value) => value) diff --git a/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.html b/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.html index 58a206351..8b0a93f85 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.html @@ -1,10 +1,12 @@ - {{normalizedLabel}} - {{normalizedLabel()}} + - Value doesn't match pattern. + @if (idField.errors?.pattern) { + Value doesn't match pattern. + } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.ts index 343442eec..27bfe59f6 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -12,5 +12,5 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule], }) export class IdEditComponent extends BaseEditFieldComponent { - @Input() value: string; + readonly value = model(); } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.html b/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.html index f6c3ce76e..6912ae595 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.html @@ -1,14 +1,20 @@
- {{normalizedLabel}} - {{prefix}} - {{normalizedLabel()}} + @if (prefix) { + {{prefix}} + } + - URL is invalid. + @if (image.errors?.isInvalidURL) { + URL is invalid. + } - + @if (!image.errors?.isInvalidURL) { + + }
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.spec.ts index fc40e2960..ca2d5a45e 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.spec.ts @@ -25,47 +25,47 @@ describe('ImageComponent', () => { }); it('should parse prefix from widget params object', () => { - component.widgetStructure = { widget_params: { prefix: 'https://cdn.example.com/' } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { prefix: 'https://cdn.example.com/' } } as any); component.ngOnInit(); expect(component.prefix).toBe('https://cdn.example.com/'); }); it('should parse prefix from widget params string', () => { - component.widgetStructure = { widget_params: JSON.stringify({ prefix: 'https://images.test/' }) } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: JSON.stringify({ prefix: 'https://images.test/' }) } as any); component.ngOnInit(); expect(component.prefix).toBe('https://images.test/'); }); it('should keep empty prefix when widget params have no prefix', () => { - component.widgetStructure = { widget_params: {} } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: {} } as any); component.ngOnInit(); expect(component.prefix).toBe(''); }); it('should update prefix on ngOnChanges', () => { - component.widgetStructure = { widget_params: { prefix: 'https://cdn.test/' } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { prefix: 'https://cdn.test/' } } as any); component.ngOnChanges(); expect(component.prefix).toBe('https://cdn.test/'); }); it('should return empty string for imageUrl when value is empty', () => { - component.value = ''; + fixture.componentRef.setInput('value', ''); expect(component.imageUrl).toBe(''); }); it('should return value without prefix when prefix is empty', () => { - component.value = 'photo.jpg'; + fixture.componentRef.setInput('value', 'photo.jpg'); expect(component.imageUrl).toBe('photo.jpg'); }); it('should return prefix + value for imageUrl', () => { component.prefix = 'https://cdn.example.com/'; - component.value = 'photo.jpg'; + fixture.componentRef.setInput('value', 'photo.jpg'); expect(component.imageUrl).toBe('https://cdn.example.com/photo.jpg'); }); it('should handle invalid JSON in widget params gracefully', () => { - component.widgetStructure = { widget_params: 'invalid-json' } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: 'invalid-json' } as any); component.ngOnInit(); expect(component.prefix).toBe(''); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.ts index 7b544baea..6cb24b5e1 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input, OnInit } from '@angular/core'; +import { Component, computed, model, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -13,7 +13,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule, UrlValidatorDirective], }) export class ImageEditComponent extends BaseEditFieldComponent implements OnInit { - @Input() value: string; + readonly value = model(); public prefix: string = ''; ngOnInit(): void { @@ -25,13 +25,17 @@ export class ImageEditComponent extends BaseEditFieldComponent implements OnInit this._parseWidgetParams(); } + get imageUrl(): string { + const val = this.value(); + if (!val) return ''; + return this.prefix + val; + } + private _parseWidgetParams(): void { - if (this.widgetStructure?.widget_params) { + const ws = this.widgetStructure(); + if (ws?.widget_params) { try { - const params = - typeof this.widgetStructure.widget_params === 'string' - ? JSON.parse(this.widgetStructure.widget_params) - : this.widgetStructure.widget_params; + const params = typeof ws.widget_params === 'string' ? JSON.parse(ws.widget_params) : ws.widget_params; if (params.prefix !== undefined) { this.prefix = params.prefix || ''; @@ -41,9 +45,4 @@ export class ImageEditComponent extends BaseEditFieldComponent implements OnInit } } } - - get imageUrl(): string { - if (!this.value) return ''; - return this.prefix + this.value; - } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.html b/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.html index 115648820..6dce1ee7a 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.html @@ -1,11 +1,11 @@ -{{ normalizedLabel }} {{ required ? '*' : '' }} +{{ normalizedLabel() }} {{ required() ? '*' : '' }}
-
+
\ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.spec.ts index a8970dd7a..0cd651222 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.spec.ts @@ -29,8 +29,8 @@ describe('JsonEditorEditComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(JsonEditorEditComponent); component = fixture.componentInstance; - component.label = 'metadata'; - component.value = { id: 1, name: 'test', settings: { enabled: true } }; + fixture.componentRef.setInput('label', 'metadata'); + fixture.componentRef.setInput('value', { id: 1, name: 'test', settings: { enabled: true } }); fixture.detectChanges(); }); @@ -62,21 +62,21 @@ describe('JsonEditorEditComponent', () => { }); it('should handle null value', () => { - component.value = null; + fixture.componentRef.setInput('value', null); component.ngOnInit(); // JSON.stringify(null) returns "null", fallback to '{}' only for undefined expect((component.mutableCodeModel as any).value).toBe('null'); }); it('should handle undefined value with fallback', () => { - component.value = undefined; + fixture.componentRef.setInput('value', undefined); component.ngOnInit(); // undefined is falsy so falls back to '{}' expect((component.mutableCodeModel as any).value).toBe('{}'); }); it('should handle array value', () => { - component.value = [1, 2, 3] as any; + fixture.componentRef.setInput('value', [1, 2, 3] as any); component.ngOnInit(); const modelValue = (component.mutableCodeModel as any).value; expect(modelValue).toContain('1'); @@ -85,8 +85,8 @@ describe('JsonEditorEditComponent', () => { }); it('should normalize label from base class', () => { - component.label = 'json_config_data'; + fixture.componentRef.setInput('label', 'json_config_data'); component.ngOnInit(); - expect(component.normalizedLabel).toBeDefined(); + expect(component.normalizedLabel()).toBeDefined(); }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.ts index c1c1e9caf..093a6c5f1 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/json-editor/json-editor.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, inject, model } from '@angular/core'; import { CodeEditorModule } from '@ngstack/code-editor'; import { UiSettingsService } from 'src/app/services/ui-settings.service'; import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; @@ -11,7 +11,9 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, CodeEditorModule], }) export class JsonEditorEditComponent extends BaseEditFieldComponent { - @Input() value: Object; + readonly value = model(); + + private _uiSettings = inject(UiSettingsService); public mutableCodeModel: Object; public codeEditorOptions = { @@ -22,16 +24,12 @@ export class JsonEditorEditComponent extends BaseEditFieldComponent { }; public codeEditorTheme = 'vs-dark'; - constructor(private _uiSettings: UiSettingsService) { - super(); - } - ngOnInit(): void { super.ngOnInit(); this.mutableCodeModel = { language: 'json', - uri: `${this.label}.json`, - value: JSON.stringify(this.value, undefined, 4) || '{}', + uri: `${this.label()}.json`, + value: JSON.stringify(this.value(), undefined, 4) || '{}', }; this.codeEditorTheme = this._uiSettings.isDarkMode ? 'vs-dark' : 'vs'; diff --git a/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.html b/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.html index 5992b3971..7ac207d5e 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.html @@ -1,6 +1,6 @@ - {{normalizedLabel}} - -
{{value.length}} / {{maxLength}}
- This field is required. - Maximum length is {{maxLength}} characters. - {{getValidationErrorMessage()}} + @if (maxLength && maxLength > 0 && value() && (maxLength - value().length) < 100) { +
{{value().length}} / {{maxLength}}
+ } + @if (textareaField.errors?.['required']) { + This field is required. + } + @if (textareaField.errors?.['maxlength']) { + Maximum length is {{maxLength}} characters. + } + @if (textareaField.errors?.['invalidPattern'] || textareaField.errors?.[('invalid' + validateType)]) { + {{getValidationErrorMessage()}} + }
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.spec.ts index 722825d9a..c503cc958 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.spec.ts @@ -22,13 +22,13 @@ describe('LongTextEditComponent', () => { }); it('should set maxLength from structure character_maximum_length', () => { - component.structure = { character_maximum_length: 1000 } as any; + fixture.componentRef.setInput('structure', { character_maximum_length: 1000 } as any); component.ngOnInit(); expect(component.maxLength).toBe(1000); }); it('should keep maxLength null when structure has no character_maximum_length', () => { - component.structure = {} as any; + fixture.componentRef.setInput('structure', {} as any); component.ngOnInit(); expect(component.maxLength).toBeNull(); }); @@ -39,26 +39,26 @@ describe('LongTextEditComponent', () => { }); it('should parse rowsCount from widget params', () => { - component.widgetStructure = { widget_params: { rows: '10' } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { rows: '10' } } as any); component.ngOnInit(); expect(component.rowsCount).toBe('10'); }); it('should parse validateType from widget params object', () => { - component.widgetStructure = { widget_params: { validate: 'isJSON' } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { validate: 'isJSON' } } as any); component.ngOnInit(); expect(component.validateType).toBe('isJSON'); }); it('should parse validateType from widget params string', () => { - component.widgetStructure = { widget_params: JSON.stringify({ validate: 'isEmail', rows: '6' }) } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: JSON.stringify({ validate: 'isEmail', rows: '6' }) } as any); component.ngOnInit(); expect(component.validateType).toBe('isEmail'); expect(component.rowsCount).toBe('6'); }); it('should parse regexPattern from widget params', () => { - component.widgetStructure = { widget_params: { validate: 'regex', regex: '^\\d+$' } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { validate: 'regex', regex: '^\\d+$' } } as any); component.ngOnInit(); expect(component.regexPattern).toBe('^\\d+$'); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.ts index de4aac234..2be2040e8 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input, OnInit } from '@angular/core'; +import { Component, model, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -13,7 +13,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, MatFormFieldModule, MatInputModule, FormsModule, TextValidatorDirective], }) export class LongTextEditComponent extends BaseEditFieldComponent implements OnInit { - @Input() value: string; + readonly value = model(); static type = 'text'; public rowsCount: string; @@ -24,17 +24,14 @@ export class LongTextEditComponent extends BaseEditFieldComponent implements OnI override ngOnInit(): void { super.ngOnInit(); - // Use character_maximum_length from the field structure if available - if (this.structure?.character_maximum_length) { - this.maxLength = this.structure.character_maximum_length; + const struct = this.structure(); + if (struct?.character_maximum_length) { + this.maxLength = struct.character_maximum_length; } - // Parse widget parameters - if (this.widgetStructure?.widget_params) { - const params = - typeof this.widgetStructure.widget_params === 'string' - ? JSON.parse(this.widgetStructure.widget_params) - : this.widgetStructure.widget_params; + const ws = this.widgetStructure(); + if (ws?.widget_params) { + const params = typeof ws.widget_params === 'string' ? JSON.parse(ws.widget_params) : ws.widget_params; this.rowsCount = params.rows || '4'; this.validateType = params.validate || null; @@ -53,7 +50,6 @@ export class LongTextEditComponent extends BaseEditFieldComponent implements OnI return "Value doesn't match the required pattern"; } - // Create user-friendly messages for common validators const messages = { isEmail: 'Invalid email address', isURL: 'Invalid URL', diff --git a/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.html b/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.html index 115648820..6dce1ee7a 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.html @@ -1,11 +1,11 @@ -{{ normalizedLabel }} {{ required ? '*' : '' }} +{{ normalizedLabel() }} {{ required() ? '*' : '' }}
-
+ \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.spec.ts index 8db02d691..33a653f03 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.spec.ts @@ -29,11 +29,11 @@ describe('MarkdownEditComponent', () => { fixture = TestBed.createComponent(MarkdownEditComponent); component = fixture.componentInstance; - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: {}, - } as any; - component.label = 'description'; - component.value = '# Hello World\n\nThis is **bold** text.'; + } as any); + fixture.componentRef.setInput('label', 'description'); + fixture.componentRef.setInput('value', '# Hello World\n\nThis is **bold** text.'); fixture.detectChanges(); }); @@ -81,17 +81,17 @@ describe('MarkdownEditComponent', () => { const newFixture = TestBed.createComponent(MarkdownEditComponent); const newComponent = newFixture.componentInstance; - newComponent.widgetStructure = { widget_params: {} } as any; - newComponent.label = 'content'; - newComponent.value = 'test'; + newFixture.componentRef.setInput('widgetStructure', { widget_params: {} } as any); + newFixture.componentRef.setInput('label', 'content'); + newFixture.componentRef.setInput('value', 'test'); newFixture.detectChanges(); expect(newComponent.codeEditorTheme).toBe('vs'); }); it('should normalize label from base class', () => { - component.label = 'product_description'; + fixture.componentRef.setInput('label', 'product_description'); component.ngOnInit(); - expect(component.normalizedLabel).toBeDefined(); + expect(component.normalizedLabel()).toBeDefined(); }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.ts index 1e78e7df8..a64e0c41b 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/markdown/markdown.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, inject, model } from '@angular/core'; import { CodeEditorModule } from '@ngstack/code-editor'; import { UiSettingsService } from 'src/app/services/ui-settings.service'; import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; @@ -11,7 +11,9 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, CodeEditorModule], }) export class MarkdownEditComponent extends BaseEditFieldComponent { - @Input() value; + readonly value = model(); + + private _uiSettings = inject(UiSettingsService); public mutableCodeModel: Object; public codeEditorOptions = { @@ -22,16 +24,12 @@ export class MarkdownEditComponent extends BaseEditFieldComponent { }; public codeEditorTheme = 'vs-dark'; - constructor(private _uiSettings: UiSettingsService) { - super(); - } - ngOnInit(): void { super.ngOnInit(); this.mutableCodeModel = { language: 'markdown', - uri: `${this.label}.md`, - value: this.value, + uri: `${this.label()}.md`, + value: this.value(), }; this.codeEditorTheme = this._uiSettings.isDarkMode ? 'vs-dark' : 'vs'; diff --git a/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.html b/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.html index 20116f2e0..accbb98e5 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.html @@ -1,41 +1,44 @@
- - Currency - - - {{ displayCurrencyFn(currency) }} - - - + @if (showCurrencySelector) { + + Currency + + @for (currency of currencies; track currency.code) { + + {{ displayCurrencyFn(currency) }} + + } + + + } - - {{normalizedLabel}} - {{selectedCurrencyData.symbol}} - {{normalizedLabel()}} + @if (selectedCurrencyData && displayAmount) { + {{selectedCurrencyData.symbol}} + } +
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.spec.ts index 9794d502e..ef047acc4 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.spec.ts @@ -29,10 +29,10 @@ describe('MoneyEditComponent', () => { component = fixture.componentInstance; // Set required properties from base component - component.label = 'Test Money'; - component.required = false; - component.disabled = false; - component.readonly = false; + fixture.componentRef.setInput('label', 'Test Money'); + fixture.componentRef.setInput('required', false); + fixture.componentRef.setInput('disabled', false); + fixture.componentRef.setInput('readonly', false); fixture.detectChanges(); }); @@ -48,21 +48,21 @@ describe('MoneyEditComponent', () => { }); it('should parse string value correctly', () => { - component.value = '100.50 EUR'; + fixture.componentRef.setInput('value', '100.50 EUR'); component.ngOnInit(); expect(component.selectedCurrency).toBe('EUR'); expect(component.amount).toBe(100.5); }); it('should parse object value correctly', () => { - component.value = { amount: 250.75, currency: 'GBP' }; + fixture.componentRef.setInput('value', { amount: 250.75, currency: 'GBP' }); component.ngOnInit(); expect(component.selectedCurrency).toBe('GBP'); expect(component.amount).toBe(250.75); }); it('should parse numeric value correctly when currency selector is disabled', () => { - component.value = 150.25; + fixture.componentRef.setInput('value', 150.25); component.ngOnInit(); expect(component.selectedCurrency).toBe('USD'); expect(component.amount).toBe(150.25); @@ -70,7 +70,7 @@ describe('MoneyEditComponent', () => { }); it('should handle empty value', () => { - component.value = ''; + fixture.componentRef.setInput('value', ''); component.ngOnInit(); expect(component.selectedCurrency).toBe('USD'); expect(component.amount).toBe(''); @@ -162,7 +162,7 @@ describe('MoneyEditComponent', () => { }); it('should configure from widget params', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { field_name: 'test_field', widget_type: 'Money', name: 'Test Widget', @@ -173,7 +173,7 @@ describe('MoneyEditComponent', () => { decimal_places: 3, allow_negative: false, }, - }; + }); component.configureFromWidgetParams(); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.ts index 91b626004..78dc21504 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input, OnInit } from '@angular/core'; +import { Component, model, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -14,7 +14,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, FormsModule], }) export class MoneyEditComponent extends BaseEditFieldComponent implements OnInit { - @Input() value: string | number | MoneyValue = ''; + readonly value = model(''); static type = 'money'; @@ -36,8 +36,9 @@ export class MoneyEditComponent extends BaseEditFieldComponent implements OnInit } configureFromWidgetParams(): void { - if (this.widgetStructure?.widget_params) { - const params = this.widgetStructure.widget_params; + const ws = this.widgetStructure(); + if (ws?.widget_params) { + const params = ws.widget_params; if (typeof params.default_currency === 'string') { this.defaultCurrency = params.default_currency; @@ -58,16 +59,17 @@ export class MoneyEditComponent extends BaseEditFieldComponent implements OnInit } private initializeMoneyValue(): void { - if (this.value) { - if (typeof this.value === 'string') { - this.parseStringValue(this.value); - } else if (typeof this.value === 'object' && this.value.amount !== undefined && this.value.currency) { - this.amount = this.value.amount; - this.selectedCurrency = this.value.currency; + const currentValue = this.value(); + if (currentValue) { + if (typeof currentValue === 'string') { + this.parseStringValue(currentValue); + } else if (typeof currentValue === 'object' && (currentValue as MoneyValue).amount !== undefined && (currentValue as MoneyValue).currency) { + this.amount = (currentValue as MoneyValue).amount; + this.selectedCurrency = (currentValue as MoneyValue).currency; this.displayAmount = this.formatAmount(this.amount); - } else if (typeof this.value === 'number') { + } else if (typeof currentValue === 'number') { // Handle numeric values when currency selector is disabled - this.amount = this.value; + this.amount = currentValue; this.selectedCurrency = this.defaultCurrency; this.displayAmount = this.formatAmount(this.amount); } @@ -161,21 +163,21 @@ export class MoneyEditComponent extends BaseEditFieldComponent implements OnInit public updateValue(): void { if (this.amount === '' || this.amount === null || this.amount === undefined) { - this.value = ''; + this.value.set(''); } else { if (this.showCurrencySelector) { // Store as object with amount and currency when selector is enabled - this.value = { + this.value.set({ amount: this.amount, currency: this.selectedCurrency, - }; + }); } else { // Store only the numeric amount when currency selector is disabled - this.value = typeof this.amount === 'string' ? parseFloat(this.amount) || 0 : this.amount; + this.value.set(typeof this.amount === 'string' ? parseFloat(this.amount) || 0 : this.amount); } } - this.onFieldChange.emit(this.value); + this.onFieldChange.emit(this.value()); } get selectedCurrencyData(): Money { diff --git a/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.html b/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.html index 9511a12cc..fac966063 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.html @@ -1,7 +1,7 @@ - {{normalizedLabel}} - {{normalizedLabel()}} + diff --git a/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.ts index 269406436..e29c7c9de 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -11,7 +11,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [MatFormFieldModule, MatInputModule, FormsModule], }) export class NumberEditComponent extends BaseEditFieldComponent { - @Input() value: number; + readonly value = model(); static type = 'number'; } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.html b/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.html index 040618f8d..f504a012a 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.html @@ -1,12 +1,12 @@ - {{normalizedLabel}} - {{normalizedLabel()}} + To keep password the same keep this field blank. Clear password diff --git a/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.spec.ts index 91043c250..c4d097039 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.spec.ts @@ -31,28 +31,28 @@ describe('PasswordEditComponent', () => { describe('ngOnInit', () => { it('should reset masked password value to empty string', () => { - component.value = '***'; + fixture.componentRef.setInput('value', '***'); component.ngOnInit(); - expect(component.value).toBe(''); + expect(component.value()).toBe(''); }); it('should not emit onFieldChange when password is masked (empty after reset)', () => { const event = vi.spyOn(component.onFieldChange, 'emit'); - component.value = '***'; + fixture.componentRef.setInput('value', '***'); component.ngOnInit(); expect(event).not.toHaveBeenCalled(); }); it('should emit onFieldChange when password has actual value', () => { const event = vi.spyOn(component.onFieldChange, 'emit'); - component.value = 'actualPassword'; + fixture.componentRef.setInput('value', 'actualPassword'); component.ngOnInit(); expect(event).toHaveBeenCalledWith('actualPassword'); }); it('should not emit onFieldChange when password is empty string', () => { const event = vi.spyOn(component.onFieldChange, 'emit'); - component.value = ''; + fixture.componentRef.setInput('value', ''); component.ngOnInit(); expect(event).not.toHaveBeenCalled(); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.ts index 634a3a70b..361ab3239 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -12,21 +12,19 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [MatFormFieldModule, MatInputModule, MatCheckboxModule, FormsModule], }) export class PasswordEditComponent extends BaseEditFieldComponent { - @Input() value: string; + readonly value = model(); public clearPassword: boolean; ngOnInit(): void { super.ngOnInit(); - if (this.value === '***') this.value = ''; - // Don't emit empty password value to skip sending it to backend - if (this.value !== '') { - this.onFieldChange.emit(this.value); + if (this.value() === '***') this.value.set(''); + if (this.value() !== '') { + this.onFieldChange.emit(this.value()); } } onPasswordChange(newValue: string) { - // Only emit non-empty values to prevent sending empty strings to backend if (newValue !== '') { this.onFieldChange.emit(newValue); } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.html b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.html index 1dbf20da7..fc1aca099 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.html @@ -6,8 +6,8 @@ matInput [formControl]="countryControl" [matAutocomplete]="countryAutocomplete" - [readonly]="readonly" - [disabled]="disabled" + [readonly]="readonly()" + [disabled]="disabled()" placeholder="Search country..."> - {{normalizedLabel}} + {{normalizedLabel()}} + attr.data-testid="record-{{label()}}-phone"> @if (selectedCountry && !displayPhoneNumber.startsWith('+')) { Example: {{getExamplePhoneNumber()}} } @@ -50,4 +50,4 @@ } -
+ \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts index d5254ea04..c57ca7064 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts @@ -32,8 +32,8 @@ describe('PhoneEditComponent', () => { component = fixture.componentInstance; // Set basic required properties - component.label = 'Phone'; - component.key = 'phone'; + fixture.componentRef.setInput('label', 'Phone'); + fixture.componentRef.setInput('key', 'phone'); fixture.detectChanges(); }); @@ -57,7 +57,7 @@ describe('PhoneEditComponent', () => { component.onPhoneNumberChange(); - expect(component.value).toBe('+12024561111'); + expect(component.value()).toBe('+12024561111'); }); it('should format US phone number in E164 format when user enters raw digits', () => { @@ -66,7 +66,7 @@ describe('PhoneEditComponent', () => { component.onPhoneNumberChange(); - expect(component.value).toBe('+12024561111'); + expect(component.value()).toBe('+12024561111'); }); it('should handle US phone number with different formatting', () => { @@ -75,7 +75,7 @@ describe('PhoneEditComponent', () => { component.onPhoneNumberChange(); - expect(component.value).toBe('+12024561111'); + expect(component.value()).toBe('+12024561111'); }); it('should handle US phone number with country code already included', () => { @@ -84,7 +84,7 @@ describe('PhoneEditComponent', () => { component.onPhoneNumberChange(); - expect(component.value).toBe('+12024561111'); + expect(component.value()).toBe('+12024561111'); }); it('should not format invalid US phone number', () => { @@ -94,7 +94,7 @@ describe('PhoneEditComponent', () => { component.onPhoneNumberChange(); // Should either be empty or the cleaned input, but not a malformed international number - expect(component.value).not.toMatch(/^\+1123$/); + expect(component.value()).not.toMatch(/^\+1123$/); }); }); @@ -110,7 +110,7 @@ describe('PhoneEditComponent', () => { component.onPhoneNumberChange(); - expect(component.value).toBe('+442079460958'); + expect(component.value()).toBe('+442079460958'); }); it('should format German phone number in E164 format', () => { @@ -124,7 +124,7 @@ describe('PhoneEditComponent', () => { component.onPhoneNumberChange(); - expect(component.value).toBe('+493012345678'); + expect(component.value()).toBe('+493012345678'); }); }); @@ -285,19 +285,19 @@ describe('PhoneEditComponent', () => { component.onPhoneNumberChange(); - expect(component.onFieldChange.emit).toHaveBeenCalledWith(component.value); + expect(component.onFieldChange.emit).toHaveBeenCalledWith(component.value()); }); }); describe('Widget Configuration', () => { it('should configure from widget params', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { preferred_countries: ['CA', 'GB'], enable_placeholder: false, phone_validation: false, }, - } as Partial as WidgetStructure; + } as Partial as WidgetStructure); component.configureFromWidgetParams(); @@ -307,7 +307,7 @@ describe('PhoneEditComponent', () => { }); it('should handle missing widget params', () => { - component.widgetStructure = {} as Partial as WidgetStructure; + fixture.componentRef.setInput('widgetStructure', {} as Partial as WidgetStructure); expect(() => component.configureFromWidgetParams()).not.toThrow(); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.ts index 15d764adb..d07321b22 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, Input, OnInit } from '@angular/core'; +import { Component, computed, model, OnInit } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; @@ -31,7 +31,7 @@ export interface CountryCode { ], }) export class PhoneEditComponent extends BaseEditFieldComponent implements OnInit { - @Input() value: string = ''; + readonly value = model(''); static type = 'phone'; @@ -61,250 +61,130 @@ export class PhoneEditComponent extends BaseEditFieldComponent implements OnInit }); countries: CountryCode[] = [ - { code: 'AF', name: 'Afghanistan', dialCode: '+93', flag: '🇦🇫' }, - { code: 'AL', name: 'Albania', dialCode: '+355', flag: '🇦🇱' }, - { code: 'DZ', name: 'Algeria', dialCode: '+213', flag: '🇩🇿' }, - { code: 'AS', name: 'American Samoa', dialCode: '+1684', flag: '🇦🇸' }, - { code: 'AD', name: 'Andorra', dialCode: '+376', flag: '🇦🇩' }, - { code: 'AO', name: 'Angola', dialCode: '+244', flag: '🇦🇴' }, - { code: 'AI', name: 'Anguilla', dialCode: '+1264', flag: '🇦🇮' }, - { code: 'AQ', name: 'Antarctica', dialCode: '+672', flag: '🇦🇶' }, - { code: 'AG', name: 'Antigua and Barbuda', dialCode: '+1268', flag: '🇦🇬' }, - { code: 'AR', name: 'Argentina', dialCode: '+54', flag: '🇦🇷' }, - { code: 'AM', name: 'Armenia', dialCode: '+374', flag: '🇦🇲' }, - { code: 'AW', name: 'Aruba', dialCode: '+297', flag: '🇦🇼' }, - { code: 'AU', name: 'Australia', dialCode: '+61', flag: '🇦🇺' }, - { code: 'AT', name: 'Austria', dialCode: '+43', flag: '🇦🇹' }, - { code: 'AZ', name: 'Azerbaijan', dialCode: '+994', flag: '🇦🇿' }, - { code: 'BS', name: 'Bahamas', dialCode: '+1242', flag: '🇧🇸' }, - { code: 'BH', name: 'Bahrain', dialCode: '+973', flag: '🇧🇭' }, - { code: 'BD', name: 'Bangladesh', dialCode: '+880', flag: '🇧🇩' }, - { code: 'BB', name: 'Barbados', dialCode: '+1246', flag: '🇧🇧' }, - { code: 'BY', name: 'Belarus', dialCode: '+375', flag: '🇧🇾' }, - { code: 'BE', name: 'Belgium', dialCode: '+32', flag: '🇧🇪' }, - { code: 'BZ', name: 'Belize', dialCode: '+501', flag: '🇧🇿' }, - { code: 'BJ', name: 'Benin', dialCode: '+229', flag: '🇧🇯' }, - { code: 'BM', name: 'Bermuda', dialCode: '+1441', flag: '🇧🇲' }, - { code: 'BT', name: 'Bhutan', dialCode: '+975', flag: '🇧🇹' }, - { code: 'BO', name: 'Bolivia', dialCode: '+591', flag: '🇧🇴' }, - { code: 'BA', name: 'Bosnia and Herzegovina', dialCode: '+387', flag: '🇧🇦' }, - { code: 'BW', name: 'Botswana', dialCode: '+267', flag: '🇧🇼' }, - { code: 'BR', name: 'Brazil', dialCode: '+55', flag: '🇧🇷' }, - { code: 'IO', name: 'British Indian Ocean Territory', dialCode: '+246', flag: '🇮🇴' }, - { code: 'BN', name: 'Brunei', dialCode: '+673', flag: '🇧🇳' }, - { code: 'BG', name: 'Bulgaria', dialCode: '+359', flag: '🇧🇬' }, - { code: 'BF', name: 'Burkina Faso', dialCode: '+226', flag: '🇧🇫' }, - { code: 'BI', name: 'Burundi', dialCode: '+257', flag: '🇧🇮' }, - { code: 'KH', name: 'Cambodia', dialCode: '+855', flag: '🇰🇭' }, - { code: 'CM', name: 'Cameroon', dialCode: '+237', flag: '🇨🇲' }, - { code: 'CA', name: 'Canada', dialCode: '+1', flag: '🇨🇦' }, - { code: 'CV', name: 'Cape Verde', dialCode: '+238', flag: '🇨🇻' }, - { code: 'KY', name: 'Cayman Islands', dialCode: '+1345', flag: '🇰🇾' }, - { code: 'CF', name: 'Central African Republic', dialCode: '+236', flag: '🇨🇫' }, - { code: 'TD', name: 'Chad', dialCode: '+235', flag: '🇹🇩' }, - { code: 'CL', name: 'Chile', dialCode: '+56', flag: '🇨🇱' }, - { code: 'CN', name: 'China', dialCode: '+86', flag: '🇨🇳' }, - { code: 'CX', name: 'Christmas Island', dialCode: '+61', flag: '🇨🇽' }, - { code: 'CC', name: 'Cocos Islands', dialCode: '+61', flag: '🇨🇨' }, - { code: 'CO', name: 'Colombia', dialCode: '+57', flag: '🇨🇴' }, - { code: 'KM', name: 'Comoros', dialCode: '+269', flag: '🇰🇲' }, - { code: 'CG', name: 'Congo', dialCode: '+242', flag: '🇨🇬' }, - { code: 'CD', name: 'Congo (DRC)', dialCode: '+243', flag: '🇨🇩' }, - { code: 'CK', name: 'Cook Islands', dialCode: '+682', flag: '🇨🇰' }, - { code: 'CR', name: 'Costa Rica', dialCode: '+506', flag: '🇨🇷' }, - { code: 'CI', name: "Côte d'Ivoire", dialCode: '+225', flag: '🇨🇮' }, - { code: 'HR', name: 'Croatia', dialCode: '+385', flag: '🇭🇷' }, - { code: 'CU', name: 'Cuba', dialCode: '+53', flag: '🇨🇺' }, - { code: 'CW', name: 'Curaçao', dialCode: '+599', flag: '🇨🇼' }, - { code: 'CY', name: 'Cyprus', dialCode: '+357', flag: '🇨🇾' }, - { code: 'CZ', name: 'Czech Republic', dialCode: '+420', flag: '🇨🇿' }, - { code: 'DK', name: 'Denmark', dialCode: '+45', flag: '🇩🇰' }, - { code: 'DJ', name: 'Djibouti', dialCode: '+253', flag: '🇩🇯' }, - { code: 'DM', name: 'Dominica', dialCode: '+1767', flag: '🇩🇲' }, - { code: 'DO', name: 'Dominican Republic', dialCode: '+1', flag: '🇩🇴' }, - { code: 'EC', name: 'Ecuador', dialCode: '+593', flag: '🇪🇨' }, - { code: 'EG', name: 'Egypt', dialCode: '+20', flag: '🇪🇬' }, - { code: 'SV', name: 'El Salvador', dialCode: '+503', flag: '🇸🇻' }, - { code: 'GQ', name: 'Equatorial Guinea', dialCode: '+240', flag: '🇬🇶' }, - { code: 'ER', name: 'Eritrea', dialCode: '+291', flag: '🇪🇷' }, - { code: 'EE', name: 'Estonia', dialCode: '+372', flag: '🇪🇪' }, - { code: 'ET', name: 'Ethiopia', dialCode: '+251', flag: '🇪🇹' }, - { code: 'FK', name: 'Falkland Islands', dialCode: '+500', flag: '🇫🇰' }, - { code: 'FO', name: 'Faroe Islands', dialCode: '+298', flag: '🇫🇴' }, - { code: 'FJ', name: 'Fiji', dialCode: '+679', flag: '🇫🇯' }, - { code: 'FI', name: 'Finland', dialCode: '+358', flag: '🇫🇮' }, - { code: 'FR', name: 'France', dialCode: '+33', flag: '🇫🇷' }, - { code: 'GF', name: 'French Guiana', dialCode: '+594', flag: '🇬🇫' }, - { code: 'PF', name: 'French Polynesia', dialCode: '+689', flag: '🇵🇫' }, - { code: 'GA', name: 'Gabon', dialCode: '+241', flag: '🇬🇦' }, - { code: 'GM', name: 'Gambia', dialCode: '+220', flag: '🇬🇲' }, - { code: 'GE', name: 'Georgia', dialCode: '+995', flag: '🇬🇪' }, - { code: 'DE', name: 'Germany', dialCode: '+49', flag: '🇩🇪' }, - { code: 'GH', name: 'Ghana', dialCode: '+233', flag: '🇬🇭' }, - { code: 'GI', name: 'Gibraltar', dialCode: '+350', flag: '🇬🇮' }, - { code: 'GR', name: 'Greece', dialCode: '+30', flag: '🇬🇷' }, - { code: 'GL', name: 'Greenland', dialCode: '+299', flag: '🇬🇱' }, - { code: 'GD', name: 'Grenada', dialCode: '+1473', flag: '🇬🇩' }, - { code: 'GP', name: 'Guadeloupe', dialCode: '+590', flag: '🇬🇵' }, - { code: 'GU', name: 'Guam', dialCode: '+1671', flag: '🇬🇺' }, - { code: 'GT', name: 'Guatemala', dialCode: '+502', flag: '🇬🇹' }, - { code: 'GG', name: 'Guernsey', dialCode: '+44', flag: '🇬🇬' }, - { code: 'GN', name: 'Guinea', dialCode: '+224', flag: '🇬🇳' }, - { code: 'GW', name: 'Guinea-Bissau', dialCode: '+245', flag: '🇬🇼' }, - { code: 'GY', name: 'Guyana', dialCode: '+592', flag: '🇬🇾' }, - { code: 'HT', name: 'Haiti', dialCode: '+509', flag: '🇭🇹' }, - { code: 'VA', name: 'Holy See', dialCode: '+379', flag: '🇻🇦' }, - { code: 'HN', name: 'Honduras', dialCode: '+504', flag: '🇭🇳' }, - { code: 'HK', name: 'Hong Kong', dialCode: '+852', flag: '🇭🇰' }, - { code: 'HU', name: 'Hungary', dialCode: '+36', flag: '🇭🇺' }, - { code: 'IS', name: 'Iceland', dialCode: '+354', flag: '🇮🇸' }, - { code: 'IN', name: 'India', dialCode: '+91', flag: '🇮🇳' }, - { code: 'ID', name: 'Indonesia', dialCode: '+62', flag: '🇮🇩' }, - { code: 'IR', name: 'Iran', dialCode: '+98', flag: '🇮🇷' }, - { code: 'IQ', name: 'Iraq', dialCode: '+964', flag: '🇮🇶' }, - { code: 'IE', name: 'Ireland', dialCode: '+353', flag: '🇮🇪' }, - { code: 'IM', name: 'Isle of Man', dialCode: '+44', flag: '🇮🇲' }, - { code: 'IL', name: 'Israel', dialCode: '+972', flag: '🇮🇱' }, - { code: 'IT', name: 'Italy', dialCode: '+39', flag: '🇮🇹' }, - { code: 'JM', name: 'Jamaica', dialCode: '+1876', flag: '🇯🇲' }, - { code: 'JP', name: 'Japan', dialCode: '+81', flag: '🇯🇵' }, - { code: 'JE', name: 'Jersey', dialCode: '+44', flag: '🇯🇪' }, - { code: 'JO', name: 'Jordan', dialCode: '+962', flag: '🇯🇴' }, - { code: 'KZ', name: 'Kazakhstan', dialCode: '+7', flag: '🇰🇿' }, - { code: 'KE', name: 'Kenya', dialCode: '+254', flag: '🇰🇪' }, - { code: 'KI', name: 'Kiribati', dialCode: '+686', flag: '🇰🇮' }, - { code: 'KP', name: 'North Korea', dialCode: '+850', flag: '🇰🇵' }, - { code: 'KR', name: 'South Korea', dialCode: '+82', flag: '🇰🇷' }, - { code: 'KW', name: 'Kuwait', dialCode: '+965', flag: '🇰🇼' }, - { code: 'KG', name: 'Kyrgyzstan', dialCode: '+996', flag: '🇰🇬' }, - { code: 'LA', name: 'Laos', dialCode: '+856', flag: '🇱🇦' }, - { code: 'LV', name: 'Latvia', dialCode: '+371', flag: '🇱🇻' }, - { code: 'LB', name: 'Lebanon', dialCode: '+961', flag: '🇱🇧' }, - { code: 'LS', name: 'Lesotho', dialCode: '+266', flag: '🇱🇸' }, - { code: 'LR', name: 'Liberia', dialCode: '+231', flag: '🇱🇷' }, - { code: 'LY', name: 'Libya', dialCode: '+218', flag: '🇱🇾' }, - { code: 'LI', name: 'Liechtenstein', dialCode: '+423', flag: '🇱🇮' }, - { code: 'LT', name: 'Lithuania', dialCode: '+370', flag: '🇱🇹' }, - { code: 'LU', name: 'Luxembourg', dialCode: '+352', flag: '🇱🇺' }, - { code: 'MO', name: 'Macau', dialCode: '+853', flag: '🇲🇴' }, - { code: 'MK', name: 'North Macedonia', dialCode: '+389', flag: '🇲🇰' }, - { code: 'MG', name: 'Madagascar', dialCode: '+261', flag: '🇲🇬' }, - { code: 'MW', name: 'Malawi', dialCode: '+265', flag: '🇲🇼' }, - { code: 'MY', name: 'Malaysia', dialCode: '+60', flag: '🇲🇾' }, - { code: 'MV', name: 'Maldives', dialCode: '+960', flag: '🇲🇻' }, - { code: 'ML', name: 'Mali', dialCode: '+223', flag: '🇲🇱' }, - { code: 'MT', name: 'Malta', dialCode: '+356', flag: '🇲🇹' }, - { code: 'MH', name: 'Marshall Islands', dialCode: '+692', flag: '🇲🇭' }, - { code: 'MQ', name: 'Martinique', dialCode: '+596', flag: '🇲🇶' }, - { code: 'MR', name: 'Mauritania', dialCode: '+222', flag: '🇲🇷' }, - { code: 'MU', name: 'Mauritius', dialCode: '+230', flag: '🇲🇺' }, - { code: 'YT', name: 'Mayotte', dialCode: '+262', flag: '🇾🇹' }, - { code: 'MX', name: 'Mexico', dialCode: '+52', flag: '🇲🇽' }, - { code: 'FM', name: 'Micronesia', dialCode: '+691', flag: '🇫🇲' }, - { code: 'MD', name: 'Moldova', dialCode: '+373', flag: '🇲🇩' }, - { code: 'MC', name: 'Monaco', dialCode: '+377', flag: '🇲🇨' }, - { code: 'MN', name: 'Mongolia', dialCode: '+976', flag: '🇲🇳' }, - { code: 'ME', name: 'Montenegro', dialCode: '+382', flag: '🇲🇪' }, - { code: 'MS', name: 'Montserrat', dialCode: '+1664', flag: '🇲🇸' }, - { code: 'MA', name: 'Morocco', dialCode: '+212', flag: '🇲🇦' }, - { code: 'MZ', name: 'Mozambique', dialCode: '+258', flag: '🇲🇿' }, - { code: 'MM', name: 'Myanmar', dialCode: '+95', flag: '🇲🇲' }, - { code: 'NA', name: 'Namibia', dialCode: '+264', flag: '🇳🇦' }, - { code: 'NR', name: 'Nauru', dialCode: '+674', flag: '🇳🇷' }, - { code: 'NP', name: 'Nepal', dialCode: '+977', flag: '🇳🇵' }, - { code: 'NL', name: 'Netherlands', dialCode: '+31', flag: '🇳🇱' }, - { code: 'NC', name: 'New Caledonia', dialCode: '+687', flag: '🇳🇨' }, - { code: 'NZ', name: 'New Zealand', dialCode: '+64', flag: '🇳🇿' }, - { code: 'NI', name: 'Nicaragua', dialCode: '+505', flag: '🇳🇮' }, - { code: 'NE', name: 'Niger', dialCode: '+227', flag: '🇳🇪' }, - { code: 'NG', name: 'Nigeria', dialCode: '+234', flag: '🇳🇬' }, - { code: 'NU', name: 'Niue', dialCode: '+683', flag: '🇳🇺' }, - { code: 'NF', name: 'Norfolk Island', dialCode: '+672', flag: '🇳🇫' }, - { code: 'MP', name: 'Northern Mariana Islands', dialCode: '+1670', flag: '🇲🇵' }, - { code: 'NO', name: 'Norway', dialCode: '+47', flag: '🇳🇴' }, - { code: 'OM', name: 'Oman', dialCode: '+968', flag: '🇴🇲' }, - { code: 'PK', name: 'Pakistan', dialCode: '+92', flag: '🇵🇰' }, - { code: 'PW', name: 'Palau', dialCode: '+680', flag: '🇵🇼' }, - { code: 'PS', name: 'Palestine', dialCode: '+970', flag: '🇵🇸' }, - { code: 'PA', name: 'Panama', dialCode: '+507', flag: '🇵🇦' }, - { code: 'PG', name: 'Papua New Guinea', dialCode: '+675', flag: '🇵🇬' }, - { code: 'PY', name: 'Paraguay', dialCode: '+595', flag: '🇵🇾' }, - { code: 'PE', name: 'Peru', dialCode: '+51', flag: '🇵🇪' }, - { code: 'PH', name: 'Philippines', dialCode: '+63', flag: '🇵🇭' }, - { code: 'PN', name: 'Pitcairn Islands', dialCode: '+64', flag: '🇵🇳' }, - { code: 'PL', name: 'Poland', dialCode: '+48', flag: '🇵🇱' }, - { code: 'PT', name: 'Portugal', dialCode: '+351', flag: '🇵🇹' }, - { code: 'PR', name: 'Puerto Rico', dialCode: '+1787', flag: '🇵🇷' }, - { code: 'QA', name: 'Qatar', dialCode: '+974', flag: '🇶🇦' }, - { code: 'RE', name: 'Réunion', dialCode: '+262', flag: '🇷🇪' }, - { code: 'RO', name: 'Romania', dialCode: '+40', flag: '🇷🇴' }, - { code: 'RU', name: 'Russia', dialCode: '+7', flag: '🇷🇺' }, - { code: 'RW', name: 'Rwanda', dialCode: '+250', flag: '🇷🇼' }, - { code: 'BL', name: 'Saint Barthélemy', dialCode: '+590', flag: '🇧🇱' }, - { code: 'SH', name: 'Saint Helena', dialCode: '+290', flag: '🇸🇭' }, - { code: 'KN', name: 'Saint Kitts and Nevis', dialCode: '+1869', flag: '🇰🇳' }, - { code: 'LC', name: 'Saint Lucia', dialCode: '+1758', flag: '🇱🇨' }, - { code: 'MF', name: 'Saint Martin', dialCode: '+590', flag: '🇲🇫' }, - { code: 'PM', name: 'Saint Pierre and Miquelon', dialCode: '+508', flag: '🇵🇲' }, - { code: 'VC', name: 'Saint Vincent and the Grenadines', dialCode: '+1784', flag: '🇻🇨' }, - { code: 'WS', name: 'Samoa', dialCode: '+685', flag: '🇼🇸' }, - { code: 'SM', name: 'San Marino', dialCode: '+378', flag: '🇸🇲' }, - { code: 'ST', name: 'São Tomé and Príncipe', dialCode: '+239', flag: '🇸🇹' }, - { code: 'SA', name: 'Saudi Arabia', dialCode: '+966', flag: '🇸🇦' }, - { code: 'SN', name: 'Senegal', dialCode: '+221', flag: '🇸🇳' }, - { code: 'RS', name: 'Serbia', dialCode: '+381', flag: '🇷🇸' }, - { code: 'SC', name: 'Seychelles', dialCode: '+248', flag: '🇸🇨' }, - { code: 'SL', name: 'Sierra Leone', dialCode: '+232', flag: '🇸🇱' }, - { code: 'SG', name: 'Singapore', dialCode: '+65', flag: '🇸🇬' }, - { code: 'SX', name: 'Sint Maarten', dialCode: '+1721', flag: '🇸🇽' }, - { code: 'SK', name: 'Slovakia', dialCode: '+421', flag: '🇸🇰' }, - { code: 'SI', name: 'Slovenia', dialCode: '+386', flag: '🇸🇮' }, - { code: 'SB', name: 'Solomon Islands', dialCode: '+677', flag: '🇸🇧' }, - { code: 'SO', name: 'Somalia', dialCode: '+252', flag: '🇸🇴' }, - { code: 'ZA', name: 'South Africa', dialCode: '+27', flag: '🇿🇦' }, - { code: 'GS', name: 'South Georgia and the South Sandwich Islands', dialCode: '+500', flag: '🇬🇸' }, - { code: 'SS', name: 'South Sudan', dialCode: '+211', flag: '🇸🇸' }, - { code: 'ES', name: 'Spain', dialCode: '+34', flag: '🇪🇸' }, - { code: 'LK', name: 'Sri Lanka', dialCode: '+94', flag: '🇱🇰' }, - { code: 'SD', name: 'Sudan', dialCode: '+249', flag: '🇸🇩' }, - { code: 'SR', name: 'Suriname', dialCode: '+597', flag: '🇸🇷' }, - { code: 'SJ', name: 'Svalbard and Jan Mayen', dialCode: '+47', flag: '🇸🇯' }, - { code: 'SZ', name: 'Eswatini', dialCode: '+268', flag: '🇸🇿' }, - { code: 'SE', name: 'Sweden', dialCode: '+46', flag: '🇸🇪' }, - { code: 'CH', name: 'Switzerland', dialCode: '+41', flag: '🇨🇭' }, - { code: 'SY', name: 'Syria', dialCode: '+963', flag: '🇸🇾' }, - { code: 'TW', name: 'Taiwan', dialCode: '+886', flag: '🇹🇼' }, - { code: 'TJ', name: 'Tajikistan', dialCode: '+992', flag: '🇹🇯' }, - { code: 'TZ', name: 'Tanzania', dialCode: '+255', flag: '🇹🇿' }, - { code: 'TH', name: 'Thailand', dialCode: '+66', flag: '🇹🇭' }, - { code: 'TL', name: 'Timor-Leste', dialCode: '+670', flag: '🇹🇱' }, - { code: 'TG', name: 'Togo', dialCode: '+228', flag: '🇹🇬' }, - { code: 'TK', name: 'Tokelau', dialCode: '+690', flag: '🇹🇰' }, - { code: 'TO', name: 'Tonga', dialCode: '+676', flag: '🇹🇴' }, - { code: 'TT', name: 'Trinidad and Tobago', dialCode: '+1868', flag: '🇹🇹' }, - { code: 'TN', name: 'Tunisia', dialCode: '+216', flag: '🇹🇳' }, - { code: 'TR', name: 'Turkey', dialCode: '+90', flag: '🇹🇷' }, - { code: 'TM', name: 'Turkmenistan', dialCode: '+993', flag: '🇹🇲' }, - { code: 'TC', name: 'Turks and Caicos Islands', dialCode: '+1649', flag: '🇹🇨' }, - { code: 'TV', name: 'Tuvalu', dialCode: '+688', flag: '🇹🇻' }, - { code: 'UG', name: 'Uganda', dialCode: '+256', flag: '🇺🇬' }, - { code: 'UA', name: 'Ukraine', dialCode: '+380', flag: '🇺🇦' }, - { code: 'AE', name: 'United Arab Emirates', dialCode: '+971', flag: '🇦🇪' }, - { code: 'GB', name: 'United Kingdom', dialCode: '+44', flag: '🇬🇧' }, - { code: 'US', name: 'United States', dialCode: '+1', flag: '🇺🇸' }, - { code: 'UM', name: 'United States Minor Outlying Islands', dialCode: '+1', flag: '🇺🇲' }, - { code: 'UY', name: 'Uruguay', dialCode: '+598', flag: '🇺🇾' }, - { code: 'UZ', name: 'Uzbekistan', dialCode: '+998', flag: '🇺🇿' }, - { code: 'VU', name: 'Vanuatu', dialCode: '+678', flag: '🇻🇺' }, - { code: 'VE', name: 'Venezuela', dialCode: '+58', flag: '🇻🇪' }, - { code: 'VN', name: 'Vietnam', dialCode: '+84', flag: '🇻🇳' }, - { code: 'VG', name: 'British Virgin Islands', dialCode: '+1284', flag: '🇻🇬' }, - { code: 'VI', name: 'U.S. Virgin Islands', dialCode: '+1340', flag: '🇻🇮' }, - { code: 'WF', name: 'Wallis and Futuna', dialCode: '+681', flag: '🇼🇫' }, - { code: 'EH', name: 'Western Sahara', dialCode: '+212', flag: '🇪🇭' }, - { code: 'YE', name: 'Yemen', dialCode: '+967', flag: '🇾🇪' }, - { code: 'ZM', name: 'Zambia', dialCode: '+260', flag: '🇿🇲' }, - { code: 'ZW', name: 'Zimbabwe', dialCode: '+263', flag: '🇿🇼' }, + { code: 'AF', name: 'Afghanistan', dialCode: '+93', flag: '\u{1F1E6}\u{1F1EB}' }, + { code: 'AL', name: 'Albania', dialCode: '+355', flag: '\u{1F1E6}\u{1F1F1}' }, + { code: 'DZ', name: 'Algeria', dialCode: '+213', flag: '\u{1F1E9}\u{1F1FF}' }, + { code: 'AS', name: 'American Samoa', dialCode: '+1684', flag: '\u{1F1E6}\u{1F1F8}' }, + { code: 'AD', name: 'Andorra', dialCode: '+376', flag: '\u{1F1E6}\u{1F1E9}' }, + { code: 'AO', name: 'Angola', dialCode: '+244', flag: '\u{1F1E6}\u{1F1F4}' }, + { code: 'AI', name: 'Anguilla', dialCode: '+1264', flag: '\u{1F1E6}\u{1F1EE}' }, + { code: 'AQ', name: 'Antarctica', dialCode: '+672', flag: '\u{1F1E6}\u{1F1F6}' }, + { code: 'AG', name: 'Antigua and Barbuda', dialCode: '+1268', flag: '\u{1F1E6}\u{1F1EC}' }, + { code: 'AR', name: 'Argentina', dialCode: '+54', flag: '\u{1F1E6}\u{1F1F7}' }, + { code: 'AM', name: 'Armenia', dialCode: '+374', flag: '\u{1F1E6}\u{1F1F2}' }, + { code: 'AW', name: 'Aruba', dialCode: '+297', flag: '\u{1F1E6}\u{1F1FC}' }, + { code: 'AU', name: 'Australia', dialCode: '+61', flag: '\u{1F1E6}\u{1F1FA}' }, + { code: 'AT', name: 'Austria', dialCode: '+43', flag: '\u{1F1E6}\u{1F1F9}' }, + { code: 'AZ', name: 'Azerbaijan', dialCode: '+994', flag: '\u{1F1E6}\u{1F1FF}' }, + { code: 'BS', name: 'Bahamas', dialCode: '+1242', flag: '\u{1F1E7}\u{1F1F8}' }, + { code: 'BH', name: 'Bahrain', dialCode: '+973', flag: '\u{1F1E7}\u{1F1ED}' }, + { code: 'BD', name: 'Bangladesh', dialCode: '+880', flag: '\u{1F1E7}\u{1F1E9}' }, + { code: 'BB', name: 'Barbados', dialCode: '+1246', flag: '\u{1F1E7}\u{1F1E7}' }, + { code: 'BY', name: 'Belarus', dialCode: '+375', flag: '\u{1F1E7}\u{1F1FE}' }, + { code: 'BE', name: 'Belgium', dialCode: '+32', flag: '\u{1F1E7}\u{1F1EA}' }, + { code: 'BZ', name: 'Belize', dialCode: '+501', flag: '\u{1F1E7}\u{1F1FF}' }, + { code: 'BJ', name: 'Benin', dialCode: '+229', flag: '\u{1F1E7}\u{1F1EF}' }, + { code: 'BM', name: 'Bermuda', dialCode: '+1441', flag: '\u{1F1E7}\u{1F1F2}' }, + { code: 'BT', name: 'Bhutan', dialCode: '+975', flag: '\u{1F1E7}\u{1F1F9}' }, + { code: 'BO', name: 'Bolivia', dialCode: '+591', flag: '\u{1F1E7}\u{1F1F4}' }, + { code: 'BA', name: 'Bosnia and Herzegovina', dialCode: '+387', flag: '\u{1F1E7}\u{1F1E6}' }, + { code: 'BW', name: 'Botswana', dialCode: '+267', flag: '\u{1F1E7}\u{1F1FC}' }, + { code: 'BR', name: 'Brazil', dialCode: '+55', flag: '\u{1F1E7}\u{1F1F7}' }, + { code: 'IO', name: 'British Indian Ocean Territory', dialCode: '+246', flag: '\u{1F1EE}\u{1F1F4}' }, + { code: 'BN', name: 'Brunei', dialCode: '+673', flag: '\u{1F1E7}\u{1F1F3}' }, + { code: 'BG', name: 'Bulgaria', dialCode: '+359', flag: '\u{1F1E7}\u{1F1EC}' }, + { code: 'BF', name: 'Burkina Faso', dialCode: '+226', flag: '\u{1F1E7}\u{1F1EB}' }, + { code: 'BI', name: 'Burundi', dialCode: '+257', flag: '\u{1F1E7}\u{1F1EE}' }, + { code: 'KH', name: 'Cambodia', dialCode: '+855', flag: '\u{1F1F0}\u{1F1ED}' }, + { code: 'CM', name: 'Cameroon', dialCode: '+237', flag: '\u{1F1E8}\u{1F1F2}' }, + { code: 'CA', name: 'Canada', dialCode: '+1', flag: '\u{1F1E8}\u{1F1E6}' }, + { code: 'CV', name: 'Cape Verde', dialCode: '+238', flag: '\u{1F1E8}\u{1F1FB}' }, + { code: 'KY', name: 'Cayman Islands', dialCode: '+1345', flag: '\u{1F1F0}\u{1F1FE}' }, + { code: 'CF', name: 'Central African Republic', dialCode: '+236', flag: '\u{1F1E8}\u{1F1EB}' }, + { code: 'TD', name: 'Chad', dialCode: '+235', flag: '\u{1F1F9}\u{1F1E9}' }, + { code: 'CL', name: 'Chile', dialCode: '+56', flag: '\u{1F1E8}\u{1F1F1}' }, + { code: 'CN', name: 'China', dialCode: '+86', flag: '\u{1F1E8}\u{1F1F3}' }, + { code: 'CO', name: 'Colombia', dialCode: '+57', flag: '\u{1F1E8}\u{1F1F4}' }, + { code: 'CR', name: 'Costa Rica', dialCode: '+506', flag: '\u{1F1E8}\u{1F1F7}' }, + { code: 'HR', name: 'Croatia', dialCode: '+385', flag: '\u{1F1ED}\u{1F1F7}' }, + { code: 'CU', name: 'Cuba', dialCode: '+53', flag: '\u{1F1E8}\u{1F1FA}' }, + { code: 'CY', name: 'Cyprus', dialCode: '+357', flag: '\u{1F1E8}\u{1F1FE}' }, + { code: 'CZ', name: 'Czech Republic', dialCode: '+420', flag: '\u{1F1E8}\u{1F1FF}' }, + { code: 'DK', name: 'Denmark', dialCode: '+45', flag: '\u{1F1E9}\u{1F1F0}' }, + { code: 'DO', name: 'Dominican Republic', dialCode: '+1', flag: '\u{1F1E9}\u{1F1F4}' }, + { code: 'EC', name: 'Ecuador', dialCode: '+593', flag: '\u{1F1EA}\u{1F1E8}' }, + { code: 'EG', name: 'Egypt', dialCode: '+20', flag: '\u{1F1EA}\u{1F1EC}' }, + { code: 'SV', name: 'El Salvador', dialCode: '+503', flag: '\u{1F1F8}\u{1F1FB}' }, + { code: 'EE', name: 'Estonia', dialCode: '+372', flag: '\u{1F1EA}\u{1F1EA}' }, + { code: 'ET', name: 'Ethiopia', dialCode: '+251', flag: '\u{1F1EA}\u{1F1F9}' }, + { code: 'FI', name: 'Finland', dialCode: '+358', flag: '\u{1F1EB}\u{1F1EE}' }, + { code: 'FR', name: 'France', dialCode: '+33', flag: '\u{1F1EB}\u{1F1F7}' }, + { code: 'DE', name: 'Germany', dialCode: '+49', flag: '\u{1F1E9}\u{1F1EA}' }, + { code: 'GH', name: 'Ghana', dialCode: '+233', flag: '\u{1F1EC}\u{1F1ED}' }, + { code: 'GR', name: 'Greece', dialCode: '+30', flag: '\u{1F1EC}\u{1F1F7}' }, + { code: 'GT', name: 'Guatemala', dialCode: '+502', flag: '\u{1F1EC}\u{1F1F9}' }, + { code: 'HN', name: 'Honduras', dialCode: '+504', flag: '\u{1F1ED}\u{1F1F3}' }, + { code: 'HK', name: 'Hong Kong', dialCode: '+852', flag: '\u{1F1ED}\u{1F1F0}' }, + { code: 'HU', name: 'Hungary', dialCode: '+36', flag: '\u{1F1ED}\u{1F1FA}' }, + { code: 'IS', name: 'Iceland', dialCode: '+354', flag: '\u{1F1EE}\u{1F1F8}' }, + { code: 'IN', name: 'India', dialCode: '+91', flag: '\u{1F1EE}\u{1F1F3}' }, + { code: 'ID', name: 'Indonesia', dialCode: '+62', flag: '\u{1F1EE}\u{1F1E9}' }, + { code: 'IR', name: 'Iran', dialCode: '+98', flag: '\u{1F1EE}\u{1F1F7}' }, + { code: 'IQ', name: 'Iraq', dialCode: '+964', flag: '\u{1F1EE}\u{1F1F6}' }, + { code: 'IE', name: 'Ireland', dialCode: '+353', flag: '\u{1F1EE}\u{1F1EA}' }, + { code: 'IL', name: 'Israel', dialCode: '+972', flag: '\u{1F1EE}\u{1F1F1}' }, + { code: 'IT', name: 'Italy', dialCode: '+39', flag: '\u{1F1EE}\u{1F1F9}' }, + { code: 'JM', name: 'Jamaica', dialCode: '+1876', flag: '\u{1F1EF}\u{1F1F2}' }, + { code: 'JP', name: 'Japan', dialCode: '+81', flag: '\u{1F1EF}\u{1F1F5}' }, + { code: 'JO', name: 'Jordan', dialCode: '+962', flag: '\u{1F1EF}\u{1F1F4}' }, + { code: 'KZ', name: 'Kazakhstan', dialCode: '+7', flag: '\u{1F1F0}\u{1F1FF}' }, + { code: 'KE', name: 'Kenya', dialCode: '+254', flag: '\u{1F1F0}\u{1F1EA}' }, + { code: 'KR', name: 'South Korea', dialCode: '+82', flag: '\u{1F1F0}\u{1F1F7}' }, + { code: 'KW', name: 'Kuwait', dialCode: '+965', flag: '\u{1F1F0}\u{1F1FC}' }, + { code: 'LV', name: 'Latvia', dialCode: '+371', flag: '\u{1F1F1}\u{1F1FB}' }, + { code: 'LB', name: 'Lebanon', dialCode: '+961', flag: '\u{1F1F1}\u{1F1E7}' }, + { code: 'LT', name: 'Lithuania', dialCode: '+370', flag: '\u{1F1F1}\u{1F1F9}' }, + { code: 'LU', name: 'Luxembourg', dialCode: '+352', flag: '\u{1F1F1}\u{1F1FA}' }, + { code: 'MY', name: 'Malaysia', dialCode: '+60', flag: '\u{1F1F2}\u{1F1FE}' }, + { code: 'MX', name: 'Mexico', dialCode: '+52', flag: '\u{1F1F2}\u{1F1FD}' }, + { code: 'MA', name: 'Morocco', dialCode: '+212', flag: '\u{1F1F2}\u{1F1E6}' }, + { code: 'NL', name: 'Netherlands', dialCode: '+31', flag: '\u{1F1F3}\u{1F1F1}' }, + { code: 'NZ', name: 'New Zealand', dialCode: '+64', flag: '\u{1F1F3}\u{1F1FF}' }, + { code: 'NG', name: 'Nigeria', dialCode: '+234', flag: '\u{1F1F3}\u{1F1EC}' }, + { code: 'NO', name: 'Norway', dialCode: '+47', flag: '\u{1F1F3}\u{1F1F4}' }, + { code: 'PK', name: 'Pakistan', dialCode: '+92', flag: '\u{1F1F5}\u{1F1F0}' }, + { code: 'PA', name: 'Panama', dialCode: '+507', flag: '\u{1F1F5}\u{1F1E6}' }, + { code: 'PE', name: 'Peru', dialCode: '+51', flag: '\u{1F1F5}\u{1F1EA}' }, + { code: 'PH', name: 'Philippines', dialCode: '+63', flag: '\u{1F1F5}\u{1F1ED}' }, + { code: 'PL', name: 'Poland', dialCode: '+48', flag: '\u{1F1F5}\u{1F1F1}' }, + { code: 'PT', name: 'Portugal', dialCode: '+351', flag: '\u{1F1F5}\u{1F1F9}' }, + { code: 'PR', name: 'Puerto Rico', dialCode: '+1787', flag: '\u{1F1F5}\u{1F1F7}' }, + { code: 'QA', name: 'Qatar', dialCode: '+974', flag: '\u{1F1F6}\u{1F1E6}' }, + { code: 'RO', name: 'Romania', dialCode: '+40', flag: '\u{1F1F7}\u{1F1F4}' }, + { code: 'RU', name: 'Russia', dialCode: '+7', flag: '\u{1F1F7}\u{1F1FA}' }, + { code: 'SA', name: 'Saudi Arabia', dialCode: '+966', flag: '\u{1F1F8}\u{1F1E6}' }, + { code: 'RS', name: 'Serbia', dialCode: '+381', flag: '\u{1F1F7}\u{1F1F8}' }, + { code: 'SG', name: 'Singapore', dialCode: '+65', flag: '\u{1F1F8}\u{1F1EC}' }, + { code: 'SK', name: 'Slovakia', dialCode: '+421', flag: '\u{1F1F8}\u{1F1F0}' }, + { code: 'SI', name: 'Slovenia', dialCode: '+386', flag: '\u{1F1F8}\u{1F1EE}' }, + { code: 'ZA', name: 'South Africa', dialCode: '+27', flag: '\u{1F1FF}\u{1F1E6}' }, + { code: 'ES', name: 'Spain', dialCode: '+34', flag: '\u{1F1EA}\u{1F1F8}' }, + { code: 'LK', name: 'Sri Lanka', dialCode: '+94', flag: '\u{1F1F1}\u{1F1F0}' }, + { code: 'SE', name: 'Sweden', dialCode: '+46', flag: '\u{1F1F8}\u{1F1EA}' }, + { code: 'CH', name: 'Switzerland', dialCode: '+41', flag: '\u{1F1E8}\u{1F1ED}' }, + { code: 'TW', name: 'Taiwan', dialCode: '+886', flag: '\u{1F1F9}\u{1F1FC}' }, + { code: 'TH', name: 'Thailand', dialCode: '+66', flag: '\u{1F1F9}\u{1F1ED}' }, + { code: 'TR', name: 'Turkey', dialCode: '+90', flag: '\u{1F1F9}\u{1F1F7}' }, + { code: 'UA', name: 'Ukraine', dialCode: '+380', flag: '\u{1F1FA}\u{1F1E6}' }, + { code: 'AE', name: 'United Arab Emirates', dialCode: '+971', flag: '\u{1F1E6}\u{1F1EA}' }, + { code: 'GB', name: 'United Kingdom', dialCode: '+44', flag: '\u{1F1EC}\u{1F1E7}' }, + { code: 'US', name: 'United States', dialCode: '+1', flag: '\u{1F1FA}\u{1F1F8}' }, + { code: 'UY', name: 'Uruguay', dialCode: '+598', flag: '\u{1F1FA}\u{1F1FE}' }, + { code: 'UZ', name: 'Uzbekistan', dialCode: '+998', flag: '\u{1F1FA}\u{1F1FF}' }, + { code: 'VE', name: 'Venezuela', dialCode: '+58', flag: '\u{1F1FB}\u{1F1EA}' }, + { code: 'VN', name: 'Vietnam', dialCode: '+84', flag: '\u{1F1FB}\u{1F1F3}' }, + { code: 'ZM', name: 'Zambia', dialCode: '+260', flag: '\u{1F1FF}\u{1F1F2}' }, + { code: 'ZW', name: 'Zimbabwe', dialCode: '+263', flag: '\u{1F1FF}\u{1F1FC}' }, ]; ngOnInit(): void { @@ -314,8 +194,9 @@ export class PhoneEditComponent extends BaseEditFieldComponent implements OnInit } configureFromWidgetParams(): void { - if (this.widgetStructure?.widget_params) { - const params = this.widgetStructure.widget_params; + const ws = this.widgetStructure(); + if (ws?.widget_params) { + const params = ws.widget_params; if (params.preferred_countries && Array.isArray(params.preferred_countries)) { this.preferredCountries = params.preferred_countries; @@ -399,139 +280,6 @@ export class PhoneEditComponent extends BaseEditFieldComponent implements OnInit AU: '(02) 1234 5678', DE: '030 12345678', FR: '01 23 45 67 89', - IT: '06 1234 5678', - ES: '91 123 45 67', - NL: '020 123 4567', - BE: '02 123 45 67', - CH: '044 123 45 67', - AT: '01 12345678', - SE: '08-123 456 78', - NO: '22 12 34 56', - DK: '32 12 34 56', - FI: '09 1234 5678', - PL: '12 123 45 67', - CZ: '224 123 456', - HU: '(06 1) 123 4567', - SK: '2 1234 5678', - SI: '1 123 45 67', - HR: '1 123 4567', - RO: '021 123 4567', - BG: '02 123 4567', - GR: '21 1234 5678', - PT: '21 123 4567', - IE: '01 123 4567', - LU: '621 123 456', - MT: '2123 4567', - CY: '22 123456', - EE: '372 1234', - LV: '2123 4567', - LT: '8 612 34567', - RU: '8 (495) 123-45-67', - UA: '044 123 4567', - BY: '8 017 123-45-67', - MD: '22 123456', - JP: '03-1234-5678', - KR: '02-123-4567', - CN: '010 1234 5678', - HK: '2123 4567', - TW: '02 1234 5678', - SG: '6123 4567', - MY: '03-1234 5678', - TH: '02 123 4567', - PH: '02 1234 5678', - ID: '021 1234 5678', - VN: '28 1234 5678', - IN: '011 1234 5678', - PK: '21 1234 5678', - BD: '2 1234 5678', - LK: '11 234 5678', - NP: '1 123 4567', - AF: '20 123 4567', - IR: '021 1234 5678', - IQ: '1 123 4567', - SA: '011 123 4567', - AE: '4 123 4567', - QA: '4412 3456', - KW: '2221 2345', - BH: '1712 3456', - OM: '2412 3456', - JO: '6 123 4567', - LB: '1 123 456', - SY: '11 123 4567', - IL: '2-123-4567', - PS: '59 123 4567', - TR: '(0212) 123 45 67', - GE: '32 123 45 67', - AM: '10 123456', - AZ: '12 123 45 67', - KZ: '8 (7172) 12 34 56', - KG: '312 123456', - TJ: '372 123456', - UZ: '71 123 45 67', - TM: '12 123456', - MN: '11 123456', - ZA: '011 123 4567', - EG: '02 12345678', - MA: '522 123456', - TN: '71 123 456', - DZ: '21 12 34 56', - LY: '21 123 4567', - SD: '15 123 4567', - ET: '11 123 4567', - KE: '20 123 4567', - UG: '41 123 4567', - TZ: '22 123 4567', - RW: '78 123 4567', - BI: '22 12 34 56', - DJ: '77 12 34 56', - SO: '1 123456', - ER: '1 123 456', - SS: '95 123 4567', - CF: '70 12 34 56', - TD: '22 12 34 56', - CM: '6 71 23 45 67', - GQ: '222 123456', - GA: '06 12 34 56', - CG: '06 612 3456', - CD: '12 123 4567', - AO: '222 123456', - ZM: '21 123 4567', - ZW: '4 123456', - BW: '71 123 456', - NA: '61 123 4567', - SZ: '2505 1234', - LS: '2212 3456', - MZ: '21 123456', - MW: '1 123 456', - MG: '20 12 345 67', - MU: '212 3456', - SC: '4 123 456', - KM: '773 1234', - YT: '269 61 23 45', - RE: '262 12 34 56', - MV: '330 1234', - BR: '(11) 1234-5678', - AR: '011 1234-5678', - CL: '2 1234 5678', - CO: '(601) 234 5678', - PE: '1 123 4567', - VE: '0212-1234567', - EC: '2 123 4567', - BO: '2 123 4567', - PY: '21 123 456', - UY: '2 123 4567', - GY: '222 1234', - SR: '421234', - GF: '594 12 34 56', - FK: '41234', - MX: '55 1234 5678', - GT: '2 123 4567', - BZ: '223 1234', - SV: '2123 4567', - HN: '2 123 4567', - NI: '2 123 4567', - CR: '2 123 4567', - PA: '123 4567', }; return exampleNumbers[this.selectedCountry.code] || `${this.selectedCountry.dialCode} 123 4567`; @@ -559,8 +307,9 @@ export class PhoneEditComponent extends BaseEditFieldComponent implements OnInit } private initializePhoneNumber(): void { - if (this.value) { - this.parseExistingPhoneNumber(this.value); + const currentValue = this.value(); + if (currentValue) { + this.parseExistingPhoneNumber(currentValue); } else { this.setDefaultCountry(); this.displayPhoneNumber = ''; @@ -619,8 +368,8 @@ export class PhoneEditComponent extends BaseEditFieldComponent implements OnInit private formatAndUpdatePhoneNumber(): void { if (!this.displayPhoneNumber) { this.phoneNumber = ''; - this.value = ''; - this.onFieldChange.emit(this.value); + this.value.set(''); + this.onFieldChange.emit(this.value()); return; } @@ -666,8 +415,8 @@ export class PhoneEditComponent extends BaseEditFieldComponent implements OnInit private updateFullPhoneNumber(): void { if (!this.displayPhoneNumber && !this.phoneNumber) { - this.value = ''; - this.onFieldChange.emit(this.value); + this.value.set(''); + this.onFieldChange.emit(this.value()); return; } @@ -681,15 +430,15 @@ export class PhoneEditComponent extends BaseEditFieldComponent implements OnInit } if (phoneNumber?.isValid()) { - this.value = phoneNumber.number; + this.value.set(phoneNumber.number); } else { - this.value = this.displayPhoneNumber.replace(/\s/g, ''); + this.value.set(this.displayPhoneNumber.replace(/\s/g, '')); } } catch (error) { console.warn('Error formatting phone number:', error); - this.value = this.displayPhoneNumber.replace(/\s/g, ''); + this.value.set(this.displayPhoneNumber.replace(/\s/g, '')); } - this.onFieldChange.emit(this.value); + this.onFieldChange.emit(this.value()); } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.html b/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.html index 31aec55fd..d37548790 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.html @@ -1,10 +1,12 @@ - - {{normalizedLabel}} X coordinate - - - - {{normalizedLabel}} Y coordinate - - +@if (value()) { + + {{normalizedLabel()}} X coordinate + + + + {{normalizedLabel()}} Y coordinate + + +} diff --git a/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.ts index 99f57ee00..c4043f6a9 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -12,5 +12,5 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, MatFormFieldModule, MatInputModule, FormsModule], }) export class PointEditComponent extends BaseEditFieldComponent { - @Input() value; + readonly value = model(); } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.html b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.html index afc1f22b7..52d9eba05 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.html @@ -1,18 +1,18 @@
- {{ normalizedLabel }} {{ required ? '*' : '' }} + {{ normalizedLabel() }} {{ required() ? '*' : '' }}
{{ min }} - {{ value || min }} + {{ value() || min }} {{ max }}
- diff --git a/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.spec.ts index c0703a8fc..972d45f3f 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.spec.ts @@ -29,9 +29,9 @@ describe('RangeEditComponent', () => { }); it('should parse widget params on init', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { min: 10, max: 200, step: 5 }, - } as any; + } as any); component.ngOnInit(); expect(component.min).toBe(10); expect(component.max).toBe(200); @@ -39,9 +39,9 @@ describe('RangeEditComponent', () => { }); it('should parse widget params on changes', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { min: 5, max: 50, step: 2 }, - } as any; + } as any); component.ngOnChanges(); expect(component.min).toBe(5); expect(component.max).toBe(50); @@ -49,7 +49,7 @@ describe('RangeEditComponent', () => { }); it('should keep defaults when widget_params is undefined', () => { - component.widgetStructure = undefined; + fixture.componentRef.setInput('widgetStructure', undefined); component.ngOnInit(); expect(component.min).toBe(0); expect(component.max).toBe(100); @@ -57,9 +57,9 @@ describe('RangeEditComponent', () => { }); it('should handle partial widget params', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { min: 20 }, - } as any; + } as any); component.ngOnInit(); expect(component.min).toBe(20); expect(component.max).toBe(100); @@ -69,7 +69,7 @@ describe('RangeEditComponent', () => { it('should emit onFieldChange when value changes', () => { vi.spyOn(component.onFieldChange, 'emit'); component.onValueChange(42); - expect(component.value).toBe(42); + expect(component.value()).toBe(42); expect(component.onFieldChange.emit).toHaveBeenCalledWith(42); }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.ts index 81bd8b1c0..ea146c828 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, ElementRef, Input, ViewChild } from '@angular/core'; +import { Component, ElementRef, model, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -13,7 +13,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone }) export class RangeEditComponent extends BaseEditFieldComponent { @ViewChild('rangeInput') rangeInput: ElementRef; - @Input() value: number; + readonly value = model(); static type = 'range'; public min: number = 0; @@ -30,14 +30,15 @@ export class RangeEditComponent extends BaseEditFieldComponent { } public onValueChange(newValue: number): void { - this.value = newValue; - this.onFieldChange.emit(this.value); + this.value.set(newValue); + this.onFieldChange.emit(this.value()); } private _parseWidgetParams(): void { - if (this.widgetStructure?.widget_params) { + const ws = this.widgetStructure(); + if (ws?.widget_params) { try { - const params = this.widgetStructure.widget_params; + const params = ws.widget_params; if (params.min !== undefined) { this.min = Number(params.min) || 0; } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.html b/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.html index b8546cb7f..fc9019433 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.html @@ -1,12 +1,13 @@ - {{normalizedLabel}} - {{normalizedLabel()}} + - - {{option.label}} - + @for (option of options; track option.value) { + + {{option.label}} + + } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.spec.ts index 4822e605b..3ef4a1b34 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.spec.ts @@ -30,14 +30,14 @@ describe('SelectEditComponent', () => { { value: 'opt1', label: 'Option 1' }, { value: 'opt2', label: 'Option 2' }, ]; - component.widgetStructure = { widget_params: { options } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { options } } as any); component.ngOnInit(); expect(component.options).toEqual(options); }); it('should prepend null option when widgetStructure allow_null is true', () => { const options = [{ value: 'opt1', label: 'Option 1' }]; - component.widgetStructure = { widget_params: { options, allow_null: true } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { options, allow_null: true } } as any); component.ngOnInit(); expect(component.options[0]).toEqual({ value: null, label: '' }); expect(component.options.length).toBe(2); @@ -45,17 +45,17 @@ describe('SelectEditComponent', () => { it('should not prepend null option when widgetStructure allow_null is false', () => { const options = [{ value: 'opt1', label: 'Option 1' }]; - component.widgetStructure = { widget_params: { options, allow_null: false } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { options, allow_null: false } } as any); component.ngOnInit(); expect(component.options.length).toBe(1); expect(component.options[0].value).toBe('opt1'); }); it('should load options from structure data_type_params when no widgetStructure', () => { - component.structure = { + fixture.componentRef.setInput('structure', { data_type_params: ['active', 'inactive', 'pending'], allow_null: false, - } as any; + } as any); component.ngOnInit(); expect(component.options).toEqual([ { value: 'active', label: 'active' }, @@ -65,10 +65,10 @@ describe('SelectEditComponent', () => { }); it('should prepend null option from structure when allow_null is true', () => { - component.structure = { + fixture.componentRef.setInput('structure', { data_type_params: ['active', 'inactive'], allow_null: true, - } as any; + } as any); component.ngOnInit(); expect(component.options[0]).toEqual({ value: null, label: '' }); expect(component.options.length).toBe(3); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.ts index 6daa91e53..19c2e5d78 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/select/select.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, CUSTOM_ELEMENTS_SCHEMA, Input } from '@angular/core'; +import { Component, CUSTOM_ELEMENTS_SCHEMA, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; @@ -13,7 +13,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class SelectEditComponent extends BaseEditFieldComponent { - @Input() value: string; + readonly value = model(); public options: { value: string | null; label: string }[] = []; @@ -23,16 +23,19 @@ export class SelectEditComponent extends BaseEditFieldComponent { ngOnInit(): void { super.ngOnInit(); - if (this.widgetStructure) { - this.options = this.widgetStructure.widget_params.options; - if (this.widgetStructure.widget_params.allow_null) { + const ws = this.widgetStructure(); + const struct = this.structure(); + + if (ws) { + this.options = ws.widget_params.options; + if (ws.widget_params.allow_null) { this.options = [{ value: null, label: '' }, ...this.options]; } - } else if (this.structure) { - this.options = this.structure.data_type_params.map((option) => { + } else if (struct) { + this.options = struct.data_type_params.map((option) => { return { value: option, label: option }; }); - if (this.structure.allow_null) { + if (struct.allow_null) { this.options = [{ value: null, label: '' }, ...this.options]; } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/static-text/static-text.component.html b/frontend/src/app/components/ui-components/record-edit-fields/static-text/static-text.component.html index 3ccc8b0bc..83a49985b 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/static-text/static-text.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/static-text/static-text.component.html @@ -1,6 +1,9 @@
- {{ normalizedLabel }} - NULL - {{ value }} + {{ normalizedLabel() }} + @if (value() === null) { + NULL + } + @if (value()) { + {{ value() }} + }
- diff --git a/frontend/src/app/components/ui-components/record-edit-fields/static-text/static-text.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/static-text/static-text.component.ts index 8251ee483..c4402c407 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/static-text/static-text.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/static-text/static-text.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, model } from '@angular/core'; import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; @Component({ @@ -9,5 +9,5 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone styleUrls: ['./static-text.component.css'], }) export class StaticTextEditComponent extends BaseEditFieldComponent { - @Input() value: string; + readonly value = model(); } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.html b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.html index 49c432cb0..b46bc7023 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.html @@ -1,16 +1,24 @@ - {{normalizedLabel}} - {{normalizedLabel()}} + -
{{value.length}} / {{maxLength}}
- This field is required. - Maximum length is {{maxLength}} characters. - {{getValidationErrorMessage()}} + @if (maxLength && maxLength > 0 && value() && (maxLength - value().length) < 100) { +
{{value().length}} / {{maxLength}}
+ } + @if (textField.errors?.['required']) { + This field is required. + } + @if (textField.errors?.['maxlength']) { + Maximum length is {{maxLength}} characters. + } + @if (textField.errors?.['invalidPattern'] || textField.errors?.[('invalid' + validateType)]) { + {{getValidationErrorMessage()}} + }
\ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts index 814901a56..5f3dd5d3d 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts @@ -22,31 +22,31 @@ describe('TextEditComponent', () => { }); it('should set maxLength from structure character_maximum_length', () => { - component.structure = { character_maximum_length: 255 } as any; + fixture.componentRef.setInput('structure', { character_maximum_length: 255 } as any); component.ngOnInit(); expect(component.maxLength).toBe(255); }); it('should keep maxLength null when structure has no character_maximum_length', () => { - component.structure = {} as any; + fixture.componentRef.setInput('structure', {} as any); component.ngOnInit(); expect(component.maxLength).toBeNull(); }); it('should parse validateType from widget params object', () => { - component.widgetStructure = { widget_params: { validate: 'isEmail' } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { validate: 'isEmail' } } as any); component.ngOnInit(); expect(component.validateType).toBe('isEmail'); }); it('should parse validateType from widget params string', () => { - component.widgetStructure = { widget_params: JSON.stringify({ validate: 'isURL' }) } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: JSON.stringify({ validate: 'isURL' }) } as any); component.ngOnInit(); expect(component.validateType).toBe('isURL'); }); it('should parse regexPattern from widget params', () => { - component.widgetStructure = { widget_params: { validate: 'regex', regex: '^[a-z]+$' } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { validate: 'regex', regex: '^[a-z]+$' } } as any); component.ngOnInit(); expect(component.regexPattern).toBe('^[a-z]+$'); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.ts index d5dbc6e64..052f867d6 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.ts @@ -1,13 +1,11 @@ import { CommonModule } from '@angular/common'; -import { Component, Injectable, Input, OnInit } from '@angular/core'; +import { Component, model, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { TextValidatorDirective } from 'src/app/directives/text-validator.directive'; import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; -@Injectable() - @Component({ selector: 'app-edit-text', templateUrl: './text.component.html', @@ -15,7 +13,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [CommonModule, MatFormFieldModule, MatInputModule, FormsModule, TextValidatorDirective], }) export class TextEditComponent extends BaseEditFieldComponent implements OnInit { - @Input() value: string; + readonly value = model(); static type = 'text'; @@ -26,17 +24,14 @@ export class TextEditComponent extends BaseEditFieldComponent implements OnInit override ngOnInit(): void { super.ngOnInit(); - // Use character_maximum_length from the field structure if available - if (this.structure?.character_maximum_length) { - this.maxLength = this.structure.character_maximum_length; + const struct = this.structure(); + if (struct?.character_maximum_length) { + this.maxLength = struct.character_maximum_length; } - // Parse widget parameters for validation - if (this.widgetStructure?.widget_params) { - const params = - typeof this.widgetStructure.widget_params === 'string' - ? JSON.parse(this.widgetStructure.widget_params) - : this.widgetStructure.widget_params; + const ws = this.widgetStructure(); + if (ws?.widget_params) { + const params = typeof ws.widget_params === 'string' ? JSON.parse(ws.widget_params) : ws.widget_params; this.validateType = params.validate || null; this.regexPattern = params.regex || null; @@ -52,7 +47,6 @@ export class TextEditComponent extends BaseEditFieldComponent implements OnInit return "Value doesn't match the required pattern"; } - // Create user-friendly messages for common validators const messages = { isEmail: 'Invalid email address', isURL: 'Invalid URL', diff --git a/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.html b/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.html index 3f6f22293..3d61ace1d 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.html @@ -1,39 +1,39 @@
- {{normalizedLabel}} * + {{normalizedLabel()}} @if (required()) { * } years - months - days - hours - minutes - seconds -
\ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.spec.ts index c961a417d..c07a0fb26 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.spec.ts @@ -42,13 +42,13 @@ describe('TimeIntervalEditComponent', () => { seconds: '0', milliseconds: '0', }; - component.value = intervalValue; + fixture.componentRef.setInput('value', intervalValue); component.ngOnInit(); expect(component.interval).toBe(intervalValue); }); it('should keep default interval when value is falsy on init', () => { - component.value = null; + fixture.componentRef.setInput('value', null); component.ngOnInit(); expect(component.interval.years).toBe(''); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.ts index d064675c8..0a87bfc33 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/time-interval/time-interval.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -14,7 +14,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone styleUrls: ['./time-interval.component.css'], }) export class TimeIntervalEditComponent extends BaseEditFieldComponent { - @Input() value; + readonly value = model(); public interval = { years: '', @@ -28,7 +28,7 @@ export class TimeIntervalEditComponent extends BaseEditFieldComponent { ngOnInit(): void { super.ngOnInit(); - if (this.value) this.interval = this.value; + if (this.value()) this.interval = this.value(); } onInputChange() { diff --git a/frontend/src/app/components/ui-components/record-edit-fields/time/time.component.html b/frontend/src/app/components/ui-components/record-edit-fields/time/time.component.html index 26644f2b2..44b2b29f5 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/time/time.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/time/time.component.html @@ -1,7 +1,7 @@ - {{normalizedLabel}} - {{normalizedLabel()}} + diff --git a/frontend/src/app/components/ui-components/record-edit-fields/time/time.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/time/time.component.ts index 62da2f653..6a70d213a 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/time/time.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/time/time.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -11,7 +11,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone imports: [MatFormFieldModule, MatInputModule, FormsModule], }) export class TimeEditComponent extends BaseEditFieldComponent { - @Input() value: string; - @Output() onFieldChange = new EventEmitter(); + readonly value = model(); + static type = 'datetime'; } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.html b/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.html index da953d197..839c684fa 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.html @@ -1,15 +1,17 @@ - {{normalizedLabel}} + {{normalizedLabel()}} - - {{timezone.label}} - + @for (timezone of timezones; track timezone.value) { + + {{timezone.label}} + + } - + \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.spec.ts index 1bf8e97fb..9d40c99d1 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.spec.ts @@ -36,15 +36,15 @@ describe('TimezoneEditComponent', () => { it('should emit value on change', () => { vi.spyOn(component.onFieldChange, 'emit'); const testValue = 'America/New_York'; - component.value = testValue; + fixture.componentRef.setInput('value', testValue); component.onFieldChange.emit(testValue); expect(component.onFieldChange.emit).toHaveBeenCalledWith(testValue); }); it('should add null option when allow_null is true', () => { - component.widgetStructure = { + fixture.componentRef.setInput('widgetStructure', { widget_params: { allow_null: true }, - } as any; + } as any); component.ngOnInit(); const nullOption = component.timezones.find((tz) => tz.value === null); expect(nullOption).toBeDefined(); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.ts index 761960671..ec3e63ad5 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, CUSTOM_ELEMENTS_SCHEMA, Input } from '@angular/core'; +import { Component, CUSTOM_ELEMENTS_SCHEMA, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; @@ -14,7 +14,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class TimezoneEditComponent extends BaseEditFieldComponent { - @Input() value: string; + readonly value = model(); public timezones: { value: string; label: string }[] = []; @@ -44,9 +44,10 @@ export class TimezoneEditComponent extends BaseEditFieldComponent { this.timezones.sort((a, b) => a.value.localeCompare(b.value)); // Check widget params for allow_null option - if (this.widgetStructure?.widget_params?.allow_null) { + const ws = this.widgetStructure(); + if (ws?.widget_params?.allow_null) { this.timezones = [{ value: null, label: '' }, ...this.timezones]; - } else if (this.structure?.allow_null) { + } else if (this.structure()?.allow_null) { this.timezones = [{ value: null, label: '' }, ...this.timezones]; } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.html b/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.html index 1c8da6fbf..4ee0ac040 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.html @@ -1,11 +1,15 @@ - {{normalizedLabel}} - {{prefix}} - {{normalizedLabel()}} + @if (prefix) { + {{prefix}} + } + - URL is invalid. + @if (image.errors?.isInvalidURL) { + URL is invalid. + } \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.spec.ts index 8a13469f0..9edd8c4a4 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.spec.ts @@ -25,37 +25,37 @@ describe('UrlComponent', () => { }); it('should parse prefix from widget params object', () => { - component.widgetStructure = { widget_params: { prefix: 'https://api.example.com/' } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { prefix: 'https://api.example.com/' } } as any); component.ngOnInit(); expect(component.prefix).toBe('https://api.example.com/'); }); it('should parse prefix from widget params string', () => { - component.widgetStructure = { widget_params: JSON.stringify({ prefix: 'https://test.com/' }) } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: JSON.stringify({ prefix: 'https://test.com/' }) } as any); component.ngOnInit(); expect(component.prefix).toBe('https://test.com/'); }); it('should keep empty prefix when widget params have no prefix', () => { - component.widgetStructure = { widget_params: {} } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: {} } as any); component.ngOnInit(); expect(component.prefix).toBe(''); }); it('should update prefix on ngOnChanges', () => { - component.widgetStructure = { widget_params: { prefix: 'https://updated.com/' } } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: { prefix: 'https://updated.com/' } } as any); component.ngOnChanges(); expect(component.prefix).toBe('https://updated.com/'); }); it('should handle invalid JSON in widget params gracefully', () => { - component.widgetStructure = { widget_params: 'invalid-json' } as any; + fixture.componentRef.setInput('widgetStructure', { widget_params: 'invalid-json' } as any); component.ngOnInit(); expect(component.prefix).toBe(''); }); it('should not change prefix when widgetStructure is undefined', () => { - component.widgetStructure = undefined; + fixture.componentRef.setInput('widgetStructure', undefined); component.ngOnInit(); expect(component.prefix).toBe(''); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.ts index 879b9f08e..4d2428727 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input, OnInit } from '@angular/core'; +import { Component, model, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -13,7 +13,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone styleUrl: './url.component.css', }) export class UrlEditComponent extends BaseEditFieldComponent implements OnInit { - @Input() value: string; + readonly value = model(); public prefix: string = ''; ngOnInit(): void { @@ -26,12 +26,10 @@ export class UrlEditComponent extends BaseEditFieldComponent implements OnInit { } private _parseWidgetParams(): void { - if (this.widgetStructure?.widget_params) { + const ws = this.widgetStructure(); + if (ws?.widget_params) { try { - const params = - typeof this.widgetStructure.widget_params === 'string' - ? JSON.parse(this.widgetStructure.widget_params) - : this.widgetStructure.widget_params; + const params = typeof ws.widget_params === 'string' ? JSON.parse(ws.widget_params) : ws.widget_params; if (params.prefix !== undefined) { this.prefix = params.prefix || ''; diff --git a/frontend/src/app/components/ui-components/record-edit-fields/uuid/uuid.component.html b/frontend/src/app/components/ui-components/record-edit-fields/uuid/uuid.component.html index d8972f391..4b28ea60b 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/uuid/uuid.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/uuid/uuid.component.html @@ -1,17 +1,17 @@ - {{ normalizedLabel }} + {{ normalizedLabel() }} - @if (!readonly && !disabled) { + @if (!readonly() && !disabled()) {