Speedup DS and repo updates, DSUpdateSlots, lazyloading history objects

This commit is contained in:
FinnStutzenstein 2019-05-06 11:44:56 +02:00
parent 8fb12a6882
commit fe81ea6ff9
43 changed files with 524 additions and 235 deletions

View File

@ -3,7 +3,7 @@ import { Injectable, Injector } from '@angular/core';
import { plugins } from '../../../plugins';
import { CommonAppConfig } from '../../site/common/common.config';
import { AppConfig, SearchableModelEntry, ModelEntry } from '../app-config';
import { CollectionStringMapperService } from './collectionStringMapper.service';
import { CollectionStringMapperService } from './collection-string-mapper.service';
import { MediafileAppConfig } from '../../site/mediafiles/mediafile.config';
import { MotionsAppConfig } from '../../site/motions/motions.config';
import { ConfigAppConfig } from '../../site/config/config.config';

View File

@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import { WebsocketService } from './websocket.service';
import { CollectionStringMapperService } from './collectionStringMapper.service';
import { CollectionStringMapperService } from './collection-string-mapper.service';
import { DataStoreService } from './data-store.service';
import { BaseModel } from '../../shared/models/base/base-model';
import { DataStoreUpdateManagerService } from './data-store-update-manager.service';
interface AutoupdateFormat {
/**
@ -54,7 +55,8 @@ export class AutoupdateService {
public constructor(
private websocketService: WebsocketService,
private DS: DataStoreService,
private modelMapper: CollectionStringMapperService
private modelMapper: CollectionStringMapperService,
private DSUpdateManager: DataStoreUpdateManagerService
) {
this.websocketService.getOberservable<AutoupdateFormat>('autoupdate').subscribe(response => {
this.storeResponse(response);
@ -105,6 +107,8 @@ export class AutoupdateService {
// Normal autoupdate
if (autoupdate.from_change_id <= maxChangeId + 1 && autoupdate.to_change_id > maxChangeId) {
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
// Delete the removed objects from the DataStore
for (const collection of Object.keys(autoupdate.deleted)) {
await this.DS.remove(collection, autoupdate.deleted[collection]);
@ -120,6 +124,8 @@ export class AutoupdateService {
}
await this.DS.flushToStorage(autoupdate.to_change_id);
this.DSUpdateManager.commit(updateSlot);
} else {
// autoupdate fully in the future. we are missing something!
this.requestChanges();

View File

@ -0,0 +1,17 @@
import { TestBed, inject } from '@angular/core/testing';
import { E2EImportsModule } from '../../../e2e-imports.module';
import { CollectionStringMapperService } from './collection-string-mapper.service';
describe('CollectionStringMapperService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
providers: [CollectionStringMapperService]
});
});
it('should be created', inject([CollectionStringMapperService], (service: CollectionStringMapperService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -17,6 +17,12 @@ interface UnifiedConstructors {
*/
type TypeIdentifier = UnifiedConstructors | BaseRepository<any, any> | string;
type CollectionStringMappedTypes = [
ModelConstructor<BaseModel>,
ViewModelConstructor<BaseViewModel>,
BaseRepository<BaseViewModel, BaseModel>
];
/**
* Registeres the mapping between collection strings, models constructors, view
* model constructors and repositories.
@ -30,11 +36,7 @@ export class CollectionStringMapperService {
* Maps collection strings to mapping entries
*/
private collectionStringMapping: {
[collectionString: string]: [
ModelConstructor<BaseModel>,
ViewModelConstructor<BaseViewModel>,
BaseRepository<BaseViewModel, BaseModel>
];
[collectionString: string]: CollectionStringMappedTypes;
} = {};
public constructor() {}
@ -65,6 +67,9 @@ export class CollectionStringMapperService {
}
}
/**
* @returns true, if the given collection is known by this service.
*/
public isCollectionRegistered(collectionString: string): boolean {
return !!this.collectionStringMapping[collectionString];
}
@ -100,4 +105,11 @@ export class CollectionStringMapperService {
return this.collectionStringMapping[this.getCollectionString(obj)][2] as BaseRepository<V, M>;
}
}
/**
* @returns all registered repositories.
*/
public getAllRepositories(): BaseRepository<any, any>[] {
return Object.values(this.collectionStringMapping).map((types: CollectionStringMappedTypes) => types[2]);
}
}

View File

@ -1,3 +0,0 @@
describe('CollectionStringModelMapperService', () => {
beforeEach(() => {});
});

View File

