Merge pull request #4578 from GabrielInTheWorld/theming
Implements a mechanism for a fallback theme
This commit is contained in:
commit
51b4b6aba6
@ -1,3 +1,4 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
<os-global-spinner></os-global-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,7 +25,7 @@ describe('AppComponent', () => {
|
|||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.debugElement.componentInstance;
|
const app = fixture.debugElement.componentInstance;
|
||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
tick();
|
tick(1000);
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
expect(servertimeService.startScheduler).toHaveBeenCalled();
|
expect(servertimeService.startScheduler).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, ApplicationRef } from '@angular/core';
|
import { Component, ApplicationRef } from '@angular/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { take, filter } from 'rxjs/operators';
|
import { take, filter, auditTime } from 'rxjs/operators';
|
||||||
|
|
||||||
import { ConfigService } from './core/ui-services/config.service';
|
import { ConfigService } from './core/ui-services/config.service';
|
||||||
import { ConstantsService } from './core/core-services/constants.service';
|
import { ConstantsService } from './core/core-services/constants.service';
|
||||||
@ -15,6 +15,8 @@ import { DataStoreUpgradeService } from './core/core-services/data-store-upgrade
|
|||||||
import { UpdateService } from './core/ui-services/update.service';
|
import { UpdateService } from './core/ui-services/update.service';
|
||||||
import { PrioritizeService } from './core/core-services/prioritize.service';
|
import { PrioritizeService } from './core/core-services/prioritize.service';
|
||||||
import { PingService } from './core/core-services/ping.service';
|
import { PingService } from './core/core-services/ping.service';
|
||||||
|
import { SpinnerService } from './core/ui-services/spinner.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Angular's global App Component
|
* Angular's global App Component
|
||||||
@ -48,10 +50,12 @@ export class AppComponent {
|
|||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
appRef: ApplicationRef,
|
appRef: ApplicationRef,
|
||||||
servertimeService: ServertimeService,
|
servertimeService: ServertimeService,
|
||||||
|
router: Router,
|
||||||
operator: OperatorService,
|
operator: OperatorService,
|
||||||
loginDataService: LoginDataService,
|
loginDataService: LoginDataService,
|
||||||
constantsService: ConstantsService, // Needs to be started, so it can register itself to the WebsocketService
|
constantsService: ConstantsService, // Needs to be started, so it can register itself to the WebsocketService
|
||||||
themeService: ThemeService,
|
themeService: ThemeService,
|
||||||
|
spinnerService: SpinnerService,
|
||||||
countUsersService: CountUsersService, // Needed to register itself.
|
countUsersService: CountUsersService, // Needed to register itself.
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
loadFontService: LoadFontService,
|
loadFontService: LoadFontService,
|
||||||
@ -70,13 +74,27 @@ export class AppComponent {
|
|||||||
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
|
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
|
||||||
// change default JS functions
|
// change default JS functions
|
||||||
this.overloadArrayToString();
|
this.overloadArrayToString();
|
||||||
|
// Show the spinner initial
|
||||||
|
spinnerService.setVisibility(true, translate.instant('Loading data. Please wait...'));
|
||||||
|
|
||||||
appRef.isStable
|
appRef.isStable
|
||||||
.pipe(
|
.pipe(
|
||||||
|
// take only the stable state
|
||||||
filter(s => s),
|
filter(s => s),
|
||||||
take(1)
|
take(1)
|
||||||
)
|
)
|
||||||
.subscribe(() => servertimeService.startScheduler());
|
.subscribe(() => servertimeService.startScheduler());
|
||||||
|
|
||||||
|
// Subscribe to hide the spinner if the application has changed.
|
||||||
|
appRef.isStable
|
||||||
|
.pipe(
|
||||||
|
filter(s => s),
|
||||||
|
auditTime(1000)
|
||||||
|
)
|
||||||
|
.pipe(take(2))
|
||||||
|
.subscribe(() => {
|
||||||
|
spinnerService.setVisibility(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,6 +17,7 @@ import { OpenSlidesTranslateModule } from './core/translate/openslides-translate
|
|||||||
// PWA
|
// PWA
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
|
import { GlobalSpinnerComponent } from './site/global-spinner/global-spinner.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a function that returns a promis that will be resolved, if all apps are loaded.
|
* Returns a function that returns a promis that will be resolved, if all apps are loaded.
|
||||||
@ -30,7 +31,7 @@ export function AppLoaderFactory(appLoadService: AppLoadService): () => Promise<
|
|||||||
* Global App Module. Keep it as clean as possible.
|
* Global App Module. Keep it as clean as possible.
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent, GlobalSpinnerComponent],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
|
@ -13,6 +13,10 @@ export interface LoginData {
|
|||||||
privacy_policy: string;
|
privacy_policy: string;
|
||||||
legal_notice: string;
|
legal_notice: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
|
logo_web_header: {
|
||||||
|
path: string;
|
||||||
|
display_name: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOGIN_DATA_STORAGE_KEY = 'LoginData';
|
const LOGIN_DATA_STORAGE_KEY = 'LoginData';
|
||||||
@ -61,6 +65,21 @@ export class LoginDataService {
|
|||||||
return this._theme.asObservable();
|
return this._theme.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the custom web header
|
||||||
|
*/
|
||||||
|
private readonly _logo_web_header = new BehaviorSubject<{ path: string; display_name: string }>({
|
||||||
|
path: '',
|
||||||
|
display_name: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable for the web header
|
||||||
|
*/
|
||||||
|
public get logo_web_header(): Observable<{ path: string; display_name: string }> {
|
||||||
|
return this._logo_web_header.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs this service. The config service is needed to update the privacy
|
* Constructs this service. The config service is needed to update the privacy
|
||||||
* policy and legal notice, when their config values change.
|
* policy and legal notice, when their config values change.
|
||||||
@ -83,6 +102,10 @@ export class LoginDataService {
|
|||||||
this._theme.next(value);
|
this._theme.next(value);
|
||||||
this.storeLoginData();
|
this.storeLoginData();
|
||||||
});
|
});
|
||||||
|
configService.get<{ path: string; display_name: string }>('logo_web_header').subscribe(value => {
|
||||||
|
this._logo_web_header.next(value);
|
||||||
|
this.storeLoginData();
|
||||||
|
});
|
||||||
|
|
||||||
this.loadLoginData();
|
this.loadLoginData();
|
||||||
}
|
}
|
||||||
@ -120,7 +143,8 @@ export class LoginDataService {
|
|||||||
loginData = {
|
loginData = {
|
||||||
privacy_policy: this._privacy_policy.getValue(),
|
privacy_policy: this._privacy_policy.getValue(),
|
||||||
legal_notice: this._legal_notice.getValue(),
|
legal_notice: this._legal_notice.getValue(),
|
||||||
theme: this._theme.getValue()
|
theme: this._theme.getValue(),
|
||||||
|
logo_web_header: this._logo_web_header.getValue()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!this.OSStatus.isInHistoryMode) {
|
if (!this.OSStatus.isInHistoryMode) {
|
||||||
|
12
client/src/app/core/ui-services/spinner.service.spec.ts
Normal file
12
client/src/app/core/ui-services/spinner.service.spec.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SpinnerService } from './spinner.service';
|
||||||
|
|
||||||
|
describe('SpinnerService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: SpinnerService = TestBed.get(SpinnerService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
40
client/src/app/core/ui-services/spinner.service.ts
Normal file
40
client/src/app/core/ui-services/spinner.service.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// External imports
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for the `global-spinner.component`
|
||||||
|
*
|
||||||
|
* Handles the visibility of the global-spinner.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class SpinnerService {
|
||||||
|
/**
|
||||||
|
* Subject, that holds the visibility and message. The component can observe this.
|
||||||
|
*/
|
||||||
|
private visibility: Subject<{ isVisible: boolean; text?: string }> = new Subject<{
|
||||||
|
isVisible: boolean;
|
||||||
|
text?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to change the visibility of the `global-spinner.component`.
|
||||||
|
*
|
||||||
|
* @param isVisible flag, if the spinner should be shown.
|
||||||
|
* @param text optional. If the spinner should show a message.
|
||||||
|
*/
|
||||||
|
public setVisibility(isVisible: boolean, text?: string): void {
|
||||||
|
setTimeout(() => this.visibility.next({ isVisible, text }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get the visibility as observable.
|
||||||
|
*
|
||||||
|
* @returns class member `visibility`.
|
||||||
|
*/
|
||||||
|
public getVisibility(): Observable<{ isVisible: boolean; text?: string }> {
|
||||||
|
return this.visibility;
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,26 @@ import { LoginDataService } from './login-data.service';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ThemeService {
|
export class ThemeService {
|
||||||
|
/**
|
||||||
|
* Constant, that describes the default theme class.
|
||||||
|
*/
|
||||||
|
public static DEFAULT_THEME = 'openslides-theme';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant path of the logo with dark colors for bright themes.
|
||||||
|
*/
|
||||||
|
public static STANDARD_LOGO = '/assets/img/openslides-logo-h.svg';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant path of the logo with white colors for dark themes.
|
||||||
|
*/
|
||||||
|
public static STANDARD_LOGO_DARK_THEME = '/assets/img/openslides-logo-h-dark-transparent.svg';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the current theme as member.
|
||||||
|
*/
|
||||||
|
private currentTheme: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Here it will subscribe to the observer from login data service. The stheme is part of
|
* Here it will subscribe to the observer from login data service. The stheme is part of
|
||||||
* the login data, so get it from there and not from the config. This service will
|
* the login data, so get it from there and not from the config. This service will
|
||||||
@ -21,13 +41,31 @@ export class ThemeService {
|
|||||||
if (!newTheme) {
|
if (!newTheme) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.currentTheme = newTheme;
|
||||||
|
|
||||||
const classList = document.getElementsByTagName('body')[0].classList; // Get the classlist of the body.
|
const classList = document.getElementsByTagName('body')[0].classList; // Get the classlist of the body.
|
||||||
const toRemove = Array.from(classList).filter((item: string) => item.includes('theme'));
|
const toRemove = Array.from(classList).filter((item: string) => item.includes('theme'));
|
||||||
if (toRemove.length) {
|
if (toRemove.length) {
|
||||||
classList.remove(...toRemove); // Remove all old themes.
|
classList.remove(...toRemove); // Remove all old themes.
|
||||||
}
|
}
|
||||||
classList.add(newTheme); // Add the new theme.
|
classList.add(newTheme, ThemeService.DEFAULT_THEME); // Add the new theme.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the logo relative to the used theme.
|
||||||
|
*
|
||||||
|
* @param shouldDefault If this method should return the default logo.
|
||||||
|
*
|
||||||
|
* @returns the path to the logo.
|
||||||
|
*/
|
||||||
|
public getLogoRelativeToTheme(shouldDefault?: boolean): string {
|
||||||
|
if (this.currentTheme) {
|
||||||
|
return this.currentTheme.includes('dark') && !shouldDefault
|
||||||
|
? ThemeService.STANDARD_LOGO_DARK_THEME
|
||||||
|
: ThemeService.STANDARD_LOGO;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
<div *ngIf="getImage() != ''" class="logo-container">
|
<div *ngIf="getImage()" class="logo-container">
|
||||||
<img [src]="getImage()">
|
<img [src]="getImage('true')" class="default" />
|
||||||
|
<img [src]="getImage()" class="dark"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ img {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-container {
|
.logo-container {
|
||||||
|
@ -1,63 +1,27 @@
|
|||||||
import { Component, OnInit, Input } from '@angular/core';
|
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
|
||||||
import { MediaManageService } from 'app/core/ui-services/media-manage.service';
|
import { ThemeService } from 'app/core/ui-services/theme.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { LoginDataService } from 'app/core/ui-services/login-data.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable Logo component for Apps.
|
* Component to hold the logo for the app.
|
||||||
*
|
|
||||||
* Following actions are possible:
|
|
||||||
* * "logo_projector_main"
|
|
||||||
* * "logo_projector_header"
|
|
||||||
* * "logo_web_header"
|
|
||||||
* * "logo_pdf_header_L"
|
|
||||||
* * "logo_pdf_header_R"
|
|
||||||
* * "logo_pdf_footer_L"
|
|
||||||
* * "logo_pdf_footer_R"
|
|
||||||
* * "logo_pdf_ballot_paper"
|
|
||||||
*
|
|
||||||
* ## Examples:
|
|
||||||
*
|
|
||||||
* ### Usage of the selector:
|
|
||||||
*
|
|
||||||
* ```html
|
|
||||||
* <os-logo
|
|
||||||
* inputAction="logo_projector_main"
|
|
||||||
* [footer]="false"
|
|
||||||
* [alignment]="right">
|
|
||||||
* </os-logo>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Sidenote: The footer variable is optional. Only if you want
|
|
||||||
* alternating logos, i.E. in the sidenav. the Alignment is also
|
|
||||||
* optional.
|
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-logo',
|
selector: 'os-logo',
|
||||||
templateUrl: './logo.component.html',
|
templateUrl: './logo.component.html',
|
||||||
styleUrls: ['./logo.component.scss']
|
styleUrls: ['./logo.component.scss']
|
||||||
})
|
})
|
||||||
export class LogoComponent implements OnInit {
|
export class LogoComponent implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Constant path of the logo with dark colors for bright themes
|
* Local variable to hold the path for a custom web header.
|
||||||
*/
|
*/
|
||||||
public static STANDARD_LOGO = '/assets/img/openslides-logo-h.svg';
|
private logoPath: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constant path of the logo with white colors for dark themes
|
* Local variable to hold the subscription to unsubscribe if exists.
|
||||||
*/
|
*/
|
||||||
public static STANDARD_LOGO_DARK_THEME = '/assets/img/openslides-logo-h-dark-transparent.svg';
|
private logoSubscription: Subscription;
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the actions for logos. Updated via an observable
|
|
||||||
*/
|
|
||||||
public logoActions: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* decides based on the actionString how to display the logo
|
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
public inputAction: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* determines if the current picture is displayed in the footer.
|
* determines if the current picture is displayed in the footer.
|
||||||
@ -66,117 +30,46 @@ export class LogoComponent implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
public footer = false;
|
public footer = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* influences text-alignment in the .logo-container css class
|
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
public alignment = 'center';
|
|
||||||
/**
|
/**
|
||||||
* The constructor
|
* The constructor
|
||||||
*
|
*
|
||||||
* @param mmservice The Media Manage Service
|
* @param loginDataService Reference to the `LoginDataService`
|
||||||
* @param configService The ConfigService to subscribe to theme-changes
|
* @param themeService Reference to the `ThemeService`
|
||||||
*/
|
*/
|
||||||
public constructor(private mmservice: MediaManageService, private configService: ConfigService) {}
|
public constructor(private loginDataService: LoginDataService, private themeService: ThemeService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialization function
|
* On init method
|
||||||
*/
|
*/
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.mmservice.getLogoActions().subscribe(action => {
|
this.logoSubscription = this.loginDataService.logo_web_header.subscribe(nextLogo => {
|
||||||
this.logoActions = action;
|
if (nextLogo) {
|
||||||
|
this.logoPath = nextLogo.path;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gets the image based on the inputAction and location.
|
* On destroy method
|
||||||
* Possible inputActions are in the class description.
|
*/
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
if (this.logoSubscription) {
|
||||||
|
this.logoSubscription.unsubscribe();
|
||||||
|
this.logoSubscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the image based on custom images and footer.
|
||||||
|
* If a custom image is set and this component is displayed as footer or there is no custom image, then the OpenSlides logo is used.
|
||||||
*
|
*
|
||||||
* @returns path to image
|
* @returns path to image
|
||||||
*/
|
*/
|
||||||
public getImage(): string {
|
public getImage(shouldDefault?: boolean): string {
|
||||||
if (this.footer) {
|
if ((!this.logoPath && !this.footer) || (!!this.logoPath && this.footer)) {
|
||||||
const path = this.getFooterImage(this.inputAction);
|
return this.themeService.getLogoRelativeToTheme(shouldDefault);
|
||||||
return path;
|
|
||||||
} else {
|
} else {
|
||||||
const path = this.getHeaderImage(this.inputAction, this.alignment);
|
return this.logoPath;
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the user uses a dark theme or a 'bright' theme.
|
|
||||||
* In relation to the theme this will return the corresponding imagepath.
|
|
||||||
*
|
|
||||||
* @returns path of the image corresponding to the chosen theme.
|
|
||||||
*/
|
|
||||||
protected getImagePathRelatedToTheme(): string {
|
|
||||||
const theme = this.configService.instant<string>('openslides_theme');
|
|
||||||
if (theme) {
|
|
||||||
return theme.includes('dark') ? LogoComponent.STANDARD_LOGO_DARK_THEME : LogoComponent.STANDARD_LOGO;
|
|
||||||
} else {
|
|
||||||
return LogoComponent.STANDARD_LOGO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gets the header image based on logo action
|
|
||||||
*
|
|
||||||
* @param logoAction the logo action to be used
|
|
||||||
* @param alignment the alignment of the logo (optional)
|
|
||||||
* @returns path to image
|
|
||||||
*/
|
|
||||||
protected getHeaderImage(logoAction: string, alignment: string = 'center'): string {
|
|
||||||
if (alignment !== 'center') {
|
|
||||||
this.setAlignment(alignment);
|
|
||||||
}
|
|
||||||
let path = '';
|
|
||||||
/* check if datastore is loaded and custom logo can be read */
|
|
||||||
if (this.logoActions === undefined) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (this.mmservice !== undefined) {
|
|
||||||
if (this.mmservice.isImageConfigObject(this.mmservice.getMediaConfig(logoAction))) {
|
|
||||||
const imageConfig = this.mmservice.getMediaConfig(logoAction);
|
|
||||||
path = imageConfig.path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (path === '') {
|
|
||||||
path = this.getImagePathRelatedToTheme();
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the alignment from center to either 'left' or 'right'
|
|
||||||
*
|
|
||||||
* @param alignment either 'right' or 'left'
|
|
||||||
*/
|
|
||||||
private setAlignment(alignment: string): void {
|
|
||||||
if (alignment === 'left' || alignment === 'right') {
|
|
||||||
const cssLogoContainer = document.getElementsByClassName('logo-container') as HTMLCollectionOf<HTMLElement>;
|
|
||||||
if (cssLogoContainer.length !== 0) {
|
|
||||||
cssLogoContainer[0].style.textAlign = alignment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the image-path for the footer
|
|
||||||
*
|
|
||||||
* @param logoAction the logo action to be used
|
|
||||||
* @returns '' if no logo is set and path to standard logo if a custom
|
|
||||||
* logo was set
|
|
||||||
*/
|
|
||||||
protected getFooterImage(logoAction: string): string {
|
|
||||||
if (
|
|
||||||
this.getHeaderImage(logoAction) === LogoComponent.STANDARD_LOGO ||
|
|
||||||
this.getHeaderImage(logoAction) === LogoComponent.STANDARD_LOGO_DARK_THEME ||
|
|
||||||
this.getHeaderImage(logoAction) === ''
|
|
||||||
) {
|
|
||||||
return '';
|
|
||||||
} else {
|
|
||||||
return this.getImagePathRelatedToTheme();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
<div
|
||||||
|
*ngIf="isVisible"
|
||||||
|
class="global-spinner-component">
|
||||||
|
<div class="spinner-container">
|
||||||
|
<div>
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<div class="text">{{ text }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="backdrop"></div>
|
||||||
|
</div>
|
@ -0,0 +1,83 @@
|
|||||||
|
@import '~@angular/material/theming';
|
||||||
|
|
||||||
|
@mixin os-global-spinner-theme($theme) {
|
||||||
|
$primary: map-get($theme, primary);
|
||||||
|
$accent: map-get($theme, accent);
|
||||||
|
$warn: map-get($theme, warn);
|
||||||
|
$background: map-get($theme, background);
|
||||||
|
$foreground: map-get($theme, foreground);
|
||||||
|
|
||||||
|
$contrast-primary: map-get($primary, contrast);
|
||||||
|
$contrast-accent: map-get($accent, contrast);
|
||||||
|
|
||||||
|
.global-spinner-component,
|
||||||
|
.backdrop,
|
||||||
|
.spinner-container {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-spinner-component {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
.spinner-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin: -136px 0 0 -53px;
|
||||||
|
|
||||||
|
height: 100px;
|
||||||
|
width: 100px;
|
||||||
|
border: 6px solid #000;
|
||||||
|
border-radius: 100%;
|
||||||
|
opacity: 0.2;
|
||||||
|
|
||||||
|
animation: rotation 1s infinite linear;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
left: -6px;
|
||||||
|
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 100%;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 6px;
|
||||||
|
border-color: white transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotation {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(359deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.backdrop {
|
||||||
|
z-index: 899;
|
||||||
|
background-color: #303030;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { GlobalSpinnerComponent } from './global-spinner.component';
|
||||||
|
|
||||||
|
describe('GlobalSpinnerComponent', () => {
|
||||||
|
let component: GlobalSpinnerComponent;
|
||||||
|
let fixture: ComponentFixture<GlobalSpinnerComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [GlobalSpinnerComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(GlobalSpinnerComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,74 @@
|
|||||||
|
// External imports
|
||||||
|
import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
// Internal imports
|
||||||
|
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for the global spinner.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-global-spinner',
|
||||||
|
templateUrl: './global-spinner.component.html',
|
||||||
|
styleUrls: ['./global-spinner.component.scss']
|
||||||
|
})
|
||||||
|
export class GlobalSpinnerComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* Text, which will be shown if the spinner is shown.
|
||||||
|
*/
|
||||||
|
public text: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag, that defines when the spinner is shown.
|
||||||
|
*/
|
||||||
|
public isVisible = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription for the service to handle the visibility and text for the spinner.
|
||||||
|
*/
|
||||||
|
private spinnerSubscription: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant string as default message when the spinner is shown.
|
||||||
|
*/
|
||||||
|
private LOADING = 'Loading data. Please wait...';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param spinnerService Reference to the service for this spinner.
|
||||||
|
* @param translate Service to get translations for the messages.
|
||||||
|
* @param detector Service to manual initiate a change of the UI.
|
||||||
|
*/
|
||||||
|
public constructor(private spinnerService: SpinnerService, private detector: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init method
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.spinnerSubscription = this.spinnerService // subscribe to the service.
|
||||||
|
.getVisibility()
|
||||||
|
.subscribe((value: { isVisible: boolean; text: string }) => {
|
||||||
|
this.isVisible = value.isVisible;
|
||||||
|
this.text = value.text;
|
||||||
|
|
||||||
|
if (!this.text) {
|
||||||
|
this.text = this.LOADING;
|
||||||
|
}
|
||||||
|
this.detector.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy method
|
||||||
|
*
|
||||||
|
* Deletes the subscription and marks the spinner as invisible.
|
||||||
|
*/
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
if (this.spinnerSubscription) {
|
||||||
|
this.spinnerSubscription.unsubscribe();
|
||||||
|
this.isVisible = false;
|
||||||
|
}
|
||||||
|
this.spinnerSubscription = null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,4 @@
|
|||||||
<div class="form-wrapper">
|
<div class="form-wrapper">
|
||||||
<!-- Spinner -->
|
|
||||||
<mat-spinner *ngIf="inProcess"></mat-spinner>
|
|
||||||
|
|
||||||
<!-- Install notice -->
|
<!-- Install notice -->
|
||||||
<div class="login-container" *ngIf="installationNotice">
|
<div class="login-container" *ngIf="installationNotice">
|
||||||
|
@ -13,6 +13,7 @@ import { environment } from 'environments/environment';
|
|||||||
import { LoginDataService, LoginData } from 'app/core/ui-services/login-data.service';
|
import { LoginDataService, LoginData } from 'app/core/ui-services/login-data.service';
|
||||||
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
|
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
|
||||||
import { HttpService } from 'app/core/core-services/http.service';
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
|
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||||
|
|
||||||
interface LoginDataWithInfoText extends LoginData {
|
interface LoginDataWithInfoText extends LoginData {
|
||||||
info_text?: string;
|
info_text?: string;
|
||||||
@ -54,11 +55,6 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
|||||||
*/
|
*/
|
||||||
public parentErrorStateMatcher = new ParentErrorStateMatcher();
|
public parentErrorStateMatcher = new ParentErrorStateMatcher();
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the Spinner if validation is in process
|
|
||||||
*/
|
|
||||||
public inProcess = false;
|
|
||||||
|
|
||||||
public operatorSubscription: Subscription | null;
|
public operatorSubscription: Subscription | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,6 +67,7 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
|||||||
* @param httpService used to get information before the login
|
* @param httpService used to get information before the login
|
||||||
* @param OpenSlides The Service for OpenSlides
|
* @param OpenSlides The Service for OpenSlides
|
||||||
* @param loginDataService provide information about the legal notice and privacy policy
|
* @param loginDataService provide information about the legal notice and privacy policy
|
||||||
|
* @param spinnerService Service to show the spinner when the user is signing in
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
title: Title,
|
title: Title,
|
||||||
@ -81,9 +78,12 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private httpService: HttpService,
|
private httpService: HttpService,
|
||||||
private loginDataService: LoginDataService
|
private loginDataService: LoginDataService,
|
||||||
|
private spinnerService: SpinnerService
|
||||||
) {
|
) {
|
||||||
super(title, translate);
|
super(title, translate);
|
||||||
|
// Hide the spinner if the user is at `login-mask`
|
||||||
|
spinnerService.setVisibility(false);
|
||||||
this.createForm();
|
this.createForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,8 +149,8 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
|||||||
* Send username and password to the {@link AuthService}
|
* Send username and password to the {@link AuthService}
|
||||||
*/
|
*/
|
||||||
public async formLogin(): Promise<void> {
|
public async formLogin(): Promise<void> {
|
||||||
|
this.spinnerService.setVisibility(true, this.translate.instant('Loading data. Please wait...'));
|
||||||
this.loginErrorMsg = '';
|
this.loginErrorMsg = '';
|
||||||
this.inProcess = true;
|
|
||||||
try {
|
try {
|
||||||
await this.authService.login(this.loginForm.value.username, this.loginForm.value.password, () => {
|
await this.authService.login(this.loginForm.value.username, this.loginForm.value.password, () => {
|
||||||
this.clearOperatorSubscription(); // We take control, not the subscription.
|
this.clearOperatorSubscription(); // We take control, not the subscription.
|
||||||
@ -161,7 +161,7 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
|||||||
});
|
});
|
||||||
this.loginErrorMsg = e;
|
this.loginErrorMsg = e;
|
||||||
}
|
}
|
||||||
this.inProcess = false;
|
this.spinnerService.setVisibility(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,6 +18,7 @@ import { TreeService } from 'app/core/ui-services/tree.service';
|
|||||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||||
import { ViewMotion } from '../models/view-motion';
|
import { ViewMotion } from '../models/view-motion';
|
||||||
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
|
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
|
||||||
|
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains all multiselect actions for the motion list view.
|
* Contains all multiselect actions for the motion list view.
|
||||||
@ -26,6 +27,8 @@ import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflo
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class MotionMultiselectService {
|
export class MotionMultiselectService {
|
||||||
|
private messageForSpinner = 'Motions are in process. Please wait...';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does nothing.
|
* Does nothing.
|
||||||
*
|
*
|
||||||
@ -42,6 +45,7 @@ export class MotionMultiselectService {
|
|||||||
* @param httpService
|
* @param httpService
|
||||||
* @param treeService
|
* @param treeService
|
||||||
* @param personalNoteService
|
* @param personalNoteService
|
||||||
|
* @param spinnerService to show a spinner when http-requests are made.
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private repo: MotionRepositoryService,
|
private repo: MotionRepositoryService,
|
||||||
@ -56,7 +60,8 @@ export class MotionMultiselectService {
|
|||||||
private motionBlockRepo: MotionBlockRepositoryService,
|
private motionBlockRepo: MotionBlockRepositoryService,
|
||||||
private httpService: HttpService,
|
private httpService: HttpService,
|
||||||
private treeService: TreeService,
|
private treeService: TreeService,
|
||||||
private personalNoteService: PersonalNoteService
|
private personalNoteService: PersonalNoteService,
|
||||||
|
private spinnerService: SpinnerService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,9 +72,19 @@ export class MotionMultiselectService {
|
|||||||
public async delete(motions: ViewMotion[]): Promise<void> {
|
public async delete(motions: ViewMotion[]): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete all selected motions?');
|
const title = this.translate.instant('Are you sure you want to delete all selected motions?');
|
||||||
if (await this.promptService.open(title, null)) {
|
if (await this.promptService.open(title, null)) {
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
for (const motion of motions) {
|
for (const motion of motions) {
|
||||||
|
++i;
|
||||||
|
const message =
|
||||||
|
this.translate.instant(this.messageForSpinner) +
|
||||||
|
`\n${i} ` +
|
||||||
|
this.translate.instant('of') +
|
||||||
|
` ${motions.length}`;
|
||||||
|
this.spinnerService.setVisibility(true, message);
|
||||||
await this.repo.delete(motion);
|
await this.repo.delete(motion);
|
||||||
}
|
}
|
||||||
|
this.spinnerService.setVisibility(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +117,13 @@ export class MotionMultiselectService {
|
|||||||
}));
|
}));
|
||||||
const selectedChoice = await this.choiceService.open(title, choices);
|
const selectedChoice = await this.choiceService.open(title, choices);
|
||||||
if (selectedChoice) {
|
if (selectedChoice) {
|
||||||
await this.repo.setMultiState(motions, selectedChoice.items as number);
|
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||||
|
this.spinnerService.setVisibility(true, message);
|
||||||
|
await this.repo.setMultiState(motions, selectedChoice.items as number).catch(error => {
|
||||||
|
this.spinnerService.setVisibility(false);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
this.spinnerService.setVisibility(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,9 +148,18 @@ export class MotionMultiselectService {
|
|||||||
id: motion.id,
|
id: motion.id,
|
||||||
recommendation: selectedChoice.action ? 0 : (selectedChoice.items as number)
|
recommendation: selectedChoice.action ? 0 : (selectedChoice.items as number)
|
||||||
}));
|
}));
|
||||||
await this.httpService.post('/rest/motions/motion/manage_multiple_recommendation/', {
|
|
||||||
motions: requestData
|
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||||
});
|
this.spinnerService.setVisibility(true, message);
|
||||||
|
await this.httpService
|
||||||
|
.post('/rest/motions/motion/manage_multiple_recommendation/', {
|
||||||
|
motions: requestData
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.spinnerService.setVisibility(false);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
this.spinnerService.setVisibility(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,12 +179,21 @@ export class MotionMultiselectService {
|
|||||||
clearChoice
|
clearChoice
|
||||||
);
|
);
|
||||||
if (selectedChoice) {
|
if (selectedChoice) {
|
||||||
|
let i = 0;
|
||||||
for (const motion of motions) {
|
for (const motion of motions) {
|
||||||
|
++i;
|
||||||
|
const message =
|
||||||
|
this.translate.instant(this.messageForSpinner) +
|
||||||
|
`\n${i} ` +
|
||||||
|
this.translate.instant('of') +
|
||||||
|
` ${motions.length}`;
|
||||||
|
this.spinnerService.setVisibility(true, message);
|
||||||
await this.repo.update(
|
await this.repo.update(
|
||||||
{ category_id: selectedChoice.action ? null : (selectedChoice.items as number) },
|
{ category_id: selectedChoice.action ? null : (selectedChoice.items as number) },
|
||||||
motion
|
motion
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.spinnerService.setVisibility(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,26 +213,33 @@ export class MotionMultiselectService {
|
|||||||
true,
|
true,
|
||||||
choices
|
choices
|
||||||
);
|
);
|
||||||
if (selectedChoice && selectedChoice.action === choices[0]) {
|
if (selectedChoice) {
|
||||||
const requestData = motions.map(motion => {
|
let requestData = null;
|
||||||
let submitterIds = [...motion.sorted_submitters_id, ...(selectedChoice.items as number[])];
|
if (selectedChoice.action === choices[0]) {
|
||||||
submitterIds = submitterIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
|
requestData = motions.map(motion => {
|
||||||
return {
|
let submitterIds = [...motion.sorted_submitters_id, ...(selectedChoice.items as number[])];
|
||||||
id: motion.id,
|
submitterIds = submitterIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
|
||||||
submitters: submitterIds
|
return {
|
||||||
};
|
id: motion.id,
|
||||||
});
|
submitters: submitterIds
|
||||||
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters/', { motions: requestData });
|
};
|
||||||
} else if (selectedChoice && selectedChoice.action === choices[1]) {
|
});
|
||||||
const requestData = motions.map(motion => {
|
// await this.httpService.post('/rest/motions/motion/manage_multiple_submitters/', { motions: requestData });
|
||||||
const submitterIdsToRemove = selectedChoice.items as number[];
|
} else if (selectedChoice.action === choices[1]) {
|
||||||
const submitterIds = motion.sorted_submitters_id.filter(id => !submitterIdsToRemove.includes(id));
|
requestData = motions.map(motion => {
|
||||||
return {
|
const submitterIdsToRemove = selectedChoice.items as number[];
|
||||||
id: motion.id,
|
const submitterIds = motion.sorted_submitters_id.filter(id => !submitterIdsToRemove.includes(id));
|
||||||
submitters: submitterIds
|
return {
|
||||||
};
|
id: motion.id,
|
||||||
});
|
submitters: submitterIds
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||||
|
this.spinnerService.setVisibility(true, message);
|
||||||
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters/', { motions: requestData });
|
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters/', { motions: requestData });
|
||||||
|
this.spinnerService.setVisibility(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,34 +261,41 @@ export class MotionMultiselectService {
|
|||||||
true,
|
true,
|
||||||
choices
|
choices
|
||||||
);
|
);
|
||||||
if (selectedChoice && selectedChoice.action === choices[0]) {
|
if (selectedChoice) {
|
||||||
const requestData = motions.map(motion => {
|
let requestData = null;
|
||||||
let tagIds = [...motion.tags_id, ...(selectedChoice.items as number[])];
|
if (selectedChoice.action === choices[0]) {
|
||||||
tagIds = tagIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
|
requestData = motions.map(motion => {
|
||||||
return {
|
let tagIds = [...motion.tags_id, ...(selectedChoice.items as number[])];
|
||||||
id: motion.id,
|
tagIds = tagIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
|
||||||
tags: tagIds
|
return {
|
||||||
};
|
id: motion.id,
|
||||||
});
|
tags: tagIds
|
||||||
await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
};
|
||||||
} else if (selectedChoice && selectedChoice.action === choices[1]) {
|
});
|
||||||
const requestData = motions.map(motion => {
|
// await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
||||||
const tagIdsToRemove = selectedChoice.items as number[];
|
} else if (selectedChoice.action === choices[1]) {
|
||||||
const tagIds = motion.tags_id.filter(id => !tagIdsToRemove.includes(id));
|
requestData = motions.map(motion => {
|
||||||
return {
|
const tagIdsToRemove = selectedChoice.items as number[];
|
||||||
id: motion.id,
|
const tagIds = motion.tags_id.filter(id => !tagIdsToRemove.includes(id));
|
||||||
tags: tagIds
|
return {
|
||||||
};
|
id: motion.id,
|
||||||
});
|
tags: tagIds
|
||||||
await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
};
|
||||||
} else if (selectedChoice && selectedChoice.action === choices[2]) {
|
});
|
||||||
const requestData = motions.map(motion => {
|
// await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
||||||
return {
|
} else if (selectedChoice.action === choices[2]) {
|
||||||
id: motion.id,
|
requestData = motions.map(motion => {
|
||||||
tags: []
|
return {
|
||||||
};
|
id: motion.id,
|
||||||
});
|
tags: []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||||
|
this.spinnerService.setVisibility(true, message);
|
||||||
await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
||||||
|
this.spinnerService.setVisibility(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,10 +315,19 @@ export class MotionMultiselectService {
|
|||||||
clearChoice
|
clearChoice
|
||||||
);
|
);
|
||||||
if (selectedChoice) {
|
if (selectedChoice) {
|
||||||
|
let i = 0;
|
||||||
for (const motion of motions) {
|
for (const motion of motions) {
|
||||||
|
++i;
|
||||||
|
const message =
|
||||||
|
this.translate.instant(this.messageForSpinner) +
|
||||||
|
`\n${i} ` +
|
||||||
|
this.translate.instant('of') +
|
||||||
|
` ${motions.length}`;
|
||||||
|
this.spinnerService.setVisibility(true, message);
|
||||||
const blockId = selectedChoice.action ? null : (selectedChoice.items as number);
|
const blockId = selectedChoice.action ? null : (selectedChoice.items as number);
|
||||||
await this.repo.update({ motion_block_id: blockId }, motion);
|
await this.repo.update({ motion_block_id: blockId }, motion);
|
||||||
}
|
}
|
||||||
|
this.spinnerService.setVisibility(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,8 +379,11 @@ export class MotionMultiselectService {
|
|||||||
];
|
];
|
||||||
const selectedChoice = await this.choiceService.open(title, choices);
|
const selectedChoice = await this.choiceService.open(title, choices);
|
||||||
if (selectedChoice && motions.length) {
|
if (selectedChoice && motions.length) {
|
||||||
|
const message = this.translate.instant(`I have ${motions.length} favorite motions. Please wait...`);
|
||||||
const star = (selectedChoice.items as number) === choices[0].id;
|
const star = (selectedChoice.items as number) === choices[0].id;
|
||||||
|
this.spinnerService.setVisibility(true, message);
|
||||||
await this.personalNoteService.bulkSetStar(motions, star);
|
await this.personalNoteService.bulkSetStar(motions, star);
|
||||||
|
this.spinnerService.setVisibility(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<div class="nav-toolbar">
|
<div class="nav-toolbar">
|
||||||
<!-- logo -->
|
<!-- logo -->
|
||||||
<a routerLink="/" (click)="toggleSideNav()">
|
<a routerLink="/" (click)="toggleSideNav()">
|
||||||
<os-logo class="os-logo-container" inputAction="logo_web_header" [footer]="false"></os-logo>
|
<os-logo class="os-logo-container" [footer]="false"></os-logo>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -130,7 +130,9 @@
|
|||||||
<small><os-copyright-sign></os-copyright-sign> Copyright by OpenSlides</small>
|
<small><os-copyright-sign></os-copyright-sign> Copyright by OpenSlides</small>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="os-footer-logo-container"><os-logo inputAction="logo_web_header" footer="true"> </os-logo></div>
|
<div class="os-footer-logo-container">
|
||||||
|
<os-logo [footer]="true"></os-logo>
|
||||||
|
</div>
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
<mat-sidenav-content>
|
<mat-sidenav-content>
|
||||||
|
75
client/src/assets/styles/openslides-developer-theme.scss
Normal file
75
client/src/assets/styles/openslides-developer-theme.scss
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
$openslides-developer-primary: (
|
||||||
|
50 : #000000,
|
||||||
|
100 : #000000,
|
||||||
|
200 : #000000,
|
||||||
|
300 : #000000,
|
||||||
|
400 : #000000,
|
||||||
|
500 : #000000,
|
||||||
|
600 : #000000,
|
||||||
|
700 : #000000,
|
||||||
|
800 : #000000,
|
||||||
|
900 : #000000,
|
||||||
|
A100 : #000000,
|
||||||
|
A200 : #000000,
|
||||||
|
A400 : #000000,
|
||||||
|
A700 : #000000,
|
||||||
|
contrast: (
|
||||||
|
50 : #000000,
|
||||||
|
100 : #000000,
|
||||||
|
200 : #000000,
|
||||||
|
300 : #000000,
|
||||||
|
400 : #000000,
|
||||||
|
500 : #000000,
|
||||||
|
600 : #000000,
|
||||||
|
700 : #000000,
|
||||||
|
800 : #000000,
|
||||||
|
900 : #000000,
|
||||||
|
A100 : #000000,
|
||||||
|
A200 : #000000,
|
||||||
|
A400 : #000000,
|
||||||
|
A700 : #000000,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$openslides-developer-accent: (
|
||||||
|
50 : #f4f9e1,
|
||||||
|
100 : #e5f0b3,
|
||||||
|
200 : #d3e781,
|
||||||
|
300 : #c1dd4e,
|
||||||
|
400 : #b4d528,
|
||||||
|
500 : #a7ce02,
|
||||||
|
600 : #9fc902,
|
||||||
|
700 : #96c201,
|
||||||
|
800 : #8cbc01,
|
||||||
|
900 : #7cb001,
|
||||||
|
A100 : #f2ffda,
|
||||||
|
A200 : #e1ffa7,
|
||||||
|
A400 : #cfff74,
|
||||||
|
A700 : #c6ff5a,
|
||||||
|
contrast: (
|
||||||
|
50 : #000000,
|
||||||
|
100 : #000000,
|
||||||
|
200 : #000000,
|
||||||
|
300 : #000000,
|
||||||
|
400 : #000000,
|
||||||
|
500 : #000000,
|
||||||
|
600 : #000000,
|
||||||
|
700 : #000000,
|
||||||
|
800 : #000000,
|
||||||
|
900 : #000000,
|
||||||
|
A100 : #000000,
|
||||||
|
A200 : #000000,
|
||||||
|
A400 : #000000,
|
||||||
|
A700 : #000000,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$openslides-primary: mat-palette($openslides-developer-primary);
|
||||||
|
$openslides-accent: mat-palette($openslides-developer-accent);
|
||||||
|
$openslides-warn: mat-palette($mat-pink);
|
||||||
|
|
||||||
|
$openslides-developer-theme: mat-dark-theme(
|
||||||
|
$openslides-primary,
|
||||||
|
$openslides-accent,
|
||||||
|
$openslides-warn
|
||||||
|
);
|
40
client/src/assets/styles/openslides-general-theme.scss
Normal file
40
client/src/assets/styles/openslides-general-theme.scss
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
$openslides-primary-dark: (
|
||||||
|
50 : #e0e0e0,
|
||||||
|
100 : #b3b3b3,
|
||||||
|
200 : #808080,
|
||||||
|
300 : #4d4d4d,
|
||||||
|
400 : #262626,
|
||||||
|
500 : #000000,
|
||||||
|
600 : #000000,
|
||||||
|
700 : #000000,
|
||||||
|
800 : #000000,
|
||||||
|
900 : #000000,
|
||||||
|
A100 : #a6a6a6,
|
||||||
|
A200 : #8c8c8c,
|
||||||
|
A400 : #737373,
|
||||||
|
A700 : #666666,
|
||||||
|
contrast: (
|
||||||
|
50 : #000000,
|
||||||
|
100 : #000000,
|
||||||
|
200 : #000000,
|
||||||
|
300 : #ffffff,
|
||||||
|
400 : #ffffff,
|
||||||
|
500 : #ffffff,
|
||||||
|
600 : #ffffff,
|
||||||
|
700 : #ffffff,
|
||||||
|
800 : #ffffff,
|
||||||
|
900 : #ffffff,
|
||||||
|
A100 : #000000,
|
||||||
|
A200 : #000000,
|
||||||
|
A400 : #ffffff,
|
||||||
|
A700 : #ffffff,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$openslides-primary: mat-palette($openslides-primary-dark, 500, 700, 900);
|
||||||
|
|
||||||
|
$openslides-general-theme: mat-light-theme(
|
||||||
|
$openslides-primary,
|
||||||
|
$openslides-primary,
|
||||||
|
$openslides-primary
|
||||||
|
);
|
@ -13,7 +13,7 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="openslides-theme">
|
<body class="general-theme">
|
||||||
<os-root></os-root>
|
<os-root></os-root>
|
||||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||||
</body>
|
</body>
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
@include mat-core();
|
@include mat-core();
|
||||||
|
|
||||||
/** Import brand theme */
|
/** Import brand theme */
|
||||||
|
@import './assets/styles/openslides-general-theme.scss';
|
||||||
@import './assets/styles/openslides-theme.scss';
|
@import './assets/styles/openslides-theme.scss';
|
||||||
@import './assets/styles/openslides-dark-theme.scss';
|
@import './assets/styles/openslides-dark-theme.scss';
|
||||||
@import './assets/styles/openslides-green-theme.scss';
|
@import './assets/styles/openslides-green-theme.scss';
|
||||||
|
@import './assets/styles/openslides-developer-theme.scss';
|
||||||
|
|
||||||
/** Import the component-related style sheets here */
|
/** Import the component-related style sheets here */
|
||||||
@import './app/site/site.component.scss-theme.scss';
|
@import './app/site/site.component.scss-theme.scss';
|
||||||
@ -13,6 +15,7 @@
|
|||||||
@import './app/shared/components/projector-button/projector-button.component.scss';
|
@import './app/shared/components/projector-button/projector-button.component.scss';
|
||||||
@import './app/site/agenda/components/list-of-speakers/list-of-speakers.component.scss-theme.scss';
|
@import './app/site/agenda/components/list-of-speakers/list-of-speakers.component.scss-theme.scss';
|
||||||
@import './app/shared/components/sorting-tree/sorting-tree.component.scss';
|
@import './app/shared/components/sorting-tree/sorting-tree.component.scss';
|
||||||
|
@import './app/site/global-spinner/global-spinner.component.scss';
|
||||||
|
|
||||||
/** fonts */
|
/** fonts */
|
||||||
@import './assets/styles/fonts.scss';
|
@import './assets/styles/fonts.scss';
|
||||||
@ -25,6 +28,7 @@
|
|||||||
@include os-projector-button-style($theme);
|
@include os-projector-button-style($theme);
|
||||||
@include os-list-of-speakers-style($theme);
|
@include os-list-of-speakers-style($theme);
|
||||||
@include os-sorting-tree-style($theme);
|
@include os-sorting-tree-style($theme);
|
||||||
|
@include os-global-spinner-theme($theme);
|
||||||
/** More components are added here */
|
/** More components are added here */
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,10 +38,34 @@
|
|||||||
/** Load projector specific SCSS values */
|
/** Load projector specific SCSS values */
|
||||||
@import './assets/styles/projector.scss';
|
@import './assets/styles/projector.scss';
|
||||||
|
|
||||||
|
.general-theme {
|
||||||
|
@include os-global-spinner-theme($openslides-general-theme);
|
||||||
|
}
|
||||||
|
|
||||||
/** Define the classes to switch between themes */
|
/** Define the classes to switch between themes */
|
||||||
.openslides-theme {
|
.openslides-theme {
|
||||||
@include angular-material-theme($openslides-theme);
|
@include angular-material-theme($openslides-theme);
|
||||||
@include openslides-components-theme($openslides-theme);
|
@include openslides-components-theme($openslides-theme);
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
img.dark {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
img.default {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.openslides-dark-theme, .openslides-developer-dark-theme {
|
||||||
|
.logo-container {
|
||||||
|
img.dark {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
img.default {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.openslides-dark-theme {
|
.openslides-dark-theme {
|
||||||
@ -50,17 +78,71 @@
|
|||||||
@include openslides-components-theme($openslides-green-theme);
|
@include openslides-components-theme($openslides-green-theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openslides-developer-dark-theme {
|
||||||
|
@include angular-material-theme($openslides-developer-theme);
|
||||||
|
@include openslides-components-theme($openslides-developer-theme);
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: monospace !important;
|
||||||
|
color: mat-color(map-get($openslides-developer-theme, accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
os-site {
|
||||||
|
.active mat-icon, .active span {
|
||||||
|
color: mat-color(map-get($openslides-developer-theme, accent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table-header {
|
||||||
|
background: #000;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-toolbar.sticky-toolbar {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-drawer-inner-container {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-drawer-inner-container > * {
|
||||||
|
background: #000 !important;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
.os-site.nav-toolbar {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-drawer-content > * {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-family: 'Material Icons' !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mini-fab.projector-active {
|
||||||
|
background-color: mat-color(map-get($openslides-developer-theme, warn));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mce-ico {
|
||||||
|
font-family: 'tinymce', Arial !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Define the general style-rules */
|
/** Define the general style-rules */
|
||||||
* {
|
* {
|
||||||
font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif;
|
font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
.mat-toolbar h2,
|
.mat-toolbar h2,
|
||||||
.mat-dialog-title {
|
.mat-dialog-title {
|
||||||
font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif !important;
|
font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-icon {
|
mat-icon {
|
||||||
font-family: 'Material Icons Baseline';
|
font-family: 'Material Icons' !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -121,6 +121,10 @@ def get_config_variables():
|
|||||||
{"value": "openslides-theme", "display_name": "OpenSlides Default"},
|
{"value": "openslides-theme", "display_name": "OpenSlides Default"},
|
||||||
{"value": "openslides-dark-theme", "display_name": "OpenSlides Dark"},
|
{"value": "openslides-dark-theme", "display_name": "OpenSlides Dark"},
|
||||||
{"value": "openslides-green-theme", "display_name": "OpenSlides Green"},
|
{"value": "openslides-green-theme", "display_name": "OpenSlides Green"},
|
||||||
|
{
|
||||||
|
"value": "openslides-developer-dark-theme",
|
||||||
|
"display_name": "OpenSlides Developer",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
weight=141,
|
weight=141,
|
||||||
group="General",
|
group="General",
|
||||||
|
@ -562,6 +562,7 @@ class UserLoginView(WhoAmIDataView):
|
|||||||
context["legal_notice"] = config["general_event_legal_notice"]
|
context["legal_notice"] = config["general_event_legal_notice"]
|
||||||
# Add the theme, so the loginpage is themed correctly
|
# Add the theme, so the loginpage is themed correctly
|
||||||
context["theme"] = config["openslides_theme"]
|
context["theme"] = config["openslides_theme"]
|
||||||
|
context["logo_web_header"] = config["logo_web_header"]
|
||||||
else:
|
else:
|
||||||
# self.request.method == 'POST'
|
# self.request.method == 'POST'
|
||||||
context.update(self.get_whoami_data())
|
context.update(self.get_whoami_data())
|
||||||
|
Loading…
Reference in New Issue
Block a user