mirror of https://github.com/Squidex/squidex.git
20 changed files with 615 additions and 374 deletions
@ -1,8 +1,2 @@ |
|||||
@import '_vars'; |
@import '_vars'; |
||||
@import '_mixins'; |
@import '_mixins'; |
||||
|
|
||||
.table-item-row-details { |
|
||||
&::before { |
|
||||
right: 5.2rem; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,163 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { IMock, It, Mock, Times } from 'typemoq'; |
||||
|
|
||||
|
import { |
||||
|
AppLanguageDto, |
||||
|
AppLanguagesDto, |
||||
|
AppLanguagesService, |
||||
|
AppsState, |
||||
|
DialogService, |
||||
|
ImmutableArray, |
||||
|
LanguageDto, |
||||
|
LanguagesService, |
||||
|
LanguagesState, |
||||
|
UpdateAppLanguageDto, |
||||
|
Version, |
||||
|
Versioned |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
describe('LanguagesState', () => { |
||||
|
const app = 'my-app'; |
||||
|
const version = new Version('1'); |
||||
|
const newVersion = new Version('2'); |
||||
|
|
||||
|
const languageDE = new LanguageDto('de', 'German'); |
||||
|
const languageEN = new LanguageDto('en', 'English'); |
||||
|
const languageIT = new LanguageDto('it', 'Italian'); |
||||
|
const languageES = new LanguageDto('es', 'Spanish'); |
||||
|
|
||||
|
const oldLanguages = [ |
||||
|
new AppLanguageDto(languageEN.iso2Code, languageEN.englishName, true, false, []), |
||||
|
new AppLanguageDto(languageDE.iso2Code, languageDE.englishName, false, true, [languageEN.iso2Code]) |
||||
|
]; |
||||
|
|
||||
|
let dialogs: IMock<DialogService>; |
||||
|
let appsState: IMock<AppsState>; |
||||
|
let allLanguagesService: IMock<LanguagesService>; |
||||
|
let languagesService: IMock<AppLanguagesService>; |
||||
|
let languagesState: LanguagesState; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
dialogs = Mock.ofType<DialogService>(); |
||||
|
|
||||
|
appsState = Mock.ofType<AppsState>(); |
||||
|
|
||||
|
appsState.setup(x => x.appName) |
||||
|
.returns(() => app); |
||||
|
|
||||
|
allLanguagesService = Mock.ofType<LanguagesService>(); |
||||
|
|
||||
|
allLanguagesService.setup(x => x.getLanguages()) |
||||
|
.returns(() => Observable.of([languageDE, languageEN, languageIT, languageES])); |
||||
|
|
||||
|
languagesService = Mock.ofType<AppLanguagesService>(); |
||||
|
|
||||
|
languagesService.setup(x => x.getLanguages(app)) |
||||
|
.returns(() => Observable.of(new AppLanguagesDto(oldLanguages, version))); |
||||
|
|
||||
|
languagesState = new LanguagesState(languagesService.object, appsState.object, dialogs.object, allLanguagesService.object); |
||||
|
languagesState.load().subscribe(); |
||||
|
}); |
||||
|
|
||||
|
it('should load languages', () => { |
||||
|
expect(languagesState.snapshot.languages.values).toEqual([ |
||||
|
{ |
||||
|
language: oldLanguages[0], |
||||
|
fallbackLanguages: ImmutableArray.empty(), |
||||
|
fallbackLanguagesNew: ImmutableArray.of([oldLanguages[1]]) |
||||
|
}, { |
||||
|
language: oldLanguages[1], |
||||
|
fallbackLanguages: ImmutableArray.of([oldLanguages[0]]), |
||||
|
fallbackLanguagesNew: ImmutableArray.empty() |
||||
|
} |
||||
|
]); |
||||
|
expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageIT, languageES]); |
||||
|
expect(languagesState.snapshot.isLoaded).toBeTruthy(); |
||||
|
expect(languagesState.snapshot.version).toEqual(version); |
||||
|
|
||||
|
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); |
||||
|
}); |
||||
|
|
||||
|
it('should show notification on load when flag is true', () => { |
||||
|
languagesState.load(true).subscribe(); |
||||
|
|
||||
|
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); |
||||
|
}); |
||||
|
|
||||
|
it('should add language to snapshot when assigned', () => { |
||||
|
const newLanguage = new AppLanguageDto(languageIT.iso2Code, languageIT.englishName, false, false, []); |
||||
|
|
||||
|
languagesService.setup(x => x.postLanguage(app, It.isAny(), version)) |
||||
|
.returns(() => Observable.of(new Versioned<AppLanguageDto>(newVersion, newLanguage))); |
||||
|
|
||||
|
languagesState.add(languageIT).subscribe(); |
||||
|
|
||||
|
expect(languagesState.snapshot.languages.values).toEqual([ |
||||
|
{ |
||||
|
language: oldLanguages[0], |
||||
|
fallbackLanguages: ImmutableArray.empty(), |
||||
|
fallbackLanguagesNew: ImmutableArray.of([oldLanguages[1], newLanguage]) |
||||
|
}, { |
||||
|
language: oldLanguages[1], |
||||
|
fallbackLanguages: ImmutableArray.of([oldLanguages[0]]), |
||||
|
fallbackLanguagesNew: ImmutableArray.of([newLanguage]) |
||||
|
}, { |
||||
|
language: newLanguage, |
||||
|
fallbackLanguages: ImmutableArray.of(), |
||||
|
fallbackLanguagesNew: ImmutableArray.of([oldLanguages[0], oldLanguages[1]]) |
||||
|
} |
||||
|
]); |
||||
|
expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageES]); |
||||
|
expect(languagesState.snapshot.version).toEqual(newVersion); |
||||
|
}); |
||||
|
|
||||
|
it('should update language in snapshot when updated', () => { |
||||
|
const request = new UpdateAppLanguageDto(true, false, []); |
||||
|
|
||||
|
languagesService.setup(x => x.putLanguage(app, oldLanguages[1].iso2Code, request, version)) |
||||
|
.returns(() => Observable.of(new Versioned<any>(newVersion, {}))); |
||||
|
|
||||
|
languagesState.update(oldLanguages[1], request).subscribe(); |
||||
|
|
||||
|
const newLanguage1 = new AppLanguageDto(languageDE.iso2Code, languageDE.englishName, true, false, []); |
||||
|
const newLanguage2 = new AppLanguageDto(languageEN.iso2Code, languageEN.englishName, false, false, []); |
||||
|
|
||||
|
expect(languagesState.snapshot.languages.values).toEqual([ |
||||
|
{ |
||||
|
language: newLanguage1, |
||||
|
fallbackLanguages: ImmutableArray.empty(), |
||||
|
fallbackLanguagesNew: ImmutableArray.of([newLanguage2]) |
||||
|
}, { |
||||
|
language: newLanguage2, |
||||
|
fallbackLanguages: ImmutableArray.empty(), |
||||
|
fallbackLanguagesNew: ImmutableArray.of([newLanguage1]) |
||||
|
} |
||||
|
]); |
||||
|
expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageIT, languageES]); |
||||
|
expect(languagesState.snapshot.version).toEqual(newVersion); |
||||
|
}); |
||||
|
|
||||
|
it('should remove language from snapshot when deleted', () => { |
||||
|
languagesService.setup(x => x.deleteLanguage(app, oldLanguages[1].iso2Code, version)) |
||||
|
.returns(() => Observable.of(new Versioned<any>(newVersion, {}))); |
||||
|
|
||||
|
languagesState.remove(oldLanguages[1]).subscribe(); |
||||
|
|
||||
|
expect(languagesState.snapshot.languages.values).toEqual([ |
||||
|
{ |
||||
|
language: oldLanguages[0], |
||||
|
fallbackLanguages: ImmutableArray.empty(), |
||||
|
fallbackLanguagesNew: ImmutableArray.empty() |
||||
|
} |
||||
|
]); |
||||
|
expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]); |
||||
|
expect(languagesState.snapshot.version).toEqual(newVersion); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,223 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
|
||||
|
import '@app/framework/utils/rxjs-extensions'; |
||||
|
|
||||
|
import { |
||||
|
DialogService, |
||||
|
Form, |
||||
|
ImmutableArray, |
||||
|
State, |
||||
|
Version |
||||
|
} from '@app/framework'; |
||||
|
|
||||
|
import { AddAppLanguageDto, AppLanguageDto, AppLanguagesService, UpdateAppLanguageDto } from './../services/app-languages.service'; |
||||
|
import { LanguageDto, LanguagesService } from './../services/languages.service'; |
||||
|
import { AppsState } from './apps.state'; |
||||
|
|
||||
|
|
||||
|
export class EditLanguageForm extends Form<FormGroup> { |
||||
|
constructor(formBuilder: FormBuilder) { |
||||
|
super(formBuilder.group({ |
||||
|
isMaster: false, |
||||
|
isOptional: false |
||||
|
})); |
||||
|
|
||||
|
this.form.controls['isMaster'].valueChanges |
||||
|
.subscribe(value => { |
||||
|
if (value) { |
||||
|
this.form.controls['isOptional'].setValue(false); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.form.controls['isOptional'].valueChanges |
||||
|
.subscribe(value => { |
||||
|
if (value) { |
||||
|
this.form.controls['isMaster'].setValue(false); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class AddLanguageForm extends Form<FormGroup> { |
||||
|
constructor(formBuilder: FormBuilder) { |
||||
|
super(formBuilder.group({ |
||||
|
language: [null, |
||||
|
[ |
||||
|
Validators.required |
||||
|
] |
||||
|
] |
||||
|
})); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
interface SnapshotLanguage { |
||||
|
language: AppLanguageDto; |
||||
|
|
||||
|
fallbackLanguages: ImmutableArray<LanguageDto>; |
||||
|
fallbackLanguagesNew: ImmutableArray<LanguageDto>; |
||||
|
} |
||||
|
|
||||
|
interface Snapshot { |
||||
|
plainLanguages: ImmutableArray<AppLanguageDto>; |
||||
|
|
||||
|
allLanguages: ImmutableArray<LanguageDto>; |
||||
|
allLanguagesNew: ImmutableArray<LanguageDto>; |
||||
|
|
||||
|
languages: ImmutableArray<SnapshotLanguage>; |
||||
|
|
||||
|
isLoaded?: boolean; |
||||
|
|
||||
|
version: Version; |
||||
|
} |
||||
|
|
||||
|
@Injectable() |
||||
|
export class LanguagesState extends State<Snapshot> { |
||||
|
public languages = |
||||
|
this.changes.map(x => x.languages) |
||||
|
.distinctUntilChanged(); |
||||
|
|
||||
|
public newLanguages = |
||||
|
this.changes.map(x => x.allLanguagesNew) |
||||
|
.distinctUntilChanged(); |
||||
|
|
||||
|
public isLoaded = |
||||
|
this.changes.map(x => !!x.isLoaded) |
||||
|
.distinctUntilChanged(); |
||||
|
|
||||
|
constructor( |
||||
|
private readonly appLanguagesService: AppLanguagesService, |
||||
|
private readonly appsState: AppsState, |
||||
|
private readonly dialogs: DialogService, |
||||
|
private readonly languagesService: LanguagesService |
||||
|
) { |
||||
|
super({ |
||||
|
plainLanguages: ImmutableArray.empty(), |
||||
|
allLanguages: ImmutableArray.empty(), |
||||
|
allLanguagesNew: ImmutableArray.empty(), |
||||
|
languages: ImmutableArray.empty(), |
||||
|
version: new Version('') |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public load(notifyLoad = false): Observable<any> { |
||||
|
return Observable.forkJoin( |
||||
|
this.languagesService.getLanguages(), |
||||
|
this.appLanguagesService.getLanguages(this.appName), |
||||
|
(allLanguages, languages) => ({ allLanguages, languages }) |
||||
|
) |
||||
|
.do(dtos => { |
||||
|
if (notifyLoad) { |
||||
|
this.dialogs.notifyInfo('Languages reloaded.'); |
||||
|
} |
||||
|
|
||||
|
const sorted = ImmutableArray.of(dtos.allLanguages).sortByStringAsc(x => x.englishName); |
||||
|
|
||||
|
this.replaceLanguages(ImmutableArray.of(dtos.languages.languages), dtos.languages.version, sorted); |
||||
|
}) |
||||
|
.notify(this.dialogs); |
||||
|
} |
||||
|
|
||||
|
public add(language: LanguageDto): Observable<any> { |
||||
|
return this.appLanguagesService.postLanguage(this.appName, new AddAppLanguageDto(language.iso2Code), this.version) |
||||
|
.do(dto => { |
||||
|
const languages = this.snapshot.plainLanguages.push(dto.payload).sortByStringAsc(x => x.englishName); |
||||
|
|
||||
|
this.replaceLanguages(languages, dto.version); |
||||
|
}) |
||||
|
.notify(this.dialogs); |
||||
|
} |
||||
|
|
||||
|
public remove(language: AppLanguageDto): Observable<any> { |
||||
|
return this.appLanguagesService.deleteLanguage(this.appName, language.iso2Code, this.version) |
||||
|
.do(dto => { |
||||
|
const languages = this.snapshot.plainLanguages.filter(x => x.iso2Code !== language.iso2Code); |
||||
|
|
||||
|
this.replaceLanguages(languages, dto.version); |
||||
|
}) |
||||
|
.notify(this.dialogs); |
||||
|
} |
||||
|
|
||||
|
public update(language: AppLanguageDto, request: UpdateAppLanguageDto): Observable<any> { |
||||
|
return this.appLanguagesService.putLanguage(this.appName, language.iso2Code, request, this.version) |
||||
|
.do(dto => { |
||||
|
const languages = this.snapshot.plainLanguages.map(l => { |
||||
|
if (l.iso2Code === language.iso2Code) { |
||||
|
return update(l, request.isMaster, request.isOptional, request.fallback); |
||||
|
} else if (l.isMaster && request.isMaster) { |
||||
|
return update(l, false, l.isOptional, l.fallback); |
||||
|
} else { |
||||
|
return l; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.replaceLanguages(languages, dto.version); |
||||
|
}) |
||||
|
.notify(this.dialogs); |
||||
|
} |
||||
|
|
||||
|
private replaceLanguages(languages: ImmutableArray<AppLanguageDto>, version: Version, allLanguages?: ImmutableArray<LanguageDto>) { |
||||
|
this.next(s => { |
||||
|
allLanguages = allLanguages || s.allLanguages; |
||||
|
|
||||
|
return { |
||||
|
...s, |
||||
|
languages: languages.sort((a, b) => { |
||||
|
if (a.isMaster === b.isMaster) { |
||||
|
return a.englishName.localeCompare(b.englishName); |
||||
|
} else { |
||||
|
return (a.isMaster ? 0 : 1) - (b.isMaster ? 0 : 1); |
||||
|
} |
||||
|
}).map(x => this.createLanguage(x, languages)), |
||||
|
plainLanguages: languages, |
||||
|
allLanguages: allLanguages, |
||||
|
allLanguagesNew: allLanguages.filter(x => !languages.find(l => l.iso2Code === x.iso2Code)), |
||||
|
isLoaded: true, |
||||
|
version: version |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private get appName() { |
||||
|
return this.appsState.appName; |
||||
|
} |
||||
|
|
||||
|
private get version() { |
||||
|
return this.snapshot.version; |
||||
|
} |
||||
|
|
||||
|
private createLanguage(language: AppLanguageDto, languages: ImmutableArray<AppLanguageDto>): SnapshotLanguage { |
||||
|
return { |
||||
|
language, |
||||
|
fallbackLanguages: |
||||
|
ImmutableArray.of( |
||||
|
language.fallback |
||||
|
.map(l => languages.find(x => x.iso2Code === l)).filter(x => !!x) |
||||
|
.map(x => <AppLanguageDto>x)), |
||||
|
fallbackLanguagesNew: |
||||
|
languages |
||||
|
.filter(l => |
||||
|
language.iso2Code !== l.iso2Code && |
||||
|
language.fallback.indexOf(l.iso2Code) < 0) |
||||
|
.sortByStringAsc(x => x.englishName) |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
const update = (language: AppLanguageDto, isMaster: boolean, isOptional: boolean, fallback: string[]) => |
||||
|
new AppLanguageDto( |
||||
|
language.iso2Code, |
||||
|
language.englishName, |
||||
|
isMaster, |
||||
|
isOptional, |
||||
|
fallback); |
||||
Loading…
Reference in new issue