-
-
Notifications
You must be signed in to change notification settings - Fork 18
language widget and markdown dark theme support #1435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||
| 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 { 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({ | ||
|
||
| 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; | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import Language.