Commit e92491d1 authored by Christopher Mark Fullarton's avatar Christopher Mark Fullarton
Browse files

Fixes member-group manager and init of singlechoice questions

parent bea6d4d2
......@@ -31,7 +31,7 @@ pushd ../..
popd
# Change absolute paths to relative ones
sed -i www/index.html -e"s|/theme-default.css|./theme-default.css"
sed -i www/index.html -e"s|/theme-default.css|./theme-default.css|g"
find www -type f -exec sed -e's|(/assets/|(./assets/|g' -i {} \;
sed -e's|:"/|:"./|g' -i www/assets/meta/*/linkNodes.json
......
......@@ -22,7 +22,7 @@
</h4>
</div>
</div>
<div class="d-flex align-self-end align-items-center">
<div class="d-flex align-items-center">
<div class="text-right cursor-pointer pr-2" *ngIf="motdData.getUnseenMotds()?.length || motdData.getPinnedMotds()?.length">
<button type="button" class="btn btn-primary d-flex align-items-center" (click)="motdService.showMotdModal(true)">
<fa-icon class="mr-1" size="lg" [icon]="'info-circle'" ></fa-icon>
......
......@@ -11,9 +11,8 @@ import { SurveyQuestionEntity } from './entities/question/SurveyQuestionEntity';
import { TrueFalseSingleChoiceQuestionEntity } from './entities/question/TrueFalseSingleChoiceQuestionEntity';
import { YesNoSingleChoiceQuestionEntity } from './entities/question/YesNoSingleChoiceQuestionEntity';
import { QuestionType } from './enums/QuestionType';
import {SettingsService} from '../service/settings/settings.service';
export const getQuestionForType = (settingsService: SettingsService, type: QuestionType, data = {}): AbstractQuestionEntity => {
export const getQuestionForType = (settingsService, type: QuestionType, data = {}): AbstractQuestionEntity => {
switch (type) {
case QuestionType.FreeTextQuestion:
return new FreeTextQuestionEntity(settingsService, data);
......@@ -36,7 +35,7 @@ export const getQuestionForType = (settingsService: SettingsService, type: Quest
}
};
export const getDefaultQuestionForType = (settingsService: SettingsService, translateService: TranslateService, type: QuestionType, data = {}) => {
export const getDefaultQuestionForType = (settingsService, translateService: TranslateService, type: QuestionType, data = {}) => {
switch (type) {
case QuestionType.TrueFalseSingleChoiceQuestion:
return new TrueFalseSingleChoiceQuestionEntity(settingsService, {
......
export const hexToRgb = (hex): { r: number, g: number, b: number } => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
} : null;
};
export const transformForegroundColor = (rgbObj: { r: number, g: number, b: number }): string => {
const o = Math.round((
(
rgbObj.r * 299
) + (
rgbObj.g * 587
) + (
rgbObj.b * 114
)
) / 1000);
return o < 125 ? 'ffffff' : '000000';
};
export const ColorTransform = { hexToRgb, transformForegroundColor };
......@@ -5,7 +5,7 @@ import { AbstractChoiceQuestionEntity } from './AbstractChoiceQuestionEntity';
export class SingleChoiceQuestionEntity extends AbstractChoiceQuestionEntity {
public TYPE = QuestionType.SingleChoiceQuestion;
constructor(protected settingsService, props) {
constructor(settingsService, props) {
super(settingsService, props);
}
......
......@@ -79,7 +79,7 @@ describe('LeaderboardComponent', () => {
provide: QuizService,
useValue: {
quizUpdateEmitter: of(null),
loadDataToPlay: () => new Promise(resolve => resolve()),
loadDataToPlay: () => new Promise<void>(resolve => resolve()),
},
}, I18nService, HeaderLabelService, {
provide: AttendeeService,
......
......@@ -12,7 +12,7 @@
class="my-1 d-flex flex-wrap flex-sm-nowrap col-auto">
<p class="text-center mr-sm-2 d-flex justify-content-center flex-grow-1 align-self-center text-nowrap">{{'component.lobby.own_nickname' | translate}}</p>
<div [style.background-color]="'#' + sanitizeHTML(myself.colorCode)"
[style.color]="'#' + sanitizeHTML(transformForegroundColor(hexToRgb(myself.colorCode)))"
[style.color]="'#' + sanitizeHTML(ColorTransform.transformForegroundColor(ColorTransform.hexToRgb(myself.colorCode)))"
class="p-2 rounded d-flex align-items-center justify-content-center own-nick w-100">
<div *ngIf="myself.groupName" class="mr-2 flex-grow-1 flex-shrink-0">
<div *ngIf="isHtmlNickname(myself.groupName)"
......@@ -40,7 +40,7 @@
<div (click)="openKickMemberModal(removeMemberModal, elem.name)"
[class.cursor-pointer]="quizService.isOwner"
[style.background-color]="sanitizeHTML(getColorForNick(elem))"
[style.color]="'#' + sanitizeHTML(transformForegroundColor(hexToRgb(getColorForNick(elem))))"
[style.color]="'#' + sanitizeHTML(ColorTransform.transformForegroundColor(ColorTransform.hexToRgb(getColorForNick(elem))))"
class="p-2 rounded d-flex justify-content-center align-items-center w-100">
<div *ngIf="elem.groupName" class="mr-2 flex-grow-1 flex-shrink-0">
<div *ngIf="isHtmlNickname(elem.groupName)"
......
......@@ -6,6 +6,7 @@ import { NgbActiveModal, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstra
import { SimpleMQ } from 'ng2-simple-mq';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ColorTransform } from '../../../lib/color-transform';
import { MemberEntity } from '../../../lib/entities/member/MemberEntity';
import { AudioPlayerConfigTarget } from '../../../lib/enums/AudioPlayerConfigTarget';
import { StorageKey } from '../../../lib/enums/enums';
......@@ -50,6 +51,7 @@ export class QuizLobbyComponent implements OnInit, OnDestroy, IHasTriggeredNavig
private readonly _messageSubscriptions: Array<string> = [];
public hasTriggeredNavigation: boolean;
public musicConfig: IAudioPlayerConfig;
public readonly ColorTransform = ColorTransform;
get nickToRemove(): string {
return this._nickToRemove;
......@@ -167,28 +169,6 @@ export class QuizLobbyComponent implements OnInit, OnDestroy, IHasTriggeredNavig
this.memberApiService.deleteMember(this.quizService.quiz.name, name).subscribe();
}
public hexToRgb(hex): { r: number, g: number, b: number } {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
} : null;
}
public transformForegroundColor(rgbObj: { r: number, g: number, b: number }): string {
const o = Math.round((
(
rgbObj.r * 299
) + (
rgbObj.g * 587
) + (
rgbObj.b * 114
)
) / 1000);
return o < 125 ? 'ffffff' : '000000';
}
public sanitizeHTML(value: string): string {
return this.sanitizer.sanitize(SecurityContext.HTML, `${value}`);
}
......@@ -324,7 +304,7 @@ export class QuizLobbyComponent implements OnInit, OnDestroy, IHasTriggeredNavig
const promise = this.attendeeService.attendees.length ? //
this.ngbModal.open(EditModeConfirmComponent).result : //
new Promise<any>(resolve => resolve());
new Promise<void>(resolve => resolve());
promise.then(() => {
this.hasTriggeredNavigation = true;
this._destroy.next();
......
......@@ -49,7 +49,7 @@
(keyup.enter)="addMemberGroup()"
(keypress)="formGroup.updateValueAndValidity()"
[placeholder]="'component.membergroup-manager.create-team-placeholder' | translate"
[ngbTypeahead]="search.bind(self)"
[ngbTypeahead]="search"
[attr.aria-label]="'component.membergroup-manager.create-team-placeholder' | translate"
[inputFormatter]="inputFormatter.bind(self)"
[resultTemplate]="resultTemplate"
......@@ -70,9 +70,9 @@
</div>
<div *ngIf="!formGroup.get('memberGroupName').valid" class="position-relative">
<div *ngIf="formGroup.get('memberGroupName').errors.full" class="invalid-tooltip d-block">{{'component.membergroup-manager.validation-error.full-member-group' | translate}}</div>
<div *ngIf="formGroup.get('memberGroupName').errors.invalid" class="invalid-tooltip d-block">{{'component.membergroup-manager.validation-error.invalid-member-group' | translate}}</div>
<div *ngIf="formGroup.get('memberGroupName').errors.duplicate" class="invalid-tooltip d-block">{{'component.membergroup-manager.validation-error.duplicate-member-group' | translate}}</div>
<div *ngIf="formGroup.get('memberGroupName').hasError('full')" class="invalid-tooltip d-block">{{'component.membergroup-manager.validation-error.full-member-group' | translate}}</div>
<div *ngIf="formGroup.get('memberGroupName').hasError('invalid')" class="invalid-tooltip d-block">{{'component.membergroup-manager.validation-error.invalid-member-group' | translate}}</div>
<div *ngIf="formGroup.get('memberGroupName').hasError('duplicate')" class="invalid-tooltip d-block">{{'component.membergroup-manager.validation-error.duplicate-member-group' | translate}}</div>
</div>
</form>
......@@ -104,13 +104,16 @@
<div class="form-row">
<div *ngFor="let color of groupColors"
class="col-6 col-sm-3 col-lg-2 mb-1">
<button [disabled]="group.color === color || hasGroupColorSelected(color)"
class="btn btn-sm color-picker"
<button class="btn btn-sm color-picker"
[disabled]="group.color === color || hasGroupColorSelected(color)"
[style.border-color]="color"
[style.background-color]="group.color !== color && !hasGroupColorSelected(color) ? color : 'transparent'"
[style.background-color]="color"
[style.color]="'#' + sanitizeHTML(ColorTransform.transformForegroundColor(ColorTransform.hexToRgb(color)))"
(click)="group.color = color">
<fa-icon *ngIf="group.color === color" [icon]="'check'"></fa-icon>
<fa-icon *ngIf="group.color !== color && hasGroupColorSelected(color)" [icon]="'times'"></fa-icon>
<fa-icon *ngIf="group.color === color"
[icon]="'check'"></fa-icon>
<fa-icon *ngIf="group.color !== color && hasGroupColorSelected(color)"
[icon]="'times'"></fa-icon>
</button>
</div>
</div>
......
......@@ -26,6 +26,10 @@
.color-picker {
height: 40px;
width: 100%;
&:disabled {
opacity: 1;
}
}
:host ::ng-deep ngb-typeahead-window {
......
import { isPlatformBrowser } from '@angular/common';
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID, SecurityContext, ViewChild } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, ValidationErrors, Validators } from '@angular/forms';
import { AbstractControl, FormBuilder, FormControl, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
import { DomSanitizer } from '@angular/platform-browser';
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core';
import { merge, Observable, Subject } from 'rxjs';
import { merge, Observable, OperatorFunction, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, map, takeUntil } from 'rxjs/operators';
import { ColorTransform } from '../../../lib/color-transform';
import { StorageKey } from '../../../lib/enums/enums';
import { IMemberGroupBase } from '../../../lib/interfaces/users/IMemberGroupBase';
import { NickApiService } from '../../../service/api/nick/nick-api.service';
......@@ -25,24 +26,6 @@ interface IMemberGroupInput {
styleUrls: ['./member-group-manager.component.scss'],
})
export class MemberGroupManagerComponent implements OnInit, OnDestroy {
public static readonly TYPE = 'MemberGroupManagerComponent';
private _memberGroups: Array<IMemberGroupBase> = [];
private _maxMembersPerGroup: number;
private _autoJoinToGroup: boolean;
private availableEmojis: Array<string> = [];
private readonly _destroy = new Subject();
public readonly groupColors: Array<string> = [
'#ff0000', '#008000', '#800080', '#add8e6', '#ffa500', '#ffc0cb', '#5f9ea0', '#fff8dc', '#7fffd4', '#bf0202', '#025abf', '#e6dd26',
];
public readonly formGroup = this.formBuilder.group({
memberGroupName: new FormControl(null, { validators: [Validators.required, this.hasValidGroupSelected.bind(this)], updateOn: 'change' }),
});
@ViewChild('instance', { static: true }) public instance: NgbTypeahead;
public focus$ = new Subject<string>();
public click$ = new Subject<string>();
public readonly self = this;
get memberGroups(): Array<IMemberGroupBase> {
return this._memberGroups;
......@@ -63,6 +46,25 @@ export class MemberGroupManagerComponent implements OnInit, OnDestroy {
set autoJoinToGroup(value: boolean) {
this._autoJoinToGroup = value;
}
public static readonly TYPE = 'MemberGroupManagerComponent';
private _memberGroups: Array<IMemberGroupBase> = [];
private _maxMembersPerGroup: number;
private _autoJoinToGroup: boolean;
private availableEmojis: Array<string> = [];
private readonly _destroy = new Subject();
public readonly groupColors: Array<string> = [
'#ff0000', '#008000', '#800080', '#add8e6', '#ffa500', '#ffc0cb', '#5f9ea0', '#fff8dc', '#7fffd4', '#bf0202', '#025abf', '#e6dd26',
];
public readonly formGroup = this.formBuilder.group({
memberGroupName: new FormControl(null, { validators: [Validators.required, this.hasValidGroupSelected()], updateOn: 'change' }),
});
@ViewChild('instance', { static: true }) public instance: NgbTypeahead;
public focus$ = new Subject<string>();
public click$ = new Subject<string>();
public readonly self = this;
public readonly ColorTransform = ColorTransform;
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
......@@ -76,14 +78,28 @@ export class MemberGroupManagerComponent implements OnInit, OnDestroy {
private nickApiService: NickApiService,
) {
this.headerLabelService.headerLabel = '';
this.footerBarService.TYPE_REFERENCE = MemberGroupManagerComponent.TYPE;
footerBarService.replaceFooterElements([
this.footerBarService.footerElemBack,
]);
}
public readonly search = (text$: Observable<string>): Observable<Array<IMemberGroupInput>> => {
const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen()));
const inputFocus$ = this.focus$;
if (isPlatformBrowser(this.platformId)) {
this.quizService.loadDataToEdit(sessionStorage.getItem(StorageKey.CurrentQuizName));
}
return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$).pipe(map(term => {
if (!term.length) {
return [];
}
return this.availableEmojis.filter(emoji => emoji.startsWith(term)).slice(0, 10).map(value => ({
html: this.parseNickname(value),
raw: value,
}));
}));
}
public ngOnInit(): void {
......@@ -98,6 +114,10 @@ export class MemberGroupManagerComponent implements OnInit, OnDestroy {
}, error => {
console.log(MemberGroupManagerComponent.TYPE, ': GetPredefinedNicks failed', error);
});
if (isPlatformBrowser(this.platformId)) {
this.quizService.loadDataToEdit(sessionStorage.getItem(StorageKey.CurrentQuizName));
}
}
public ngOnDestroy(): void {
......@@ -117,25 +137,9 @@ export class MemberGroupManagerComponent implements OnInit, OnDestroy {
return `${groupName.raw}`;
}
public search(text$: Observable<string>): Observable<Array<IMemberGroupInput>> {
const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen()));
const inputFocus$ = this.focus$;
return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$).pipe(map(term => {
if (!term.length) {
return [];
}
return this.availableEmojis.filter(emoji => emoji.startsWith(term)).slice(0, 10).map(value => ({
html: this.parseNickname(value),
raw: value,
}));
}));
}
public addMemberGroup(): void {
if (!this.formGroup.get('memberGroupName').value || this.memberGroups.length === this.groupColors.length) {
console.log('no value found or group limit reached');
return;
}
......@@ -191,30 +195,37 @@ export class MemberGroupManagerComponent implements OnInit, OnDestroy {
}) > -1;
}
private hasValidGroupSelected(control: AbstractControl): ValidationErrors {
if (control.pristine) {
return {};
}
private hasValidGroupSelected(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (control.pristine) {
console.log('returning empty object');
return {};
}
if (this.memberGroups.length === this.groupColors.length) {
return { full: true };
}
if (this.memberGroups.length === this.groupColors.length) {
console.log('returning full:true object');
return { full: true };
}
if (this.memberGroupExists()) {
return { duplicate: true };
}
if (this.memberGroupExists()) {
console.log('returning duplicate:true object');
return { duplicate: true };
}
let emojiMatch;
if (control.value?.raw) {
emojiMatch = control.value.raw.match(/:[\w\+\-]+:/);
} else {
emojiMatch = control.value?.match(/:[\w\+\-]+:/);
}
let emojiMatch;
if (control.value?.raw) {
emojiMatch = control.value.raw.match(/:[\w\+\-]+:/);
} else {
emojiMatch = control.value?.match(/:[\w\+\-]+:/);
}
if (emojiMatch && emojiMatch[0] !== emojiMatch.input) {
return { invalid: true };
}
if (emojiMatch && emojiMatch[0] !== emojiMatch.input) {
console.log('returning invalid:true object');
return { invalid: true };
}
return null;
console.log('returning null');
return null;
};
}
}
......@@ -8,6 +8,7 @@ import { IAvailableNicks } from '../../../lib/interfaces/IAvailableNicks';
import { NickApiService } from '../../../service/api/nick/nick-api.service';
import { CustomMarkdownService } from '../../../service/custom-markdown/custom-markdown.service';
import { FooterBarService } from '../../../service/footer-bar/footer-bar.service';
import { HeaderLabelService } from '../../../service/header-label/header-label.service';
import { QuizService } from '../../../service/quiz/quiz.service';
@Component({
......@@ -56,12 +57,15 @@ export class NicknameManagerComponent implements OnInit, OnDestroy {
public quizService: QuizService,
@Inject(PLATFORM_ID) private platformId: Object,
private sanitizer: DomSanitizer,
private headerLabelService: HeaderLabelService,
private footerBarService: FooterBarService,
private nickApiService: NickApiService,
private customMarkdownService: CustomMarkdownService,
private cd: ChangeDetectorRef,
) {
this.headerLabelService.headerLabel = '';
this.footerBarService.TYPE_REFERENCE = NicknameManagerComponent.TYPE;
const footerElements = [this.footerBarService.footerElemBack, this.footerBarService.footerElemBlockRudeNicknames];
this.footerBarService.replaceFooterElements(footerElements);
......
......@@ -8,6 +8,7 @@ import { AudioPlayerConfigTarget } from '../../../lib/enums/AudioPlayerConfigTar
import { StorageKey } from '../../../lib/enums/enums';
import { IAudioPlayerConfig, ISong } from '../../../lib/interfaces/IAudioConfig';
import { FooterBarService } from '../../../service/footer-bar/footer-bar.service';
import { HeaderLabelService } from '../../../service/header-label/header-label.service';
import { QuizService } from '../../../service/quiz/quiz.service';
import {SettingsService} from '../../../service/settings/settings.service';
......@@ -53,8 +54,11 @@ export class SoundManagerComponent implements OnInit, OnDestroy {
private footerBarService: FooterBarService,
private quizService: QuizService,
private settingsService: SettingsService,
private headerLabelService: HeaderLabelService,
) {
this.headerLabelService.headerLabel = '';
this.footerBarService.TYPE_REFERENCE = SoundManagerComponent.TYPE;
footerBarService.replaceFooterElements([
this.footerBarService.footerElemBack,
......
......@@ -64,18 +64,18 @@ export class HeaderLabelService {
private regenerateTitle(): void {
if (!this._headerLabel || this._headerLabel === Title.Default) {
this.titleService.setTitle(this.settingsService.frontEnv?.appName);
} else {
this.translateService
.get(this._headerLabel, this.headerLabelParams)
.subscribe(translatedValue => {
if (this.hasHeaderLabelParams()) {
this.titleService.setTitle(this.settingsService.frontEnv?.title + ' - ' + translatedValue);
} else {
this.titleService.setTitle(this.settingsService.frontEnv?.title);
}
});
return this.titleService.setTitle(this.settingsService.frontEnv?.appName);
}
this.translateService
.get(this._headerLabel, this.headerLabelParams)
.subscribe(translatedValue => {
if (this.hasHeaderLabelParams()) {
this.titleService.setTitle(this.settingsService.frontEnv?.appName + ' - ' + translatedValue);
} else {
this.titleService.setTitle(this.settingsService.frontEnv?.appName);
}
});
}
}
......@@ -211,7 +211,7 @@ export class QuizService {
if (this.quiz) {
console.log('QuizService: aborting loadDataToPlay since the quiz is already present', quizName);
this._isInEditMode = false;
resolve();
resolve(true);
return;
}
......@@ -221,7 +221,7 @@ export class QuizService {
this._isInEditMode = false;
this.isOwner = !!quiz;
console.log('QuizService: isOwner', this.isOwner);
this.restoreSettings(quizName).then(() => resolve());
this.restoreSettings(quizName).then(() => resolve(true));
}).catch(() => reject());
});
}
......@@ -245,7 +245,7 @@ export class QuizService {
this._isOwner = true;
this.quizUpdateEmitter.next(this.quiz);
console.log('QuizService: loadDataToEdit already initialized', quizName);
resolve();
resolve(true);
return;
}
......@@ -259,7 +259,7 @@ export class QuizService {
this._isInEditMode = true;
this.quiz = new QuizEntity(this.settingsService, quiz);
console.log('QuizService: loadDataToEdit finished', quiz, quizName);
resolve();
resolve(true);
});
});
}
......@@ -297,7 +297,7 @@ export class QuizService {
}
this.quiz = response.payload.quiz;
resolve();
resolve(true);
}, () => reject());
});
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment