diff --git a/backend/src/enums/widget-type.enum.ts b/backend/src/enums/widget-type.enum.ts index 7af6c010e..a02e8989b 100644 --- a/backend/src/enums/widget-type.enum.ts +++ b/backend/src/enums/widget-type.enum.ts @@ -19,5 +19,6 @@ export enum WidgetTypeEnum { Code = 'Code', Phone = 'Phone', Country = 'Country', - Color = 'Color' + Color = 'Color', + Range = 'Range' } 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 76308ef37..c80672a8c 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 @@ -95,56 +95,46 @@ export class DbTableWidgetsComponent implements OnInit { "allow_null": false } }`, - Date: `// No settings required`, - Default: `// No settings required`, - Time: `// No settings required`, - DateTime: `// No settings required`, - JSON: `// No settings required`, - Textarea: `// provide number of strings to show. -{ - "rows": 5 -}`, - String: `// No settings required`, - Readonly: `// No settings required`, - Number: `// Configure number display with unit conversion -// Example units: "bytes", "meters", "seconds", "grams" + Code: +`// provide language of code to highlight: 'html', 'css', 'typescript', 'yaml', 'markdown' +// example: { - "unit": null -}`, - Select: -`// provide array of options to map database value (key 'value') in human readable value (key 'label'); -// for example: -// AK => Alaska, -// CA => California + "language": "html" +} +`, + Color: `// Optional: Specify output format for color values +// Supported formats: "hex", "hex_hash" (default), "rgb", "hsl" +// Example configuration: { - "allow_null": true, - "options": [ - { - "value": "UA", - "label": "πŸ‡ΊπŸ‡¦ Ukraine" - }, - { - "value": "PL", - "label": "πŸ‡΅πŸ‡± Poland" - }, - { - "value": "US", - "label": "πŸ‡ΊπŸ‡Έ United States" - } - ] -}`, - Password: -`// provide algorithm to encrypt your password, one of: -//sha1, sha3, sha224, sha256, sha512, sha384, bcrypt, scrypt, argon2, pbkdf2. -// example: + "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%)"`, + Country: `// Configure country display options +// Example: { - "encrypt": true, - "algorithm": "sha256" + "show_flag": true, + "allow_null": false } - `, + Date: `// Configure date display options +// formatDistanceWithinHours: Shows relative time (e.g., "2 hours ago") for dates within the specified hours +// Default: 48 hours. Set to 0 to disable relative time display +{ + "formatDistanceWithinHours": 48 +}`, + DateTime: `// Configure datetime display options +// formatDistanceWithinHours: Shows relative time (e.g., "2 hours ago") for dates within the specified hours +// Default: 48 hours. Set to 0 to disable relative time display +{ + "formatDistanceWithinHours": 48 +}`, + Default: `// No settings required`, File: `// provide type of file: 'hex', 'base64' or 'file' // example: @@ -152,67 +142,103 @@ export class DbTableWidgetsComponent implements OnInit { "type": "hex" } `, - Code: -`// provide language of code to highlight: 'html', 'css', 'typescript', 'yaml', 'markdown' -// example: + Foreign_key: `// Provide settings for foreign key widget { - "language": "html" + "column_name": "", // copy the name of the column you selected + "referenced_column_name": "", + "referenced_table_name": "" } `, Image: `// provide image height in px to dispaly in table +// prefix: optional URL prefix to prepend to image source // example: { - "height": 100 + "height": 100, + "prefix": "https://example.com/images/" +} } `, - URL: `// No settings required`, - Phone: -`// Configure international phone number widget + JSON: `// No settings required`, + Money: `// Configure money widget settings // example: { - "preferred_countries": ["US", "GB", "CA"], - "enable_placeholder": true, - "phone_validation": true + "default_currency": "USD", + "show_currency_selector": false, + "decimal_places": 2, + "allow_negative": true } `, - Country: `// Configure country display options -// Example: + Number: `// Configure number display with unit conversion +// Example units: "bytes", "meters", "seconds", "grams" { - "show_flag": true, - "allow_null": false + "unit": null +}`, + Password: +`// provide algorithm to encrypt your password, one of: +//sha1, sha3, sha224, sha256, sha512, sha384, bcrypt, scrypt, argon2, pbkdf2. +// example: + +{ + "encrypt": true, + "algorithm": "sha256" } + `, - Foreign_key: `// Provide settings for foreign key widget + Phone: +`// Configure international phone number widget +// example: { - "column_name": "", // copy the name of the column you selected - "referenced_column_name": "", - "referenced_table_name": "" + "preferred_countries": ["US", "GB", "CA"], + "enable_placeholder": true, + "phone_validation": true } `, - Money: `// Configure money widget settings -// example: + Range: `// Configure the minimum, maximum and step values for the range +// Default: min = 0, max = 100, step = 1 { - "default_currency": "USD", - "show_currency_selector": false, - "decimal_places": 2, - "allow_negative": true + "min": 0, + "max": 100, + "step": 1 } `, - Color: `// Optional: Specify output format for color values -// Supported formats: "hex", "hex_hash" (default), "rgb", "hsl" -// Example configuration: + Readonly: `// No settings required`, + Select: +`// provide array of options to map database value (key 'value') in human readable value (key 'label'); +// for example: +// AK => Alaska, +// CA => California { - "format": "hex_hash" // Will display colors as "#FF5733" + "allow_null": true, + "options": [ + { + "value": "UA", + "label": "πŸ‡ΊπŸ‡¦ Ukraine" + }, + { + "value": "PL", + "label": "πŸ‡΅πŸ‡± Poland" + }, + { + "value": "US", + "label": "πŸ‡ΊπŸ‡Έ United States" + } + ] +}`, + String: `// No settings required`, + Textarea: `// provide number of strings to show. +{ + "rows": 5 +}`, + Time: `// No settings required`, + URL: `// prefix: optional URL prefix to prepend to the href +// example: +{ + "prefix": "https://example.com/" } - -// 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%)"`, - UUID: `// Configure UUID generation version and parameters +`, + UUID: `// Configure UUID generation version and parameters // Available versions: "v1", "v3", "v4" (default), "v5", "v7" // For v3/v5: provide namespace and optionally name { @@ -220,7 +246,7 @@ export class DbTableWidgetsComponent implements OnInit { "namespace": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "name": "" } -` +`, } constructor( diff --git a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.css b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.css index 8301b28e1..4a8991d98 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.css +++ b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.css @@ -2,6 +2,25 @@ width: 100%; } +.country-input-container { + display: flex; + align-items: center; + width: 100%; +} + +.country-flag-prefix { + font-size: 20px; + margin-right: 8px; + line-height: 1; +} + +.country-input { + flex: 1; + border: none !important; + outline: none !important; + background: transparent !important; +} + .country-flag { margin-right: 8px; font-size: 16px; 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 0ed6862ae..abe05600e 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,10 +1,14 @@ {{normalizedLabel}} - +
+ {{selectedCountryFlag}} + +
(''); public filteredCountries: Observable<{value: string | null, label: string, flag: string}[]>; public showFlag: boolean = true; + public selectedCountryFlag: string = ''; originalOrder = () => { return 0; } @@ -58,7 +59,16 @@ export class CountryEditComponent extends BaseEditFieldComponent { private setupAutocomplete(): void { this.filteredCountries = this.countryControl.valueChanges.pipe( startWith(''), - map(value => this._filter(typeof value === 'string' ? value : (value?.label || ''))) + map(value => { + // Update flag when value changes + if (typeof value === 'object' && value !== null) { + this.selectedCountryFlag = value.flag; + } else if (typeof value === 'string') { + // Clear flag if user is typing + this.selectedCountryFlag = ''; + } + return this._filter(typeof value === 'string' ? value : (value?.label || '')); + }) ); } @@ -67,6 +77,7 @@ export class CountryEditComponent extends BaseEditFieldComponent { const country = this.countries.find(c => c.value === this.value); if (country) { this.countryControl.setValue(country); + this.selectedCountryFlag = country.flag; } } } @@ -81,11 +92,14 @@ export class CountryEditComponent extends BaseEditFieldComponent { onCountrySelected(selectedCountry: {value: string | null, label: string, flag: string}): void { this.value = selectedCountry.value; + this.selectedCountryFlag = selectedCountry.flag; this.onFieldChange.emit(this.value); } displayFn(country: any): string { - return country ? country.label : ''; + if (!country) return ''; + // Only return the country label, flag is shown separately + return typeof country === 'string' ? country : country.label; } private loadCountries(): void { @@ -93,7 +107,7 @@ export class CountryEditComponent extends BaseEditFieldComponent { value: country.code, label: country.name, flag: getCountryFlag(country.code) - })); + })).toSorted((a, b) => a.label.localeCompare(b.label)); if (this.widgetStructure?.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/image/image.component.html b/frontend/src/app/components/ui-components/record-edit-fields/image/image.component.html index ed781287f..f6c3ce76e 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,12 +1,14 @@
{{normalizedLabel}} + {{prefix}} URL is invalid. - +
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 41fdafc4f..0b78c5220 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,4 +1,4 @@ -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'; @@ -19,6 +19,37 @@ import { UrlValidatorDirective } from 'src/app/directives/url-validator.directiv UrlValidatorDirective ] }) -export class ImageEditComponent extends BaseEditFieldComponent { +export class ImageEditComponent extends BaseEditFieldComponent implements OnInit { @Input() value: string; + public prefix: string = ''; + + ngOnInit(): void { + super.ngOnInit(); + this._parseWidgetParams(); + } + + ngOnChanges(): void { + this._parseWidgetParams(); + } + + private _parseWidgetParams(): void { + if (this.widgetStructure?.widget_params) { + try { + const params = typeof this.widgetStructure.widget_params === 'string' + ? JSON.parse(this.widgetStructure.widget_params) + : this.widgetStructure.widget_params; + + if (params.prefix !== undefined) { + this.prefix = params.prefix || ''; + } + } catch (e) { + console.error('Error parsing Image widget params:', e); + } + } + } + + get imageUrl(): string { + if (!this.value) return ''; + return this.prefix + this.value; + } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.css b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.css new file mode 100644 index 000000000..74aa047b1 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.css @@ -0,0 +1,120 @@ +.range-edit-container { + width: 100%; + display: flex; + flex-direction: column; + gap: 4px; + padding-bottom: 28px; +} + +.range-label { + font-size: 14px; + color: rgba(0, 0, 0, 0.87); + margin-bottom: 8px; + display: block; +} + +.range-labels { + display: flex; + justify-content: space-between; + font-size: 12px; + color: rgba(0, 0, 0, 0.6); + padding: 0 2px; +} + +.range-current { + font-weight: 600; + color: rgba(0, 0, 0, 0.87); +} + +.range-input { + width: 100%; + height: 6px; + -webkit-appearance: none; + appearance: none; + background: transparent; + outline: none; + cursor: pointer; +} + +.range-input::-webkit-slider-track { + width: 100%; + height: 6px; + background: #e0e0e0; + border-radius: 3px; +} + +.range-input::-moz-range-track { + width: 100%; + height: 6px; + background: #e0e0e0; + border-radius: 3px; +} + +.range-input::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + background: #1976d2; + border-radius: 50%; + cursor: pointer; + margin-top: -6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: all 0.2s ease; +} + +.range-input::-moz-range-thumb { + width: 18px; + height: 18px; + background: #1976d2; + border-radius: 50%; + cursor: pointer; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: all 0.2s ease; +} + +.range-input::-webkit-slider-thumb:hover { + transform: scale(1.1); + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3); +} + +.range-input::-moz-range-thumb:hover { + transform: scale(1.1); + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3); +} + +.range-input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.range-input:disabled::-webkit-slider-thumb { + cursor: not-allowed; +} + +.range-input:disabled::-moz-range-thumb { + cursor: not-allowed; +} + +@media (prefers-color-scheme: dark) { + .range-label { + color: rgba(255, 255, 255, 0.87); + } + + .range-labels { + color: rgba(255, 255, 255, 0.6); + } + + .range-current { + color: rgba(255, 255, 255, 0.87); + } + + .range-input::-webkit-slider-track { + background: #424242; + } + + .range-input::-moz-range-track { + background: #424242; + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..afc1f22b7 --- /dev/null +++ b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.html @@ -0,0 +1,19 @@ +
+ {{ normalizedLabel }} {{ required ? '*' : '' }} +
+ {{ min }} + {{ value || min }} + {{ max }} +
+ +
\ No newline at end of file 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 new file mode 100644 index 000000000..94b00c52c --- /dev/null +++ b/frontend/src/app/components/ui-components/record-edit-fields/range/range.component.ts @@ -0,0 +1,60 @@ +import { Component, ElementRef, Input, ViewChild } 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'; + +@Component({ + selector: 'app-range-edit', + templateUrl: './range.component.html', + styleUrls: ['./range.component.css'], + imports: [ + CommonModule, + FormsModule, + MatFormFieldModule, + MatInputModule + ], +}) +export class RangeEditComponent extends BaseEditFieldComponent { + @ViewChild('rangeInput') rangeInput: ElementRef; + @Input() value: number; + static type = 'range'; + + public min: number = 0; + public max: number = 100; + public step: number = 1; + + override ngOnInit(): void { + super.ngOnInit(); + this._parseWidgetParams(); + } + + ngOnChanges(): void { + this._parseWidgetParams(); + } + + public onValueChange(newValue: number): void { + this.value = newValue; + this.onFieldChange.emit(this.value); + } + + 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); + } + } + } +} \ No newline at end of file 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 e5e3115af..1c8da6fbf 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,8 +1,10 @@ {{normalizedLabel}} + {{prefix}} URL is invalid. 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 5fd85384b..d1c8ca488 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,4 +1,4 @@ -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'; @@ -19,6 +19,32 @@ import { UrlValidatorDirective } from 'src/app/directives/url-validator.directiv templateUrl: './url.component.html', styleUrl: './url.component.css' }) -export class UrlEditComponent extends BaseEditFieldComponent { +export class UrlEditComponent extends BaseEditFieldComponent implements OnInit { @Input() value: string; + public prefix: string = ''; + + ngOnInit(): void { + super.ngOnInit(); + this._parseWidgetParams(); + } + + ngOnChanges(): void { + this._parseWidgetParams(); + } + + private _parseWidgetParams(): void { + if (this.widgetStructure?.widget_params) { + try { + const params = typeof this.widgetStructure.widget_params === 'string' + ? JSON.parse(this.widgetStructure.widget_params) + : this.widgetStructure.widget_params; + + if (params.prefix !== undefined) { + this.prefix = params.prefix || ''; + } + } catch (e) { + console.error('Error parsing URL widget params:', e); + } + } + } } 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 f4f494885..421d0f49e 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,5 @@ [Image URL] Image + [src]="srcValue" alt="Image"> {{value || 'β€”'}} 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 e2d9f7df4..8d5d69db2 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 @@ -10,10 +10,17 @@ import { Component, Injectable } from '@angular/core'; imports: [CommonModule] }) export class ImageRecordViewComponent extends BaseRecordViewFieldComponent { + get srcValue(): string { + if (!this.value) return ''; + const prefix = this.widgetStructure?.widget_params?.prefix || ''; + return prefix + this.value; + } + get isUrl(): boolean { if (!this.value) return false; try { - new URL(this.value); + // Check if the prefixed URL is valid + new URL(this.srcValue); return true; } catch { return false; diff --git a/frontend/src/app/components/ui-components/table-display-fields/date-time/date-time.component.html b/frontend/src/app/components/ui-components/table-display-fields/date-time/date-time.component.html index 43432b87e..3cd97b31e 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/date-time/date-time.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/date-time/date-time.component.html @@ -1,5 +1,5 @@
- {{formattedDateTime || 'β€”'}} + {{formattedDateTime || 'β€”'}}