Merge pull request #3895 from MaximilianKrambach/os3/settings

implement settings
This commit is contained in:
Finn Stutzenstein 2018-10-09 13:13:33 +02:00 committed by GitHub
commit 81aa763ae9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 880 additions and 268 deletions

View File

@ -6,7 +6,7 @@ import { AppConfig } from '../../site/base/app-config';
import { CollectionStringModelMapperService } from './collectionStringModelMapper.service'; import { CollectionStringModelMapperService } from './collectionStringModelMapper.service';
import { MediafileAppConfig } from '../../site/mediafiles/mediafile.config'; import { MediafileAppConfig } from '../../site/mediafiles/mediafile.config';
import { MotionsAppConfig } from '../../site/motions/motions.config'; import { MotionsAppConfig } from '../../site/motions/motions.config';
import { SettingsAppConfig } from '../../site/settings/settings.config'; import { ConfigAppConfig } from '../../site/config/config.config';
import { AgendaAppConfig } from '../../site/agenda/agenda.config'; import { AgendaAppConfig } from '../../site/agenda/agenda.config';
import { AssignmentsAppConfig } from '../../site/assignments/assignments.config'; import { AssignmentsAppConfig } from '../../site/assignments/assignments.config';
import { UsersAppConfig } from '../../site/users/users.config'; import { UsersAppConfig } from '../../site/users/users.config';
@ -17,7 +17,7 @@ import { MainMenuService } from './main-menu.service';
*/ */
const appConfigs: AppConfig[] = [ const appConfigs: AppConfig[] = [
CommonAppConfig, CommonAppConfig,
SettingsAppConfig, ConfigAppConfig,
AgendaAppConfig, AgendaAppConfig,
AssignmentsAppConfig, AssignmentsAppConfig,
MotionsAppConfig, MotionsAppConfig,

View File

@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { NativeDateAdapter } from '@angular/material';
/**
* A custom DateAdapter for the datetimepicker in the config. This is still not fully working and needs to be done later.
* See comments in PR #3895.
*/
@Injectable()
export class OpenSlidesDateAdapter extends NativeDateAdapter {
public format(date: Date, displayFormat: Object): string {
if (displayFormat === 'input') {
return this.toFullIso8601(date);
} else {
return date.toDateString();
}
}
private to2digit(n: number): string {
return ('00' + n).slice(-2);
}
public toFullIso8601(date: Date): string {
return (
[date.getUTCFullYear(), this.to2digit(date.getUTCMonth() + 1), this.to2digit(date.getUTCDate())].join('-') +
'T' +
[this.to2digit(date.getUTCHours()), this.to2digit(date.getUTCMinutes())].join(':')
);
}
}

View File

@ -0,0 +1,22 @@
import { ErrorStateMatcher } from '@angular/material';
import { FormControl, FormGroupDirective, NgForm } from '@angular/forms';
/**
* Custom state matcher for mat-errors. Enables the error for an input, if one has set the error
* with `setError` on the parent element.
*/
export class ParentErrorStateMatcher implements ErrorStateMatcher {
public isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = !!(form && form.submitted);
const controlTouched = !!(control && (control.dirty || control.touched));
const controlInvalid = !!(control && control.invalid);
const parentInvalid = !!(
control &&
control.parent &&
control.parent.invalid &&
(control.parent.dirty || control.parent.touched)
);
return (isSubmitted || controlTouched) && (controlInvalid || parentInvalid);
}
}

View File

@ -16,7 +16,10 @@ import {
MatTableModule, MatTableModule,
MatPaginatorModule, MatPaginatorModule,
MatSortModule, MatSortModule,
MatTooltipModule MatTooltipModule,
MatDatepickerModule,
MatNativeDateModule,
DateAdapter
} from '@angular/material'; } from '@angular/material';
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material'; import { MatChipsModule } from '@angular/material';
@ -46,6 +49,7 @@ import { FooterComponent } from './components/footer/footer.component';
import { LegalNoticeContentComponent } from './components/legal-notice-content/legal-notice-content.component'; import { LegalNoticeContentComponent } from './components/legal-notice-content/legal-notice-content.component';
import { PrivacyPolicyContentComponent } from './components/privacy-policy-content/privacy-policy-content.component'; import { PrivacyPolicyContentComponent } from './components/privacy-policy-content/privacy-policy-content.component';
import { SearchValueSelectorComponent } from './components/search-value-selector/search-value-selector.component'; import { SearchValueSelectorComponent } from './components/search-value-selector/search-value-selector.component';
import { OpenSlidesDateAdapter } from './date-adapter';
library.add(fas); library.add(fas);
@ -69,6 +73,8 @@ library.add(fas);
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,
MatToolbarModule, MatToolbarModule,
MatDatepickerModule,
MatNativeDateModule,
MatCardModule, MatCardModule,
MatInputModule, MatInputModule,
MatTableModule, MatTableModule,
@ -98,6 +104,7 @@ library.add(fas);
MatCheckboxModule, MatCheckboxModule,
MatToolbarModule, MatToolbarModule,
MatCardModule, MatCardModule,
MatDatepickerModule,
MatInputModule, MatInputModule,
MatTableModule, MatTableModule,
MatSortModule, MatSortModule,
@ -130,6 +137,7 @@ library.add(fas);
LegalNoticeContentComponent, LegalNoticeContentComponent,
PrivacyPolicyContentComponent, PrivacyPolicyContentComponent,
SearchValueSelectorComponent SearchValueSelectorComponent
] ],
providers: [{ provide: DateAdapter, useClass: OpenSlidesDateAdapter }]
}) })
export class SharedModule {} export class SharedModule {}

