implement settings
Whats still missing, but has not a high priority: - DateTimePicker. Entering dates through the popup works (but only with a hack in the config-field.component update() method). Displaying values from the server does not work. Also the localisation is missing. See attempts to fix it in the sheared module. - Errors, if the server cannot be reached. Should be solved in another PR fixing the datasendservice and make generic error messages. - Custom translations are missing
This commit is contained in:
parent
8a04951940
commit
5c1092b537
@ -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,
|
||||||
|
29
client/src/app/shared/date-adapter.ts
Normal file
29
client/src/app/shared/date-adapter.ts
Normal 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(':')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
22
client/src/app/shared/parent-error-state-matcher.ts
Normal file
22
client/src/app/shared/parent-error-state-matcher.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 {}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
@ -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;
|
||||||
|
}
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -0,0 +1,3 @@
|
|||||||
|
mat-card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
11
client/src/app/site/config/config-routing.module.ts
Normal file
11
client/src/app/site/config/config-routing.module.ts
Normal 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 {}
|
@ -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: [
|
@ -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', () => {
|
12
client/src/app/site/config/config.module.ts
Normal file
12
client/src/app/site/config/config.module.ts
Normal 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 {}
|
133
client/src/app/site/config/models/view-config.ts
Normal file
133
client/src/app/site/config/models/view-config.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}));
|
||||||
|
});
|
263
client/src/app/site/config/services/config-repository.service.ts
Normal file
263
client/src/app/site/config/services/config-repository.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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',
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user