Reworked config
- Move config meta data into REST - seperate views for config groups
This commit is contained in:
parent
bcba878e18
commit
cf7a5ce714
@ -2,9 +2,9 @@ import { Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
||||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
|
||||||
import { DataSendService } from 'app/core/core-services/data-send.service';
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
import { DataStoreService } from 'app/core/core-services/data-store.service';
|
import { DataStoreService } from 'app/core/core-services/data-store.service';
|
||||||
import { HttpService } from 'app/core/core-services/http.service';
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
@ -13,29 +13,9 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s
|
|||||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
import { BaseRepository } from 'app/core/repositories/base-repository';
|
||||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||||
import { Config } from 'app/shared/models/core/config';
|
import { Config } from 'app/shared/models/core/config';
|
||||||
|
import { ConfigItem } from 'app/site/config/components/config-list/config-list.component';
|
||||||
import { ConfigTitleInformation, ViewConfig } from 'app/site/config/models/view-config';
|
import { ConfigTitleInformation, ViewConfig } from 'app/site/config/models/view-config';
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* Represents a config subgroup. It can only holds items and no further groups.
|
||||||
*/
|
*/
|
||||||
@ -48,7 +28,7 @@ interface ConfigSubgroup {
|
|||||||
/**
|
/**
|
||||||
* All items in this sub group.
|
* All items in this sub group.
|
||||||
*/
|
*/
|
||||||
items: ConfigItem[];
|
configs: ViewConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,11 +44,6 @@ export interface ConfigGroup {
|
|||||||
* A list of subgroups.
|
* A list of subgroups.
|
||||||
*/
|
*/
|
||||||
subgroups: ConfigSubgroup[];
|
subgroups: ConfigSubgroup[];
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of config items that are not in any subgroup.
|
|
||||||
*/
|
|
||||||
items: ConfigItem[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,22 +57,27 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
|
|||||||
/**
|
/**
|
||||||
* Own store for config groups.
|
* Own store for config groups.
|
||||||
*/
|
*/
|
||||||
private configs: ConfigGroup[] = null;
|
private configs: ConfigGroup[] | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Own subject for config groups.
|
* Own subject for config groups.
|
||||||
*/
|
*/
|
||||||
protected configListSubject: BehaviorSubject<ConfigGroup[]> = new BehaviorSubject<ConfigGroup[]>(null);
|
private readonly configsSubject: BehaviorSubject<ConfigGroup[]> = new BehaviorSubject<ConfigGroup[]>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves, if we got config variables (the structure) from the server.
|
* Custom observer for the config
|
||||||
*/
|
*/
|
||||||
protected gotConfigsVariables = false;
|
public get configsObservable(): Observable<ConfigGroup[]> {
|
||||||
|
return this.configsSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves, if we got first configs via autoupdate or cache.
|
* Gets an observalble for all existing (main) config groups. Just the group names
|
||||||
|
* are given with this observable.
|
||||||
*/
|
*/
|
||||||
protected gotFirstUpdate = false;
|
public get availableGroupsOberservable(): Observable<string[]> {
|
||||||
|
return this.configsSubject.pipe(map((groups: ConfigGroup[]) => groups.map(group => group.name)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for ConfigRepositoryService. Requests the constants from the server and creates the config group structure.
|
* Constructor for ConfigRepositoryService. Requests the constants from the server and creates the config group structure.
|
||||||
@ -114,18 +94,15 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
|
|||||||
viewModelStoreService: ViewModelStoreService,
|
viewModelStoreService: ViewModelStoreService,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
relationManager: RelationManagerService,
|
relationManager: RelationManagerService,
|
||||||
private constantsService: ConstantsService,
|
|
||||||
private http: HttpService
|
private http: HttpService
|
||||||
) {
|
) {
|
||||||
super(DS, dataSend, mapperService, viewModelStoreService, translate, relationManager, Config);
|
super(DS, dataSend, mapperService, viewModelStoreService, translate, relationManager, Config);
|
||||||
|
|
||||||
this.constantsService.get('ConfigVariables').subscribe(constant => {
|
this.setSortFunction((a, b) => a.weight - b.weight);
|
||||||
this.createConfigStructure(constant);
|
|
||||||
this.updateConfigStructure(false, ...Object.values(this.viewModelStore));
|
this.getViewModelListObservable().subscribe(configs =>
|
||||||
this.gotConfigsVariables = true;
|
this.updateConfigStructure(configs.filter(config => !config.hidden))
|
||||||
this.checkConfigStructure();
|
);
|
||||||
this.updateConfigListObservable();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getVerboseName = (plural: boolean = false) => {
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
@ -154,96 +131,50 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
|
|||||||
throw new Error('Config variables cannot be created');
|
throw new Error('Config variables cannot be created');
|
||||||
}
|
}
|
||||||
|
|
||||||
public changedModels(ids: number[]): void {
|
|
||||||
super.changedModels(ids);
|
|
||||||
|
|
||||||
ids.forEach(id => {
|
|
||||||
this.updateConfigStructure(false, this.viewModelStore[id]);
|
|
||||||
});
|
|
||||||
this.gotFirstUpdate = true;
|
|
||||||
this.checkConfigStructure();
|
|
||||||
this.updateConfigListObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom observer for the config
|
|
||||||
*/
|
|
||||||
public getConfigListObservable(): Observable<ConfigGroup[]> {
|
|
||||||
return this.configListSubject.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom notification for the observers.
|
* Custom notification for the observers.
|
||||||
*/
|
*/
|
||||||
protected updateConfigListObservable(): void {
|
protected updateConfigListObservable(): void {
|
||||||
if (this.configs) {
|
if (this.configs) {
|
||||||
this.configListSubject.next(this.configs);
|
this.configsSubject.next(this.configs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public getConfigGroupOberservable(name: string): Observable<ConfigGroup> {
|
||||||
* Getter for the config structure
|
return this.configsSubject.pipe(
|
||||||
*/
|
map((groups: ConfigGroup[]) => groups.find(group => group.name.toLowerCase() === name))
|
||||||
public getConfigStructure(): ConfigGroup[] {
|
);
|
||||||
return this.configs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected updateConfigStructure(configs: ViewConfig[]): void {
|
||||||
* Checks the config structure, if we got configs (first data) and the
|
const groups: ConfigGroup[] = [];
|
||||||
* structure (config variables)
|
|
||||||
*/
|
|
||||||
protected checkConfigStructure(): void {
|
|
||||||
if (this.gotConfigsVariables && this.gotFirstUpdate) {
|
|
||||||
this.updateConfigStructure(true, ...Object.values(this.viewModelStore));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
configs.forEach(config => {
|
||||||
* With a given (and maybe partially filled) config structure, all given view configs are put into it.
|
if (groups.length === 0 || groups[groups.length - 1].name !== config.group) {
|
||||||
* @param check Whether to check, if all given configs are there (according to the config structure).
|
groups.push({
|
||||||
* If check is true and one viewConfig is missing, the user will get an error message.
|
name: config.group,
|
||||||
* @param viewConfigs All view configs to put into the structure
|
subgroups: []
|
||||||
*/
|
});
|
||||||
protected updateConfigStructure(check: boolean, ...viewConfigs: ViewConfig[]): void {
|
}
|
||||||
if (!this.configs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map the viewConfigs to their keys.
|
const subgroupsLength = groups[groups.length - 1].subgroups.length;
|
||||||
const keyConfigMap: { [key: string]: ViewConfig } = {};
|
if (
|
||||||
viewConfigs.forEach(viewConfig => {
|
subgroupsLength === 0 ||
|
||||||
keyConfigMap[viewConfig.key] = viewConfig;
|
groups[groups.length - 1].subgroups[subgroupsLength - 1].name !== config.subgroup
|
||||||
|
) {
|
||||||
|
groups[groups.length - 1].subgroups.push({
|
||||||
|
name: config.subgroup,
|
||||||
|
configs: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
groups[groups.length - 1].subgroups[groups[groups.length - 1].subgroups.length - 1].configs.push(config);
|
||||||
});
|
});
|
||||||
|
|
||||||
// traverse through configs structure and replace all given viewConfigs
|
this.configsSubject.next(groups);
|
||||||
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];
|
|
||||||
} else if (check) {
|
|
||||||
throw new Error(
|
|
||||||
`No config variable found for "${item.key}". Please migrate the database or rebuild the servercache.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const item of group.items) {
|
|
||||||
if (keyConfigMap[item.key]) {
|
|
||||||
keyConfigMap[item.key].setConstantsInfo(item.data);
|
|
||||||
item.config = keyConfigMap[item.key];
|
|
||||||
} else if (check) {
|
|
||||||
throw new Error(
|
|
||||||
`No config variable found for "${item.key}". Please migrate the database or rebuild the servercache.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves a config value.
|
* Saves a config value. The server needs the key instead of the id to fetch the config variable.
|
||||||
*/
|
*/
|
||||||
public async update(config: Partial<Config>, viewConfig: ViewConfig): Promise<void> {
|
public async update(config: Partial<Config>, viewConfig: ViewConfig): Promise<void> {
|
||||||
const updatedConfig = viewConfig.getUpdatedModel(config);
|
const updatedConfig = viewConfig.getUpdatedModel(config);
|
||||||
@ -251,43 +182,24 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* initially create the config structure from the given constant.
|
* Function to update multiple settings.
|
||||||
* @param constant
|
*
|
||||||
|
* @param configItems An array of `ConfigItem` with the key of the changed setting and the value for that setting.
|
||||||
|
*
|
||||||
|
* @returns Either a promise containing errors or null, if there are no errors.
|
||||||
*/
|
*/
|
||||||
private createConfigStructure(constant: any): void {
|
public async bulkUpdate(configItems: ConfigItem[]): Promise<{ errors: { [key: string]: string } } | null> {
|
||||||
this.configs = [];
|
return await this.http.post(`/rest/core/config/bulk_update/`, configItems);
|
||||||
for (const group of constant) {
|
}
|
||||||
const _group: ConfigGroup = {
|
|
||||||
name: group.name,
|
/**
|
||||||
subgroups: [],
|
* Function to send a `reset`-poll for every group to the server.
|
||||||
items: []
|
*
|
||||||
};
|
* @param groups The names of the groups, that should be updated.
|
||||||
// The server always sends subgroups. But if it has an empty name, there is no subgroup..
|
*
|
||||||
for (const subgroup of group.subgroups) {
|
* @returns The answer of the server.
|
||||||
if (subgroup.name) {
|
*/
|
||||||
const _subgroup: ConfigSubgroup = {
|
public async resetGroups(groups: string[]): Promise<void> {
|
||||||
name: subgroup.name,
|
return await this.http.post(`/rest/core/config/reset_groups/`, groups);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,57 +1,65 @@
|
|||||||
<mat-card
|
<mat-card
|
||||||
class="block-tile"
|
class="block-tile"
|
||||||
[style.display]="orientation === 'horizontal' ? 'flex' : 'block'"
|
[style.display]="orientation === 'horizontal' ? 'flex' : 'block'"
|
||||||
|
[ngClass]="{ 'is-square': isSquare, vertical: orientation === 'vertical' }"
|
||||||
(click)="onClick($event)"
|
(click)="onClick($event)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
[ngSwitch]="blockType"
|
||||||
|
class="block-node-container"
|
||||||
|
[ngClass]="{ 'no-padding': noPaddingBlock }"
|
||||||
|
*ngIf="showBlockNode"
|
||||||
>
|
>
|
||||||
<div [ngSwitch]="blockType" class="block-node-container">
|
<div
|
||||||
<div *ngSwitchCase="'text'" class="tile-text stretch-to-fill-parent tile-color" [style.border-radius]="orientation === 'horizontal' ? '4px 0 0 4px' : '4px 4px 0 0'">
|
#blockNode
|
||||||
<table>
|
class="tile-text stretch-to-fill-parent tile-color"
|
||||||
<tbody>
|
[style.border-radius]="orientation === 'horizontal' ? '4px 0 0 4px' : '4px 4px 0 0'"
|
||||||
<tr>
|
>
|
||||||
<td>
|
<ng-container *ngSwitchCase="'text'">
|
||||||
{{ block }}
|
{{ block }}
|
||||||
</td>
|
</ng-container>
|
||||||
</tr>
|
<ng-container *ngSwitchCase="'node'">
|
||||||
</tbody>
|
<ng-container [ngTemplateOutlet]="blockNodeTemplate" [ngTemplateOutletContext]="data"></ng-container>
|
||||||
</table>
|
</ng-container>
|
||||||
</div>
|
|
||||||
<div *ngSwitchCase="'image'">
|
|
||||||
<img mat-card-image [src]="block" alt="" />
|
|
||||||
</div>
|
|
||||||
<div *ngSwitchCase="'node'" class="tile-text stretch-to-fill-parent tile-color" [style.border-radius]="orientation === 'horizontal' ? '4px 0 0 4px' : '4px 4px 0 0'">
|
|
||||||
<ng-container
|
|
||||||
[ngTemplateOutlet]="blockNode"
|
|
||||||
[ngTemplateOutletContext]="data"></ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile-content-node-container">
|
<div class="tile-content-node-container" *ngIf="showContentNode || showActions">
|
||||||
<mat-card-content class="tile-content">
|
<mat-card-content class="tile-content">
|
||||||
<mat-card-title class="tile-content-title stretch-to-fill-parent" *ngIf="!only || only === 'title'">
|
<ng-container *ngIf="showContentNode">
|
||||||
{{ title }}
|
<ng-container *ngIf="!only || only === 'title'">
|
||||||
</mat-card-title>
|
<mat-card-title class="tile-content-title">
|
||||||
<mat-card-subtitle class="tile-content-subtitle" *ngIf="subtitle">
|
{{ title }}
|
||||||
{{ subtitle }}
|
</mat-card-title>
|
||||||
</mat-card-subtitle>
|
<mat-card-subtitle class="tile-content-subtitle" *ngIf="subtitle">
|
||||||
<mat-divider *ngIf="!only"></mat-divider>
|
{{ subtitle }}
|
||||||
<div *ngIf="!only || only === 'content'" class="tile-content-extra">
|
</mat-card-subtitle>
|
||||||
<ng-container
|
</ng-container>
|
||||||
[ngTemplateOutlet]="contentNode"
|
<mat-divider *ngIf="!only"></mat-divider>
|
||||||
[ngTemplateOutletContext]="data"></ng-container>
|
<div
|
||||||
</div>
|
*ngIf="!only || only === 'content'"
|
||||||
<mat-card-actions *ngIf="showActions">
|
class="tile-content-extra"
|
||||||
<ng-container
|
[ngClass]="{ 'only-content': only === 'content' }"
|
||||||
[ngTemplateOutlet]="actionNode"></ng-container>
|
#contentNode
|
||||||
|
>
|
||||||
|
<ng-container
|
||||||
|
[ngTemplateOutlet]="contentNodeTemplate"
|
||||||
|
[ngTemplateOutletContext]="data"
|
||||||
|
></ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<mat-card-actions *ngIf="showActions" #actionNode>
|
||||||
|
<ng-container [ngTemplateOutlet]="actionNodeTemplate"></ng-container>
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<ng-template #blockNode>
|
<ng-template #blockNodeTemplate>
|
||||||
<ng-content select=".block-node"></ng-content>
|
<ng-content select=".block-node"></ng-content>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #contentNode>
|
<ng-template #contentNodeTemplate>
|
||||||
<ng-content select=".block-content-node"></ng-content>
|
<ng-content select=".block-content-node"></ng-content>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #actionNode>
|
<ng-template #actionNodeTemplate>
|
||||||
<ng-content select=".block-action-node"></ng-content>
|
<ng-content select=".block-action-node"></ng-content>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -3,11 +3,34 @@
|
|||||||
.block-tile {
|
.block-tile {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
&.is-square:not(.vertical) {
|
||||||
|
.block-node-container {
|
||||||
|
padding-bottom: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
&.is-square {
|
||||||
|
padding-bottom: 100%;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-node-container {
|
||||||
|
padding-bottom: 50%;
|
||||||
|
}
|
||||||
|
.tile-content-node-container {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.block-node-container {
|
.block-node-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: 50%;
|
|
||||||
min-width: 30%;
|
min-width: 30%;
|
||||||
|
|
||||||
|
&:not(.no-padding) {
|
||||||
|
padding-bottom: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.tile-text {
|
.tile-text {
|
||||||
table {
|
table {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -20,9 +43,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tile-content-node-container {
|
.tile-content-node-container {
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 8px 16px !important;
|
margin: 8px 16px;
|
||||||
|
|
||||||
.tile-content {
|
.tile-content {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -47,7 +69,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-content-extra {
|
.tile-content-extra:not(.only-content) {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,24 @@
|
|||||||
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
|
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||||
|
|
||||||
import { TileComponent } from '../tile/tile.component';
|
import { TileComponent } from '../tile/tile.component';
|
||||||
|
|
||||||
/**
|
|
||||||
* Enumeration to define if the content is only text or a node.
|
|
||||||
*/
|
|
||||||
export enum ContentType {
|
|
||||||
text = 'text',
|
|
||||||
node = 'node'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enumeration to define of which the big block is.
|
* Enumeration to define of which the big block is.
|
||||||
*/
|
*/
|
||||||
export enum BlockType {
|
export enum BlockType {
|
||||||
text = 'text',
|
text = 'text',
|
||||||
node = 'node',
|
node = 'node'
|
||||||
picture = 'picture'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells, whether to align the block and content next to each other or one below the other.
|
* Tells, whether to align the block and content next to each other or one below the other.
|
||||||
*/
|
*/
|
||||||
export enum Orientation {
|
export type Orientation = 'horizontal' | 'vertical';
|
||||||
horizontal = 'horizontal',
|
|
||||||
vertical = 'vertical'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells, if the tile should only display the content or the title in the content part.
|
* Tells, if the tile should only display the content or the title in the content part.
|
||||||
*/
|
*/
|
||||||
export enum ShowOnly {
|
export type ShowOnly = 'title' | 'content' | null;
|
||||||
title = 'title',
|
|
||||||
content = 'content'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class, that extends the `tile.component`.
|
* Class, that extends the `tile.component`.
|
||||||
@ -45,30 +30,42 @@ export enum ShowOnly {
|
|||||||
templateUrl: './block-tile.component.html',
|
templateUrl: './block-tile.component.html',
|
||||||
styleUrls: ['./block-tile.component.scss']
|
styleUrls: ['./block-tile.component.scss']
|
||||||
})
|
})
|
||||||
export class BlockTileComponent extends TileComponent {
|
export class BlockTileComponent extends TileComponent implements AfterViewInit {
|
||||||
/**
|
/**
|
||||||
* Reference to the content of the content part.
|
* Reference to the content of the content part.
|
||||||
*/
|
*/
|
||||||
@ContentChild(TemplateRef, { static: true })
|
@ViewChild('contentNode', { static: false })
|
||||||
public contentNode: TemplateRef<any>;
|
public contentNode: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to the block part, if it is a node.
|
* Reference to the block part, if it is a node.
|
||||||
*/
|
*/
|
||||||
@ContentChild(TemplateRef, { static: true })
|
@ViewChild('blockNode', { static: false })
|
||||||
public blockNode: TemplateRef<any>;
|
public blockNode: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to the action buttons in the content part, if used.
|
* Reference to the action buttons in the content part, if used.
|
||||||
*/
|
*/
|
||||||
@ContentChild(TemplateRef, { static: true })
|
@ViewChild('actionNode', { static: false })
|
||||||
public actionNode: TemplateRef<any>;
|
public actionNode: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the type of the primary block.
|
* Defines the type of the primary block.
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
public blockType: BlockType;
|
public blockType: BlockType = BlockType.node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually remove the padding, the block is surrounded by.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public noPaddingBlock = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the block-tile as a square.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public isSquare = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Input for the primary block content.
|
* Input for the primary block content.
|
||||||
@ -77,12 +74,6 @@ export class BlockTileComponent extends TileComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
public block: string;
|
public block: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the type of the content.
|
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
public contentType: ContentType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The title in the content part.
|
* The title in the content part.
|
||||||
*/
|
*/
|
||||||
@ -100,17 +91,130 @@ export class BlockTileComponent extends TileComponent {
|
|||||||
* whether the block part should be displayed above the content or next to it.
|
* whether the block part should be displayed above the content or next to it.
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
public orientation: Orientation;
|
public orientation: Orientation = 'horizontal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells, whether the tile should display only one of `Title` or `Content` in the content part.
|
* Tells, whether the tile should display only one of `Title` or `Content` in the content part.
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
public only: ShowOnly;
|
public only: ShowOnly = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean, if the block-part of the tile is shown or not.
|
||||||
|
*/
|
||||||
|
private _showBlockNode: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To decide, whether the block-node should always be shown.
|
||||||
|
* Otherwise this will decide automatically.
|
||||||
|
*
|
||||||
|
* @param show Whether the block-part should be shown or not.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set showBlockNode(show: boolean) {
|
||||||
|
this._showBlockNode = show;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns A boolean whether the block-part of the tile should be shown.
|
||||||
|
* If this is not set manually, it will return `true` for the first time to check,
|
||||||
|
* if this part contains any nodes.
|
||||||
|
*/
|
||||||
|
public get showBlockNode(): boolean {
|
||||||
|
return typeof this._showBlockNode === 'undefined' ? true : this._showBlockNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean, if the content-part of the tile is shown or not.
|
||||||
|
*/
|
||||||
|
private _showContentNode: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To decide, whether the content-node should always be shown.
|
||||||
|
* Otherwise this will decide automatically.
|
||||||
|
*
|
||||||
|
* @param show Whether the content-part should be shown or not.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set showContentNode(show: boolean) {
|
||||||
|
this._showContentNode = show;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns A boolean whether the content-part of the tile should be shown.
|
||||||
|
* If this is not set manually, it will return `true` for the first time to check,
|
||||||
|
* if this part contains any nodes.
|
||||||
|
*/
|
||||||
|
public get showContentNode(): boolean {
|
||||||
|
return typeof this._showContentNode === 'undefined'
|
||||||
|
? true
|
||||||
|
: this._showContentNode || !!this.only || !!this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean, if the part with actions of the tile is shown or not.
|
||||||
|
*/
|
||||||
|
private _showActionNode: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boolean, whether to show action buttons in the content part.
|
* Boolean, whether to show action buttons in the content part.
|
||||||
|
*
|
||||||
|
* @param show Whether the action-part should be shown or not.
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
public showActions: boolean;
|
public set showActions(show: boolean) {
|
||||||
|
this._showActionNode = show;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns A boolean whether the action-part of the tile should be shown.
|
||||||
|
* If this is not set manually, it will return `true` for the first time to check,
|
||||||
|
* if this part contains any nodes.
|
||||||
|
*/
|
||||||
|
public get showActions(): boolean {
|
||||||
|
return typeof this._showActionNode === 'undefined' ? true : this._showActionNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor.
|
||||||
|
*
|
||||||
|
* @param cd ChangeDetectorRef
|
||||||
|
*/
|
||||||
|
public constructor(private cd: ChangeDetectorRef) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AfterViewInit.
|
||||||
|
*
|
||||||
|
* Here it will check, if the visibility of the three parts of the tile is set manually.
|
||||||
|
* If not, it will check, if the parts contain nodes to display or not.
|
||||||
|
*/
|
||||||
|
public ngAfterViewInit(): void {
|
||||||
|
if (typeof this._showBlockNode === 'undefined') {
|
||||||
|
this.showBlockNode = this.checkForContent(this.blockNode);
|
||||||
|
}
|
||||||
|
if (typeof this._showContentNode === 'undefined') {
|
||||||
|
this.showContentNode = this.checkForContent(this.contentNode);
|
||||||
|
}
|
||||||
|
if (typeof this._showActionNode === 'undefined') {
|
||||||
|
this.showActions = this.checkForContent(this.actionNode);
|
||||||
|
}
|
||||||
|
this.cd.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to test, whether the child-nodes of the given parent-element
|
||||||
|
* are a comment or not. If not, then the parent-element contains content to display.
|
||||||
|
*
|
||||||
|
* @param parentElement The element whose child-nodes are tested.
|
||||||
|
*
|
||||||
|
* @returns `True`, if there is at least one node other than a comment.
|
||||||
|
*/
|
||||||
|
private checkForContent(parentElement: ElementRef<HTMLElement>): boolean {
|
||||||
|
if (!parentElement) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Array.from(parentElement.nativeElement.childNodes).some(item => item.nodeType !== 8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<!-- Button to open the global search -->
|
<!-- Button to open the global search -->
|
||||||
<button *ngIf="!editMode" mat-icon-button (click)="openSearch()">
|
<button *ngIf="!editMode && isSearchEnabled" mat-icon-button (click)="openSearch()">
|
||||||
<mat-icon>search</mat-icon>
|
<mat-icon>search</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -40,7 +40,8 @@
|
|||||||
<!-- Main action button - desktop -->
|
<!-- Main action button - desktop -->
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
*ngIf="mainButton && !editMode && !vp.isMobile && !multiSelectMode"
|
*ngIf="hasMainButton && !editMode && !vp.isMobile && !multiSelectMode"
|
||||||
|
[disabled]="!isMainButtonEnabled"
|
||||||
(click)="sendMainEvent()"
|
(click)="sendMainEvent()"
|
||||||
matTooltip="{{ mainActionTooltip | translate }}"
|
matTooltip="{{ mainActionTooltip | translate }}"
|
||||||
>
|
>
|
||||||
@ -62,7 +63,8 @@
|
|||||||
<button
|
<button
|
||||||
mat-fab
|
mat-fab
|
||||||
class="head-button"
|
class="head-button"
|
||||||
*ngIf="mainButton && !editMode && vp.isMobile && !multiSelectMode"
|
*ngIf="hasMainButton && !editMode && vp.isMobile && !multiSelectMode"
|
||||||
|
[disabled]="!isMainButtonEnabled"
|
||||||
(click)="sendMainEvent()"
|
(click)="sendMainEvent()"
|
||||||
matTooltip="{{ mainActionTooltip | translate }}"
|
matTooltip="{{ mainActionTooltip | translate }}"
|
||||||
>
|
>
|
||||||
|
@ -21,7 +21,7 @@ import { ViewportService } from 'app/core/ui-services/viewport.service';
|
|||||||
* saveText="Create"
|
* saveText="Create"
|
||||||
* [nav]="false"
|
* [nav]="false"
|
||||||
* [goBack]="true"
|
* [goBack]="true"
|
||||||
* [mainButton]="opCanEdit()"
|
* [hasMainButton]="opCanEdit()"
|
||||||
* [mainButtonIcon]="edit"
|
* [mainButtonIcon]="edit"
|
||||||
* [backButtonIcon]="arrow_back"
|
* [backButtonIcon]="arrow_back"
|
||||||
* [editMode]="editMotion"
|
* [editMode]="editMotion"
|
||||||
@ -82,6 +82,12 @@ export class HeadBarComponent implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
public editMode = false;
|
public editMode = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine, if the search should not be available.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public isSearchEnabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The save button can manually be disabled.
|
* The save button can manually be disabled.
|
||||||
*/
|
*/
|
||||||
@ -98,7 +104,13 @@ export class HeadBarComponent implements OnInit {
|
|||||||
* Determine if there should be the main action button
|
* Determine if there should be the main action button
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
public mainButton = false;
|
public hasMainButton = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the main action button should be enabled or not.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public isMainButtonEnabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set to true if the component should use location.back instead
|
* Set to true if the component should use location.back instead
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<ng-container
|
<ng-container [ngTemplateOutlet]="tileContext" [ngTemplateOutletContext]="data"> </ng-container>
|
||||||
[ngTemplateOutlet]="tileContext"
|
|
||||||
[ngTemplateOutletContext]="data">
|
<ng-template #tileContext>
|
||||||
</ng-container>
|
<ng-content></ng-content>
|
||||||
|
</ng-template>
|
||||||
|
@ -149,7 +149,7 @@ export class TileComponent implements OnInit {
|
|||||||
*
|
*
|
||||||
* @param size how great the tile should be
|
* @param size how great the tile should be
|
||||||
*/
|
*/
|
||||||
private setTabletSize(size: number): void {
|
private setTabletSize(size: number = 4): void {
|
||||||
if (size <= 8 && size >= 0) {
|
if (size <= 8 && size >= 0) {
|
||||||
this.tabletSize = size;
|
this.tabletSize = size;
|
||||||
} else {
|
} else {
|
||||||
@ -162,7 +162,7 @@ export class TileComponent implements OnInit {
|
|||||||
*
|
*
|
||||||
* @param size how great the tile should be
|
* @param size how great the tile should be
|
||||||
*/
|
*/
|
||||||
private setMediumSize(size: number): void {
|
private setMediumSize(size: number = 4): void {
|
||||||
if (size <= 12 && size >= 0) {
|
if (size <= 12 && size >= 0) {
|
||||||
this.mediumSize = size;
|
this.mediumSize = size;
|
||||||
} else {
|
} else {
|
||||||
@ -175,7 +175,7 @@ export class TileComponent implements OnInit {
|
|||||||
*
|
*
|
||||||
* @param size how great the tile should be
|
* @param size how great the tile should be
|
||||||
*/
|
*/
|
||||||
private setLargeSize(size: number): void {
|
private setLargeSize(size: number = 4): void {
|
||||||
if (size <= 16 && size >= 0) {
|
if (size <= 16 && size >= 0) {
|
||||||
this.largeSize = size;
|
this.largeSize = size;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,5 +1,35 @@
|
|||||||
import { BaseModel } from '../base/base-model';
|
import { BaseModel } from '../base/base-model';
|
||||||
|
|
||||||
|
export interface ConfigChoice {
|
||||||
|
value: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All valid input types for config variables.
|
||||||
|
*/
|
||||||
|
export type ConfigInputType =
|
||||||
|
| 'text'
|
||||||
|
| 'string'
|
||||||
|
| 'boolean'
|
||||||
|
| 'markupText'
|
||||||
|
| 'integer'
|
||||||
|
| 'choice'
|
||||||
|
| 'datetimepicker'
|
||||||
|
| 'colorpicker'
|
||||||
|
| 'translations';
|
||||||
|
|
||||||
|
export interface ConfigData {
|
||||||
|
defaultValue: any;
|
||||||
|
inputType: ConfigInputType;
|
||||||
|
label: string;
|
||||||
|
helpText?: string;
|
||||||
|
choices?: ConfigChoice[];
|
||||||
|
weight: number;
|
||||||
|
group: string;
|
||||||
|
subgroup?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of a config variable
|
* Representation of a config variable
|
||||||
* @ignore
|
* @ignore
|
||||||
@ -8,7 +38,8 @@ export class Config extends BaseModel {
|
|||||||
public static COLLECTIONSTRING = 'core/config';
|
public static COLLECTIONSTRING = 'core/config';
|
||||||
public id: number;
|
public id: number;
|
||||||
public key: string;
|
public key: string;
|
||||||
public value: Object;
|
public value: any;
|
||||||
|
public data?: ConfigData;
|
||||||
|
|
||||||
public constructor(input?: any) {
|
public constructor(input?: any) {
|
||||||
super(Config.COLLECTIONSTRING, input);
|
super(Config.COLLECTIONSTRING, input);
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { inject, TestBed } from '@angular/core/testing';
|
import { inject, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { WatchSortingTreeGuard } from './watch-sorting-tree.guard';
|
import { WatchForChangesGuard } from './watch-for-changes.guard';
|
||||||
|
|
||||||
describe('WatchSortingTreeGuard', () => {
|
describe('WatchSortingTreeGuard', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [WatchSortingTreeGuard]
|
providers: [WatchForChangesGuard]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ...', inject([WatchSortingTreeGuard], (guard: WatchSortingTreeGuard) => {
|
it('should ...', inject([WatchForChangesGuard], (guard: WatchForChangesGuard) => {
|
||||||
expect(guard).toBeTruthy();
|
expect(guard).toBeTruthy();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
@ -11,7 +11,7 @@ export interface CanComponentDeactivate {
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class WatchSortingTreeGuard implements CanDeactivate<CanComponentDeactivate> {
|
export class WatchForChangesGuard implements CanDeactivate<CanComponentDeactivate> {
|
||||||
/**
|
/**
|
||||||
* Function to determine whether the route will change or not.
|
* Function to determine whether the route will change or not.
|
||||||
*
|
*
|
@ -4,7 +4,7 @@ import { RouterModule, Routes } from '@angular/router';
|
|||||||
import { AgendaImportListComponent } from './components/agenda-import-list/agenda-import-list.component';
|
import { AgendaImportListComponent } from './components/agenda-import-list/agenda-import-list.component';
|
||||||
import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
|
import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
|
||||||
import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component';
|
import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component';
|
||||||
import { WatchSortingTreeGuard } from 'app/shared/utils/watch-sorting-tree.guard';
|
import { WatchForChangesGuard } from 'app/shared/utils/watch-for-changes.guard';
|
||||||
import { ListOfSpeakersComponent } from './components/list-of-speakers/list-of-speakers.component';
|
import { ListOfSpeakersComponent } from './components/list-of-speakers/list-of-speakers.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@ -13,7 +13,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'sort-agenda',
|
path: 'sort-agenda',
|
||||||
component: AgendaSortComponent,
|
component: AgendaSortComponent,
|
||||||
canDeactivate: [WatchSortingTreeGuard],
|
canDeactivate: [WatchForChangesGuard],
|
||||||
data: { basePerm: 'agenda.can_manage' }
|
data: { basePerm: 'agenda.can_manage' }
|
||||||
},
|
},
|
||||||
{ path: 'speakers', component: ListOfSpeakersComponent, data: { basePerm: 'agenda.can_see_list_of_speakers' } },
|
{ path: 'speakers', component: ListOfSpeakersComponent, data: { basePerm: 'agenda.can_see_list_of_speakers' } },
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar [mainButton]="canManage" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
<os-head-bar [hasMainButton]="canManage" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot"><h2 translate>Agenda</h2></div>
|
<div class="title-slot"><h2 translate>Agenda</h2></div>
|
||||||
<!-- Menu -->
|
<!-- Menu -->
|
||||||
@ -119,7 +119,13 @@
|
|||||||
<!-- Import -->
|
<!-- Import -->
|
||||||
<button mat-menu-item *osPerms="'agenda.can_manage'" routerLink="import">
|
<button mat-menu-item *osPerms="'agenda.can_manage'" routerLink="import">
|
||||||
<mat-icon>cloud_upload</mat-icon>
|
<mat-icon>cloud_upload</mat-icon>
|
||||||
<span translate>Import</span><span> ...</span>
|
<span translate>Import</span>
|
||||||
|
</button>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<!-- Settings -->
|
||||||
|
<button mat-menu-item *osPerms="'core.can_manage_config'" routerLink="/settings/agenda">
|
||||||
|
<mat-icon>settings</mat-icon>
|
||||||
|
<span translate>Settings</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<os-head-bar
|
<os-head-bar
|
||||||
[mainButton]="hasPerms('manage')"
|
[hasMainButton]="hasPerms('manage')"
|
||||||
mainButtonIcon="edit"
|
mainButtonIcon="edit"
|
||||||
[nav]="false"
|
[nav]="false"
|
||||||
[editMode]="editAssignment"
|
[editMode]="editAssignment"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<os-head-bar
|
<os-head-bar
|
||||||
[mainButton]="operator.hasPerms('assignments.can_manage')"
|
[hasMainButton]="operator.hasPerms('assignments.can_manage')"
|
||||||
(mainEvent)="onPlusButton()"
|
(mainEvent)="onPlusButton()"
|
||||||
[multiSelectMode]="isMultiSelect"
|
[multiSelectMode]="isMultiSelect"
|
||||||
>
|
>
|
||||||
@ -91,6 +91,12 @@
|
|||||||
<mat-icon>archive</mat-icon>
|
<mat-icon>archive</mat-icon>
|
||||||
<span translate>Export ...</span>
|
<span translate>Export ...</span>
|
||||||
</button>
|
</button>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<!-- Settings -->
|
||||||
|
<button mat-menu-item *osPerms="'core.can_manage_config'" routerLink="/settings/elections">
|
||||||
|
<mat-icon>settings</mat-icon>
|
||||||
|
<span translate>Settings</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="isMultiSelect">
|
<div *ngIf="isMultiSelect">
|
||||||
|
@ -8,7 +8,7 @@ import { SortDefinition } from 'app/core/ui-services/base-sort.service';
|
|||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
||||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
|
import { CanComponentDeactivate } from 'app/shared/utils/watch-for-changes.guard';
|
||||||
import { BaseViewComponent } from './base-view';
|
import { BaseViewComponent } from './base-view';
|
||||||
import { BaseViewModel } from './base-view-model';
|
import { BaseViewModel } from './base-view-model';
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<!-- required for all kinds of input -->
|
<!-- required for all kinds of input -->
|
||||||
<mat-label>{{ configItem.label | translate }}</mat-label>
|
<mat-label>{{ configItem.label | translate }}</mat-label>
|
||||||
<span matSuffix>
|
<span matSuffix>
|
||||||
<mat-icon pull="right" class="text-success" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
<mat-icon pull="right" class="red-warning-text" *ngIf="error">error</mat-icon>
|
||||||
</span>
|
</span>
|
||||||
<mat-error *ngIf="error"> {{ error }} </mat-error>
|
<mat-error *ngIf="error"> {{ error }} </mat-error>
|
||||||
|
|
||||||
@ -57,7 +57,7 @@
|
|||||||
[value]="translatedValue"
|
[value]="translatedValue"
|
||||||
></textarea>
|
></textarea>
|
||||||
<span matSuffix>
|
<span matSuffix>
|
||||||
<mat-icon pull="right" class="text-success" *ngIf="updateSuccessIcon">
|
<mat-icon pull="right" class="red-warning-text" *ngIf="updateSuccessIcon">
|
||||||
check_circle
|
check_circle
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
</span>
|
</span>
|
||||||
@ -77,7 +77,7 @@
|
|||||||
/>
|
/>
|
||||||
<mat-hint *ngIf="configItem.helpText">{{ configItem.helpText | translate }}</mat-hint>
|
<mat-hint *ngIf="configItem.helpText">{{ configItem.helpText | translate }}</mat-hint>
|
||||||
<div class="suffix-wrapper" matSuffix>
|
<div class="suffix-wrapper" matSuffix>
|
||||||
<mat-icon class="text-success" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
<mat-icon class="red-warning-text" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
||||||
<mat-datepicker-toggle
|
<mat-datepicker-toggle
|
||||||
[for]="datepicker"
|
[for]="datepicker"
|
||||||
(click)="$event.preventDefault()"
|
(click)="$event.preventDefault()"
|
||||||
@ -89,7 +89,7 @@
|
|||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input matInput [format]="24" formControlName="time" [ngxTimepicker]="timepicker" />
|
<input matInput [format]="24" formControlName="time" [ngxTimepicker]="timepicker" />
|
||||||
<div class="suffix-wrapper" matSuffix>
|
<div class="suffix-wrapper" matSuffix>
|
||||||
<mat-icon class="text-success" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
<mat-icon class="red-warning-text" *ngIf="updateSuccessIcon">error</mat-icon>
|
||||||
<ngx-material-timepicker-toggle [for]="timepicker"></ngx-material-timepicker-toggle>
|
<ngx-material-timepicker-toggle [for]="timepicker"></ngx-material-timepicker-toggle>
|
||||||
</div>
|
</div>
|
||||||
<mat-error *ngIf="error"> {{ error }} </mat-error>
|
<mat-error *ngIf="error"> {{ error }} </mat-error>
|
||||||
@ -103,16 +103,13 @@
|
|||||||
<h4>{{ configItem.label | translate }}</h4>
|
<h4>{{ configItem.label | translate }}</h4>
|
||||||
<editor formControlName="value" [init]="getTinyMceSettings()"></editor>
|
<editor formControlName="value" [init]="getTinyMceSettings()"></editor>
|
||||||
<span matSuffix>
|
<span matSuffix>
|
||||||
<mat-icon pull="right" class="text-success" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
<mat-icon pull="right" class="red-warning-text" *ngIf="updateSuccessIcon">error</mat-icon>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Custom Translations -->
|
<!-- Custom Translations -->
|
||||||
<div *ngIf="configItem.inputType === 'translations'">
|
<div *ngIf="configItem.inputType === 'translations'">
|
||||||
<os-custom-translation formControlName="value"></os-custom-translation>
|
<os-custom-translation formControlName="value"></os-custom-translation>
|
||||||
<span matSuffix>
|
|
||||||
<mat-icon pull="right" class="text-success" *ngIf="updateSuccessIcon">check_circle</mat-icon>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -122,9 +119,4 @@
|
|||||||
<mat-icon>help_outline</mat-icon>
|
<mat-icon>help_outline</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="reset-button">
|
|
||||||
<button mat-icon-button *ngIf="hasDefault()" matTooltip="{{ 'Reset' | translate }}" (click)="onResetButton()">
|
|
||||||
<mat-icon>replay</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,9 +2,11 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
Output,
|
||||||
ViewEncapsulation
|
ViewEncapsulation
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
@ -18,6 +20,7 @@ import { distinctUntilChanged } from 'rxjs/operators';
|
|||||||
import { BaseComponent } from 'app/base.component';
|
import { BaseComponent } from 'app/base.component';
|
||||||
import { ConfigRepositoryService } from 'app/core/repositories/config/config-repository.service';
|
import { ConfigRepositoryService } from 'app/core/repositories/config/config-repository.service';
|
||||||
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
|
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
|
||||||
|
import { ConfigItem } from '../config-list/config-list.component';
|
||||||
import { ViewConfig } from '../../models/view-config';
|
import { ViewConfig } from '../../models/view-config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,7 +29,7 @@ import { ViewConfig } from '../../models/view-config';
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* <os-config-field [item]="item.config"></os-config-field>
|
* <os-config-field [config]="<ViewConfig>"></os-config-field>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
@ -44,16 +47,6 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
|||||||
*/
|
*/
|
||||||
public updateSuccessIcon = false;
|
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.
|
* A possible error send by the server.
|
||||||
*/
|
*/
|
||||||
@ -69,8 +62,8 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
|||||||
* populated constants-info.
|
* populated constants-info.
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
public set item(value: ViewConfig) {
|
public set config(value: ViewConfig) {
|
||||||
if (value && value.hasConstantsInfo) {
|
if (value) {
|
||||||
this.configItem = value;
|
this.configItem = value;
|
||||||
|
|
||||||
if (this.form) {
|
if (this.form) {
|
||||||
@ -90,6 +83,25 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passes the list of errors as object.
|
||||||
|
*
|
||||||
|
* The function looks, if the key of this config-item is contained in the list.
|
||||||
|
*
|
||||||
|
* @param errorList The object containing all errors.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public set errorList(errorList: { [key: string]: any }) {
|
||||||
|
const hasError = Object.keys(errorList).find(errorKey => errorKey === this.configItem.key);
|
||||||
|
if (hasError) {
|
||||||
|
this.error = errorList[hasError];
|
||||||
|
this.updateError(true);
|
||||||
|
} else {
|
||||||
|
this.error = null;
|
||||||
|
this.updateError(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The form for this configItem.
|
* The form for this configItem.
|
||||||
*/
|
*/
|
||||||
@ -100,6 +112,9 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
|||||||
*/
|
*/
|
||||||
public matcher = new ParentErrorStateMatcher();
|
public matcher = new ParentErrorStateMatcher();
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public update = new EventEmitter<ConfigItem>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The usual component constructor. datetime pickers will set their locale
|
* The usual component constructor. datetime pickers will set their locale
|
||||||
* to the current language chosen
|
* to the current language chosen
|
||||||
@ -172,7 +187,7 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
|||||||
private unixToDateAndTime(unix: number): { date: Moment; time: string } {
|
private unixToDateAndTime(unix: number): { date: Moment; time: string } {
|
||||||
const date = moment.unix(unix);
|
const date = moment.unix(unix);
|
||||||
const time = date.hours() + ':' + date.minutes();
|
const time = date.hours() + ':' + date.minutes();
|
||||||
return { date: date, time: time };
|
return { date, time };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -211,12 +226,7 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
|||||||
const time = this.form.get('time').value;
|
const time = this.form.get('time').value;
|
||||||
value = this.dateAndTimeToUnix(date, time);
|
value = this.dateAndTimeToUnix(date, time);
|
||||||
}
|
}
|
||||||
if (this.debounceTimeout !== null) {
|
this.sendUpdate(value);
|
||||||
clearTimeout(<any>this.debounceTimeout);
|
|
||||||
}
|
|
||||||
this.debounceTimeout = <any>setTimeout(() => {
|
|
||||||
this.update(value);
|
|
||||||
}, this.configItem.getDebouncingTimeout());
|
|
||||||
this.cd.detectChanges();
|
this.cd.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,53 +243,22 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
|||||||
* Sends an update request for the config item to the server.
|
* Sends an update request for the config item to the server.
|
||||||
* @param value The new value to set.
|
* @param value The new value to set.
|
||||||
*/
|
*/
|
||||||
private update(value: any): void {
|
private sendUpdate(value: any): void {
|
||||||
this.debounceTimeout = null;
|
this.update.emit({ key: this.configItem.key, value });
|
||||||
this.repo.update({ value: value }, this.configItem).then(() => {
|
|
||||||
this.error = null;
|
|
||||||
this.showSuccessIcon();
|
|
||||||
}, this.setError.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the green success icon on the component. The icon gets automatically cleared.
|
* Function to update the form-control to display or hide an error.
|
||||||
|
*
|
||||||
|
* @param error `true | false`, if an error should be shown. `null`, if there is no error.
|
||||||
*/
|
*/
|
||||||
private showSuccessIcon(): void {
|
private updateError(error: boolean | null): void {
|
||||||
if (this.updateSuccessIconTimeout !== null) {
|
if (this.form) {
|
||||||
clearTimeout(<any>this.updateSuccessIconTimeout);
|
this.form.setErrors(error ? { error } : null);
|
||||||
}
|
|
||||||
this.updateSuccessIconTimeout = <any>setTimeout(() => {
|
|
||||||
this.updateSuccessIcon = false;
|
|
||||||
if (!this.wasViewDestroyed()) {
|
|
||||||
this.cd.detectChanges();
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
this.updateSuccessIcon = true;
|
|
||||||
if (!this.wasViewDestroyed()) {
|
|
||||||
this.cd.detectChanges();
|
this.cd.detectChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns true, if the view was destroyed. Note: This
|
|
||||||
* needs to access internal attributes from the change detection
|
|
||||||
* reference.
|
|
||||||
*/
|
|
||||||
private wasViewDestroyed(): boolean {
|
|
||||||
return (<any>this.cd).destroyed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the error on this field.
|
|
||||||
*
|
|
||||||
* @param error The error as string.
|
|
||||||
*/
|
|
||||||
private setError(error: string): void {
|
|
||||||
this.error = error;
|
|
||||||
this.form.setErrors({ error: true });
|
|
||||||
this.cd.detectChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses the configItem to determine the kind of interation:
|
* Uses the configItem to determine the kind of interation:
|
||||||
* input, textarea, choice or date
|
* input, textarea, choice or date
|
||||||
@ -311,16 +290,6 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
|||||||
return excluded.includes(type);
|
return excluded.includes(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if a reset buton should be offered.
|
|
||||||
* TODO: is 'null' a valid default in some cases?
|
|
||||||
*
|
|
||||||
* @returns true if any default exists
|
|
||||||
*/
|
|
||||||
public hasDefault(): boolean {
|
|
||||||
return this.configItem.defaultValue !== undefined && this.configItem.defaultValue !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Amends the application-wide tinyMCE settings with update triggers that
|
* Amends the application-wide tinyMCE settings with update triggers that
|
||||||
* send updated values only after leaving focus (Blur) or closing the editor (Remove)
|
* send updated values only after leaving focus (Blur) or closing the editor (Remove)
|
||||||
@ -333,7 +302,7 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
|||||||
setup: editor => {
|
setup: editor => {
|
||||||
editor.on('Blur', ev => {
|
editor.on('Blur', ev => {
|
||||||
if (ev.target.getContent() !== this.translatedValue) {
|
if (ev.target.getContent() !== this.translatedValue) {
|
||||||
this.update(ev.target.getContent());
|
this.sendUpdate(ev.target.getContent());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
editor.on('Remove', ev => {
|
editor.on('Remove', ev => {
|
||||||
@ -341,7 +310,7 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes
|
|||||||
// fast navigation, when the editor is not fully loaded. Then the content is empty
|
// fast navigation, when the editor is not fully loaded. Then the content is empty
|
||||||
// and would trigger an update with empty data.
|
// and would trigger an update with empty data.
|
||||||
if (ev.target.getContent() && ev.target.getContent() !== this.translatedValue) {
|
if (ev.target.getContent() && ev.target.getContent() !== this.translatedValue) {
|
||||||
this.update(ev.target.getContent());
|
this.sendUpdate(ev.target.getContent());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,45 @@
|
|||||||
<os-head-bar>
|
<os-head-bar [nav]="false" [hasMainButton]="false" [isSearchEnabled]="false">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 translate>Settings</h2>
|
<h2 *ngIf="configGroup">
|
||||||
|
{{ configGroup.name | translate }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
|
<div class="menu-slot">
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
[disabled]="!hasChanges()"
|
||||||
|
(click)="saveAll()"
|
||||||
|
matTooltip="{{ 'Save all changes' | translate }}"
|
||||||
|
>
|
||||||
|
<strong class="upper">{{ 'Save' | translate }}</strong>
|
||||||
|
</button>
|
||||||
|
<button type="button" mat-icon-button [matMenuTriggerFor]="settingsMenu"><mat-icon>more_vert</mat-icon></button>
|
||||||
</div>
|
</div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<div class="spacer-top-20"></div>
|
<div class="spacer-top-20"></div>
|
||||||
<mat-accordion>
|
|
||||||
<ng-container *ngFor="let group of this.configs">
|
<mat-card class="os-card" *ngIf="configGroup">
|
||||||
<mat-expansion-panel displayMode="flat">
|
<div id="wrapper">
|
||||||
<mat-expansion-panel-header>
|
<ng-container *ngFor="let subgroup of configGroup.subgroups; trackBy: trackByIndex">
|
||||||
<mat-panel-title>
|
<h3 class="accent" *ngIf="configGroup.subgroups.length > 1">{{ subgroup.name | translate }}</h3>
|
||||||
{{ group.name | translate }}
|
<ng-container *ngFor="let config of subgroup.configs">
|
||||||
</mat-panel-title>
|
<os-config-field
|
||||||
</mat-expansion-panel-header>
|
(update)="updateConfigGroup($event)"
|
||||||
<div *ngFor="let subgroup of group.subgroups">
|
[config]="config"
|
||||||
<h3 class="accent">{{ subgroup.name | translate }}</h3>
|
[errorList]="errors"
|
||||||
<div *ngFor="let item of subgroup.items">
|
></os-config-field>
|
||||||
<os-config-field [item]="item.config"></os-config-field>
|
</ng-container>
|
||||||
</div>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div *ngFor="let item of group.items">
|
</mat-card>
|
||||||
<os-config-field [item]="item.config"></os-config-field>
|
|
||||||
</div>
|
<mat-menu #settingsMenu="matMenu">
|
||||||
</mat-expansion-panel>
|
<button mat-menu-item (click)="resetAll()">
|
||||||
</ng-container>
|
<mat-icon>undo</mat-icon>
|
||||||
</mat-accordion>
|
<span translate>Reset all to default</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
mat-expansion-panel {
|
#wrapper {
|
||||||
max-width: 770px;
|
font-size: 14px;
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
h3.accent {
|
h3.accent {
|
||||||
margin-top: 30px;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,29 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { BaseComponent } from 'app/base.component';
|
import { BaseComponent } from 'app/base.component';
|
||||||
import { ConfigGroup, ConfigRepositoryService } from 'app/core/repositories/config/config-repository.service';
|
import { ConfigGroup, ConfigRepositoryService } from 'app/core/repositories/config/config-repository.service';
|
||||||
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
|
import { CanComponentDeactivate } from 'app/shared/utils/watch-for-changes.guard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key-value-pair to set a setting with its associated value.
|
||||||
|
*/
|
||||||
|
export interface ConfigItem {
|
||||||
|
/**
|
||||||
|
* The key has to be a string.
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value can be any.
|
||||||
|
*/
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List view for the global settings
|
* List view for the global settings
|
||||||
@ -14,13 +33,27 @@ import { ConfigGroup, ConfigRepositoryService } from 'app/core/repositories/conf
|
|||||||
templateUrl: './config-list.component.html',
|
templateUrl: './config-list.component.html',
|
||||||
styleUrls: ['./config-list.component.scss']
|
styleUrls: ['./config-list.component.scss']
|
||||||
})
|
})
|
||||||
export class ConfigListComponent extends BaseComponent implements OnInit {
|
export class ConfigListComponent extends BaseComponent implements CanComponentDeactivate, OnInit, OnDestroy {
|
||||||
public configs: ConfigGroup[];
|
public configGroup: ConfigGroup;
|
||||||
|
|
||||||
|
public configGroupSubscription: Subscription | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object containing all errors.
|
||||||
|
*/
|
||||||
|
public errors = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of all changed settings.
|
||||||
|
*/
|
||||||
|
private configItems: ConfigItem[] = [];
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
protected titleService: Title,
|
protected titleService: Title,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
private repo: ConfigRepositoryService
|
private repo: ConfigRepositoryService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private promptDialog: PromptService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate);
|
super(titleService, translate);
|
||||||
}
|
}
|
||||||
@ -29,10 +62,85 @@ export class ConfigListComponent extends BaseComponent implements OnInit {
|
|||||||
* Sets the title, inits the table and calls the repo
|
* Sets the title, inits the table and calls the repo
|
||||||
*/
|
*/
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
super.setTitle('Settings');
|
const settings = this.translate.instant('Settings');
|
||||||
|
this.route.params.subscribe(params => {
|
||||||
this.repo.getConfigListObservable().subscribe(configs => {
|
this.clearSubscription();
|
||||||
this.configs = configs;
|
this.configGroupSubscription = this.repo.getConfigGroupOberservable(params.group).subscribe(configGroup => {
|
||||||
|
if (configGroup) {
|
||||||
|
const groupName = this.translate.instant(configGroup.name);
|
||||||
|
super.setTitle(`${settings} - ${groupName}`);
|
||||||
|
this.configGroup = configGroup;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the specified config-item indicated by the given key.
|
||||||
|
*
|
||||||
|
* @param key The key of the config-item.
|
||||||
|
* @param value The next value the config-item has.
|
||||||
|
*/
|
||||||
|
public updateConfigGroup(update: ConfigItem): void {
|
||||||
|
const { key, value }: ConfigItem = update;
|
||||||
|
const index = this.configItems.findIndex(item => item.key === key);
|
||||||
|
if (index === -1) {
|
||||||
|
this.configItems.push({ key, value });
|
||||||
|
} else {
|
||||||
|
this.configItems[index] = { key, value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves every field in this config-group.
|
||||||
|
*/
|
||||||
|
public saveAll(): void {
|
||||||
|
this.repo.bulkUpdate(this.configItems).then(result => {
|
||||||
|
this.errors = result.errors;
|
||||||
|
if (Object.keys(result.errors).length === 0) {
|
||||||
|
this.configItems = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This resets all values to their defaults.
|
||||||
|
*/
|
||||||
|
public resetAll(): void {
|
||||||
|
this.repo.resetGroups([this.configGroup.name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns, if there are changes depending on the `configMap`.
|
||||||
|
*
|
||||||
|
* @returns True, if the array `configMap` has at least one member.
|
||||||
|
*/
|
||||||
|
public hasChanges(): boolean {
|
||||||
|
return !!this.configItems.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearSubscription(): void {
|
||||||
|
if (this.configGroupSubscription) {
|
||||||
|
this.configGroupSubscription.unsubscribe();
|
||||||
|
this.configGroupSubscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
this.clearSubscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle-hook to hook into, before the route changes.
|
||||||
|
*
|
||||||
|
* @returns The answer of the user, if he made changes, `true` otherwise.
|
||||||
|
*/
|
||||||
|
public async canDeactivate(): Promise<boolean> {
|
||||||
|
if (this.hasChanges()) {
|
||||||
|
const title = this.translate.instant('Do you really want to exit this page?');
|
||||||
|
const content = this.translate.instant('You made changes.');
|
||||||
|
return await this.promptDialog.open(title, content);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
<os-head-bar [isSearchEnabled]="false">
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="title-slot">
|
||||||
|
<h2 translate>Settings</h2>
|
||||||
|
</div>
|
||||||
|
<!-- Menu -->
|
||||||
|
<div class="menu-slot">
|
||||||
|
<button type="button" mat-icon-button [matMenuTriggerFor]="settingsMenu"><mat-icon>more_vert</mat-icon></button>
|
||||||
|
</div>
|
||||||
|
</os-head-bar>
|
||||||
|
|
||||||
|
<os-grid-layout>
|
||||||
|
<os-block-tile
|
||||||
|
*ngFor="let group of this.groups"
|
||||||
|
orientation="vertical"
|
||||||
|
[only]="'content'"
|
||||||
|
[preferredSize]="2"
|
||||||
|
[isSquare]="true"
|
||||||
|
>
|
||||||
|
<ng-container class="block-node">
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td [ngSwitch]="group">
|
||||||
|
<mat-icon *ngSwitchCase="'General'">home</mat-icon>
|
||||||
|
<mat-icon *ngSwitchCase="'Agenda'">today</mat-icon>
|
||||||
|
<mat-icon *ngSwitchCase="'Motions'">assignment</mat-icon>
|
||||||
|
<mat-icon *ngSwitchCase="'Elections'">how_to_vote</mat-icon>
|
||||||
|
<mat-icon *ngSwitchCase="'Participants'">groups</mat-icon>
|
||||||
|
<mat-icon *ngSwitchCase="'Custom translations'">language</mat-icon>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container class="block-content-node">
|
||||||
|
<a class="stretch-to-fill-parent" [routerLink]="group.toLowerCase()"></a>
|
||||||
|
<div class="link-wrapper">
|
||||||
|
{{ group | translate }}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</os-block-tile>
|
||||||
|
</os-grid-layout>
|
||||||
|
|
||||||
|
<mat-menu #settingsMenu="matMenu">
|
||||||
|
<button mat-menu-item (click)="resetAll()">
|
||||||
|
<mat-icon>undo</mat-icon>
|
||||||
|
<span translate>Reset every group to default</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
@ -0,0 +1,14 @@
|
|||||||
|
.parent-wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding: 50%;
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-wrapper {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 10% 0;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ConfigOverviewComponent } from './config-overview.component';
|
||||||
|
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||||
|
|
||||||
|
describe('ConfigOverviewComponent', () => {
|
||||||
|
let component: ConfigOverviewComponent;
|
||||||
|
let fixture: ComponentFixture<ConfigOverviewComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [ConfigOverviewComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ConfigOverviewComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,50 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { BaseComponent } from 'app/base.component';
|
||||||
|
import { ConfigRepositoryService } from 'app/core/repositories/config/config-repository.service';
|
||||||
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List view for the global settings
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-config-overview',
|
||||||
|
templateUrl: './config-overview.component.html',
|
||||||
|
styleUrls: ['./config-overview.component.scss']
|
||||||
|
})
|
||||||
|
export class ConfigOverviewComponent extends BaseComponent implements OnInit {
|
||||||
|
public groups: string[] = [];
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected titleService: Title,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
public repo: ConfigRepositoryService,
|
||||||
|
private promptDialog: PromptService
|
||||||
|
) {
|
||||||
|
super(titleService, translate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the title, inits the table and calls the repo
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {
|
||||||
|
super.setTitle('Settings');
|
||||||
|
|
||||||
|
this.repo.availableGroupsOberservable.subscribe(groups => {
|
||||||
|
this.groups = groups;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets every config for all registered group.
|
||||||
|
*/
|
||||||
|
public async resetAll(): Promise<void> {
|
||||||
|
const title = this.translate.instant('Are you sure, you want to reset all groups?');
|
||||||
|
if (await this.promptDialog.open(title)) {
|
||||||
|
await this.repo.resetGroups(this.groups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,32 @@
|
|||||||
<!-- Add new translation button -->
|
<form [formGroup]="translationForm">
|
||||||
<div *ngFor="let translation of translations; let i = index">
|
<div
|
||||||
<form>
|
class="inline-form"
|
||||||
<mat-form-field>
|
formArrayName="translationBoxes"
|
||||||
<input matInput [value]="translation.original" (input)="onChangeOriginal($event.target.value, i)" />
|
*ngFor="let translation of translationBoxes.controls; let i = index"
|
||||||
</mat-form-field>
|
>
|
||||||
<mat-icon>arrow_forward</mat-icon>
|
<ng-container [formGroupName]="i">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input matInput [value]="translation.translation" (input)="onChangeTranslation($event.target.value, i)" />
|
<input formControlName="original" matInput placeholder="{{ 'Original' | translate }}" />
|
||||||
</mat-form-field>
|
<mat-error translate>You have to fill this field.</mat-error>
|
||||||
<button mat-icon-button>
|
</mat-form-field>
|
||||||
<mat-icon (click)="onRemoveTranslation(i)">close</mat-icon>
|
<mat-icon>arrow_forward</mat-icon>
|
||||||
</button>
|
<mat-form-field>
|
||||||
</form>
|
<input formControlName="translation" matInput placeholder="{{ 'Translation' | translate }}" />
|
||||||
</div>
|
<mat-error translate>You have to fill this field.</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
type="button"
|
||||||
|
(click)="onRemoveTranslation(i)"
|
||||||
|
matTooltip="{{ 'Cancel' | translate }}"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Add new translation button -->
|
<!-- Add new translation button -->
|
||||||
<button mat-button (click)="onAddNewTranslation()">{{ 'Add new custom translation' | translate }}</button>
|
<button mat-button (click)="addNewTranslation()">
|
||||||
|
<mat-icon>add</mat-icon>{{ 'Add new custom translation' | translate }}
|
||||||
|
</button>
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
.inline-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
import { Component, forwardRef } from '@angular/core';
|
import { Component, forwardRef, OnInit } from '@angular/core';
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
import { ControlValueAccessor, FormArray, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
|
||||||
|
|
||||||
import { CustomTranslation, CustomTranslations } from 'app/core/translate/translation-parser';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom translations as custom form component
|
* Custom translations as custom form component
|
||||||
@ -23,16 +21,39 @@ import { CustomTranslation, CustomTranslations } from 'app/core/translate/transl
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CustomTranslationComponent implements ControlValueAccessor {
|
export class CustomTranslationComponent implements ControlValueAccessor, OnInit {
|
||||||
/**
|
/**
|
||||||
* Holds the custom translations in a list
|
* The parent form-group.
|
||||||
*/
|
*/
|
||||||
public translations: CustomTranslations = [];
|
public translationForm: FormGroup;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empty constructor
|
* Reference to the form-control within the `translationForm`.
|
||||||
*/
|
*/
|
||||||
public constructor() {}
|
public translationBoxes: FormArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor.
|
||||||
|
*
|
||||||
|
* @param fb FormBuilder
|
||||||
|
*/
|
||||||
|
public constructor(private fb: FormBuilder) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the form-controls.
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.translationForm = this.fb.group({
|
||||||
|
translationBoxes: this.fb.array([])
|
||||||
|
});
|
||||||
|
|
||||||
|
this.translationBoxes = this.translationForm.get('translationBoxes') as FormArray;
|
||||||
|
this.translationBoxes.valueChanges.subscribe(value => {
|
||||||
|
if (this.translationBoxes.valid) {
|
||||||
|
this.propagateChange(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to determine which information to give to the parent form
|
* Helper function to determine which information to give to the parent form
|
||||||
@ -46,7 +67,9 @@ export class CustomTranslationComponent implements ControlValueAccessor {
|
|||||||
*/
|
*/
|
||||||
public writeValue(obj: any): void {
|
public writeValue(obj: any): void {
|
||||||
if (obj) {
|
if (obj) {
|
||||||
this.translations = obj;
|
for (const item of obj) {
|
||||||
|
this.addNewTranslation(item.original, item.translation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,46 +96,28 @@ export class CustomTranslationComponent implements ControlValueAccessor {
|
|||||||
*/
|
*/
|
||||||
public setDisabledState?(isDisabled: boolean): void {}
|
public setDisabledState?(isDisabled: boolean): void {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects changes to the "original" word
|
|
||||||
*
|
|
||||||
* @param value the value that was typed
|
|
||||||
* @param index the index of the change
|
|
||||||
*/
|
|
||||||
public onChangeOriginal(value: string, index: number): void {
|
|
||||||
this.translations[index].original = value;
|
|
||||||
this.propagateChange(this.translations);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects changes to the translation
|
|
||||||
* @param value the value that was typed
|
|
||||||
* @param index the index of the change
|
|
||||||
*/
|
|
||||||
public onChangeTranslation(value: string, index: number): void {
|
|
||||||
this.translations[index].translation = value;
|
|
||||||
this.propagateChange(this.translations);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a custom translation
|
* Removes a custom translation
|
||||||
|
*
|
||||||
* @param index the translation to remove
|
* @param index the translation to remove
|
||||||
*/
|
*/
|
||||||
public onRemoveTranslation(index: number): void {
|
public onRemoveTranslation(index: number): void {
|
||||||
this.translations.splice(index, 1);
|
this.translationBoxes.removeAt(index);
|
||||||
this.propagateChange(this.translations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new custom translation to the list and to the server
|
* Function to add a new translation-field to the form-array.
|
||||||
|
* If strings are passed, they are passed as the fields' value.
|
||||||
|
*
|
||||||
|
* @param original The original string to translate.
|
||||||
|
* @param translation The translation for the given string.
|
||||||
*/
|
*/
|
||||||
public onAddNewTranslation(): void {
|
public addNewTranslation(original: string = '', translation: string = ''): void {
|
||||||
const newCustomTranslation: CustomTranslation = {
|
this.translationBoxes.push(
|
||||||
original: 'New',
|
this.fb.group({
|
||||||
translation: 'New'
|
original: [original, Validators.required],
|
||||||
};
|
translation: [translation, Validators.required]
|
||||||
|
})
|
||||||
this.translations.push(newCustomTranslation);
|
);
|
||||||
this.propagateChange(this.translations);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { WatchForChangesGuard } from 'app/shared/utils/watch-for-changes.guard';
|
||||||
import { ConfigListComponent } from './components/config-list/config-list.component';
|
import { ConfigListComponent } from './components/config-list/config-list.component';
|
||||||
|
import { ConfigOverviewComponent } from './components/config-overview/config-overview.component';
|
||||||
|
|
||||||
const routes: Routes = [{ path: '', component: ConfigListComponent, pathMatch: 'full' }];
|
const routes: Routes = [
|
||||||
|
{ path: '', component: ConfigOverviewComponent, pathMatch: 'full' },
|
||||||
|
{ path: ':group', component: ConfigListComponent, canDeactivate: [WatchForChangesGuard] }
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
@ -3,13 +3,14 @@ import { NgModule } from '@angular/core';
|
|||||||
|
|
||||||
import { ConfigFieldComponent } from './components/config-field/config-field.component';
|
import { ConfigFieldComponent } from './components/config-field/config-field.component';
|
||||||
import { ConfigListComponent } from './components/config-list/config-list.component';
|
import { ConfigListComponent } from './components/config-list/config-list.component';
|
||||||
|
import { ConfigOverviewComponent } from './components/config-overview/config-overview.component';
|
||||||
import { ConfigRoutingModule } from './config-routing.module';
|
import { ConfigRoutingModule } from './config-routing.module';
|
||||||
import { CustomTranslationComponent } from './components/custom-translation/custom-translation.component';
|
import { CustomTranslationComponent } from './components/custom-translation/custom-translation.component';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, ConfigRoutingModule, SharedModule],
|
imports: [CommonModule, ConfigRoutingModule, SharedModule],
|
||||||
declarations: [ConfigListComponent, ConfigFieldComponent, CustomTranslationComponent],
|
declarations: [ConfigOverviewComponent, ConfigListComponent, ConfigFieldComponent, CustomTranslationComponent],
|
||||||
entryComponents: [CustomTranslationComponent]
|
entryComponents: [CustomTranslationComponent]
|
||||||
})
|
})
|
||||||
export class ConfigModule {}
|
export class ConfigModule {}
|
||||||
|
@ -1,37 +1,6 @@
|
|||||||
import { Config } from 'app/shared/models/core/config';
|
import { Config, ConfigChoice, ConfigData, ConfigInputType } from 'app/shared/models/core/config';
|
||||||
import { BaseViewModel } from '../../base/base-view-model';
|
import { BaseViewModel } from '../../base/base-view-model';
|
||||||
|
|
||||||
interface ConfigChoice {
|
|
||||||
value: string;
|
|
||||||
displayName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All valid input types for config variables.
|
|
||||||
*/
|
|
||||||
type ConfigInputType =
|
|
||||||
| 'text'
|
|
||||||
| 'string'
|
|
||||||
| 'boolean'
|
|
||||||
| 'markupText'
|
|
||||||
| 'integer'
|
|
||||||
| '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[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigTitleInformation {
|
export interface ConfigTitleInformation {
|
||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
@ -43,22 +12,6 @@ export class ViewConfig extends BaseViewModel<Config> implements ConfigTitleInfo
|
|||||||
public static COLLECTIONSTRING = Config.COLLECTIONSTRING;
|
public static COLLECTIONSTRING = Config.COLLECTIONSTRING;
|
||||||
protected _collectionString = Config.COLLECTIONSTRING;
|
protected _collectionString = Config.COLLECTIONSTRING;
|
||||||
|
|
||||||
/* This private members are set by setConstantsInfo. */
|
|
||||||
private _helpText: string;
|
|
||||||
private _inputType: ConfigInputType;
|
|
||||||
private _label: string;
|
|
||||||
private _choices: ConfigChoice[];
|
|
||||||
private _defaultValue: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves, if this config already got constants information.
|
|
||||||
*/
|
|
||||||
private _hasConstantsInfo = false;
|
|
||||||
|
|
||||||
public get hasConstantsInfo(): boolean {
|
|
||||||
return this._hasConstantsInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get config(): Config {
|
public get config(): Config {
|
||||||
return this._model;
|
return this._model;
|
||||||
}
|
}
|
||||||
@ -67,28 +20,48 @@ export class ViewConfig extends BaseViewModel<Config> implements ConfigTitleInfo
|
|||||||
return this.config.key;
|
return this.config.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get value(): Object {
|
public get value(): any {
|
||||||
return this.config.value;
|
return this.config.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get data(): ConfigData | null {
|
||||||
|
return this.config.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hidden(): boolean {
|
||||||
|
return !this.data;
|
||||||
|
}
|
||||||
|
|
||||||
public get label(): string {
|
public get label(): string {
|
||||||
return this._label;
|
return this.data.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get inputType(): ConfigInputType {
|
public get inputType(): ConfigInputType | null {
|
||||||
return this._inputType;
|
return this.data.inputType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get helpText(): string {
|
public get helpText(): string | null {
|
||||||
return this._helpText;
|
return this.data.helpText;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get choices(): Object {
|
public get choices(): ConfigChoice[] | null {
|
||||||
return this._choices;
|
return this.data.choices;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get defaultValue(): any {
|
public get defaultValue(): any {
|
||||||
return this._defaultValue;
|
return this.data.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get weight(): number {
|
||||||
|
return this.hidden ? 0 : this.data.weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get group(): string {
|
||||||
|
return this.data.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get subgroup(): string | null {
|
||||||
|
return this.data.subgroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,19 +78,4 @@ export class ViewConfig extends BaseViewModel<Config> implements ConfigTitleInfo
|
|||||||
return 100;
|
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;
|
|
||||||
if (constant.default_value !== undefined) {
|
|
||||||
this._defaultValue = constant.default_value;
|
|
||||||
}
|
|
||||||
this._hasConstantsInfo = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar [mainButton]="canEdit" [multiSelectMode]="isMultiSelect" (mainEvent)="onMainEvent()">
|
<os-head-bar [hasMainButton]="canEdit" [multiSelectMode]="isMultiSelect" (mainEvent)="onMainEvent()">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 translate>Files</h2>
|
<h2 translate>Files</h2>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
import { WatchSortingTreeGuard } from 'app/shared/utils/watch-sorting-tree.guard';
|
import { WatchForChangesGuard } from 'app/shared/utils/watch-for-changes.guard';
|
||||||
import { CallListComponent } from './call-list.component';
|
import { CallListComponent } from './call-list.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: CallListComponent, pathMatch: 'full', canDeactivate: [WatchSortingTreeGuard] }
|
{ path: '', component: CallListComponent, pathMatch: 'full', canDeactivate: [WatchForChangesGuard] }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
import { WatchSortingTreeGuard } from 'app/shared/utils/watch-sorting-tree.guard';
|
import { WatchForChangesGuard } from 'app/shared/utils/watch-for-changes.guard';
|
||||||
import { CategoriesSortComponent } from './components/categories-sort/categories-sort.component';
|
import { CategoriesSortComponent } from './components/categories-sort/categories-sort.component';
|
||||||
import { CategoryDetailComponent } from './components/category-detail/category-detail.component';
|
import { CategoryDetailComponent } from './components/category-detail/category-detail.component';
|
||||||
import { CategoryListComponent } from './components/category-list/category-list.component';
|
import { CategoryListComponent } from './components/category-list/category-list.component';
|
||||||
@ -9,8 +9,8 @@ import { CategoryMotionsSortComponent } from './components/category-motions-sort
|
|||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: CategoryListComponent, pathMatch: 'full' },
|
{ path: '', component: CategoryListComponent, pathMatch: 'full' },
|
||||||
{ path: ':id/sort', component: CategoryMotionsSortComponent, canDeactivate: [WatchSortingTreeGuard] },
|
{ path: ':id/sort', component: CategoryMotionsSortComponent, canDeactivate: [WatchForChangesGuard] },
|
||||||
{ path: 'sort', component: CategoriesSortComponent, canDeactivate: [WatchSortingTreeGuard] },
|
{ path: 'sort', component: CategoriesSortComponent, canDeactivate: [WatchForChangesGuard] },
|
||||||
{ path: ':id', component: CategoryDetailComponent }
|
{ path: ':id', component: CategoryDetailComponent }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { Observable } from 'rxjs';
|
|||||||
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
|
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
|
||||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
|
import { CanComponentDeactivate } from 'app/shared/utils/watch-for-changes.guard';
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { ViewCategory } from 'app/site/motions/models/view-category';
|
import { ViewCategory } from 'app/site/motions/models/view-category';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="canEdit" (mainEvent)="onPlusButton()">
|
<os-head-bar prevUrl="../.." [nav]="false" [hasMainButton]="canEdit" (mainEvent)="onPlusButton()">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 translate>Categories</h2>
|
<h2 translate>Categories</h2>
|
||||||
|
@ -11,7 +11,7 @@ import { MotionRepositoryService } from 'app/core/repositories/motions/motion-re
|
|||||||
import { ChoiceService } from 'app/core/ui-services/choice.service';
|
import { ChoiceService } from 'app/core/ui-services/choice.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { SortingListComponent } from 'app/shared/components/sorting-list/sorting-list.component';
|
import { SortingListComponent } from 'app/shared/components/sorting-list/sorting-list.component';
|
||||||
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
|
import { CanComponentDeactivate } from 'app/shared/utils/watch-for-changes.guard';
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { ViewCategory } from 'app/site/motions/models/view-category';
|
import { ViewCategory } from 'app/site/motions/models/view-category';
|
||||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="canEdit" (mainEvent)="onPlusButton()">
|
<os-head-bar prevUrl="../.." [nav]="false" [hasMainButton]="canEdit" (mainEvent)="onPlusButton()">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot"><h2 translate>Motion blocks</h2></div>
|
<div class="title-slot"><h2 translate>Motion blocks</h2></div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="true" (mainEvent)="openDialog()">
|
<os-head-bar prevUrl="../.." [nav]="false" [hasMainButton]="true" (mainEvent)="openDialog()">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 translate>Comment fields</h2>
|
<h2 translate>Comment fields</h2>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<os-head-bar
|
<os-head-bar
|
||||||
[mainButton]="perms.isAllowed('can_create_amendments', motion)"
|
[hasMainButton]="perms.isAllowed('can_create_amendments', motion)"
|
||||||
mainActionTooltip="New amendment"
|
mainActionTooltip="New amendment"
|
||||||
[prevUrl]="getPrevUrl()"
|
[prevUrl]="getPrevUrl()"
|
||||||
[nav]="false"
|
[nav]="false"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar [mainButton]="perms.isAllowed('create')" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
<os-head-bar [hasMainButton]="perms.isAllowed('create')" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot"><h2 translate>Motions</h2></div>
|
<div class="title-slot"><h2 translate>Motions</h2></div>
|
||||||
|
|
||||||
@ -315,6 +315,14 @@
|
|||||||
<span translate>Import</span>
|
<span translate>Import</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<button mat-menu-item *osPerms="'core.can_manage_config'" routerLink="/settings/motions">
|
||||||
|
<mat-icon>settings</mat-icon>
|
||||||
|
<span translate>Settings</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="isMultiSelect">
|
<div *ngIf="isMultiSelect">
|
||||||
<button mat-menu-item (click)="selectAll()">
|
<button mat-menu-item (click)="selectAll()">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar [nav]="false" [mainButton]="true" (mainEvent)="onNewStateButton()">
|
<os-head-bar [nav]="false" [hasMainButton]="true" (mainEvent)="onNewStateButton()">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 *ngIf="workflow">
|
<h2 *ngIf="workflow">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="true" (mainEvent)="onNewButton(newWorkflowDialog)">
|
<os-head-bar prevUrl="../.." [nav]="false" [hasMainButton]="true" (mainEvent)="onNewButton(newWorkflowDialog)">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot"><h2 translate>Workflows</h2></div>
|
<div class="title-slot"><h2 translate>Workflows</h2></div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar prevUrl="../.." [nav]="false" [mainButton]="true" (mainEvent)="openDialog()">
|
<os-head-bar prevUrl="../.." [nav]="false" [hasMainButton]="true" (mainEvent)="openDialog()">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 translate>Statute</h2>
|
<h2 translate>Statute</h2>
|
||||||
@ -54,7 +54,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button mat-menu-item *osPerms="'motions.can_manage'" routerLink="import">
|
<button mat-menu-item *osPerms="'motions.can_manage'" routerLink="import">
|
||||||
<mat-icon>cloud_upload</mat-icon>
|
<mat-icon>cloud_upload</mat-icon>
|
||||||
<span translate>Import</span><span> ...</span>
|
<span translate>Import</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar [nav]="true" [mainButton]="canManage" (mainEvent)="onPlusButton()">
|
<os-head-bar [nav]="true" [hasMainButton]="canManage" (mainEvent)="onPlusButton()">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 translate>Projectors</h2>
|
<h2 translate>Projectors</h2>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar [mainButton]="true" [nav]="true" (mainEvent)="openTagDialog()" [multiSelectMode]="isMultiSelect">
|
<os-head-bar [hasMainButton]="true" [nav]="true" (mainEvent)="openTagDialog()" [multiSelectMode]="isMultiSelect">
|
||||||
<div class="title-slot"><h2 translate>Tags</h2></div>
|
<div class="title-slot"><h2 translate>Tags</h2></div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<os-head-bar
|
<os-head-bar
|
||||||
[mainButton]="isAllowed('edit')"
|
[hasMainButton]="isAllowed('edit')"
|
||||||
mainButtonIcon="edit"
|
mainButtonIcon="edit"
|
||||||
[nav]="false"
|
[nav]="false"
|
||||||
[goBack]="true"
|
[goBack]="true"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar [mainButton]="true" [nav]="false" (mainEvent)="setEditMode(!editGroup)">
|
<os-head-bar [hasMainButton]="true" [nav]="false" (mainEvent)="setEditMode(!editGroup)">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 translate>Groups</h2>
|
<h2 translate>Groups</h2>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar (mainEvent)="goBack()" [mainButton]="true" [nav]="false" [editMode]="true" (saveEvent)="save()">a
|
<os-head-bar (mainEvent)="goBack()" [hasMainButton]="true" [nav]="false" [editMode]="true" (saveEvent)="save()">a
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot"><h2 translate>Change password</h2></div>
|
<div class="title-slot"><h2 translate>Change password</h2></div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar [mainButton]="false" [nav]="false">
|
<os-head-bar [hasMainButton]="false" [nav]="false">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot"><h2 translate>Presence</h2></div>
|
<div class="title-slot"><h2 translate>Presence</h2></div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<os-head-bar
|
<os-head-bar
|
||||||
[mainButton]="isAllowed('changePersonal')"
|
[hasMainButton]="isAllowed('changePersonal')"
|
||||||
mainButtonIcon="edit"
|
mainButtonIcon="edit"
|
||||||
[nav]="false"
|
[nav]="false"
|
||||||
[goBack]="!isAllowed('seeOtherUsers')"
|
[goBack]="!isAllowed('seeOtherUsers')"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar [mainButton]="canAddUser" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
<os-head-bar [hasMainButton]="canAddUser" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot"><h2 translate>Participants</h2></div>
|
<div class="title-slot"><h2 translate>Participants</h2></div>
|
||||||
|
|
||||||
@ -124,9 +124,9 @@
|
|||||||
|
|
||||||
<!-- Presence -->
|
<!-- Presence -->
|
||||||
<button mat-menu-item (click)="setPresent(user)">
|
<button mat-menu-item (click)="setPresent(user)">
|
||||||
<mat-icon color="accent"> {{ user.is_present ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
|
<mat-icon color="accent"> {{ user.is_present ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
|
||||||
<span translate>Present</span>
|
<span translate>Present</span>
|
||||||
</button>
|
</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
@ -166,7 +166,15 @@
|
|||||||
|
|
||||||
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="import">
|
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="import">
|
||||||
<mat-icon>cloud_upload</mat-icon>
|
<mat-icon>cloud_upload</mat-icon>
|
||||||
<span translate>Import</span><span> ...</span>
|
<span translate>Import</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<button mat-menu-item *osPerms="'core.can_manage_config'" routerLink="/settings/participants">
|
||||||
|
<mat-icon>settings</mat-icon>
|
||||||
|
<span translate>Settings</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="isMultiSelect">
|
<div *ngIf="isMultiSelect">
|
||||||
@ -199,11 +207,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="import">
|
|
||||||
<mat-icon>save_alt</mat-icon>
|
|
||||||
<span translate>Import</span><span> ...</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div *osPerms="'users.can_manage'">
|
<div *osPerms="'users.can_manage'">
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
@ -34,7 +34,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
//custom table header for search button, filtering and more. Used in ListViews
|
//custom table header for search button, filtering and more. Used in ListViews
|
||||||
.custom-table-header {
|
.custom-table-header,
|
||||||
|
.background--default {
|
||||||
background: mat-color($background, background);
|
background: mat-color($background, background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ def get_config_variables():
|
|||||||
help_text="Input format: DD.MM.YYYY HH:MM",
|
help_text="Input format: DD.MM.YYYY HH:MM",
|
||||||
weight=200,
|
weight=200,
|
||||||
group="Agenda",
|
group="Agenda",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -30,7 +29,6 @@ def get_config_variables():
|
|||||||
label="Show subtitles in the agenda",
|
label="Show subtitles in the agenda",
|
||||||
weight=201,
|
weight=201,
|
||||||
group="Agenda",
|
group="Agenda",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Numbering
|
# Numbering
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from collections import OrderedDict
|
from typing import Any, Dict
|
||||||
from operator import attrgetter
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -127,8 +125,6 @@ class CoreAppConfig(AppConfig):
|
|||||||
yield self.get_model(model_name)
|
yield self.get_model(model_name)
|
||||||
|
|
||||||
def get_angular_constants(self):
|
def get_angular_constants(self):
|
||||||
from .config import config
|
|
||||||
|
|
||||||
constants: Dict[str, Any] = {}
|
constants: Dict[str, Any] = {}
|
||||||
|
|
||||||
# Client settings
|
# Client settings
|
||||||
@ -147,34 +143,7 @@ class CoreAppConfig(AppConfig):
|
|||||||
pass
|
pass
|
||||||
constants["Settings"] = client_settings_dict
|
constants["Settings"] = client_settings_dict
|
||||||
|
|
||||||
# Config variables
|
|
||||||
config_groups: List[Any] = []
|
|
||||||
for config_variable in sorted(
|
|
||||||
config.config_variables.values(), key=attrgetter("weight")
|
|
||||||
):
|
|
||||||
if config_variable.is_hidden():
|
|
||||||
# Skip hidden config variables. Do not even check groups and subgroups.
|
|
||||||
continue
|
|
||||||
if not config_groups or config_groups[-1]["name"] != config_variable.group:
|
|
||||||
# Add new group.
|
|
||||||
config_groups.append(
|
|
||||||
OrderedDict(name=config_variable.group, subgroups=[])
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
not config_groups[-1]["subgroups"]
|
|
||||||
or config_groups[-1]["subgroups"][-1]["name"]
|
|
||||||
!= config_variable.subgroup
|
|
||||||
):
|
|
||||||
# Add new subgroup.
|
|
||||||
config_groups[-1]["subgroups"].append(
|
|
||||||
OrderedDict(name=config_variable.subgroup, items=[])
|
|
||||||
)
|
|
||||||
# Add the config variable to the current group and subgroup.
|
|
||||||
config_groups[-1]["subgroups"][-1]["items"].append(config_variable.data)
|
|
||||||
constants["ConfigVariables"] = config_groups
|
|
||||||
|
|
||||||
constants["SchemaVersion"] = schema_version_handler.get()
|
constants["SchemaVersion"] = schema_version_handler.get()
|
||||||
|
|
||||||
return constants
|
return constants
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ INPUT_TYPE_MAPPING = {
|
|||||||
"translations": list,
|
"translations": list,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ALLOWED_NONE = ("datetimepicker",)
|
||||||
|
|
||||||
build_key_to_id_lock = asyncio.Lock()
|
build_key_to_id_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
@ -119,12 +121,15 @@ class ConfigHandler:
|
|||||||
expected_type = INPUT_TYPE_MAPPING[config_variable.input_type]
|
expected_type = INPUT_TYPE_MAPPING[config_variable.input_type]
|
||||||
|
|
||||||
# Try to convert value into the expected datatype
|
# Try to convert value into the expected datatype
|
||||||
try:
|
if value is None and config_variable.input_type not in ALLOWED_NONE:
|
||||||
value = expected_type(value)
|
raise ConfigError(f"Got None for {key}")
|
||||||
except ValueError:
|
elif value is not None:
|
||||||
raise ConfigError(
|
try:
|
||||||
f"Wrong datatype. Expected {expected_type}, got {type(value)}."
|
value = expected_type(value)
|
||||||
)
|
except (ValueError, TypeError):
|
||||||
|
raise ConfigError(
|
||||||
|
f"Wrong datatype. Expected {expected_type}, got {type(value)}."
|
||||||
|
)
|
||||||
|
|
||||||
if config_variable.input_type == "choice":
|
if config_variable.input_type == "choice":
|
||||||
# Choices can be a callable. In this case call it at this place
|
# Choices can be a callable. In this case call it at this place
|
||||||
@ -267,12 +272,14 @@ OnChangeType = Callable[[], None]
|
|||||||
ConfigVariableDict = TypedDict(
|
ConfigVariableDict = TypedDict(
|
||||||
"ConfigVariableDict",
|
"ConfigVariableDict",
|
||||||
{
|
{
|
||||||
"key": str,
|
"defaultValue": Any,
|
||||||
"default_value": Any,
|
"inputType": str,
|
||||||
"input_type": str,
|
|
||||||
"label": str,
|
"label": str,
|
||||||
"help_text": str,
|
"helpText": str,
|
||||||
"choices": ChoiceType,
|
"choices": ChoiceType,
|
||||||
|
"weight": int,
|
||||||
|
"group": str,
|
||||||
|
"subgroup": Optional[str],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -314,8 +321,8 @@ class ConfigVariable:
|
|||||||
choices: ChoiceCallableType = None,
|
choices: ChoiceCallableType = None,
|
||||||
hidden: bool = False,
|
hidden: bool = False,
|
||||||
weight: int = 0,
|
weight: int = 0,
|
||||||
group: str = None,
|
group: str = "General",
|
||||||
subgroup: str = None,
|
subgroup: str = "General",
|
||||||
validators: ValidatorsType = None,
|
validators: ValidatorsType = None,
|
||||||
on_change: OnChangeType = None,
|
on_change: OnChangeType = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -339,28 +346,26 @@ class ConfigVariable:
|
|||||||
self.choices = choices
|
self.choices = choices
|
||||||
self.hidden = hidden
|
self.hidden = hidden
|
||||||
self.weight = weight
|
self.weight = weight
|
||||||
self.group = group or "General"
|
self.group = group
|
||||||
self.subgroup = subgroup
|
self.subgroup = subgroup
|
||||||
self.validators = validators or ()
|
self.validators = validators or ()
|
||||||
self.on_change = on_change
|
self.on_change = on_change
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self) -> ConfigVariableDict:
|
def data(self) -> Optional[ConfigVariableDict]:
|
||||||
"""
|
"""
|
||||||
Property with all data for AngularJS variable on startup.
|
Property with all data for Angular variable on startup.
|
||||||
"""
|
"""
|
||||||
return ConfigVariableDict(
|
if self.hidden:
|
||||||
key=self.name,
|
return None
|
||||||
default_value=self.default_value,
|
|
||||||
input_type=self.input_type,
|
|
||||||
label=self.label,
|
|
||||||
help_text=self.help_text,
|
|
||||||
choices=self.choices() if callable(self.choices) else self.choices,
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_hidden(self) -> bool:
|
return ConfigVariableDict(
|
||||||
"""
|
defaultValue=self.default_value,
|
||||||
Returns True if the config variable is hidden so it can be removed
|
inputType=self.input_type,
|
||||||
from response of OPTIONS request.
|
label=self.label,
|
||||||
"""
|
helpText=self.help_text,
|
||||||
return self.hidden
|
choices=self.choices() if callable(self.choices) else self.choices,
|
||||||
|
weight=self.weight,
|
||||||
|
group=self.group,
|
||||||
|
subgroup=self.subgroup,
|
||||||
|
)
|
||||||
|
@ -18,7 +18,6 @@ def get_config_variables():
|
|||||||
default_value="OpenSlides",
|
default_value="OpenSlides",
|
||||||
label="Event name",
|
label="Event name",
|
||||||
weight=110,
|
weight=110,
|
||||||
group="General",
|
|
||||||
subgroup="Event",
|
subgroup="Event",
|
||||||
validators=(MaxLengthValidator(100),),
|
validators=(MaxLengthValidator(100),),
|
||||||
)
|
)
|
||||||
@ -28,7 +27,6 @@ def get_config_variables():
|
|||||||
default_value="Presentation and assembly system",
|
default_value="Presentation and assembly system",
|
||||||
label="Short description of event",
|
label="Short description of event",
|
||||||
weight=115,
|
weight=115,
|
||||||
group="General",
|
|
||||||
subgroup="Event",
|
subgroup="Event",
|
||||||
validators=(MaxLengthValidator(100),),
|
validators=(MaxLengthValidator(100),),
|
||||||
)
|
)
|
||||||
@ -38,7 +36,6 @@ def get_config_variables():
|
|||||||
default_value="",
|
default_value="",
|
||||||
label="Event date",
|
label="Event date",
|
||||||
weight=120,
|
weight=120,
|
||||||
group="General",
|
|
||||||
subgroup="Event",
|
subgroup="Event",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,7 +44,6 @@ def get_config_variables():
|
|||||||
default_value="",
|
default_value="",
|
||||||
label="Event location",
|
label="Event location",
|
||||||
weight=125,
|
weight=125,
|
||||||
group="General",
|
|
||||||
subgroup="Event",
|
subgroup="Event",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -60,7 +56,6 @@ def get_config_variables():
|
|||||||
input_type="markupText",
|
input_type="markupText",
|
||||||
label="Legal notice",
|
label="Legal notice",
|
||||||
weight=132,
|
weight=132,
|
||||||
group="General",
|
|
||||||
subgroup="Event",
|
subgroup="Event",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -70,7 +65,6 @@ def get_config_variables():
|
|||||||
input_type="markupText",
|
input_type="markupText",
|
||||||
label="Privacy policy",
|
label="Privacy policy",
|
||||||
weight=132,
|
weight=132,
|
||||||
group="General",
|
|
||||||
subgroup="Event",
|
subgroup="Event",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,7 +73,6 @@ def get_config_variables():
|
|||||||
default_value="Welcome to OpenSlides",
|
default_value="Welcome to OpenSlides",
|
||||||
label="Front page title",
|
label="Front page title",
|
||||||
weight=134,
|
weight=134,
|
||||||
group="General",
|
|
||||||
subgroup="Event",
|
subgroup="Event",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,7 +82,6 @@ def get_config_variables():
|
|||||||
input_type="markupText",
|
input_type="markupText",
|
||||||
label="Front page text",
|
label="Front page text",
|
||||||
weight=136,
|
weight=136,
|
||||||
group="General",
|
|
||||||
subgroup="Event",
|
subgroup="Event",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -101,7 +93,6 @@ def get_config_variables():
|
|||||||
input_type="boolean",
|
input_type="boolean",
|
||||||
label="Allow access for anonymous guest users",
|
label="Allow access for anonymous guest users",
|
||||||
weight=138,
|
weight=138,
|
||||||
group="General",
|
|
||||||
subgroup="System",
|
subgroup="System",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -110,7 +101,6 @@ def get_config_variables():
|
|||||||
default_value="",
|
default_value="",
|
||||||
label="Show this text on the login page",
|
label="Show this text on the login page",
|
||||||
weight=140,
|
weight=140,
|
||||||
group="General",
|
|
||||||
subgroup="System",
|
subgroup="System",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -129,7 +119,6 @@ def get_config_variables():
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
weight=141,
|
weight=141,
|
||||||
group="General",
|
|
||||||
subgroup="System",
|
subgroup="System",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -140,7 +129,6 @@ def get_config_variables():
|
|||||||
default_value=",",
|
default_value=",",
|
||||||
label="Separator used for all csv exports and examples",
|
label="Separator used for all csv exports and examples",
|
||||||
weight=160,
|
weight=160,
|
||||||
group="General",
|
|
||||||
subgroup="Export",
|
subgroup="Export",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -154,7 +142,6 @@ def get_config_variables():
|
|||||||
{"value": "iso-8859-15", "display_name": "ISO-8859-15"},
|
{"value": "iso-8859-15", "display_name": "ISO-8859-15"},
|
||||||
),
|
),
|
||||||
weight=162,
|
weight=162,
|
||||||
group="General",
|
|
||||||
subgroup="Export",
|
subgroup="Export",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -169,7 +156,6 @@ def get_config_variables():
|
|||||||
{"value": "right", "display_name": "Right"},
|
{"value": "right", "display_name": "Right"},
|
||||||
),
|
),
|
||||||
weight=164,
|
weight=164,
|
||||||
group="General",
|
|
||||||
subgroup="Export",
|
subgroup="Export",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -184,7 +170,6 @@ def get_config_variables():
|
|||||||
{"value": "12", "display_name": "12"},
|
{"value": "12", "display_name": "12"},
|
||||||
),
|
),
|
||||||
weight=166,
|
weight=166,
|
||||||
group="General",
|
|
||||||
subgroup="Export",
|
subgroup="Export",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -198,7 +183,6 @@ def get_config_variables():
|
|||||||
{"value": "A5", "display_name": "DIN A5"},
|
{"value": "A5", "display_name": "DIN A5"},
|
||||||
),
|
),
|
||||||
weight=168,
|
weight=168,
|
||||||
group="General",
|
|
||||||
subgroup="Export",
|
subgroup="Export",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from ..core.config import config
|
||||||
from ..utils.projector import projector_slides
|
from ..utils.projector import projector_slides
|
||||||
from ..utils.rest_api import (
|
from ..utils.rest_api import (
|
||||||
Field,
|
Field,
|
||||||
IdPrimaryKeyRelatedField,
|
IdPrimaryKeyRelatedField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
ModelSerializer,
|
ModelSerializer,
|
||||||
|
SerializerMethodField,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
from ..utils.validate import validate_html
|
from ..utils.validate import validate_html
|
||||||
@ -136,10 +138,14 @@ class ConfigSerializer(ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
value = JSONSerializerField()
|
value = JSONSerializerField()
|
||||||
|
data = SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConfigStore
|
model = ConfigStore
|
||||||
fields = ("id", "key", "value")
|
fields = ("id", "key", "value", "data")
|
||||||
|
|
||||||
|
def get_data(self, db_config):
|
||||||
|
return config.config_variables[db_config.key].data
|
||||||
|
|
||||||
|
|
||||||
class ProjectorMessageSerializer(ModelSerializer):
|
class ProjectorMessageSerializer(ModelSerializer):
|
||||||
|
@ -37,6 +37,7 @@ from ..utils.rest_api import (
|
|||||||
RetrieveModelMixin,
|
RetrieveModelMixin,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
|
list_route,
|
||||||
)
|
)
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
ConfigAccessPermissions,
|
ConfigAccessPermissions,
|
||||||
@ -390,30 +391,46 @@ class ConfigViewSet(ModelViewSet):
|
|||||||
access_permissions = ConfigAccessPermissions()
|
access_permissions = ConfigAccessPermissions()
|
||||||
queryset = ConfigStore.objects.all()
|
queryset = ConfigStore.objects.all()
|
||||||
|
|
||||||
|
can_manage_config = None
|
||||||
|
can_manage_logos_and_fonts = None
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action == "metadata":
|
|
||||||
# Every authenticated user can see the metadata and list or
|
|
||||||
# retrieve the config. Anonymous users can do so if they are
|
|
||||||
# enabled.
|
|
||||||
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
|
||||||
elif self.action in ("partial_update", "update"):
|
elif self.action in ("partial_update", "update"):
|
||||||
# The user needs 'core.can_manage_logos_and_fonts' for all config values
|
result = self.check_config_permission(self.kwargs["pk"])
|
||||||
# starting with 'logo' and 'font'. For all other config values th euser needs
|
elif self.action == "reset_groups":
|
||||||
# the default permissions 'core.can_manage_config'.
|
result = has_perm(self.request.user, "core.can_manage_config")
|
||||||
pk = self.kwargs["pk"]
|
elif self.action == "bulk_update":
|
||||||
if pk.startswith("logo") or pk.startswith("font"):
|
result = True # will be checked in the view
|
||||||
result = has_perm(self.request.user, "core.can_manage_logos_and_fonts")
|
|
||||||
else:
|
|
||||||
result = has_perm(self.request.user, "core.can_manage_config")
|
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def check_config_permission(self, key):
|
||||||
|
"""
|
||||||
|
Checks the permissions for one config key.
|
||||||
|
Users needs 'core.can_manage_logos_and_fonts' for all config values starting
|
||||||
|
with 'logo' and 'font'. For all other config values the user needs the default
|
||||||
|
permissions 'core.can_manage_config'.
|
||||||
|
The result is cached for one request to reduce has_perm queries in e.g. bulk updates.
|
||||||
|
"""
|
||||||
|
if key.startswith("logo") or key.startswith("font"):
|
||||||
|
if self.can_manage_logos_and_fonts is None:
|
||||||
|
self.can_manage_logos_and_fonts = has_perm(
|
||||||
|
self.request.user, "core.can_manage_logos_and_fonts"
|
||||||
|
)
|
||||||
|
return self.can_manage_logos_and_fonts
|
||||||
|
else:
|
||||||
|
if self.can_manage_config is None:
|
||||||
|
self.can_manage_config = has_perm(
|
||||||
|
self.request.user, "core.can_manage_config"
|
||||||
|
)
|
||||||
|
return self.can_manage_config
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Updates a config variable. Only managers can do this.
|
Updates a config variable. Only managers can do this.
|
||||||
@ -422,8 +439,6 @@ class ConfigViewSet(ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
key = kwargs["pk"]
|
key = kwargs["pk"]
|
||||||
value = request.data.get("value")
|
value = request.data.get("value")
|
||||||
if value is None:
|
|
||||||
raise ValidationError({"detail": "Invalid input. Config value is missing."})
|
|
||||||
|
|
||||||
# Validate and change value.
|
# Validate and change value.
|
||||||
try:
|
try:
|
||||||
@ -436,6 +451,60 @@ class ConfigViewSet(ModelViewSet):
|
|||||||
# Return response.
|
# Return response.
|
||||||
return Response({"key": key, "value": value})
|
return Response({"key": key, "value": value})
|
||||||
|
|
||||||
|
@list_route(methods=["post"])
|
||||||
|
def bulk_update(self, request):
|
||||||
|
"""
|
||||||
|
Updates many config variables:
|
||||||
|
[{key: <key>, value: <value>}, ...]
|
||||||
|
"""
|
||||||
|
if not isinstance(request.data, list):
|
||||||
|
raise ValidationError({"detail": "The data needs to be a list"})
|
||||||
|
|
||||||
|
for entry in request.data:
|
||||||
|
key = entry.get("key")
|
||||||
|
if not isinstance(key, str):
|
||||||
|
raise ValidationError({"detail": "The key must be a string."})
|
||||||
|
if not config.exists(key):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "The key {0} does not exist.", "args": [key]}
|
||||||
|
)
|
||||||
|
if not self.check_config_permission(key):
|
||||||
|
self.permission_denied(request, message=key)
|
||||||
|
if "value" not in entry:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "Invalid input. Config value is missing."}
|
||||||
|
)
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
for entry in request.data:
|
||||||
|
try:
|
||||||
|
config[entry["key"]] = entry["value"]
|
||||||
|
except ConfigError as err:
|
||||||
|
errors[entry["key"]] = str(err)
|
||||||
|
|
||||||
|
return Response({"errors": errors})
|
||||||
|
|
||||||
|
@list_route(methods=["post"])
|
||||||
|
def reset_groups(self, request):
|
||||||
|
"""
|
||||||
|
Resets multiple groups. The request data contains all
|
||||||
|
(main) group names: [<group1>, ...]
|
||||||
|
"""
|
||||||
|
if not isinstance(request.data, list):
|
||||||
|
raise ValidationError("The data must be a list")
|
||||||
|
for group in request.data:
|
||||||
|
if not isinstance(group, str):
|
||||||
|
raise ValidationError("Every group must be a string")
|
||||||
|
|
||||||
|
for key, config_variable in config.config_variables.items():
|
||||||
|
if (
|
||||||
|
config_variable.group in request.data
|
||||||
|
and config[key] != config_variable.default_value
|
||||||
|
):
|
||||||
|
config[key] = config_variable.default_value
|
||||||
|
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class ProjectorMessageViewSet(ModelViewSet):
|
class ProjectorMessageViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -35,7 +35,6 @@ def get_config_variables():
|
|||||||
choices=get_workflow_choices,
|
choices=get_workflow_choices,
|
||||||
weight=310,
|
weight=310,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -46,7 +45,6 @@ def get_config_variables():
|
|||||||
choices=get_workflow_choices,
|
choices=get_workflow_choices,
|
||||||
weight=312,
|
weight=312,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -55,7 +53,6 @@ def get_config_variables():
|
|||||||
label="Motion preamble",
|
label="Motion preamble",
|
||||||
weight=320,
|
weight=320,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -70,7 +67,6 @@ def get_config_variables():
|
|||||||
),
|
),
|
||||||
weight=322,
|
weight=322,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -81,7 +77,6 @@ def get_config_variables():
|
|||||||
help_text="The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40",
|
help_text="The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40",
|
||||||
weight=323,
|
weight=323,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
validators=(MinValueValidator(40),),
|
validators=(MinValueValidator(40),),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -92,7 +87,6 @@ def get_config_variables():
|
|||||||
label="Reason required for creating new motion",
|
label="Reason required for creating new motion",
|
||||||
weight=324,
|
weight=324,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -102,7 +96,6 @@ def get_config_variables():
|
|||||||
label="Hide motion text on projector",
|
label="Hide motion text on projector",
|
||||||
weight=325,
|
weight=325,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -112,7 +105,6 @@ def get_config_variables():
|
|||||||
label="Hide reason on projector",
|
label="Hide reason on projector",
|
||||||
weight=326,
|
weight=326,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -122,7 +114,6 @@ def get_config_variables():
|
|||||||
label="Hide meta information box on projector",
|
label="Hide meta information box on projector",
|
||||||
weight=327,
|
weight=327,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -132,7 +123,6 @@ def get_config_variables():
|
|||||||
label="Hide recommendation on projector",
|
label="Hide recommendation on projector",
|
||||||
weight=328,
|
weight=328,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -142,7 +132,6 @@ def get_config_variables():
|
|||||||
label="Hide referring motions",
|
label="Hide referring motions",
|
||||||
weight=329,
|
weight=329,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -153,7 +142,6 @@ def get_config_variables():
|
|||||||
help_text="In motion list, motion detail and PDF.",
|
help_text="In motion list, motion detail and PDF.",
|
||||||
weight=330,
|
weight=330,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -163,7 +151,6 @@ def get_config_variables():
|
|||||||
help_text="Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.",
|
help_text="Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.",
|
||||||
weight=332,
|
weight=332,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -173,7 +160,6 @@ def get_config_variables():
|
|||||||
help_text="Will be displayed as label before selected recommendation in statute amendments.",
|
help_text="Will be displayed as label before selected recommendation in statute amendments.",
|
||||||
weight=333,
|
weight=333,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -189,7 +175,6 @@ def get_config_variables():
|
|||||||
),
|
),
|
||||||
weight=334,
|
weight=334,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -203,7 +188,6 @@ def get_config_variables():
|
|||||||
),
|
),
|
||||||
weight=335,
|
weight=335,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Numbering
|
# Numbering
|
||||||
|
@ -23,7 +23,6 @@ def get_config_variables():
|
|||||||
),
|
),
|
||||||
weight=510,
|
weight=510,
|
||||||
group="Participants",
|
group="Participants",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -33,7 +32,6 @@ def get_config_variables():
|
|||||||
label="Enable participant presence view",
|
label="Enable participant presence view",
|
||||||
weight=511,
|
weight=511,
|
||||||
group="Participants",
|
group="Participants",
|
||||||
subgroup="General",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# PDF
|
# PDF
|
||||||
|
@ -223,7 +223,7 @@ class ConfigViewSet(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.data, {"detail": "Invalid input. Config value is missing."}
|
response.data, {"detail": "Got None for test_var_Xeiizi7ooH8Thuk5aida"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
@ -6,6 +11,7 @@ from rest_framework.test import APIClient
|
|||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.models import Projector, Tag
|
from openslides.core.models import Projector, Tag
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
|
from openslides.utils.auth import get_group_model
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DELEGATE_PK
|
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DELEGATE_PK
|
||||||
@ -187,3 +193,204 @@ class Projection(TestCase):
|
|||||||
self.assertEqual(self.projector.elements, [])
|
self.assertEqual(self.projector.elements, [])
|
||||||
self.assertEqual(self.projector.elements_preview, elements)
|
self.assertEqual(self.projector.elements_preview, elements)
|
||||||
self.assertEqual(self.projector.elements_history, [])
|
self.assertEqual(self.projector.elements_history, [])
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigViewSet(TestCase):
|
||||||
|
"""
|
||||||
|
Tests (currently just parts) of the ProjectorViewSet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
string_config_key = "general_event_name"
|
||||||
|
"""
|
||||||
|
The config used for testing. It should accept string.
|
||||||
|
"""
|
||||||
|
logo_config_key = "logo_web_header"
|
||||||
|
|
||||||
|
def random_string(self):
|
||||||
|
return "".join(
|
||||||
|
random.choice(string.ascii_letters + string.digits) for i in range(20)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_static_config_value(self):
|
||||||
|
return {
|
||||||
|
"path": f"test_path_{self.random_string()}",
|
||||||
|
"display_name": f"test_display_name_{self.random_string()}",
|
||||||
|
}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("config-list"), {"key": "test_key_fj3f2oqsjcqpsjclqwoO"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertFalse(config.exists("test_key_fj3f2oqsjcqpsjclqwoO"))
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse("config-detail", args=[self.string_config_key])
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertTrue(config.exists(self.string_config_key))
|
||||||
|
|
||||||
|
def test_update(self):
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("config-detail", args=[self.string_config_key]),
|
||||||
|
{"value": "test_name_39gw4cishcvev2acoqnw"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
config[self.string_config_key], "test_name_39gw4cishcvev2acoqnw"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_none(self):
|
||||||
|
"""
|
||||||
|
The agenda_start_event_date_time is of type "datepicker" which
|
||||||
|
can be set to None
|
||||||
|
"""
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("config-detail", args=["agenda_start_event_date_time"]),
|
||||||
|
{"value": None},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(config["agenda_start_event_date_time"], None)
|
||||||
|
|
||||||
|
def test_set_invalid_none(self):
|
||||||
|
"""
|
||||||
|
Try to set motions_identifier_min_digits to None, which should fail
|
||||||
|
"""
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("config-detail", args=["motions_identifier_min_digits"]),
|
||||||
|
{"value": None},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def degrade_admin(self, can_manage_config=False, can_manage_logos_and_fonts=False):
|
||||||
|
admin = get_user_model().objects.get(username="admin")
|
||||||
|
admin.groups.remove(GROUP_ADMIN_PK)
|
||||||
|
admin.groups.add(GROUP_DELEGATE_PK)
|
||||||
|
if can_manage_config or can_manage_logos_and_fonts:
|
||||||
|
delegate_group = get_group_model().objects.get(pk=GROUP_DELEGATE_PK)
|
||||||
|
if can_manage_config:
|
||||||
|
delegate_group.permissions.add(
|
||||||
|
Permission.objects.get(
|
||||||
|
content_type__app_label="core", codename="can_manage_config"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if can_manage_logos_and_fonts:
|
||||||
|
delegate_group.permissions.add(
|
||||||
|
Permission.objects.get(
|
||||||
|
content_type__app_label="core",
|
||||||
|
codename="can_manage_logos_and_fonts",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
inform_changed_data(delegate_group)
|
||||||
|
inform_changed_data(admin)
|
||||||
|
|
||||||
|
def test_update_no_permissions(self):
|
||||||
|
self.degrade_admin()
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("config-detail", args=[self.string_config_key]),
|
||||||
|
{"value": "test_name_vp2sjjf29jswlvwaxwre"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||||
|
|
||||||
|
def test_update_logo_no_config_permissions(self):
|
||||||
|
self.degrade_admin(can_manage_logos_and_fonts=True)
|
||||||
|
value = self.get_static_config_value()
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("config-detail", args=[self.logo_config_key]),
|
||||||
|
{"value": value},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(config[self.logo_config_key], value)
|
||||||
|
|
||||||
|
def test_bulk_update(self):
|
||||||
|
string_value = "test_value_k2jqvjwrorepjadvpo2J"
|
||||||
|
logo_value = self.get_static_config_value()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("config-bulk-update"),
|
||||||
|
[
|
||||||
|
{"key": self.string_config_key, "value": string_value},
|
||||||
|
{"key": self.logo_config_key, "value": logo_value},
|
||||||
|
],
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data["errors"], {})
|
||||||
|
self.assertEqual(config[self.string_config_key], string_value)
|
||||||
|
self.assertEqual(config[self.logo_config_key], logo_value)
|
||||||
|
|
||||||
|
def test_bulk_update_no_perm(self):
|
||||||
|
self.degrade_admin()
|
||||||
|
string_value = "test_value_gjscneuqoscmqf2qow91"
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("config-bulk-update"),
|
||||||
|
[{"key": self.string_config_key, "value": string_value}],
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||||
|
|
||||||
|
def test_bulk_update_no_list(self):
|
||||||
|
string_value = "test_value_fjewqpqayqfijnqm%cqi"
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("config-bulk-update"),
|
||||||
|
{"key": self.string_config_key, "value": string_value},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||||
|
|
||||||
|
def test_bulk_update_no_key(self):
|
||||||
|
string_value = "test_value_glwe32qc&Lml2lclmqmc"
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("config-bulk-update"), [{"value": string_value}], format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||||
|
|
||||||
|
def test_bulk_update_no_value(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("config-bulk-update"),
|
||||||
|
[{"key": self.string_config_key}],
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(config[self.string_config_key], "OpenSlides")
|
||||||
|
|
||||||
|
def test_reset_group(self):
|
||||||
|
config["general_event_name"] = "test_name_of20w2fj20clqwcm2pij" # Group General
|
||||||
|
config["agenda_show_subtitle"] = True # Group Agenda
|
||||||
|
config[
|
||||||
|
"motions_preamble"
|
||||||
|
] = "test_preamble_2390jvwohjwo1oigefoq" # Group motions
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("config-reset-groups"), ["General", "Agenda"], format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(config["general_event_name"], "OpenSlides")
|
||||||
|
self.assertEqual(config["agenda_show_subtitle"], False)
|
||||||
|
self.assertEqual(
|
||||||
|
config["motions_preamble"], "test_preamble_2390jvwohjwo1oigefoq"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_reset_group_wrong_format_1(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("config-reset-groups"), {"wrong": "format"}, format="json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_reset_group_wrong_format_2(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("config-reset-groups"),
|
||||||
|
["some_string", {"wrong": "format"}],
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
from typing import cast
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from openslides.core.config import ConfigVariable, config
|
from openslides.core.config import ConfigVariable, ConfigVariableDict, config
|
||||||
from openslides.core.exceptions import ConfigNotFound
|
from openslides.core.exceptions import ConfigNotFound
|
||||||
|
|
||||||
|
|
||||||
@ -14,15 +15,15 @@ class TestConfigVariable(TestCase):
|
|||||||
"""
|
"""
|
||||||
config_variable = ConfigVariable("test_variable", "test_default_value")
|
config_variable = ConfigVariable("test_variable", "test_default_value")
|
||||||
|
|
||||||
self.assertIn(
|
self.assertTrue(
|
||||||
"default_value",
|
"defaultValue" in cast(ConfigVariableDict, config_variable.data)
|
||||||
config_variable.data,
|
|
||||||
"Config_varialbe.data should have a key 'default_value'",
|
|
||||||
)
|
)
|
||||||
|
data = config_variable.data
|
||||||
|
self.assertTrue(data)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
config_variable.data["default_value"],
|
cast(ConfigVariableDict, config_variable.data)["defaultValue"],
|
||||||
"test_default_value",
|
"test_default_value",
|
||||||
"The value of config_variable.data['default_value'] should be the same "
|
"The value of config_variable.data['defaultValue'] should be the same "
|
||||||
"as set as second argument of ConfigVariable()",
|
"as set as second argument of ConfigVariable()",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user