View File

@ -33,9 +33,12 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
protected depsModelCtors?: ModelConstructor<BaseModel>[] protected depsModelCtors?: ModelConstructor<BaseModel>[]
) { ) {
super(); super();
this.setup();
}
protected setup(): void {
// Populate the local viewModelStore with ViewModel Objects. // Populate the local viewModelStore with ViewModel Objects.
this.DS.getAll(baseModelCtor).forEach((model: M) => { this.DS.getAll(this.baseModelCtor).forEach((model: M) => {
this.viewModelStore[model.id] = this.createViewModel(model); this.viewModelStore[model.id] = this.createViewModel(model);
this.updateViewModelObservable(model.id); this.updateViewModelObservable(model.id);
}); });
@ -63,7 +66,7 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
// Watch the Observables for deleting // Watch the Observables for deleting
this.DS.deletedObservable.subscribe(model => { this.DS.deletedObservable.subscribe(model => {
if (model.collection === CollectionStringModelMapperService.getCollectionString(baseModelCtor)) { if (model.collection === CollectionStringModelMapperService.getCollectionString(this.baseModelCtor)) {
delete this.viewModelStore[model.id]; delete this.viewModelStore[model.id];
this.updateAllObservables(model.id); this.updateAllObservables(model.id);
} }

View File

@ -0,0 +1,66 @@
<form class="config-form-group" [formGroup]="form" *ngIf="configItem.inputType !== 'boolean'">
<mat-form-field>
<!-- Decides which input-type to take (i.e) date, select, textarea, input) -->
<ng-container [ngSwitch]="configItem.inputType">
<ng-container *ngSwitchCase="'datetimepicker'">
<ng-container *ngTemplateOutlet="date"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'choice'">
<ng-container *ngTemplateOutlet="select"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'markupText'">
<ng-container *ngTemplateOutlet="textarea"></ng-container>
</ng-container>
<ng-container *ngSwitchDefault>
<ng-container *ngTemplateOutlet="input"></ng-container>
</ng-container>
</ng-container>
<!-- required for all kinds of input -->
<mat-label translate>{{ configItem.label }}</mat-label>
<mat-hint *ngIf="configItem.helpText" translate>{{ configItem.helpText }}</mat-hint>
<span matSuffix>
<fa-icon pull="right" class="text-success" *ngIf="updateSuccessIcon" icon='check-circle'></fa-icon>
</span>
<mat-error *ngIf="error">
{{ error }}
</mat-error>
<!-- templates for exchangeable inputs. Add more here if necessary -->
<ng-template #date ngProjectAs="[matInput]">
<input matInput formControlName="value" [matDatepicker]="picker" [errorStateMatcher]="matcher">
<mat-datepicker-toggle matPrefix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</ng-template>
<ng-template #select ngProjectAs="mat-select">
<mat-select formControlName="value" [errorStateMatcher]="matcher">
<mat-option *ngFor="let choice of configItem.choices" [value]="choice.value">
{{ choice.display_name | translate }}
</mat-option>
</mat-select>
</ng-template>
<ng-template #textarea ngProjectAs="[matInput]">
<textarea matInput formControlName="value" [value]="configItem.value" [errorStateMatcher]="matcher"></textarea>
</ng-template>
<ng-template #input ngProjectAs="[matInput]">
<input matInput formControlName="value" [value]="configItem.value" [errorStateMatcher]="matcher"
[type]="formType(configItem.inputType)">
</ng-template>
</mat-form-field>
</form>
<div class="config-form-group" *ngIf="configItem.inputType === 'boolean'">
<mat-checkbox [checked]="configItem.value" (change)="onChange($event.checked)">
{{ configItem.label | translate }}
</mat-checkbox>
<div class="hint" *ngIf="configItem.helpText && !error">
{{ configItem.helpText | translate }}
</div>
<div class="error" *ngIf="error">
{{ error }}
</div>
</div>

View File

@ -0,0 +1,26 @@
/* Full width form fields */
.mat-form-field {
width: 100%;
}
/* limit the color bar of color inputs */
input[type='color'] {
max-width: 100px;
}
/* Spacing between config entries */
.config-form-group {
margin-bottom: 15px;
}
/* Custom hint and error classes for the checkbox. Same values as .mat-hint and -mat-error */
.hint,
.error {
font-size: 75%;
}
.hint {
color: rgba(0, 0, 0, 0.54);
}
.error {
color: #f44336;
}

View File

@ -0,0 +1,180 @@
import { Component, OnInit, Input, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { ViewConfig } from '../../models/view-config';
import { BaseComponent } from '../../../../base.component';
import { FormGroup, FormBuilder } from '@angular/forms';
import { ConfigRepositoryService } from '../../services/config-repository.service';
import { tap } from 'rxjs/operators';
import { ParentErrorStateMatcher } from '../../../../shared/parent-error-state-matcher';
/**
* List view for the categories.
*
* TODO: Creation of new Categories
*/
@Component({
selector: 'os-config-field',
templateUrl: './config-field.component.html',
styleUrls: ['./config-field.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ConfigFieldComponent extends BaseComponent implements OnInit {
public configItem: ViewConfig;
/**
* Option to show a green check-icon.
*/
public updateSuccessIcon = false;
/**
* The timeout for the success icon to hide.
*/
private updateSuccessIconTimeout: number | null = null;
/**
* The debounce timeout for inputs request delay.
*/
private debounceTimeout: number | null = null;
/**
* A possible error send by the server.
*/
public error: string | null = null;
/**
* The config item for this component. Just accept components with already populated constants-info.
*/
@Input()
public set item(value: ViewConfig) {
if (value.hasConstantsInfo) {
this.configItem = value;
if (this.form) {
this.form.patchValue(
{
value: this.configItem.value
},
{ emitEvent: false }
);
}
}
}
/**
* The form for this configItem.
*/
public form: FormGroup;
/**
* The matcher for custom (request) errors.
*/
public matcher = new ParentErrorStateMatcher();
/**
* The usual component constructor
* @param titleService
* @param translate
*/
public constructor(
protected titleService: Title,
protected translate: TranslateService,
private formBuilder: FormBuilder,
private cdRef: ChangeDetectorRef,
public repo: ConfigRepositoryService
) {
super(titleService, translate);
}
/**
* Sets up the form for this config field.
*/
public ngOnInit(): void {
this.form = this.formBuilder.group({
value: ['']
});
this.form.patchValue({
value: this.configItem.value
});
this.form.valueChanges.subscribe(form => {
this.onChange(form.value);
});
}
/**
* Trigger an update of the data
*/
private onChange(value: any): void {
if (this.debounceTimeout !== null) {
clearTimeout(<any>this.debounceTimeout);
}
this.debounceTimeout = <any>setTimeout(() => {
this.update(value);
}, this.configItem.getDebouncingTimeout());
this.cdRef.detectChanges();
}
/**
* Updates the this config field.
*/
private update(value: any): void {
// TODO: Fix the Datetimepicker parser and formatter.
if (this.configItem.inputType === 'datetimepicker') {
value = Date.parse(value);
}
this.debounceTimeout = null;
this.repo
.update({ value: value }, this.configItem)
.pipe(
tap(
response => {
this.error = null;
this.showSuccessIcon();
},
error => {
this.setError(error.error.detail);
}
)
)
.subscribe();
}
/**
* Show the green success icon on the component. The icon gets automatically cleared.
*/
private showSuccessIcon(): void {
if (this.updateSuccessIconTimeout !== null) {
clearTimeout(<any>this.updateSuccessIconTimeout);
}
this.updateSuccessIconTimeout = <any>setTimeout(() => {
this.updateSuccessIcon = false;
this.cdRef.detectChanges();
}, 2000);
this.updateSuccessIcon = true;
this.cdRef.detectChanges();
}
/**
* Sets the error on this field.
*/
private setError(error: string): void {
this.error = error;
this.form.setErrors({ error: true });
this.cdRef.detectChanges();
}
/**
* Uses the configItem to determine the kind of interation:
* input, textarea, choice or date
*/
public formType(type: string): string {
switch (type) {
case 'integer':
return 'number';
case 'colorpicker':
return 'color';
default:
return 'text';
}
}
}

View File

@ -0,0 +1,26 @@
<os-head-bar appName="Settings"></os-head-bar>
<mat-accordion>
<ng-container *ngFor="let group of this.configs">
<mat-expansion-panel displayMode="flat">
<mat-expansion-panel-header>
<mat-panel-title>
{{ group.name | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngFor="let subgroup of group.subgroups">
<mat-card>
<mat-card-title>{{ subgroup.name | translate }}</mat-card-title>
<mat-card-content>
<div *ngFor="let item of subgroup.items">
<os-config-field [item]="item.config"></os-config-field>
</div>
</mat-card-content>
</mat-card>
</div>
<div *ngFor="let item of group.items">
<os-config-field [item]="item.config"></os-config-field>
</div>
</mat-expansion-panel>
</ng-container>
</mat-accordion>

View File

@ -0,0 +1,3 @@
mat-card {
margin-bottom: 10px;
}

View File

@ -0,0 +1,27 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from '../../../../../e2e-imports.module';
import { ConfigListComponent } from './config-list.component';
import { ConfigFieldComponent } from '../config-field/config-field.component';
describe('ConfigListComponent', () => {
let component: ConfigListComponent;
let fixture: ComponentFixture<ConfigListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [ConfigListComponent, ConfigFieldComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ConfigListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,36 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { ConfigRepositoryService, ConfigGroup } from '../../services/config-repository.service';
import { BaseComponent } from '../../../../base.component';
/**
* List view for the global settings
*/
@Component({
selector: 'os-config-list',
templateUrl: './config-list.component.html',
styleUrls: ['./config-list.component.scss']
})
export class ConfigListComponent extends BaseComponent implements OnInit {
public configs: ConfigGroup[];
public constructor(
protected titleService: Title,
protected translate: TranslateService,
private repo: ConfigRepositoryService
) {
super(titleService, translate);
}
/**
* Sets the title, inits the table and calls the repo
*/
public ngOnInit(): void {
super.setTitle('Settings');
this.repo.getConfigListObservable().subscribe(configs => {
this.configs = configs;
});
}
}

View File

@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ConfigListComponent } from './components/config-list/config-list.component';
const routes: Routes = [{ path: '', component: ConfigListComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ConfigRoutingModule {}

View File

@ -1,7 +1,7 @@
import { AppConfig } from '../base/app-config'; import { AppConfig } from '../base/app-config';
import { Config } from '../../shared/models/core/config'; import { Config } from '../../shared/models/core/config';
export const SettingsAppConfig: AppConfig = { export const ConfigAppConfig: AppConfig = {
name: 'settings', name: 'settings',
models: [{ collectionString: 'core/config', model: Config }], models: [{ collectionString: 'core/config', model: Config }],
mainMenuEntries: [ mainMenuEntries: [

View File

@ -1,10 +1,10 @@
import { SettingsModule } from './settings.module'; import { ConfigModule } from './config.module';
describe('SettingsModule', () => { describe('SettingsModule', () => {
let settingsModule: SettingsModule; let settingsModule: ConfigModule;
beforeEach(() => { beforeEach(() => {
settingsModule = new SettingsModule(); settingsModule = new ConfigModule();
}); });
it('should create an instance', () => { it('should create an instance', () => {

View File

@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '../../shared/shared.module';
import { ConfigRoutingModule } from './config-routing.module';
import { ConfigListComponent } from './components/config-list/config-list.component';
import { ConfigFieldComponent } from './components/config-field/config-field.component';
@NgModule({
imports: [CommonModule, ConfigRoutingModule, SharedModule],
declarations: [ConfigListComponent, ConfigFieldComponent]
})
export class ConfigModule {}

View File

@ -0,0 +1,133 @@
import { BaseViewModel } from '../../base/base-view-model';
import { Config } from '../../../shared/models/core/config';
interface ConfigChoice {
value: string;
displayName: string;
}
/**
* All valid input types for config variables.
*/
type ConfigInputType =
| 'text'
| 'string'
| 'boolean'
| 'markupText'
| 'integer'
| 'majorityMethod'
| 'choice'
| 'datetimepicker'
| 'colorpicker'
| 'translations';
/**
* Represents all information that is given in the constant.
*/
interface ConfigConstant {
default_value?: string;
help_text?: string;
input_type: ConfigInputType;
key: string;
label: string;
choices?: ConfigChoice[];
}
/**
* The view model for configs.
*/
export class ViewConfig extends BaseViewModel {
/**
* The underlying config.
*/
private _config: Config;
/* This private members are set by setConstantsInfo. */
private _helpText: string;
private _inputType: ConfigInputType;
private _label: string;
private _choices: ConfigChoice[];
/**
* Saves, if this config already got constants information.
*/
private _hasConstantsInfo = false;
public get hasConstantsInfo(): boolean {
return this._hasConstantsInfo;
}
public get config(): Config {
return this._config;
}
public get id(): number {
return this.config ? this.config.id : null;
}
public get key(): string {
return this.config ? this.config.key : null;
}
public get value(): Object {
return this.config ? this.config.value : null;
}
public get label(): string {
return this._label;
}
public get inputType(): ConfigInputType {
return this._inputType;
}
public get helpText(): string {
return this._helpText;
}
public get choices(): Object {
return this._choices;
}
public constructor(config: Config) {
super();
this._config = config;
}
public getTitle(): string {
return this.label;
}
public updateValues(update: Config): void {
this._config = update;
}
/**
* Returns the time this config field needs to debounce before sending a request to the server.
* A little debounce time for all inputs is given here and is usefull, if inputs sends multiple onChange-events,
* like the type="color" input...
*/
public getDebouncingTimeout(): number {
if (this.inputType === 'string' || this.inputType === 'text' || this.inputType === 'markupText') {
return 1000;
} else {
return 100;
}
}
/**
* This should be called, if the constants are loaded, so all extra info can be updated.
* @param constant The constant info
*/
public setConstantsInfo(constant: ConfigConstant): void {
this._label = constant.label;
this._helpText = constant.help_text;
this._inputType = constant.input_type;
this._choices = constant.choices;
this._hasConstantsInfo = true;
}
public copy(): ViewConfig {
return new ViewConfig(this._config);
}
}

View File

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

View File

@ -0,0 +1,263 @@
import { Injectable } from '@angular/core';
import { BaseRepository } from '../../base/base-repository';
import { ViewConfig } from '../models/view-config';
import { Config } from '../../../shared/models/core/config';
import { Observable, BehaviorSubject } from 'rxjs';
import { DataStoreService } from '../../../core/services/data-store.service';
import { ConstantsService } from '../../../core/services/constants.service';
import { HttpClient } from '@angular/common/http';
/**
* Holds a single config item.
*/
interface ConfigItem {
/**
* The key of this config variable.
*/
key: string;
/**
* The actual view config for this variable.
*/
config: ViewConfig;
/**
* The config variable data given in the constants. This is hold here, so the view
* config can be updated with this data.
*/
data: any;
}
/**
* Represents a config subgroup. It can only holds items and no further groups.
*/
interface ConfigSubgroup {
/**
* The name.
*/
name: string;
/**
* All items in this sub group.
*/
items: ConfigItem[];
}
/**
* Represents a config group with its name, subgroups and direct items.
*/
export interface ConfigGroup {
/**
* The name.
*/
name: string;
/**
* A list of subgroups.
*/
subgroups: ConfigSubgroup[];
/**
* A list of config items that are not in any subgroup.
*/
items: ConfigItem[];
}
/**
* Repository for Configs. It overrides some functions of the BaseRepository. So do not use the
* observables given by the base repository, but the {@method getConfigListObservable}.
*/
@Injectable({
providedIn: 'root'
})
export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config> {
/**
* Own store for config groups.
*/
private configs: ConfigGroup[] = null;
/**
* Own subject for config groups.
*/
protected configListSubject: BehaviorSubject<ConfigGroup[]> = new BehaviorSubject<ConfigGroup[]>(null);
/**
* Constructor for ConfigRepositoryService. Requests the constants from the server and creates the config group structure.
*/
public constructor(DS: DataStoreService, private constantsService: ConstantsService, private http: HttpClient) {
super(DS, Config);
this.constantsService.get('OpenSlidesConfigVariables').subscribe(constant => {
this.createConfigStructure(constant);
this.updateConfigStructure(...Object.values(this.viewModelStore));
this.updateConfigListObservable();
});
}
/**
* Overwritten setup. Does only care about the custom list observable and inserts changed configs into the
* config group structure.
*/
protected setup(): void {
if (!this.configListSubject) {
this.configListSubject = new BehaviorSubject<ConfigGroup[]>(null);
}
this.DS.getAll(Config).forEach((config: Config) => {
this.viewModelStore[config.id] = this.createViewModel(config);
this.updateConfigStructure(this.viewModelStore[config.id]);
});
this.updateConfigListObservable();
// Could be raise in error if the root injector is not known
this.DS.changeObservable.subscribe(model => {
if (model instanceof Config) {
this.viewModelStore[model.id] = this.createViewModel(model as Config);
this.updateConfigStructure(this.viewModelStore[model.id]);
this.updateConfigListObservable();
}
});
}
/**
* Custom observer for the config
*/
public getConfigListObservable(): Observable<ConfigGroup[]> {
return this.configListSubject.asObservable();
}
/**
* Custom notification for the observers.
*/
protected updateConfigListObservable(): void {
if (this.configs) {
this.configListSubject.next(this.configs);
}
}
/**
* Getter for the config structure
*/
public getConfigStructure(): ConfigGroup[] {
return this.configs;
}
/**
* With a given (and maybe partially filled) config structure, all given view configs are put into it.
* @param viewConfigs All view configs to put into the structure
*/
protected updateConfigStructure(...viewConfigs: ViewConfig[]): void {
if (!this.configs) {
return;
}
// Map the viewConfigs to their keys.
const keyConfigMap: { [key: string]: ViewConfig } = {};
viewConfigs.forEach(viewConfig => {
keyConfigMap[viewConfig.key] = viewConfig;
});
// traverse through configs structure and replace all given viewConfigs
for (const group of this.configs) {
for (const subgroup of group.subgroups) {
for (const item of subgroup.items) {
if (keyConfigMap[item.key]) {
keyConfigMap[item.key].setConstantsInfo(item.data);
item.config = keyConfigMap[item.key];
}
}
}
for (const item of group.items) {
if (keyConfigMap[item.key]) {
keyConfigMap[item.key].setConstantsInfo(item.data);
item.config = keyConfigMap[item.key];
}
}
}
}
/**
* Saves a config value.
*/
public update(config: Partial<Config>, viewConfig: ViewConfig): Observable<Config> {
const updatedConfig = new Config();
updatedConfig.patchValues(viewConfig.config);
updatedConfig.patchValues(config);
// TODO: Use datasendService, if it can switch correctly between put, post and patch
return this.http.put<Config>(
'rest/' + updatedConfig.collectionString + '/' + updatedConfig.key + '/',
updatedConfig
);
}
/**
* This particular function should never be necessary since the creation of config
* values is not planed.
*
* Function exists solely to correctly implement {@link BaseRepository}
*/
public delete(config: ViewConfig): Observable<Config> {
throw new Error('Config variables cannot be deleted');
}
/**
* This particular function should never be necessary since the creation of config
* values is not planed.
*
* Function exists solely to correctly implement {@link BaseRepository}
*/
public create(config: Config): Observable<Config> {
throw new Error('Config variables cannot be created');
}
/**
* Creates a new ViewConfig of a given Config object
* @param config
*/
public createViewModel(config: Config): ViewConfig {
const vm = new ViewConfig(config);
return vm;
}
/**
* initially create the config structure from the given constant.
* @param constant
*/
private createConfigStructure(constant: any): void {
this.configs = [];
for (const group of constant) {
const _group: ConfigGroup = {
name: group.name,
subgroups: [],
items: []
};
// The server always sends subgroups. But if it has an empty name, there is no subgroup..
for (const subgroup of group.subgroups) {
if (subgroup.name) {
const _subgroup: ConfigSubgroup = {
name: subgroup.name,
items: []
};
for (const item of subgroup.items) {
_subgroup.items.push({
key: item.key,
config: null,
data: item
});
}
_group.subgroups.push(_subgroup);
} else {
for (const item of subgroup.items) {
_group.items.push({
key: item.key,
config: null,
data: item
});
}
}
}
this.configs.push(_group);
}
}
}

View File

@ -4,32 +4,14 @@ import { Router } from '@angular/router';
import { BaseComponent } from 'app/base.component'; import { BaseComponent } from 'app/base.component';
import { AuthService } from 'app/core/services/auth.service'; import { AuthService } from 'app/core/services/auth.service';
import { OperatorService } from 'app/core/services/operator.service'; import { OperatorService } from 'app/core/services/operator.service';
import { ErrorStateMatcher, MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material'; import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material';
import { FormControl, FormGroupDirective, NgForm, FormGroup, Validators, FormBuilder } from '@angular/forms'; import { FormGroup, Validators, FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { HttpErrorResponse, HttpClient } from '@angular/common/http'; import { HttpErrorResponse, HttpClient } from '@angular/common/http';
import { environment } from 'environments/environment'; import { environment } from 'environments/environment';
import { OpenSlidesService } from '../../../../core/services/openslides.service'; import { OpenSlidesService } from '../../../../core/services/openslides.service';
import { LoginDataService } from '../../../../core/services/login-data.service'; import { LoginDataService } from '../../../../core/services/login-data.service';
import { ParentErrorStateMatcher } from '../../../../shared/parent-error-state-matcher';
/**
* Custom error states. Might become part of the shared module later.
*/
export class ParentErrorStateMatcher implements ErrorStateMatcher {
public isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = !!(form && form.submitted);
const controlTouched = !!(control && (control.dirty || control.touched));
const controlInvalid = !!(control && control.invalid);
const parentInvalid = !!(
control &&
control.parent &&
control.parent.invalid &&
(control.parent.dirty || control.parent.touched)
);
return isSubmitted || (controlTouched && (controlInvalid || parentInvalid));
}
}
/** /**
* Login mask component. * Login mask component.

View File

@ -1,35 +0,0 @@
import { BaseViewModel } from '../../base/base-view-model';
import { Config } from '../../../shared/models/core/config';
export class ViewConfig extends BaseViewModel {
private _config: Config;
public get config(): Config {
return this._config;
}
public get id(): number {
return this.config ? this.config.id : null;
}
public get key(): string {
return this.config ? this.config.key : null;
}
public get value(): Object {
return this.config ? this.config.value : null;
}
public constructor(config: Config) {
super();
this._config = config;
}
public getTitle(): string {
return this.key;
}
public updateValues(update: Config): void {
this._config = update;
}
}

View File

@ -1,12 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ConfigRepositoryService } from './config-repository.service';
describe('SettingsRepositoryService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: ConfigRepositoryService = TestBed.get(ConfigRepositoryService);
expect(service).toBeTruthy();
});
});

View File

@ -1,61 +0,0 @@
import { Injectable } from '@angular/core';
import { BaseRepository } from '../../base/base-repository';
import { ViewConfig } from '../models/view-config';
import { Config } from '../../../shared/models/core/config';
import { Observable } from 'rxjs';
import { DataStoreService } from '../../../core/services/data-store.service';
/**
* Repository for Configs.
*
* Documentation provided over {@link BaseRepository}
*/
@Injectable({
providedIn: 'root'
})
export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config> {
/**
* Constructor for ConfigRepositoryService
*/
public constructor(DS: DataStoreService) {
super(DS, Config);
}
/**
* Saves a config value.
*
* TODO: used over not-yet-existing detail view
*/
public update(config: Partial<Config>, viewConfig: ViewConfig): Observable<Config> {
return null;
}
/**
* This particular function should never be necessary since the creation of config
* values is not planed.
*
* Function exists solely to correctly implement {@link BaseRepository}
*/
public delete(config: ViewConfig): Observable<Config> {
return null;
}
/**
* This particular function should never be necessary since the creation of config
* values is not planed.
*
* Function exists solely to correctly implement {@link BaseRepository}
*/
public create(config: Config): Observable<Config> {
return null;
}
/**
* Creates a new ViewConfig of a given Config object
* @param config
*/
public createViewModel(config: Config): ViewConfig {
return new ViewConfig(config);
}
}

View File

@ -1,19 +0,0 @@
<os-head-bar appName="Settings"></os-head-bar>
<mat-table class='os-listview-table on-transition-fade' [dataSource]="dataSource" matSort>
<!-- name column -->
<ng-container matColumnDef="key">
<mat-header-cell *matHeaderCellDef mat-sort-header> Key </mat-header-cell>
<mat-cell *matCellDef="let config"> {{config.key}} </mat-cell>
</ng-container>
<!-- prefix column -->
<ng-container matColumnDef="value">
<mat-header-cell *matHeaderCellDef mat-sort-header> Value </mat-header-cell>
<mat-cell *matCellDef="let config"> {{config.value}}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="['key', 'value']"></mat-header-row>
<mat-row (click)='selectConfig(row)' *matRowDef="let row; columns: ['key', 'value']"></mat-row>
</mat-table>

View File

@ -1,26 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsListComponent } from './settings-list.component';
import { E2EImportsModule } from '../../../../e2e-imports.module';
describe('SettingsListComponent', () => {
let component: SettingsListComponent;
let fixture: ComponentFixture<SettingsListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [SettingsListComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SettingsListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,61 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { ConstantsService } from '../../../core/services/constants.service';
import { ListViewBaseComponent } from '../../base/list-view-base';
import { ConfigRepositoryService } from '../services/config-repository.service';
import { ViewConfig } from '../models/view-config';
/**
* List view for the global settings
*
*/
@Component({
selector: 'os-settings-list',
templateUrl: './settings-list.component.html',
styleUrls: ['./settings-list.component.css']
})
export class SettingsListComponent extends ListViewBaseComponent<ViewConfig> implements OnInit {
/**
* The usual component constructor
* @param titleService
* @param translate
*/
public constructor(
protected titleService: Title,
protected translate: TranslateService,
private repo: ConfigRepositoryService,
private constantsService: ConstantsService,
) {
super(titleService, translate);
}
/**
* Init function.
*
* Sets the title, inits the table and calls the repo
*
* TODO: Needs the constants to be working
*/
public ngOnInit(): void {
super.setTitle('Settings');
this.initTable();
this.constantsService.get('OpenSlidesConfigVariables').subscribe(data => {
console.log(data);
});
this.repo.getViewModelListObservable().subscribe(newConfig => {
this.dataSource.data = newConfig;
});
}
/**
* Triggers when user selects a row
* @param row
*
* TODO: This prints the clicked item in the log.
* Needs the constants to be working
*/
public selectConfig(row: ViewConfig): void {
console.log('change a config: ', row.value);
}
}

View File

@ -1,11 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SettingsListComponent } from './settings-list/settings-list.component';
const routes: Routes = [{ path: '', component: SettingsListComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SettingsRoutingModule {}

View File

@ -1,11 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '../../shared/shared.module';
import { SettingsRoutingModule } from './settings-routing.module';
import { SettingsListComponent } from './settings-list/settings-list.component';
@NgModule({
imports: [CommonModule, SettingsRoutingModule, SharedModule],
declarations: [SettingsListComponent]
})
export class SettingsModule {}

View File

@ -37,7 +37,7 @@ const routes: Routes = [
}, },
{ {
path: 'settings', path: 'settings',
loadChildren: './settings/settings.module#SettingsModule' loadChildren: './config/config.module#ConfigModule'
}, },
{ {
path: 'users', path: 'users',

View File

@ -31,6 +31,10 @@ body {
background-color: rgb(77, 243, 86) !important; background-color: rgb(77, 243, 86) !important;
} }
.text-success {
color: rgb(77, 243, 86);
}
.red-warning-text { .red-warning-text {
color: red; color: red;
} }