@ -0,0 +1,17 @@
import { TestBed, inject } from '@angular/core/testing';
import { E2EImportsModule } from '../../../e2e-imports.module';
import { DataStoreUpdateManagerService } from './data-store-update-manager.service';
describe('DataStoreUpdateManagerService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
providers: [DataStoreUpdateManagerService]
});
});
it('should be created', inject([DataStoreUpdateManagerService], (service: DataStoreUpdateManagerService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,182 @@
import { Injectable } from '@angular/core';
import { Deferred } from '../deferred';
import { DataStoreService } from './data-store.service';
import { CollectionStringMapperService } from './collection-string-mapper.service';
export interface CollectionIds {
[collection: string]: number[];
}
/**
* Helper class for collecting data during the update phase of the DataStore.
*/
export class UpdateSlot {
/**
* Count instnaces of this class to easily compare them.
*/
private static ID_COUTNER = 1;
/**
* Mapping of changed model ids to their collection.
*/
private changedModels: CollectionIds = {};
/**
* Mapping of deleted models to their collection.
*/
private deletedModels: CollectionIds = {};
/**
* The object's id.
*/
private _id: number;
/**
* @param DS Carries the DataStore: TODO (see below `DataStoreUpdateManagerService.getNewUpdateSlot`)
*/
public constructor(public readonly DS: DataStoreService) {
this._id = UpdateSlot.ID_COUTNER++;
}
/**
* Adds changed model information
*
* @param collection The collection
* @param id The id
*/
public addChangedModel(collection: string, id: number): void {
if (!this.changedModels[collection]) {
this.changedModels[collection] = [];
}
this.changedModels[collection].push(id);
}
/**
* Adds deleted model information
*
* @param collection The collection
* @param id The id
*/
public addDeletedModel(collection: string, id: number): void {
if (!this.deletedModels[collection]) {
this.deletedModels[collection] = [];
}
this.deletedModels[collection].push(id);
}
/**
* @param collection The collection
* @returns the list of changed model ids for the collection
*/
public getChangedModelIdsForCollection(collection: string): number[] {
return this.changedModels[collection] || [];
}
/**
* @returns the mapping of all changed models
*/
public getChangedModels(): CollectionIds {
return this.changedModels;
}
/**
* @param collection The collection
* @returns the list of deleted model ids for the collection
*/
public getDeletedModelIdsForCollection(collection: string): number[] {
return this.deletedModels[collection] || [];
}
/**
* Compares this object to another update slot.
*/
public equal(other: UpdateSlot): boolean {
return this._id === other._id;
}
}
/**
* Manages updates in the DS. Collects all ids for changed and deleted models and bulk-update
* affected repositories.
*/
@Injectable({
providedIn: 'root'
})
export class DataStoreUpdateManagerService {
/**
* The current update slot
*/
private currentUpdateSlot: UpdateSlot | null = null;
/**
* Requests for getting an update slot.
*/
private updateSlotRequests: Deferred[] = [];
/**
* @param mapperService
*/
public constructor(private mapperService: CollectionStringMapperService) {}
/**
* Retrieve the current update slot.
*/
public getCurrentUpdateSlot(): UpdateSlot | null {
return this.currentUpdateSlot;
}
/**
* Get a new update slot. Returns a promise that must be awaited, if there is another
* update in progress.
*
* @param DS The DataStore. This is a hack, becuase we cannot use the DataStore
* here, because these are cyclic dependencies... --> TODO
*/
public async getNewUpdateSlot(DS: DataStoreService): Promise<UpdateSlot> {
if (this.currentUpdateSlot) {
const request = new Deferred();
this.updateSlotRequests.push(request);
await request.promise;
}
this.currentUpdateSlot = new UpdateSlot(DS);
return this.currentUpdateSlot;
}
/**
* Commits the given update slot. THis slot must be the current one. If there are requests
* for update slots queued, the next one will be served.
*
* Note: I added this param to make sure, that only the user of the slot
* can commit the update and no one else.
*
* @param slot The slot to commit
*/
public commit(slot: UpdateSlot): void {
if (!this.currentUpdateSlot || !this.currentUpdateSlot.equal(slot)) {
throw new Error('No or wrong update slot to be finished!');
}
this.currentUpdateSlot = null;
// notify repositories in two phases
const repositories = this.mapperService.getAllRepositories();
// Phase 1: deleting and creating of view models (in this order)
repositories.forEach(repo => {
repo.deleteModels(slot.getDeletedModelIdsForCollection(repo.collectionString));
repo.changedModels(slot.getChangedModelIdsForCollection(repo.collectionString));
});
// Phase 2: updating dependencies
repositories.forEach(repo => {
repo.updateDependencies(slot.getChangedModels());
});
slot.DS.triggerModifiedObservable();
// serve next slot request
if (this.updateSlotRequests.length > 0) {
const request = this.updateSlotRequests.pop();
request.resolve();
}
}
}

View File

@ -4,7 +4,8 @@ import { Observable, Subject } from 'rxjs';
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
import { StorageService } from './storage.service';
import { CollectionStringMapperService } from './collectionStringMapper.service';
import { CollectionStringMapperService } from './collection-string-mapper.service';
import { DataStoreUpdateManagerService } from './data-store-update-manager.service';
/**
* Represents information about a deleted model.
@ -68,59 +69,23 @@ export class DataStoreService {
private jsonStore: JsonStorage = {};
/**
* Observable subject for changed models in the datastore.
* Subjects for changed elements (notified, even if there is a current update slot) for
* a specific collection.
*/
private readonly changedSubject: Subject<BaseModel> = new Subject<BaseModel>();
/**
* This is subject notify all subscribers _before_ the `secondaryModelChangeSubject`.
* It's the same subject as the changedSubject.
*/
public readonly primaryModelChangeSubject = new Subject<BaseModel>();
/**
* This is subject notify all subscribers _after_ the `primaryModelChangeSubject`.
* It's the same subject as the changedSubject.
*/
public readonly secondaryModelChangeSubject = new Subject<BaseModel>();
/**
* Observe the datastore for changes.
*
* @return an observable for changed models
*/
public get changeObservable(): Observable<BaseModel> {
return this.changedSubject.asObservable();
}
/**
* Observable subject for changed models in the datastore.
*/
private readonly deletedSubject: Subject<DeletedInformation> = new Subject<DeletedInformation>();
/**
* Observe the datastore for deletions.
*
* @return an observable for deleted objects.
*/
public get deletedObservable(): Observable<DeletedInformation> {
return this.deletedSubject.asObservable();
}
private changedSubjects: { [collection: string]: Subject<BaseModel> } = {};
/**
* Observable subject for changed or deleted models in the datastore.
*/
private readonly changedOrDeletedSubject: Subject<BaseModel | DeletedInformation> = new Subject<
BaseModel | DeletedInformation
>();
private readonly modifiedSubject: Subject<void> = new Subject<void>();
/**
* Observe the datastore for changes and deletions.
*
* @return an observable for changed and deleted objects.
*/
public get changedOrDeletedObservable(): Observable<BaseModel | DeletedInformation> {
return this.changedOrDeletedSubject.asObservable();
public get modifiedObservable(): Observable<void> {
return this.modifiedSubject.asObservable();
}
/**
@ -152,12 +117,26 @@ export class DataStoreService {
/**
* @param storageService use StorageService to preserve the DataStore.
* @param modelMapper
* @param DSUpdateManager
*/
public constructor(private storageService: StorageService, private modelMapper: CollectionStringMapperService) {
this.changeObservable.subscribe(model => {
this.primaryModelChangeSubject.next(model);
this.secondaryModelChangeSubject.next(model);
});
public constructor(
private storageService: StorageService,
private modelMapper: CollectionStringMapperService,
private DSUpdateManager: DataStoreUpdateManagerService
) {}
/**
* Get an model observable for models from a given collection. These observable will be notified,
* even if there is an active update slot. So use this with caution (-> only collections with less models).
*
* @param collectionType The collection
*/
public getChangeObservable<T extends BaseModel>(collectionType: ModelConstructor<T> | string): Observable<T> {
const collection = this.getCollectionString(collectionType);
if (!this.changedSubjects[collection]) {
this.changedSubjects[collection] = new Subject();
}
return this.changedSubjects[collection].asObservable() as Observable<T>;
}
/**
@ -165,12 +144,15 @@ export class DataStoreService {
* @returns The max change id.
*/
public async initFromStorage(): Promise<number> {
// This promise will be resolved with the maximal change id of the cache.
// This promise will be resolved with cached datastore.
const store = await this.storageService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS');
if (store) {
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this);
// There is a store. Deserialize it
this.jsonStore = store;
this.modelStore = this.deserializeJsonStore(this.jsonStore);
// Get the maxChangeId from the cache
let maxChangeId = await this.storageService.get<number>(DataStoreService.cachePrefix + 'maxChangeId');
if (!maxChangeId) {
@ -184,6 +166,8 @@ export class DataStoreService {
this.publishChangedInformation(this.modelStore[collection][id]);
});
});
this.DSUpdateManager.commit(updateSlot);
} else {
await this.clear();
}
@ -414,8 +398,17 @@ export class DataStoreService {
* @param model The model to publish
*/
private publishChangedInformation(model: BaseModel): void {
this.changedSubject.next(model);
this.changedOrDeletedSubject.next(model);
const slot = this.DSUpdateManager.getCurrentUpdateSlot();
if (slot) {
slot.addChangedModel(model.collectionString, model.id);
// triggerModifiedObservable will be called by committing the update slot.
} else {
this.triggerModifiedObservable();
}
if (this.changedSubjects[model.collectionString]) {
this.changedSubjects[model.collectionString].next(model);
}
}
/**
@ -424,8 +417,20 @@ export class DataStoreService {
* @param information The information about the deleted model
*/
private publishDeletedInformation(information: DeletedInformation): void {
this.deletedSubject.next(information);
this.changedOrDeletedSubject.next(information);
const slot = this.DSUpdateManager.getCurrentUpdateSlot();
if (slot) {
slot.addDeletedModel(information.collection, information.id);
// triggerModifiedObservable will be called by committing the update slot.
} else {
this.triggerModifiedObservable();
}
}
/**
* Triggers the modified subject.
*/
public triggerModifiedObservable(): void {
this.modifiedSubject.next();
}
/**

View File

@ -12,7 +12,7 @@ import { OpenSlidesStatusService } from './openslides-status.service';
import { ViewUser } from 'app/site/users/models/view-user';
import { OnAfterAppsLoaded } from '../onAfterAppsLoaded';
import { UserRepositoryService } from '../repositories/users/user-repository.service';
import { CollectionStringMapperService } from './collectionStringMapper.service';
import { CollectionStringMapperService } from './collection-string-mapper.service';
import { StorageService } from './storage.service';
import { HttpService } from './http.service';
import { filter, auditTime } from 'rxjs/operators';
@ -139,23 +139,23 @@ export class OperatorService implements OnAfterAppsLoaded {
private storageService: StorageService,
private OSStatus: OpenSlidesStatusService
) {
this.DS.changeObservable.subscribe(newModel => {
if (this._user && newModel instanceof User && this._user.id === newModel.id) {
this.DS.getChangeObservable(User).subscribe(newModel => {
if (this._user && this._user.id === newModel.id) {
this._user = newModel;
this.updateUserInCurrentWhoAmI();
}
});
this.DS.changeObservable
this.DS.getChangeObservable(Group)
.pipe(
filter(
model =>
// Any group has changed if we have an operator or
// group 1 (default) for anonymous changed
model instanceof Group && (!!this._user || model.id === 1)
!!this._user || model.id === 1
),
auditTime(10)
)
.subscribe(newModel => this.updatePermissions());
.subscribe(_ => this.updatePermissions());
}
/**

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { environment } from 'environments/environment';
import { CollectionStringMapperService } from './collectionStringMapper.service';
import { CollectionStringMapperService } from './collection-string-mapper.service';
import { History } from 'app/shared/models/core/history';
import { DataStoreService } from './data-store.service';
import { WebsocketService } from './websocket.service';

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { CollectionStringMapperService } from './collectionStringMapper.service';
import { CollectionStringMapperService } from './collection-string-mapper.service';
import { BaseViewModel, ViewModelConstructor } from 'app/site/base/base-view-model';
import { BaseRepository } from '../repositories/base-repository';

View File

@ -13,7 +13,7 @@
* //
* ```
*/
export class Deferred<T> {
export class Deferred<T = void> {
/**
* The promise to wait for
*/
@ -22,7 +22,7 @@ export class Deferred<T> {
/**
* custom resolve function
*/
private _resolve: () => void;
private _resolve: (val?: T) => void;
/**
* Creates the promise and overloads the resolve function
@ -36,7 +36,7 @@ export class Deferred<T> {
/**
* Entry point for the resolve function
*/
public resolve(): void {
this._resolve();
public resolve(val?: T): void {
this._resolve(val);
}
}

View File

@ -5,7 +5,7 @@ import { Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
@ -17,6 +17,10 @@ import { BaseAgendaViewModel } from 'app/site/base/base-agenda-view-model';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
import { Motion } from 'app/shared/models/motions/motion';
import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { Topic } from 'app/shared/models/topics/topic';
import { Assignment } from 'app/shared/models/assignments/assignment';
/**
* Repository service for users
@ -47,25 +51,18 @@ export class ItemRepositoryService extends BaseRepository<ViewItem, Item> {
private config: ConfigService,
private treeService: TreeService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Item);
super(DS, dataSend, mapperService, viewModelStoreService, translate, Item, [
Topic,
Assignment,
Motion,
MotionBlock
]);
}
public getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Items' : 'Item');
};
protected setupDependencyObservation(): void {
this.DS.secondaryModelChangeSubject.subscribe(model => {
const viewModel = this.viewModelStoreService.get(model.collectionString, model.id);
const somethingChanged = this.getViewModelList().some(ownViewModel => {
return ownViewModel.updateDependencies(viewModel);
});
if (somethingChanged) {
this.updateAllObservables(model.id);
}
});
}
/**
* Creates the viewItem out of a given item
*

View File

@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { DataStoreService } from 'app/core/core-services/data-store.service';
import { DataSendService } from 'app/core/core-services/data-send.service';
import { Item } from 'app/shared/models/agenda/item';

View File

@ -5,7 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Assignment } from 'app/shared/models/assignments/assignment';
import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user';
import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { HttpService } from 'app/core/core-services/http.service';

View File

@ -2,7 +2,7 @@ import { TranslateService } from '@ngx-translate/core';
import { BaseViewModel } from '../../site/base/base-view-model';
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
import { CollectionStringMapperService } from '../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
import { DataSendService } from '../core-services/data-send.service';
import { DataStoreService } from '../core-services/data-store.service';
import { ViewModelStoreService } from '../core-services/view-model-store.service';

View File

@ -3,7 +3,7 @@ import { TranslateService } from '@ngx-translate/core';
import { BaseViewModel } from '../../site/base/base-view-model';
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
import { CollectionStringMapperService } from '../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
import { DataSendService } from '../core-services/data-send.service';
import { DataStoreService } from '../core-services/data-store.service';
import { Identifiable } from '../../shared/models/base/identifiable';
@ -11,6 +11,7 @@ import { auditTime } from 'rxjs/operators';
import { ViewModelStoreService } from '../core-services/view-model-store.service';
import { OnAfterAppsLoaded } from '../onAfterAppsLoaded';
import { Collection } from 'app/shared/models/base/collection';
import { CollectionIds } from '../core-services/data-store-update-manager.service';
export abstract class BaseRepository<V extends BaseViewModel, M extends BaseModel>
implements OnAfterAppsLoaded, Collection {
@ -107,6 +108,10 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
this.updateViewModelListObservable();
});
this.loadInitialFromDS();
}
protected loadInitialFromDS(): void {
// Populate the local viewModelStore with ViewModel Objects.
this.DS.getAll(this.baseModelCtor).forEach((model: M) => {
this.viewModelStore[model.id] = this.createViewModel(model);
@ -117,60 +122,71 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
this.DS.getAll(this.baseModelCtor).forEach((model: M) => {
this.updateViewModelObservable(model.id);
});
// Could be raise in error if the root injector is not known
this.DS.primaryModelChangeSubject.subscribe(model => {
if (model instanceof this.baseModelCtor) {
// Add new and updated motions to the viewModelStore
this.viewModelStore[model.id] = this.createViewModel(model as M);
this.updateAllObservables(model.id);
}
});
this.setupDependencyObservation();
// Watch the Observables for deleting
// TODO: What happens, if some related object was deleted?
// My quess: This must trigger an autoupdate also for this model, because some IDs changed, so the
// affected models will be newly created by the primaryModelChangeSubject.
this.DS.deletedObservable.subscribe(model => {
if (model.collection === this.collectionStringMapperService.getCollectionString(this.baseModelCtor)) {
delete this.viewModelStore[model.id];
this.updateAllObservables(model.id);
}
});
}
/**
* Sets up the observation of dependency subjects.
*/
protected setupDependencyObservation(): void {
if (this.depsModelCtors) {
this.DS.secondaryModelChangeSubject.subscribe(model => {
const dependencyChanged: boolean = this.depsModelCtors.some(ctor => {
return model instanceof ctor;
});
if (dependencyChanged) {
const viewModel = this.viewModelStoreService.get(model.collectionString, model.id);
this.updateDependency(viewModel);
}
});
}
}
/**
* Updates all models with the provided `update` which is a dependency.
* Deletes all models from the repository (internally, no requests). Informs all subjects.
*
* @param update The dependency to update.
* @param ids All model ids
*/
protected updateDependency(update: BaseViewModel): void {
// if an domain object we need was added or changed, update viewModelStore
this.getViewModelList().forEach(ownViewModel => {
ownViewModel.updateDependencies(update);
public deleteModels(ids: number[]): void {
ids.forEach(id => {
delete this.viewModelStore[id];
this.updateViewModelObservable(id);
});
this.updateViewModelListObservable();
}
/**
* Updates or creates all given models in the repository (internally, no requests).
* Informs all subjects.
*
* @param ids All model ids.
*/
public changedModels(ids: number[]): void {
ids.forEach(id => {
this.viewModelStore[id] = this.createViewModel(this.DS.get(this.collectionString, id));
this.updateViewModelObservable(id);
});
this.updateViewModelListObservable();
}
/**
* Updates all models in this repository with all changed models.
*
* @param changedModels A mapping of collections to ids of all changed models.
*/
public updateDependencies(changedModels: CollectionIds): void {
if (!this.depsModelCtors || this.depsModelCtors.length === 0) {
return;
}
// Get all viewModels from this repo once.
const viewModels = this.getViewModelList();
let somethingUpdated = false;
Object.keys(changedModels).forEach(collection => {
const dependencyChanged: boolean = this.depsModelCtors.some(ctor => {
return ctor.COLLECTIONSTRING === collection;
});
if (collection === this.collectionString || !dependencyChanged) {
return;
}
// Ok, we are affected by this collection. Update all viewModels from this repo.
viewModels.forEach(ownViewModel => {
changedModels[collection].forEach(id => {
ownViewModel.updateDependencies(this.viewModelStoreService.get(collection, id));
});
});
somethingUpdated = true;
});
if (somethingUpdated) {
viewModels.forEach(ownViewModel => {
this.updateViewModelObservable(ownViewModel.id);
});
this.updateViewModelListObservable();
}
}
/**
* Saves the (full) update to an existing model. So called "update"-function

View File

@ -10,7 +10,7 @@ import { DataStoreService } from 'app/core/core-services/data-store.service';
import { ConstantsService } from 'app/core/core-services/constants.service';
import { HttpService } from 'app/core/core-services/http.service';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewConfig } from 'app/site/config/models/view-config';
@ -88,6 +88,16 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config>
*/
protected configListSubject: BehaviorSubject<ConfigGroup[]> = new BehaviorSubject<ConfigGroup[]>(null);
/**
* Saves, if we got config variables (the structure) from the server.
*/
protected gotConfigsVariables = false;
/**
* Saves, if we got first configs via autoupdate or cache.
*/
protected gotFirstUpdate = false;
/**
* Constructor for ConfigRepositoryService. Requests the constants from the server and creates the config group structure.
*
@ -109,7 +119,9 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config>
this.constantsService.get('ConfigVariables').subscribe(constant => {
this.createConfigStructure(constant);
this.updateConfigStructure(true, ...Object.values(this.viewModelStore));
this.updateConfigStructure(false, ...Object.values(this.viewModelStore));
this.gotConfigsVariables = true;
this.checkConfigStructure();
this.updateConfigListObservable();
});
}
@ -146,30 +158,23 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config>
throw new Error('Config variables cannot be created');
}
/**
* Overwritten setup. Does only care about the custom list observable and inserts changed configs into the
* config group structure.
*/
public onAfterAppsLoaded(): void {
if (!this.configListSubject) {
this.configListSubject = new BehaviorSubject<ConfigGroup[]>(null);
}
protected loadInitialFromDS(): void {
this.DS.getAll(Config).forEach((config: Config) => {
this.viewModelStore[config.id] = this.createViewModel(config);
this.updateConfigStructure(false, this.viewModelStore[config.id]);
});
this.updateConfigListObservable();
// Could be raise in error if the root injector is not known
// TODO go over repo
this.DS.changeObservable.subscribe(model => {
if (model instanceof Config) {
this.viewModelStore[model.id] = this.createViewModel(model as Config);
this.updateConfigStructure(false, this.viewModelStore[model.id]);
this.updateConfigListObservable();
}
public changedModels(ids: number[]): void {
super.changedModels(ids);
ids.forEach(id => {
this.updateConfigStructure(false, this.viewModelStore[id]);
});
this.gotFirstUpdate = true;
this.checkConfigStructure();
this.updateConfigListObservable();
}
/**
@ -195,6 +200,16 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config>
return this.configs;
}
/**
* Checks the config structure, if we got configs (first data) and the
* structure (config variables)
*/
protected checkConfigStructure(): void {
if (this.gotConfigsVariables && this.gotFirstUpdate) {
this.updateConfigStructure(true, ...Object.values(this.viewModelStore));
}
}
/**
* With a given (and maybe partially filled) config structure, all given view configs are put into it.
* @param check Whether to check, if all given configs are there (according to the config structure).

View File

@ -1,13 +1,12 @@
import { Injectable } from '@angular/core';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { DataStoreService } from 'app/core/core-services/data-store.service';
import { BaseRepository } from 'app/core/repositories/base-repository';
import { History } from 'app/shared/models/core/history';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { User } from 'app/shared/models/users/user';
import { HttpService } from 'app/core/core-services/http.service';
import { ViewHistory } from 'app/site/history/models/view-history';
import { ViewHistory, ProxyHistory } from 'app/site/history/models/view-history';
import { TimeTravelService } from 'app/core/core-services/time-travel.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewUser } from 'app/site/users/models/view-user';
@ -40,7 +39,7 @@ export class HistoryRepositoryService extends BaseRepository<ViewHistory, Histor
private httpService: HttpService,
private timeTravel: TimeTravelService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, History, [User]);
super(DS, dataSend, mapperService, viewModelStoreService, translate, History);
}
public getVerboseName = (plural: boolean = false) => {
@ -54,12 +53,29 @@ export class HistoryRepositoryService extends BaseRepository<ViewHistory, Histor
* @return a new ViewHistory object
*/
public createViewModel(history: History): ViewHistory {
const user = this.viewModelStoreService.get(ViewUser, history.user_id);
const viewHistory = new ViewHistory(history, user);
const viewHistory = new ViewHistory(this.createProxyHistory(history));
viewHistory.getVerboseName = this.getVerboseName;
return viewHistory;
}
/**
* Creates a ProxyHistory from a History by wrapping it and give access to the user.
*
* @param history The History object
* @returns the ProxyHistory
*/
private createProxyHistory(history: History): ProxyHistory {
return new Proxy(history, {
get: (instance, property) => {
if (property === 'user') {
return this.viewModelStoreService.get(ViewUser, instance.user_id);
} else {
return instance[property];
}
}
});
}
/**
* Overwrites the default procedure
*

View File

@ -6,7 +6,7 @@ import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { User } from 'app/shared/models/users/user';
import { DataStoreService } from '../../core-services/data-store.service';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from 'app/core/core-services/data-send.service';
import { HttpService } from 'app/core/core-services/http.service';
import { HttpHeaders } from '@angular/common/http';

View File

@ -4,7 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
import { BaseRepository } from '../base-repository';
import { Category } from 'app/shared/models/motions/category';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';

View File

@ -13,7 +13,7 @@ import { DataStoreService } from 'app/core/core-services/data-store.service';
import { MotionChangeRecommendation } from 'app/shared/models/motions/motion-change-reco';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-change-recommendation';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
/**

View File

@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from 'app/core/core-services/data-store.service';
import { HttpService } from 'app/core/core-services/http.service';

View File

@ -6,7 +6,7 @@ import { BaseRepository } from '../base-repository';
import { ViewMotionCommentSection } from 'app/site/motions/models/view-motion-comment-section';
import { MotionCommentSection } from 'app/shared/models/motions/motion-comment-section';
import { Group } from 'app/shared/models/users/group';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { HttpService } from 'app/core/core-services/http.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewGroup } from 'app/site/users/models/view-group';

View File

@ -7,7 +7,7 @@ import { tap, map } from 'rxjs/operators';
import { Category } from 'app/shared/models/motions/category';
import { ChangeRecoMode, ViewMotion } from 'app/site/motions/models/view-motion';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { DataSendService } from '../../core-services/data-send.service';
@ -39,10 +39,10 @@ import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { PersonalNote, PersonalNoteContent } from 'app/shared/models/users/personal-note';
import { ViewPersonalNote } from 'app/site/users/models/view-personal-note';
import { OperatorService } from 'app/core/core-services/operator.service';
import { CollectionIds } from 'app/core/core-services/data-store-update-manager.service';
type SortProperty = 'callListWeight' | 'identifier';
@ -261,30 +261,48 @@ export class MotionRepositoryService extends BaseAgendaContentObjectRepository<V
}
/**
*
* @param update
*
* @overwrite
* Special handling of updating personal notes.
* @override
*/
protected updateDependency(update: BaseViewModel): void {
if (update instanceof ViewPersonalNote) {
if (this.operator.isAnonymous || update.userId !== this.operator.user.id) {
public updateDependencies(changedModels: CollectionIds): void {
if (!this.depsModelCtors || this.depsModelCtors.length === 0) {
return;
}
const notes = update.notes;
const collection = Motion.COLLECTIONSTRING;
this.getViewModelList().forEach(ownViewModel => {
if (notes && notes[collection] && notes[collection][ownViewModel.id]) {
ownViewModel.personalNote = notes[collection][ownViewModel.id];
} else {
ownViewModel.personalNote = null;
const viewModels = this.getViewModelList();
let somethingUpdated = false;
Object.keys(changedModels).forEach(collection => {
const dependencyChanged: boolean = this.depsModelCtors.some(ctor => {
return ctor.COLLECTIONSTRING === collection;
});
if (collection === this.collectionString || !dependencyChanged) {
return;
}
// Do not update personal notes, if the operator is anonymous
if (collection === PersonalNote.COLLECTIONSTRING && this.operator.isAnonymous) {
return;
}
viewModels.forEach(ownViewModel => {
changedModels[collection].forEach(id => {
const viewModel = this.viewModelStoreService.get(collection, id);
// Only update the personal note, if the operator is the right user.
if (
collection === PersonalNote.COLLECTIONSTRING &&
(<ViewPersonalNote>viewModel).userId !== this.operator.user.id
) {
return;
}
ownViewModel.updateDependencies(viewModel);
});
});
somethingUpdated = true;
});
if (somethingUpdated) {
viewModels.forEach(ownViewModel => {
this.updateViewModelObservable(ownViewModel.id);
});
this.updateViewModelListObservable();
} else {
super.updateDependency(update);
}
}

View File

@ -5,7 +5,7 @@ import { DataStoreService } from '../../core-services/data-store.service';
import { BaseRepository } from '../base-repository';
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
import { StatuteParagraph } from 'app/shared/models/motions/statute-paragraph';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { TranslateService } from '@ngx-translate/core';

View File

@ -5,7 +5,7 @@ import { ViewWorkflow } from 'app/site/motions/models/view-workflow';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { HttpService } from 'app/core/core-services/http.service';

View File

@ -5,7 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ViewCountdown } from 'app/site/projector/models/view-countdown';
import { Countdown } from 'app/shared/models/core/countdown';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';

View File

@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { Identifiable } from 'app/shared/models/base/identifiable';

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { DataStoreService } from '../../core-services/data-store.service';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ProjectorMessage } from 'app/shared/models/core/projector-message';
import { ViewProjectorMessage } from 'app/site/projector/models/view-projector-message';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { Identifiable } from 'app/shared/models/base/identifiable';

View File

@ -5,7 +5,7 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { TranslateService } from '@ngx-translate/core';

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ConstantsService } from '../../core-services/constants.service';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';

View File

@ -5,7 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { PersonalNote } from 'app/shared/models/users/personal-note';
import { ViewPersonalNote } from 'app/site/users/models/view-personal-note';

View File

@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';

View File

@ -33,9 +33,9 @@ export class ConfigService {
* Listen for changes of config variables.
*/
public constructor(private DS: DataStoreService) {
this.DS.changeObservable.subscribe(data => {
this.DS.getChangeObservable(Config).subscribe(data => {
// on changes notify the observers for specific keys.
if (data instanceof Config && this.configSubjects[data.key]) {
if (this.configSubjects[data.key]) {
this.configSubjects[data.key].next(data.value);
}
});

View File

@ -24,10 +24,8 @@ export class PersonalNoteService {
*/
public constructor(private operator: OperatorService, private DS: DataStoreService, private http: HttpService) {
operator.getUserObservable().subscribe(() => this.updatePersonalNoteObject());
this.DS.changeObservable.subscribe(model => {
if (model instanceof PersonalNote) {
this.DS.getChangeObservable(PersonalNote).subscribe(_ => {
this.updatePersonalNoteObject();
}
});
}

View File

@ -21,7 +21,7 @@ import { UserRepositoryService } from 'app/core/repositories/users/user-reposito
import { DurationService } from 'app/core/ui-services/duration.service';
import { CurrentAgendaItemService } from 'app/site/projector/services/current-agenda-item.service';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { CurrentListOfSpeakersSlideService } from 'app/site/projector/services/current-list-of-of-speakers-slide.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';

View File

@ -80,7 +80,7 @@ export class SearchComponent extends BaseViewComponent implements OnInit {
this.registeredModels = this.searchService.getRegisteredModels().map(rm => ({ ...rm, enabled: true }));
this.DS.changedOrDeletedObservable.pipe(auditTime(100)).subscribe(() => this.search());
this.DS.modifiedObservable.pipe(auditTime(100)).subscribe(() => this.search());
this.quickSearchSubject.pipe(debounceTime(250)).subscribe(query => this.search(query));
}

View File

@ -49,7 +49,7 @@
<!-- User -->
<ng-container matColumnDef="user">
<mat-header-cell *matHeaderCellDef translate>Changed by</mat-header-cell>
<mat-cell *matCellDef="let history">{{ history.user }}</mat-cell>
<mat-cell *matCellDef="let history">{{ history.user_full_name }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getRowDef()"></mat-header-row>

View File

@ -2,6 +2,8 @@ import { BaseViewModel } from 'app/site/base/base-view-model';
import { History } from 'app/shared/models/core/history';
import { ViewUser } from 'app/site/users/models/view-user';
export type ProxyHistory = History & { user?: ViewUser };
/**
* View model for history objects
*/
@ -11,38 +13,39 @@ export class ViewHistory extends BaseViewModel {
/**
* Private BaseModel of the history
*/
private _history: History;
/**
* Real representation of the user who altered the history.
* Determined from `History.user_id`
*/
private _user: ViewUser | null;
private _history: ProxyHistory;
/**
* Read the history property
*/
public get history(): History {
public get history(): ProxyHistory {
return this._history;
}
/**
* Read the user property
* Gets the users ViewUser.
*/
public get user(): ViewUser {
return this._user ? this._user : null;
public get user(): ViewUser | null {
return this.history.user;
}
/**
* Get the ID of the history object
* Get the id of the history object
* Required by BaseViewModel
*
* @returns the ID as number
* @returns the id as number
*/
public get id(): number {
return this.history.id;
}
/**
* @returns the users full name
*/
public get user_full_name(): string {
return this.history.user ? this.history.user.full_name : '';
}
/**
* Get the elementIDs of the history object
*
@ -81,10 +84,9 @@ export class ViewHistory extends BaseViewModel {
* @param history the real history BaseModel
* @param user the real user BaseModel
*/
public constructor(history: History, user?: ViewUser) {
public constructor(history: ProxyHistory) {
super(History.COLLECTIONSTRING);
this._history = history;
this._user = user;
}
/**
@ -117,14 +119,5 @@ export class ViewHistory extends BaseViewModel {
return this.history;
}
/**
* Updates the history object with new values
*
* @param update potentially the new values for history or it's components.
*/
public updateDependencies(update: BaseViewModel): void {
if (update instanceof ViewUser && this.history.user_id === update.id) {
this._user = update;
}
}
public updateDependencies(update: BaseViewModel): void {}
}

View File

@ -2,7 +2,7 @@ import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { CommonListOfSpeakersSlideData } from './common-list-of-speakers-slide-data';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { isBaseAgendaContentObjectRepository } from 'app/core/repositories/base-agenda-content-object-repository';
@Component({

View File

@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { ItemListSlideData, SlideItem } from './item-list-slide-data';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { isBaseAgendaContentObjectRepository } from 'app/core/repositories/base-agenda-content-object-repository';
@Component({