Merge pull request #3895 from MaximilianKrambach/os3/settings
implement settings
This commit is contained in:
commit
81aa763ae9
@ -6,7 +6,7 @@ import { AppConfig } from '../../site/base/app-config';
|
||||
import { CollectionStringModelMapperService } from './collectionStringModelMapper.service';
|
||||
import { MediafileAppConfig } from '../../site/mediafiles/mediafile.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 { AssignmentsAppConfig } from '../../site/assignments/assignments.config';
|
||||
import { UsersAppConfig } from '../../site/users/users.config';
|
||||
@ -17,7 +17,7 @@ import { MainMenuService } from './main-menu.service';
|
||||
*/
|
||||
const appConfigs: AppConfig[] = [
|
||||
CommonAppConfig,
|
||||
SettingsAppConfig,
|
||||
ConfigAppConfig,
|
||||
AgendaAppConfig,
|
||||
AssignmentsAppConfig,
|
||||
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,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
MatTooltipModule
|
||||
MatTooltipModule,
|
||||
MatDatepickerModule,
|
||||
MatNativeDateModule,
|
||||
DateAdapter
|
||||
} from '@angular/material';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
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 { PrivacyPolicyContentComponent } from './components/privacy-policy-content/privacy-policy-content.component';
|
||||
import { SearchValueSelectorComponent } from './components/search-value-selector/search-value-selector.component';
|
||||
import { OpenSlidesDateAdapter } from './date-adapter';
|
||||
|
||||
library.add(fas);
|
||||
|
||||
@ -69,6 +73,8 @@ library.add(fas);
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatToolbarModule,
|
||||
MatDatepickerModule,
|
||||
MatNativeDateModule,
|
||||
MatCardModule,
|
||||
MatInputModule,
|
||||
MatTableModule,
|
||||
@ -98,6 +104,7 @@ library.add(fas);
|
||||
MatCheckboxModule,
|
||||
MatToolbarModule,
|
||||
MatCardModule,
|
||||
MatDatepickerModule,
|
||||
MatInputModule,
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
@ -130,6 +137,7 @@ library.add(fas);
|
||||
LegalNoticeContentComponent,
|
||||
PrivacyPolicyContentComponent,
|
||||
SearchValueSelectorComponent
|
||||
]
|
||||
],
|
||||
providers: [{ provide: DateAdapter, useClass: OpenSlidesDateAdapter }]
|
||||
})
|
||||
export class SharedModule {}
|
||||
|
@ -33,9 +33,12 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
|
||||
protected depsModelCtors?: ModelConstructor<BaseModel>[]
|
||||
) {
|
||||
super();
|
||||
this.setup();
|
||||
}
|
||||
|
||||
protected setup(): void {
|
||||
// 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.updateViewModelObservable(model.id);
|
||||
});
|
||||
@ -63,7 +66,7 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
|
||||
|
||||
// Watch the Observables for deleting
|
||||
this.DS.deletedObservable.subscribe(model => {
|
||||
if (model.collection === CollectionStringModelMapperService.getCollectionString(baseModelCtor)) {
|
||||
if (model.collection === CollectionStringModelMapperService.getCollectionString(this.baseModelCtor)) {
|
||||
delete this.viewModelStore[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 { Config } from '../../shared/models/core/config';
|
||||
|
||||
export const SettingsAppConfig: AppConfig = {
|
||||
export const ConfigAppConfig: AppConfig = {
|
||||
name: 'settings',
|
||||
models: [{ collectionString: 'core/config', model: Config }],
|
||||
mainMenuEntries: [
|
@ -1,10 +1,10 @@
|
||||
import { SettingsModule } from './settings.module';
|
||||
import { ConfigModule } from './config.module';
|
||||
|
||||
describe('SettingsModule', () => {
|
||||
let settingsModule: SettingsModule;
|
||||
let settingsModule: ConfigModule;
|
||||
|
||||
beforeEach(() => {
|
||||
settingsModule = new SettingsModule();
|
||||
settingsModule = new ConfigModule();
|
||||
});
|
||||
|
||||
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 { AuthService } from 'app/core/services/auth.service';
|
||||
import { OperatorService } from 'app/core/services/operator.service';
|
||||
import { ErrorStateMatcher, MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material';
|
||||
import { FormControl, FormGroupDirective, NgForm, FormGroup, Validators, FormBuilder } from '@angular/forms';
|
||||
import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material';
|
||||
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { HttpErrorResponse, HttpClient } from '@angular/common/http';
|
||||
import { environment } from 'environments/environment';
|
||||
import { OpenSlidesService } from '../../../../core/services/openslides.service';
|
||||
import { LoginDataService } from '../../../../core/services/login-data.service';
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
import { ParentErrorStateMatcher } from '../../../../shared/parent-error-state-matcher';
|
||||
|
||||
/**
|
||||
* 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',
|
||||
loadChildren: './settings/settings.module#SettingsModule'
|
||||
loadChildren: './config/config.module#ConfigModule'
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
|
@ -31,6 +31,10 @@ body {
|
||||
background-color: rgb(77, 243, 86) !important;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(77, 243, 86);
|
||||
}
|
||||
|
||||
.red-warning-text {
|
||||
color: red;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user