Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ export class DbTableWidgetsComponent implements OnInit {
}
`,
JSON: `// No settings required`,
Language: `// Configure language display options
// show_flag: Display country flag emoji next to language name
// Example:
{
"show_flag": true
}`,
Markdown: `// No settings required`,
Money: `// Configure money widget settings
// example:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.language-form-field {
width: 100%;
}

.language-input-container {
display: flex;
align-items: center;
width: 100%;
}

.language-flag-prefix {
font-size: 20px;
margin-right: 8px;
line-height: 1;
}

.language-input {
flex: 1;
border: none !important;
outline: none !important;
background: transparent !important;
}

.language-flag {
margin-right: 8px;
font-size: 16px;
}

.language-name {
flex: 1;
}

.language-native {
margin-left: 8px;
opacity: 0.8;
font-size: 13px;
font-style: italic;
}

.language-code {
margin-left: 8px;
opacity: 0.7;
font-size: 12px;
}

mat-option {
display: flex;
align-items: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<mat-form-field class="language-form-field" appearance="outline">
<mat-label>{{normalizedLabel}}</mat-label>
<div class="language-input-container">
<span *ngIf="selectedLanguageFlag && showFlag" class="language-flag-prefix">{{selectedLanguageFlag}}</span>
<input type="text" matInput
[required]="required" [disabled]="disabled" [readonly]="readonly"
attr.data-testid="record-{{label}}-language"
[formControl]="languageControl"
[matAutocomplete]="auto"
class="language-input">
</div>
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
<mat-option *ngFor="let language of filteredLanguages | async"
[value]="language"
(onSelectionChange)="language && onLanguageSelected(language)">
<span *ngIf="language.flag && showFlag" class="language-flag">{{language.flag}}</span>
<span class="language-name">{{language.label}}</span>
<span *ngIf="language.nativeName && language.label !== language.nativeName" class="language-native">{{language.nativeName}}</span>
<span *ngIf="language.value" class="language-code">({{language.value}})</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { LanguageEditComponent } from './language.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { provideHttpClient } from '@angular/common/http';

describe('LanguageEditComponent', () => {
let component: LanguageEditComponent;
let fixture: ComponentFixture<LanguageEditComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
LanguageEditComponent,
BrowserAnimationsModule
],
providers: [provideHttpClient()]
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(LanguageEditComponent);
component = fixture.componentInstance;
component.widgetStructure = { widget_params: {} } as any;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should load languages on init', () => {
component.ngOnInit();
expect(component.languages.length).toBeGreaterThan(0);
});

it('should set initial value when value is provided', () => {
component.value = 'en';
component.ngOnInit();
expect(component.selectedLanguageFlag).toBeTruthy();
});

it('should parse widget params for show_flag', () => {
component.widgetStructure = {
widget_params: { show_flag: false }
} as any;
component.ngOnInit();
expect(component.showFlag).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { LANGUAGES, getLanguageFlag, Language } from '../../../../consts/languages';

Copilot AI Nov 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import Language.

Suggested change
import { LANGUAGES, getLanguageFlag, Language } from '../../../../consts/languages';
import { LANGUAGES, getLanguageFlag } from '../../../../consts/languages';

Copilot uses AI. Check for mistakes.
import { CUSTOM_ELEMENTS_SCHEMA, Component, Input } from '@angular/core';

Copilot AI Nov 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Injectable import. The @Injectable() decorator should be added to this component (see separate comment) but Injectable is not imported from @angular/core. Add Injectable to the imports on line 2.

Copilot uses AI. Check for mistakes.
import { map, startWith } from 'rxjs/operators';

import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component';
import { CommonModule } from '@angular/common';
import { FormControl } from '@angular/forms';
import { FormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { Observable } from 'rxjs';
import { ReactiveFormsModule } from '@angular/forms';

@Component({

Copilot AI Nov 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @Injectable() decorator. All record edit field components in the codebase use the @Injectable() decorator before the @Component() decorator for consistency with Angular's dependency injection system. Add this import to line 2 and add the decorator on line 15, before the @Component() decorator.

Copilot uses AI. Check for mistakes.
selector: 'app-edit-language',
imports: [CommonModule, FormsModule, ReactiveFormsModule, MatFormFieldModule, MatAutocompleteModule, MatInputModule],
templateUrl: './language.component.html',
styleUrls: ['./language.component.css'],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class LanguageEditComponent extends BaseEditFieldComponent {

Copilot AI Nov 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing OnInit interface implementation. The component uses ngOnInit() but doesn't implement the OnInit interface. Add OnInit to the imports on line 2 and implement it in the class declaration on line 22: export class LanguageEditComponent extends BaseEditFieldComponent implements OnInit.

Copilot uses AI. Check for mistakes.
@Input() value: string;

public languages: {value: string | null, label: string, flag: string, nativeName?: string}[] = [];
public languageControl = new FormControl<{value: string | null, label: string, flag: string, nativeName?: string} | string>('');
public filteredLanguages: Observable<{value: string | null, label: string, flag: string, nativeName?: string}[]>;
public showFlag: boolean = true;
public selectedLanguageFlag: string = '';

originalOrder = () => { return 0; }

ngOnInit(): void {
super.ngOnInit();
this.parseWidgetParams();
this.loadLanguages();
this.setupAutocomplete();
this.setInitialValue();
}

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.show_flag !== undefined) {
this.showFlag = params.show_flag;
}
} catch (e) {
console.error('Error parsing language widget params:', e);
}
}
}

private setupAutocomplete(): void {
this.filteredLanguages = this.languageControl.valueChanges.pipe(
startWith(''),
map(value => {
// Update flag when value changes
if (typeof value === 'object' && value !== null) {
this.selectedLanguageFlag = value.flag;
} else if (typeof value === 'string') {
// Clear flag if user is typing
this.selectedLanguageFlag = '';
}
return this._filter(typeof value === 'string' ? value : (value?.label || ''));
})
);
}

private setInitialValue(): void {
if (this.value) {
const language = this.languages.find(l => l.value && l.value.toLowerCase() === this.value.toLowerCase());
if (language) {
this.languageControl.setValue(language);
this.selectedLanguageFlag = language.flag;
}
}
}

private _filter(value: string): {value: string | null, label: string, flag: string, nativeName?: string}[] {
const filterValue = value.toLowerCase();
return this.languages.filter(language =>
language.label?.toLowerCase().includes(filterValue) ||
(language.value && language.value.toLowerCase().includes(filterValue)) ||
(language.nativeName && language.nativeName.toLowerCase().includes(filterValue))
);
}

onLanguageSelected(selectedLanguage: {value: string | null, label: string, flag: string, nativeName?: string}): void {
this.value = selectedLanguage.value;
this.selectedLanguageFlag = selectedLanguage.flag;
this.onFieldChange.emit(this.value);
}

displayFn(language: any): string {
if (!language) return '';
// Only return the language label, flag is shown separately
return typeof language === 'string' ? language : language.label;
}

private loadLanguages(): void {
this.languages = LANGUAGES.map(language => ({
value: language.code,
label: language.name,
flag: getLanguageFlag(language),
nativeName: language.nativeName
})).toSorted((a, b) => a.label.localeCompare(b.label));

if (this.widgetStructure?.widget_params?.allow_null || this.structure?.allow_null) {
this.languages = [{ value: null, label: '', flag: '' }, ...this.languages];
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
.code-editor-box {
min-height: 200px;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden;
}

@media (prefers-color-scheme: light) {
.code-editor-box {
border: 1px solid #ccc;
}
}

@media (prefers-color-scheme: dark) {
.code-editor-box {
border: 1px solid #404040;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.language-flag {
margin-right: 6px;
font-size: 1.2em;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<span class="field-value">
<span *ngIf="showFlag && languageFlag" class="language-flag">{{ languageFlag }}</span>
<span class="field-view-value">{{ languageName }}</span>
</span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { LanguageRecordViewComponent } from './language.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { provideHttpClient } from '@angular/common/http';

describe('LanguageRecordViewComponent', () => {
let component: LanguageRecordViewComponent;
let fixture: ComponentFixture<LanguageRecordViewComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LanguageRecordViewComponent, BrowserAnimationsModule],
providers: [provideHttpClient()]
}).compileComponents();

fixture = TestBed.createComponent(LanguageRecordViewComponent);
component = fixture.componentInstance;
component.value = 'en';
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should display language name from code', () => {
component.value = 'en';
component.ngOnInit();
expect(component.languageName).toBe('English');
});

it('should display em dash when value is empty', () => {
component.value = null;
component.ngOnInit();
expect(component.languageName).toBe('—');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { LANGUAGES, getLanguageFlag } from '../../../../consts/languages';
import { Component, Injectable, OnInit } from '@angular/core';

import { BaseRecordViewFieldComponent } from '../base-record-view-field/base-record-view-field.component';
import { CommonModule } from '@angular/common';

@Injectable()
@Component({
selector: 'app-language-record-view',
templateUrl: './language.component.html',
styleUrls: ['../base-record-view-field/base-record-view-field.component.css', './language.component.css'],
imports: [CommonModule]
})
export class LanguageRecordViewComponent extends BaseRecordViewFieldComponent implements OnInit {
static type = 'language';

public languageName: string = '';
public languageFlag: string = '';
public showFlag: boolean = true;

ngOnInit(): void {
this.parseWidgetParams();

if (this.value) {
const language = LANGUAGES.find(l => l.code.toLowerCase() === this.value.toLowerCase());
this.languageName = language ? language.name : this.value;
if (language) {
this.languageFlag = getLanguageFlag(language);
}
} else {
this.languageName = '—';
this.languageFlag = '';
}
}

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.show_flag !== undefined) {
this.showFlag = params.show_flag;
}
} catch (e) {
console.error('Error parsing language widget params:', e);
}
}
}
}
Loading
Loading