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}}
+ 0 && value && (maxLength - value.length) < 100" class="counter">{{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}}
+ 0 && value && (maxLength - value.length) < 100" class="counter">{{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) {
- refresh
}
-
+
@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
+
thresholdMax) {
+ return 'up';
+ }
+
+ if (thresholdMax && thresholdMin === null && numValue > thresholdMax) {
+ return 'up';
+ }
+
+ return false;
+ }
}
diff --git a/frontend/src/app/components/ui-components/table-display-fields/range/range.component.css b/frontend/src/app/components/ui-components/table-display-fields/range/range.component.css
index 136f0fa09..b897a7ac8 100644
--- a/frontend/src/app/components/ui-components/table-display-fields/range/range.component.css
+++ b/frontend/src/app/components/ui-components/table-display-fields/range/range.component.css
@@ -23,7 +23,7 @@
/* Light theme progress bar styles */
.range-progress-bar {
- --mdc-linear-progress-active-indicator-color: #1976d2;
+ --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;
@@ -57,7 +57,7 @@
}
.range-progress-bar {
- --mdc-linear-progress-active-indicator-color: #90caf9;
+ --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/table-display-fields/text/text.component.css b/frontend/src/app/components/ui-components/table-display-fields/text/text.component.css
index e69de29bb..fc157be83 100644
--- a/frontend/src/app/components/ui-components/table-display-fields/text/text.component.css
+++ b/frontend/src/app/components/ui-components/table-display-fields/text/text.component.css
@@ -0,0 +1,6 @@
+.validation-error {
+ color: var(--color-warnPalette-500);
+ text-decoration: underline;
+ text-decoration-style: wavy;
+ text-underline-offset: 2px;
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/ui-components/table-display-fields/text/text.component.html b/frontend/src/app/components/ui-components/table-display-fields/text/text.component.html
index acf48b2d5..743383066 100644
--- a/frontend/src/app/components/ui-components/table-display-fields/text/text.component.html
+++ b/frontend/src/app/components/ui-components/table-display-fields/text/text.component.html
@@ -1,5 +1,5 @@
-
{{value || '—'}}
+
{{value || '—'}}
{
+
+ if (!control.value || control.value === '') {
+ return null;
+ }
+
+ if (!validateType) {
+ return null;
+ }
+
+ const stringValue = String(control.value);
+
+ // Special case for regex validation
+ if (validateType === 'regex') {
+ if (!regexPattern) {
+ return null;
+ }
+ try {
+ const regex = new RegExp(regexPattern);
+ if (!regex.test(stringValue)) {
+ return { invalidPattern: true };
+ }
+ } catch (error) {
+ console.warn('Invalid regex pattern:', error);
+ return null;
+ }
+ return null;
+ }
+
+ // Check if validator has this method
+ const validatorMethod = validator[validateType];
+ if (typeof validatorMethod !== 'function') {
+ console.warn(`Unknown validator method: ${validateType}`);
+ return null;
+ }
+
+ try {
+ // Call the validator method
+ const isValid = validatorMethod(stringValue);
+ if (!isValid) {
+ return { [`invalid${validateType}`]: true };
+ }
+ } catch (error) {
+ console.warn(`Validation error for ${validateType}:`, error);
+ return null;
+ }
+
+ return null;
+ }
+}
diff --git a/frontend/src/app/validators/url.validator.ts b/frontend/src/app/validators/url.validator.ts
index 1b8a6c9b7..a647ac505 100644
--- a/frontend/src/app/validators/url.validator.ts
+++ b/frontend/src/app/validators/url.validator.ts
@@ -1,13 +1,11 @@
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
-import isFQDN from 'validator/lib/isFQDN';
-
export function urlValidation(prefix: string = ''):ValidatorFn {
return (control: AbstractControl) : ValidationErrors | null=> {
if (control.value) {
let url = (control.value as string);
-
+
// If there's a prefix, prepend it to the URL for validation
const fullUrl = prefix ? prefix + url : url;
diff --git a/frontend/src/custom-theme.scss b/frontend/src/custom-theme.scss
index 10de3cc9a..c4304fe5b 100644
--- a/frontend/src/custom-theme.scss
+++ b/frontend/src/custom-theme.scss
@@ -8,7 +8,6 @@ $md-app-primary-color: palette.createpalette('primaryPalette');
$md-app-accented-color: palette.createpalette('accentedPalette');
$md-app-warn-color: palette.createpalette('warnPalette');
$md-app-white-color: palette.createpalette('whitePalette');
-// $md-app-accented-dark-color: palette.createpalette('accentedDarkPalette');
$md-app-warn-dark-color: palette.createpalette('warnDarkPalette');
$custom-palette-primary: mat.m2-define-palette($md-app-primary-color);
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 67b51ccb0..05ce051df 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -43,10 +43,10 @@ const saasExtraProviders = (environment as any).saas ? [
},
] : [];
const colorConfig: IColorConfig = {
- palettes: { primaryPalette: '#212121', accentedPalette: '#C177FC', warnPalette: '#B71C1C', whitePalette: '#FFFFFF', accentedDarkPalette: '#C176FC', warnDarkPalette: '#E53935' },
+ palettes: { primaryPalette: '#212121', accentedPalette: '#C177FC', warnPalette: '#B71C1C', whitePalette: '#FFFFFF', warnDarkPalette: '#E53935' },
simpleColors: { myColorName: '#2e959a' },
};
-type Palettes = { primaryPalette: string, accentedPalette: string, warnPalette: string, whitePalette: string, accentedDarkPalette: string, warnDarkPalette: string };
+type Palettes = { primaryPalette: string, accentedPalette: string, warnPalette: string, whitePalette: string, warnDarkPalette: string };
type Colors = { myColorName: string };
const stripeKey = location.host === environment.stagingHost ? 'pk_test_51JM8FBFtHdda1TsBTjVNBFMIAA8cXLNWTmZCF22FCS5swdJIFqMk82ZEeZpvTys7oxlDekdcYIGaQ5MEFz6lWa2s000r6RziCg' : 'pk_live_51JM8FBFtHdda1TsBR7nieMFVFigZAUXbPhQTNvaSyLynIW1lbfzO6rfqqIUn0JAGJRq9mrwKwrVCsDDFOs84M7pE006xDqNgHk'