diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md
index dead28afa..86ae600ea 100644
--- a/frontend/CLAUDE.md
+++ b/frontend/CLAUDE.md
@@ -140,6 +140,11 @@ Database configurations are defined in `src/app/consts/databases.ts`.
## 📝 Code Conventions
+### Template Syntax
+- **Use built-in control flow** (`@if`, `@for`, `@switch`) instead of structural directives (`*ngIf`, `*ngFor`, `*ngSwitch`) in all new code
+- Example: `@if (condition) { ... }` instead of `
...
`
+- Example: `@for (item of items; track item.id) { ... }` instead of `...
`
+
### Naming Conventions
- **Files**: `kebab-case.component.ts`
- **Classes**: `PascalCase` (e.g., `DbTableSettingsComponent`)
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.html b/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.html
index 7a6570ebb..7f25a437e 100644
--- a/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.html
+++ b/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.html
@@ -1,22 +1,32 @@
- {{normalizedLabel}}
+ {{normalizedLabel()}}
- {{selectedLanguageFlag}}
+ @if (selectedLanguageFlag() && showFlag()) {
+ {{selectedLanguageFlag()}}
+ }
-
- {{language.flag}}
- {{language.label}}
- {{language.nativeName}}
- ({{language.value}})
-
+ @for (language of filteredLanguages | async; track language.value) {
+
+ @if (language.flag && showFlag()) {
+ {{language.flag}}
+ }
+ {{language.label}}
+ @if (language.nativeName && language.label !== language.nativeName) {
+ {{language.nativeName}}
+ }
+ @if (language.value) {
+ ({{language.value}})
+ }
+
+ }
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.spec.ts
index 649add210..caa590924 100644
--- a/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.spec.ts
+++ b/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.spec.ts
@@ -1,50 +1,45 @@
+import { provideHttpClient } from '@angular/common/http';
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';
+import { LanguageEditComponent } from './language.component';
describe('LanguageEditComponent', () => {
- let component: LanguageEditComponent;
- let fixture: ComponentFixture;
-
- 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);
- });
+ let component: LanguageEditComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [LanguageEditComponent, BrowserAnimationsModule],
+ providers: [provideHttpClient()],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LanguageEditComponent);
+ component = fixture.componentInstance;
+ fixture.componentRef.setInput('widgetStructure', { widget_params: {} });
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should load languages on init', () => {
+ expect(component.languages.length).toBeGreaterThan(0);
+ });
+
+ it('should set initial value when value is provided', () => {
+ fixture.componentRef.setInput('value', 'en');
+ component.ngOnInit();
+ expect(component.selectedLanguageFlag()).toBeTruthy();
+ });
+
+ it('should parse widget params for show_flag', () => {
+ fixture.componentRef.setInput('widgetStructure', {
+ widget_params: { show_flag: false },
+ });
+ fixture.detectChanges();
+ expect(component.showFlag()).toBe(false);
+ });
});
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.ts
index 59a71d0e5..65ee3dc33 100644
--- a/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.ts
+++ b/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.ts
@@ -1,116 +1,126 @@
-import { LANGUAGES, getLanguageFlag, } from '../../../../consts/languages';
-import { CUSTOM_ELEMENTS_SCHEMA, Component, Input } from '@angular/core';
-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 { Component, CUSTOM_ELEMENTS_SCHEMA, computed, input, OnInit, output, signal } from '@angular/core';
+import { FormControl, FormsModule, ReactiveFormsModule } 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';
+import { map, startWith } from 'rxjs/operators';
+import { TableField, TableForeignKey, WidgetStructure } from 'src/app/models/table';
+import { getLanguageFlag, LANGUAGES } from '../../../../consts/languages';
+import { normalizeFieldName } from '../../../../lib/normalize';
+
+interface LanguageOption {
+ value: string | null;
+ label: string;
+ flag: string;
+ nativeName?: string;
+}
@Component({
- selector: 'app-edit-language',
- imports: [CommonModule, FormsModule, ReactiveFormsModule, MatFormFieldModule, MatAutocompleteModule, MatInputModule],
- templateUrl: './language.component.html',
- styleUrls: ['./language.component.css'],
- schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ 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 {
- @Input() value: string;
+export class LanguageEditComponent implements OnInit {
+ readonly key = input();
+ readonly label = input();
+ readonly required = input(false);
+ readonly readonly = input(false);
+ readonly structure = input();
+ readonly disabled = input(false);
+ readonly widgetStructure = input();
+ readonly relations = input();
+ readonly value = input();
+
+ readonly onFieldChange = output();
- 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 = '';
+ readonly normalizedLabel = computed(() => normalizeFieldName(this.label() || ''));
- originalOrder = () => { return 0; }
+ readonly showFlag = computed(() => {
+ const ws = this.widgetStructure();
+ if (!ws?.widget_params) return true;
+ try {
+ const params = typeof ws.widget_params === 'string' ? JSON.parse(ws.widget_params) : ws.widget_params;
+ return params.show_flag !== undefined ? params.show_flag : true;
+ } catch {
+ return true;
+ }
+ });
- ngOnInit(): void {
- super.ngOnInit();
- this.parseWidgetParams();
- this.loadLanguages();
- this.setupAutocomplete();
- this.setInitialValue();
- }
+ public languages: LanguageOption[] = [];
+ public languageControl = new FormControl('');
+ public filteredLanguages: Observable;
+ public selectedLanguageFlag = signal('');
- 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;
+ originalOrder = () => {
+ return 0;
+ };
- if (params.show_flag !== undefined) {
- this.showFlag = params.show_flag;
- }
- } catch (e) {
- console.error('Error parsing language widget params:', e);
- }
- }
- }
+ ngOnInit(): void {
+ this.loadLanguages();
+ this.setupAutocomplete();
+ this.setInitialValue();
+ }
- 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 || ''));
- })
- );
- }
+ displayFn(language: any): string {
+ if (!language) return '';
+ return typeof language === 'string' ? language : language.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;
- }
- }
- }
+ onLanguageSelected(selectedLanguage: LanguageOption): void {
+ this.selectedLanguageFlag.set(selectedLanguage.flag);
+ this.onFieldChange.emit(selectedLanguage.value);
+ }
- 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?.toLowerCase().includes(filterValue)) ||
- (language.nativeName?.toLowerCase().includes(filterValue))
- );
- }
+ private setupAutocomplete(): void {
+ this.filteredLanguages = this.languageControl.valueChanges.pipe(
+ startWith(''),
+ map((value) => {
+ if (typeof value === 'object' && value !== null) {
+ this.selectedLanguageFlag.set(value.flag);
+ } else if (typeof value === 'string') {
+ this.selectedLanguageFlag.set('');
+ }
+ return this._filter(typeof value === 'string' ? value : value?.label || '');
+ }),
+ );
+ }
- 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);
- }
+ private setInitialValue(): void {
+ const val = this.value();
+ if (val) {
+ const language = this.languages.find((l) => l.value && l.value.toLowerCase() === val.toLowerCase());
+ if (language) {
+ this.languageControl.setValue(language);
+ this.selectedLanguageFlag.set(language.flag);
+ }
+ }
+ }
- displayFn(language: any): string {
- if (!language) return '';
- // Only return the language label, flag is shown separately
- return typeof language === 'string' ? language : language.label;
- }
+ private _filter(value: string): LanguageOption[] {
+ const filterValue = value.toLowerCase();
+ return this.languages.filter(
+ (language) =>
+ language.label?.toLowerCase().includes(filterValue) ||
+ language.value?.toLowerCase().includes(filterValue) ||
+ language.nativeName?.toLowerCase().includes(filterValue),
+ );
+ }
- 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));
+ 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];
- }
- }
+ const ws = this.widgetStructure();
+ if (ws?.widget_params?.allow_null || this.structure()?.allow_null) {
+ this.languages = [{ value: null, label: '', flag: '' }, ...this.languages];
+ }
+ }
}
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.html b/frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.html
index fbf37ce73..c61abb7f4 100644
--- a/frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.html
+++ b/frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.html
@@ -15,23 +15,29 @@
upload
Upload
-
+ @if (previewUrl()) {
+
+ }
-
diff --git a/frontend/src/app/components/ui-components/record-view-fields/language/language.component.html b/frontend/src/app/components/ui-components/record-view-fields/language/language.component.html
index 0615634dd..c09ca8c93 100644
--- a/frontend/src/app/components/ui-components/record-view-fields/language/language.component.html
+++ b/frontend/src/app/components/ui-components/record-view-fields/language/language.component.html
@@ -1,4 +1,6 @@
- {{ languageFlag }}
- {{ languageName }}
+ @if (showFlag() && languageFlag()) {
+ {{ languageFlag() }}
+ }
+ {{ languageName() }}
diff --git a/frontend/src/app/components/ui-components/record-view-fields/language/language.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/language/language.component.spec.ts
index 08ef242f0..00f7346b2 100644
--- a/frontend/src/app/components/ui-components/record-view-fields/language/language.component.spec.ts
+++ b/frontend/src/app/components/ui-components/record-view-fields/language/language.component.spec.ts
@@ -1,38 +1,37 @@
+import { provideHttpClient } from '@angular/common/http';
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';
+import { LanguageRecordViewComponent } from './language.component';
describe('LanguageRecordViewComponent', () => {
- let component: LanguageRecordViewComponent;
- let fixture: ComponentFixture;
+ let component: LanguageRecordViewComponent;
+ let fixture: ComponentFixture;
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [LanguageRecordViewComponent, BrowserAnimationsModule],
- providers: [provideHttpClient()]
- }).compileComponents();
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [LanguageRecordViewComponent, BrowserAnimationsModule],
+ providers: [provideHttpClient()],
+ }).compileComponents();
- fixture = TestBed.createComponent(LanguageRecordViewComponent);
- component = fixture.componentInstance;
- component.value = 'en';
- fixture.detectChanges();
- });
+ fixture = TestBed.createComponent(LanguageRecordViewComponent);
+ component = fixture.componentInstance;
+ fixture.componentRef.setInput('value', 'en');
+ fixture.detectChanges();
+ });
- it('should create', () => {
- expect(component).toBeTruthy();
- });
+ 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 language name from code', () => {
+ fixture.componentRef.setInput('value', 'en');
+ fixture.detectChanges();
+ expect(component.languageName()).toBe('English');
+ });
- it('should display em dash when value is empty', () => {
- component.value = null;
- component.ngOnInit();
- expect(component.languageName).toBe('—');
- });
+ it('should display em dash when value is empty', () => {
+ fixture.componentRef.setInput('value', null);
+ fixture.detectChanges();
+ expect(component.languageName()).toBe('—');
+ });
});
diff --git a/frontend/src/app/components/ui-components/record-view-fields/language/language.component.ts b/frontend/src/app/components/ui-components/record-view-fields/language/language.component.ts
index 656ae30cc..2a15ed68e 100644
--- a/frontend/src/app/components/ui-components/record-view-fields/language/language.component.ts
+++ b/frontend/src/app/components/ui-components/record-view-fields/language/language.component.ts
@@ -1,51 +1,48 @@
-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';
+import { Component, computed, input, output } from '@angular/core';
+import { TableField, WidgetStructure } from 'src/app/models/table';
+import { getLanguageFlag, LANGUAGES } from '../../../../consts/languages';
-@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]
+ 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';
+export class LanguageRecordViewComponent {
+ static type = 'language';
- public languageName: string = '';
- public languageFlag: string = '';
- public showFlag: boolean = true;
+ readonly key = input();
+ readonly value = input();
+ readonly structure = input();
+ readonly widgetStructure = input();
+ readonly rowData = input>();
+ readonly primaryKeys = input>();
- ngOnInit(): void {
- this.parseWidgetParams();
+ readonly onCopyToClipboard = output();
- 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 = '';
- }
- }
+ readonly showFlag = computed(() => {
+ const ws = this.widgetStructure();
+ if (!ws?.widget_params) return true;
+ try {
+ const params = typeof ws.widget_params === 'string' ? JSON.parse(ws.widget_params) : ws.widget_params;
+ return params.show_flag !== undefined ? params.show_flag : true;
+ } catch {
+ return true;
+ }
+ });
- 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;
+ readonly languageName = computed(() => {
+ const val = this.value();
+ if (!val) return '—';
+ const language = LANGUAGES.find((l) => l.code.toLowerCase() === val.toLowerCase());
+ return language ? language.name : val;
+ });
- if (params.show_flag !== undefined) {
- this.showFlag = params.show_flag;
- }
- } catch (e) {
- console.error('Error parsing language widget params:', e);
- }
- }
- }
+ readonly languageFlag = computed(() => {
+ const val = this.value();
+ if (!val) return '';
+ const language = LANGUAGES.find((l) => l.code.toLowerCase() === val.toLowerCase());
+ return language ? getLanguageFlag(language) : '';
+ });
}
diff --git a/frontend/src/app/components/ui-components/table-display-fields/language/language.component.html b/frontend/src/app/components/ui-components/table-display-fields/language/language.component.html
index 85f344d1e..4da896023 100644
--- a/frontend/src/app/components/ui-components/table-display-fields/language/language.component.html
+++ b/frontend/src/app/components/ui-components/table-display-fields/language/language.component.html
@@ -1,12 +1,14 @@
- {{ languageFlag }}
- {{ languageName }}
+ @if (showFlag() && languageFlag()) {
+ {{ languageFlag() }}
+ }
+ {{ languageName() }}