diff --git a/frontend/package.json b/frontend/package.json index 7154d5625..f59bf03e9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,6 +38,7 @@ "amplitude-js": "^8.21.9", "angular-password-strength-meter": "npm:@eresearchqut/angular-password-strength-meter@^13.0.7", "angulartics2": "^14.1.0", + "color-string": "^2.0.1", "convert": "^5.12.0", "date-fns": "^4.1.0", "ipaddr.js": "^2.2.0", diff --git a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts b/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts index 55af1b0b0..d3f03ff8f 100644 --- a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts +++ b/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts @@ -193,8 +193,19 @@ export class DbTableWidgetsComponent implements OnInit { "allow_negative": true } `, - Color: `// No settings required -// You can use this field to display colors in hex format, like #FF5733 or #333.`, + Color: `// Optional: Specify output format for color values +// Supported formats: "hex", "hex_hash" (default), "rgb", "hsl" +// Example configuration: + +{ + "format": "hex_hash" // Will display colors as "#FF5733" +} + +// Format options: +// - "hex": Display as "FF5733" (no hash) +// - "hex_hash": Display as "#FF5733" (default) +// - "rgb": Display as "rgb(255, 87, 51)" +// - "hsl": Display as "hsl(9, 100%, 60%)"`, } constructor( 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 99e173663..e97f8054e 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 @@ -5,12 +5,11 @@ [required]="required" [disabled]="disabled" [readonly]="readonly" attr.data-testid="record-{{label}}-color" [(ngModel)]="value" (ngModelChange)="onTextInputChange()" - placeholder="#000000" - pattern="^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"> + placeholder="e.g. #000000, rgb(0,0,0), hsl(0,0%,0%)">
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 16d8c5b66..ce38d366e 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 @@ -4,6 +4,7 @@ import { BaseEditFieldComponent } from '../base-row-field/base-row-field.compone import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import colorString from 'color-string'; @Injectable() @@ -20,13 +21,105 @@ export class ColorEditComponent extends BaseEditFieldComponent { get isValidColor(): boolean { if (!this.value) return false; - const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; - return colorRegex.test(this.value); + return this.parseColor(this.value) !== null; + } + + get normalizedColorForPicker(): string { + 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 '#000000'; + } + + get formattedColorValue(): string { + const parsed = this.parseColor(this.value); + if (!parsed) return this.value; + + 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 + 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) { + const [h, s, l, alpha] = hslParsed; + return colorString.to.hsl(h, s, l, alpha); + } + return hex; + default: + return colorString.to.hex(r, g, b, a); + } + } + + 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; - this.value = target.value; + const pickerValue = target.value; + + // Convert picker value to desired format + const parsed = this.parseColor(pickerValue); + if (parsed) { + 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); + break; + case 'hex_hash': + this.value = colorString.to.hex(r, g, b, a); + break; + case 'rgb': + this.value = 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); + } else { + this.value = hex; + } + break; + default: + this.value = colorString.to.hex(r, g, b, a); + } + } else { + this.value = pickerValue; + } + this.onFieldChange.emit(this.value); } diff --git a/frontend/src/app/components/ui-components/table-display-fields/color/color.component.html b/frontend/src/app/components/ui-components/table-display-fields/color/color.component.html index 331411389..f144cacdc 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/color/color.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/color/color.component.html @@ -2,7 +2,7 @@
{{value || '—'}} 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 ed3651077..87755852c 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 @@ -6,6 +6,7 @@ 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'; @Injectable() @Component({ @@ -17,7 +18,36 @@ import { NgIf } from '@angular/common'; export class ColorDisplayComponent extends BaseTableDisplayFieldComponent { get isValidColor(): boolean { if (!this.value) return false; - const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; - return colorRegex.test(this.value); + return this.parseColor(this.value) !== null; + } + + get normalizedColorForDisplay(): string { + 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 '#000000'; + } + + + 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; } } \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 110921d91..848bdba2e 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5691,6 +5691,13 @@ __metadata: languageName: node linkType: hard +"color-name@npm:^2.0.0": + version: 2.0.0 + resolution: "color-name@npm:2.0.0" + checksum: 10a1addae41de2987d6b90dbd3cfade266c2e6f680ce21749911df4493b4fae07654862c6b5358bdd13e155461acb4eedaa5e0ba172bf13542cdcca10866cf2b + languageName: node + linkType: hard + "color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" @@ -5698,6 +5705,15 @@ __metadata: languageName: node linkType: hard +"color-string@npm:^2.0.1": + version: 2.0.1 + resolution: "color-string@npm:2.0.1" + dependencies: + color-name: ^2.0.0 + checksum: a5ab024f78f67c0d5c1c995943ff95dce193beaa981492f6a36c05a9939a9db519e2d821d91df15677a47107fe90e4b12fd345729d755c65b543924db05c3a3f + languageName: node + linkType: hard + "colorette@npm:^2.0.10, colorette@npm:^2.0.20": version: 2.0.20 resolution: "colorette@npm:2.0.20" @@ -6655,6 +6671,7 @@ __metadata: amplitude-js: ^8.21.9 angular-password-strength-meter: "npm:@eresearchqut/angular-password-strength-meter@^13.0.7" angulartics2: ^14.1.0 + color-string: ^2.0.1 convert: ^5.12.0 date-fns: ^4.1.0 ipaddr.js: ^2.2.0