diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css index e07f8f887..944d19b6e 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css @@ -123,13 +123,25 @@ } .ai-error-message { - background-color: var(--color-warnPalette-100); - color: var(--color-warnPalette-100-contrast); border-radius: 8px; padding: 8px 8px 0; margin: 4px 0; } +@media (prefers-color-scheme: light) { + .ai-error-message { + background-color: var(--color-warnPalette-100); + color: var(--color-warnPalette-100-contrast); + } +} + +@media (prefers-color-scheme: dark) { + .ai-error-message { + background-color: var(--color-warnDarkPalette-200); + color: var(--color-warnDarkPalette-200-contrast); + } +} + .ai-message ::ng-deep ol, .ai-message ::ng-deep ul { padding-left: 28px; diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.css index 5d0e712dc..96862173a 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.css @@ -21,7 +21,18 @@ } .form__warning { - color: var(--color-warnPalette-500); margin-top: -12px; margin-bottom: 20px; +} + +@media (prefers-color-scheme: light) { + .form__warning { + color: var(--color-warnPalette-500); + } +} + +@media (prefers-color-scheme: dark) { + .form__warning { + color: var(--color-warnDarkPalette-500); + } } \ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts index 951edc19f..c0a7dfb55 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts @@ -84,17 +84,18 @@ export class DbTableWidgetsComponent implements OnInit {

A well-designed admin panel isn’t just about managing data — it’s about making that data easier to understand and interact with. By customizing how each field is displayed, you can turn raw database values into meaningful, user-friendly interfaces that save time and reduce errors.

`; + // JSON5-formatted default params public defaultParams = { Boolean: -`// Display "Yes/No" buttons and specify "allow_null" in field structure: -// Use "false" to require that one of the buttons is selected; -// Use "true" if the field might be left unspecified. +`// Display "Yes/No" buttons with configurable options: +// - allow_null: Use "false" to require selection, "true" if field can be left unspecified +// - invert_colors: Swap the color scheme (typically green=Yes, red=No becomes red=Yes, green=No) { - "structure": { - "allow_null": false - } -}`, + "allow_null": false, + "invert_colors": false +} +`, Code: `// provide language of code to highlight: 'html', 'css', 'typescript', 'yaml', 'markdown' // example: @@ -167,10 +168,13 @@ export class DbTableWidgetsComponent implements OnInit { "allow_negative": true } `, - Number: `// Configure number display with unit conversion + Number: `// Configure number display with unit conversion and threshold validation // Example units: "bytes", "meters", "seconds", "grams" +// threshold_min/threshold_max: Values outside these limits will be highlighted in red { - "unit": null + "unit": null, + "threshold_min": null, + "threshold_max": null }`, Password: `// provide algorithm to encrypt your password, one of: @@ -224,7 +228,19 @@ export class DbTableWidgetsComponent implements OnInit { } ] }`, - String: `// No settings required`, + String: `// Optional validation for string values +// validate: Any validator.js method (e.g., "isEmail", "isURL", "isUUID", "isJSON", "isAlpha", "isNumeric") +// Full list: isEmail, isURL, isMACAddress, isIP, isIPRange, isFQDN, isBoolean, isIBAN, isBIC, +// isAlpha, isAlphanumeric, isNumeric, isPort, isLowercase, isUppercase, isAscii, isBase64, +// isHexadecimal, isHexColor, isRgbColor, isHSL, isMD5, isHash, isJWT, isJSON, isUUID, +// isMongoId, isCreditCard, isISBN, isISSN, isMobilePhone, isPostalCode, isEthereumAddress, +// isCurrency, isBtcAddress, isISO8601, isISO31661Alpha2, isISO31661Alpha3, isISO4217, +// isDataURI, isMagnetURI, isMimeType, isLatLong, isSlug, isStrongPassword, isTaxID, isVAT +// OR use "regex" with a regex parameter for custom pattern matching +{ + "validate": null, + "regex": null +}`, Textarea: `// provide number of strings to show. { "rows": 5 diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.css index 8f7c02808..4ceacd56f 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widget/widget.component.css @@ -11,10 +11,6 @@ padding: 8px 4px; } -/* .widget-type__docs-link { - color: var(--color-accentedDarkPalette-500); -} */ - .code-editor-box { display: block; border: 1px solid rgba(0, 0, 0, 0.38); diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css index 86d585cf0..4e50b5293 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css @@ -11,13 +11,29 @@ } .saved-filters-list__first-time-button { - background: var(--color-accentedPalette-50); - border: 1px dashed var(--color-accentedPalette-300); transition: background 0.3s ease; } -.saved-filters-list__first-time-button:hover { - background: var(--color-accentedPalette-100) !important; +@media (prefers-color-scheme: light) { + .saved-filters-list__first-time-button { + background: var(--color-accentedPalette-50); + border: 1px dashed var(--color-accentedPalette-300); + } + + .saved-filters-list__first-time-button:hover { + background: var(--color-accentedPalette-100) !important; + } +} + +@media (prefers-color-scheme: dark) { + .saved-filters-list__first-time-button { + background: var(--color-accentedPalette-900); + border: 1px dashed var(--color-accentedPalette-600); + } + + .saved-filters-list__first-time-button:hover { + background: var(--color-accentedPalette-800) !important; + } } /* .saved-filters-tabs { diff --git a/frontend/src/app/components/ui-components/filter-fields/boolean/boolean.component.ts b/frontend/src/app/components/ui-components/filter-fields/boolean/boolean.component.ts index dba00a20a..ca4e4e400 100644 --- a/frontend/src/app/components/ui-components/filter-fields/boolean/boolean.component.ts +++ b/frontend/src/app/components/ui-components/filter-fields/boolean/boolean.component.ts @@ -30,7 +30,17 @@ export class BooleanFilterComponent extends BaseFilterFieldComponent { super.ngOnInit(); this.connectionType = this._connections.currentConnection.type; this.setBooleanValue(); - this.isRadiogroup = (this.structure?.allow_null) || !!(this.widgetStructure?.widget_params?.structure?.allow_null); + + // 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; + } + + // Check allow_null from either structure or widget params + this.isRadiogroup = (this.structure?.allow_null) || !!(parsedParams?.allow_null); } setBooleanValue() { 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 bdd457ef6..e2ec84d8a 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 @@ -39,7 +39,16 @@ export class BooleanEditComponent extends BaseEditFieldComponent { this.onFieldChange.emit(this.value); - this.isRadiogroup = (this.structure?.allow_null) || !!(this.widgetStructure?.widget_params?.structure?.allow_null); + // 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; + } + + // Check allow_null from either structure or widget params + this.isRadiogroup = (this.structure?.allow_null) || !!(parsedParams?.allow_null); } onToggleChange(optionValue: boolean): void { diff --git a/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.css b/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.css index 1d5734f00..c07e4f255 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.css +++ b/frontend/src/app/components/ui-components/record-edit-fields/long-text/long-text.component.css @@ -1,3 +1,16 @@ .form-field { + position: relative; width: 100%; +} + +.long-textarea { + margin-bottom: 20px; +} + +.counter { + position: absolute; + bottom: 8px; + font-size: 12px; + width: 90%; + background: #fff; } \ No newline at end of file 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 626b813c2..5992b3971 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,9 +1,19 @@ {{normalizedLabel}} +
{{value.length}} / {{maxLength}}
+ This field is required. + Maximum length is {{maxLength}} characters. + {{getValidationErrorMessage()}}
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 c7ebb0558..2e69cf0ee 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,29 +1,78 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; +import { CommonModule } from '@angular/common'; 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'; @Component({ selector: 'app-edit-long-text', templateUrl: './long-text.component.html', styleUrls: ['./long-text.component.css'], - imports: [MatFormFieldModule, MatInputModule, FormsModule] + imports: [CommonModule, MatFormFieldModule, MatInputModule, FormsModule, TextValidatorDirective] }) -export class LongTextEditComponent extends BaseEditFieldComponent { +export class LongTextEditComponent extends BaseEditFieldComponent implements OnInit { @Input() value: string; static type = 'text'; public rowsCount: string; + maxLength: number | null = null; + validateType: string | null = null; + regexPattern: string | null = null; - ngOnInit(): void { + override ngOnInit(): void { super.ngOnInit(); + + // Use character_maximum_length from the field structure if available + if (this.structure && this.structure.character_maximum_length) { + this.maxLength = this.structure.character_maximum_length; + } + + // Parse widget parameters if (this.widgetStructure && this.widgetStructure.widget_params) { - this.rowsCount = this.widgetStructure.widget_params.rows + const params = typeof this.widgetStructure.widget_params === 'string' + ? JSON.parse(this.widgetStructure.widget_params) + : this.widgetStructure.widget_params; + + this.rowsCount = params.rows || '4'; + this.validateType = params.validate || null; + this.regexPattern = params.regex || null; } else { - this.rowsCount = '4' - }; + this.rowsCount = '4'; + } } + getValidationErrorMessage(): string { + if (!this.validateType) { + return ''; + } + + if (this.validateType === 'regex') { + return 'Value doesn\'t match the required pattern'; + } + + // Create user-friendly messages for common validators + const messages = { + isEmail: 'Invalid email address', + isURL: 'Invalid URL', + isIP: 'Invalid IP address', + isUUID: 'Invalid UUID', + isJSON: 'Invalid JSON', + isCreditCard: 'Invalid credit card number', + isISBN: 'Invalid ISBN', + isAlpha: 'Should contain only letters', + isNumeric: 'Should contain only numbers', + isAlphanumeric: 'Should contain only letters and numbers', + isHexColor: 'Invalid hex color', + isBase64: 'Invalid Base64 string', + isMobilePhone: 'Invalid mobile phone number', + isMACAddress: 'Invalid MAC address', + isPostalCode: 'Invalid postal code', + isCurrency: 'Invalid currency format' + }; + + return messages[this.validateType] || `Invalid ${this.validateType}`; + } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.css b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.css index d1e2ca29c..f8e826b53 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.css +++ b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.css @@ -1,3 +1,7 @@ .text-form-field { width: 100%; +} + +.counter { + margin-right: 16px; } \ No newline at end of file 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 6ca6f5f34..49c432cb0 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,7 +1,16 @@ {{normalizedLabel}} +
{{value.length}} / {{maxLength}}
+ This field is required. + Maximum length is {{maxLength}} characters. + {{getValidationErrorMessage()}}
\ No newline at end of file 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 b4edca35a..3aa4f65bb 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,9 +1,11 @@ -import { Component, Injectable, Input } from '@angular/core'; +import { Component, Injectable, Input, OnInit } from '@angular/core'; import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { CommonModule } from '@angular/common'; +import { TextValidatorDirective } from 'src/app/directives/text-validator.directive'; @Injectable() @@ -11,10 +13,65 @@ import { MatInputModule } from '@angular/material/input'; selector: 'app-edit-text', templateUrl: './text.component.html', styleUrls: ['./text.component.css'], - imports: [MatFormFieldModule, MatInputModule, FormsModule] + imports: [CommonModule, MatFormFieldModule, MatInputModule, FormsModule, TextValidatorDirective] }) -export class TextEditComponent extends BaseEditFieldComponent { +export class TextEditComponent extends BaseEditFieldComponent implements OnInit { @Input() value: string; static type = 'text'; + + maxLength: number | null = null; + validateType: string | null = null; + regexPattern: string | null = null; + + override ngOnInit(): void { + super.ngOnInit(); + + // Use character_maximum_length from the field structure if available + if (this.structure && this.structure.character_maximum_length) { + this.maxLength = this.structure.character_maximum_length; + } + + // Parse widget parameters for validation + if (this.widgetStructure && this.widgetStructure.widget_params) { + const params = typeof this.widgetStructure.widget_params === 'string' + ? JSON.parse(this.widgetStructure.widget_params) + : this.widgetStructure.widget_params; + + this.validateType = params.validate || null; + this.regexPattern = params.regex || null; + } + } + + getValidationErrorMessage(): string { + if (!this.validateType) { + return ''; + } + + if (this.validateType === 'regex') { + return 'Value doesn\'t match the required pattern'; + } + + // Create user-friendly messages for common validators + const messages = { + isEmail: 'Invalid email address', + isURL: 'Invalid URL', + isIP: 'Invalid IP address', + isUUID: 'Invalid UUID', + isJSON: 'Invalid JSON', + isCreditCard: 'Invalid credit card number', + isISBN: 'Invalid ISBN', + isAlpha: 'Should contain only letters', + isNumeric: 'Should contain only numbers', + isAlphanumeric: 'Should contain only letters and numbers', + isHexColor: 'Invalid hex color', + isBase64: 'Invalid Base64 string', + isMobilePhone: 'Invalid mobile phone number', + isMACAddress: 'Invalid MAC address', + isPostalCode: 'Invalid postal code', + isCurrency: 'Invalid currency format' + }; + + return messages[this.validateType] || `Invalid ${this.validateType}`; + } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.css b/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.css index 6e7bfe409..3f62bbbde 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.css +++ b/frontend/src/app/components/ui-components/record-edit-fields/url/url.component.css @@ -1,4 +1,14 @@ .url-box { margin-bottom: 12px; width: 100%; +} + +/* Handle long URL prefixes with overflow */ +::ng-deep .mat-mdc-form-field-text-prefix { + max-width: 30%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + vertical-align: middle; } \ No newline at end of file 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 6dccab057..d8972f391 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,19 +1,20 @@ {{ normalizedLabel }} - - + @if (!readonly && !disabled) { - } - + @if (value && validateUuid(value)) { UUID {{ getUuidVersion(value) ? 'v' + getUuidVersion(value) : 'version unknown' }} } - + @if (value && !validateUuid(value)) { Invalid UUID format } 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 6afe2967d..81fbe48e8 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,3 +1,9 @@ -check_small -close_small + + check_small + + + close_small + 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 0947ac496..ef3ce9550 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 @@ -13,4 +13,7 @@ import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-rec imports: [MatIconModule, CommonModule] }) export class BooleanRecordViewComponent extends BaseRecordViewFieldComponent { + get invertColors(): boolean { + return this.widgetStructure?.widget_params?.invertColors === true; + } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css index 8b1378917..893313654 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css +++ b/frontend/src/app/components/ui-components/record-view-fields/number/number.component.css @@ -1 +1,11 @@ +.out-of-threshold { + display: flex; + align-items: center; + gap: 2px; +} +.out-of-threshold__icon { + font-size: 16px; + width: 16px; + height: 16px; +} \ No newline at end of file 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 8f3bd4c32..55d954de1 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 +1,5 @@ -{{displayValue}} + + {{displayValue}} + north + south + 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 bb3773ddf..1cd47b51a 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,5 +1,8 @@ import { Component, Injectable } from '@angular/core'; + import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; +import { CommonModule } from '@angular/common'; +import { MatIconModule } from '@angular/material/icon'; import convert from 'convert'; @Injectable() @@ -7,7 +10,7 @@ import convert from 'convert'; selector: 'app-number-record-view', templateUrl: './number.component.html', styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './number.component.css'], - imports: [] + imports: [CommonModule, MatIconModule] }) export class NumberRecordViewComponent extends BaseRecordViewFieldComponent { @@ -32,4 +35,24 @@ export class NumberRecordViewComponent extends BaseRecordViewFieldComponent { return this.value.toString(); } } + + get isOutOfThreshold(): 'up' | 'down' | false { + 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); + + if (thresholdMin !== undefined && numValue < thresholdMin) { + return 'down'; + } + + if (thresholdMax !== undefined && numValue > thresholdMax) { + return 'up'; + } + + return false; + } } diff --git a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.css b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.css new file mode 100644 index 000000000..bc99e1d74 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.css @@ -0,0 +1,64 @@ +.range-display-container { + display: flex; + flex-direction: column; + gap: 4px; + padding: 4px 0; +} + +.range-value-label { + font-size: 14px; + color: rgba(0, 0, 0, 0.87); + font-weight: 500; +} + +.range-progress-bar { + height: 8px; + border-radius: 4px; + min-width: 160px; +} + +/* Material progress bar specific styles */ +.range-progress-bar.mat-mdc-progress-bar { + height: 8px; +} + +/* Light theme progress bar styles */ +.range-progress-bar { + --mdc-linear-progress-active-indicator-color: var(--color-accentedPalette-300); + --mdc-linear-progress-track-color: #e0e0e0; + --mdc-linear-progress-active-indicator-height: 8px; + --mdc-linear-progress-track-height: 8px; +} + +/* Override Material default styles */ +.range-progress-bar ::ng-deep .mdc-linear-progress { + height: 8px !important; +} + +.range-progress-bar ::ng-deep .mdc-linear-progress__bar { + height: 8px !important; +} + +.range-progress-bar ::ng-deep .mdc-linear-progress__bar-inner { + border-top-width: 8px !important; +} + +.range-progress-bar ::ng-deep .mdc-linear-progress__buffer { + height: 8px !important; +} + +.range-progress-bar ::ng-deep .mdc-linear-progress__buffer-bar { + height: 8px !important; +} + +/* Dark theme styles */ +@media (prefers-color-scheme: dark) { + .range-value-label { + color: rgba(255, 255, 255, 0.87); + } + + .range-progress-bar { + --mdc-linear-progress-active-indicator-color: var(--color-accentedPalette-700); + --mdc-linear-progress-track-color: rgba(255, 255, 255, 0.12); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-view-fields/range/range.component.html b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.html new file mode 100644 index 000000000..673c6a710 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.html @@ -0,0 +1,8 @@ +
+
{{ displayValue }}
+ + +
\ No newline at end of file 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 new file mode 100644 index 000000000..8ba3118d5 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-view-fields/range/range.component.ts @@ -0,0 +1,69 @@ +import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; + +import { Component, Input, OnChanges, OnInit } from '@angular/core'; + +import { CommonModule } from '@angular/common'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; + +@Component({ + selector: 'app-range-record-view', + standalone: true, + templateUrl: './range.component.html', + styleUrls: ['./range.component.css'], + imports: [ + CommonModule, + MatProgressBarModule + ], +}) +export class RangeRecordViewComponent extends BaseRecordViewFieldComponent implements OnInit, OnChanges { + @Input() declare value: number; + static type = 'range'; + + public min: number = 0; + public max: number = 100; + public step: number = 1; + public displayValue: string = ''; + + ngOnInit(): void { + this._parseWidgetParams(); + this._updateDisplayValue(); + } + + ngOnChanges(): void { + this._parseWidgetParams(); + this._updateDisplayValue(); + } + + public getProgressValue(): number { + const numValue = Number(this.value) || 0; + const range = this.max - this.min; + if (range === 0) return 0; + const progress = ((numValue - this.min) / range) * 100; + // Ensure progress is between 0 and 100 + return Math.max(0, Math.min(100, progress)); + } + + private _parseWidgetParams(): void { + if (this.widgetStructure?.widget_params) { + try { + const params = this.widgetStructure.widget_params; + if (params.min !== undefined) { + this.min = Number(params.min) || 0; + } + if (params.max !== undefined) { + this.max = Number(params.max) || 100; + } + if (params.step !== undefined) { + this.step = Number(params.step) || 1; + } + } catch (error) { + console.error('Failed to parse widget params:', error); + } + } + } + + private _updateDisplayValue(): void { + const numValue = Number(this.value) || 0; + this.displayValue = `${numValue} / ${this.max}`; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.css b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.css index e69de29bb..f6622792a 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/text/text.component.css +++ b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.css @@ -0,0 +1,17 @@ +.validation-error { + text-decoration: underline; + text-decoration-style: wavy; + text-underline-offset: 2px; +} + +@media (prefers-color-scheme: light) { + .validation-error { + color: var(--color-warnPalette-500); + } +} + +@media (prefers-color-scheme: dark) { + .validation-error { + color: var(--color-warnDarkPalette-500); + } +} \ No newline at end of file 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 7dfcb3f4b..d6b7e12f0 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.ts b/frontend/src/app/components/ui-components/record-view-fields/text/text.component.ts index 788c2c8fb..68cb8dc14 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,5 +1,5 @@ import { Component, Injectable } from '@angular/core'; - +import * as validator from 'validator'; import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component'; @Injectable() @@ -10,4 +10,77 @@ import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-rec imports: [] }) export class TextRecordViewComponent extends BaseRecordViewFieldComponent { + get isInvalid(): boolean { + if (!this.value || this.value === '') { + return false; + } + + const validateType = this.widgetStructure?.widget_params?.validate; + if (!validateType) { + return false; + } + + const stringValue = String(this.value); + + // Special case for regex validation + if (validateType === 'regex') { + const regexPattern = this.widgetStructure?.widget_params?.regex; + if (!regexPattern) { + return false; + } + try { + const regex = new RegExp(regexPattern); + return !regex.test(stringValue); + } catch (error) { + console.warn('Invalid regex pattern:', error); + return false; + } + } + + // Check if validator has this method + const validatorMethod = validator[validateType]; + if (typeof validatorMethod !== 'function') { + console.warn(`Unknown validator method: ${validateType}`); + return false; + } + + try { + // Call the validator method and invert result (true if invalid) + return !validatorMethod(stringValue); + } catch (error) { + console.warn(`Validation error for ${validateType}:`, error); + return false; + } + } + + get validationErrorMessage(): string { + const validateType = this.widgetStructure?.widget_params?.validate; + if (!validateType) { + return ''; + } + + if (validateType === 'regex') { + return 'Does not match the required pattern'; + } + + // Create user-friendly messages for common validators + const messages = { + isEmail: 'Invalid email address', + isURL: 'Invalid URL', + isIP: 'Invalid IP address', + isUUID: 'Invalid UUID', + isJSON: 'Invalid JSON', + isCreditCard: 'Invalid credit card number', + isISBN: 'Invalid ISBN', + isAlpha: 'Should contain only letters', + isNumeric: 'Should contain only numbers', + isAlphanumeric: 'Should contain only letters and numbers', + isHexColor: 'Invalid hex color', + isBase64: 'Invalid Base64 string', + isMobilePhone: 'Invalid mobile phone number', + isPostalCode: 'Invalid postal code' + }; + + return messages[validateType] || `Invalid ${validateType.replace(/^is/, '').toLowerCase()}`; + } } 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 a83870454..4012df024 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,7 +1,13 @@
- check_small - close_small + + check_small + + + close_small +
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 dc78799f4..5270c0239 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 @@ -15,4 +15,15 @@ import { MatTooltipModule } from '@angular/material/tooltip'; 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; + + return params?.invert_colors === true; + } + return false; + } } diff --git a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css index 8b1378917..bc463bdea 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css +++ b/frontend/src/app/components/ui-components/table-display-fields/number/number.component.css @@ -1 +1,11 @@ +.out-of-threshold { + display: flex; + align-items: center; + gap: 2px; +} +.out-of-threshold__icon { + font-size: 16px; + width: 16px; + height: 16px; +} 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 356e592cb..50af36655 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,5 +1,9 @@
- {{displayValue}} + + {{displayValue}} + south + north +