From 337c813a36e214c4095db0e51fde8c420c45f1f6 Mon Sep 17 00:00:00 2001 From: jojohoch <joachim.hoch@iqb.hu-berlin.de> Date: Thu, 26 Oct 2023 10:56:19 +0200 Subject: [PATCH] Allow disjunction and conjunction of rules for displaying sections - Replace throwing Errors with InstantiationErors in unit, page and section in strictInstantiation mode --- docs/release-notes-editor.md | 5 ++ docs/release-notes-player.md | 8 ++- docs/unit_definition_changelog.txt | 4 ++ docs/version-history.md | 1 + projects/common/models/page.ts | 3 +- projects/common/models/section.ts | 8 ++- projects/common/models/unit.ts | 3 +- .../canvas/section-menu.component.ts | 2 + .../visibility-rules-dialog.component.html | 9 ++- .../visibility-rules-dialog.component.ts | 3 + .../editor/src/app/services/dialog.service.ts | 3 + projects/editor/src/assets/i18n/de.json | 7 +- .../src/app/components/unit/unit.component.ts | 4 +- .../section-visibility-handling.directive.ts | 72 ++++++++++++------- 14 files changed, 96 insertions(+), 36 deletions(-) diff --git a/docs/release-notes-editor.md b/docs/release-notes-editor.md index 8047de06a..6f44dad9a 100644 --- a/docs/release-notes-editor.md +++ b/docs/release-notes-editor.md @@ -1,5 +1,10 @@ Editor ====== +## 2.1.0 +### Neue Funktionen +- Für die Bedingungen zur Sichtbarkeit von Abschnitten kann nun die + logische Verknüpfung festgelegt werden: UND / ODER + ## 2.0.3 ### Fehlerbehebungen - Element-IDs gelöschter Abschnitte werden korrekt verfügbar gemacht diff --git a/docs/release-notes-player.md b/docs/release-notes-player.md index e57fadd82..99ce1892f 100644 --- a/docs/release-notes-player.md +++ b/docs/release-notes-player.md @@ -1,6 +1,12 @@ Player ====== -## 2.0.4 +## 2.1.0 +### Neue Funktionen +- Der Player zeigt nur Unitdefinitionen an, die mit der gleichen Editor-Version erstellt wurden. + Ältere Unitdefinitionen müssen daher zunächst mit einem Editor in der Version 2.1 geöffnet + und gespeichert werden. + +### Fehlerbehebungen - Klappliste-Element repariert (ist wieder anklickbar) - Optionsfelder: Textausrichtung bei mehrzeiligen Optionen orientiert sich wieder an der ersten Zeile - Optionsfelder mit Bild: Optionen richten sich wieder nach unten aus diff --git a/docs/unit_definition_changelog.txt b/docs/unit_definition_changelog.txt index 014a71e37..8b6519804 100644 --- a/docs/unit_definition_changelog.txt +++ b/docs/unit_definition_changelog.txt @@ -121,3 +121,7 @@ iqb-aspect-definition@1.0.0 - TextAreaElement, TextFieldElement, TextFieldSimpleElement, SpellCorrectElement: - remove property: softwareKeyboardShowFrench - new property: addInputAssistanceToKeyboard + +4.0.0 + - Section + - new property: logicalConnectiveOfRules: 'disjunction' | 'conjunction' diff --git a/docs/version-history.md b/docs/version-history.md index 5414ca84e..188c56cbd 100644 --- a/docs/version-history.md +++ b/docs/version-history.md @@ -1,5 +1,6 @@ | Editor | Unit Def | Player | |--------|----------|--------| +| 2.1.0 | 4.1.0 | 2.1.0 | | 2.0.0 | 4.0.0 | 2.0.0 | | 1.39.0 | 3.10.0 | 1.32.0 | | 1.38.0 | 3.10.0 | 1.31.0 | diff --git a/projects/common/models/page.ts b/projects/common/models/page.ts index 161ac8e00..0fcc78a1a 100644 --- a/projects/common/models/page.ts +++ b/projects/common/models/page.ts @@ -2,6 +2,7 @@ import { Section } from 'common/models/section'; import { UIElement } from 'common/models/elements/element'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class Page { [index: string]: unknown; @@ -26,7 +27,7 @@ export class Page { this.sections = page.sections.map(section => new Section(section)); } else { if (environment.strictInstantiation) { - throw Error('Error at Page instantiation'); + throw new InstantiationEror('Error at Page instantiation'); } if (page?.hasMaxWidth !== undefined) this.hasMaxWidth = page.hasMaxWidth; if (page?.maxWidth !== undefined) this.maxWidth = page.maxWidth; diff --git a/projects/common/models/section.ts b/projects/common/models/section.ts index d21be97a0..b460deef6 100644 --- a/projects/common/models/section.ts +++ b/projects/common/models/section.ts @@ -12,6 +12,7 @@ import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; import { VisibilityRule } from 'common/models/visibility-rule'; import { ElementFactory } from 'common/util/element.factory'; import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class Section { [index: string]: unknown; @@ -26,6 +27,7 @@ export class Section { visibilityDelay: number = 0; animatedVisibility: boolean = false; enableReHide: boolean = false; + logicalConnectiveOfRules: 'disjunction' | 'conjunction' = 'disjunction'; visibilityRules: VisibilityRule[] = []; constructor(section?: SectionProperties) { @@ -40,12 +42,13 @@ export class Section { this.visibilityDelay = section.visibilityDelay; this.animatedVisibility = section.animatedVisibility; this.enableReHide = section.enableReHide; + this.logicalConnectiveOfRules = section.logicalConnectiveOfRules; this.visibilityRules = section.visibilityRules; this.elements = section.elements .map(element => ElementFactory.createElement(element)) as PositionedUIElement[]; } else { if (environment.strictInstantiation) { - throw Error('Error at Section instantiation'); + throw new InstantiationEror('Error at Section instantiation'); } if (section?.height !== undefined) this.height = section.height; if (section?.backgroundColor !== undefined) this.backgroundColor = section.backgroundColor; @@ -57,6 +60,7 @@ export class Section { if (section?.visibilityDelay !== undefined) this.visibilityDelay = section.visibilityDelay; if (section?.animatedVisibility !== undefined) this.animatedVisibility = section.animatedVisibility; if (section?.enableReHide !== undefined) this.enableReHide = section.enableReHide; + if (section?.logicalConnectiveOfRules !== undefined) this.logicalConnectiveOfRules = section.logicalConnectiveOfRules; if (section?.visibilityRules !== undefined) this.visibilityRules = section.visibilityRules; this.elements = section?.elements !== undefined ? section.elements.map(element => ElementFactory.createElement(element)) as PositionedUIElement[] : @@ -104,6 +108,7 @@ export interface SectionProperties { visibilityDelay: number; animatedVisibility: boolean; enableReHide: boolean; + logicalConnectiveOfRules: 'disjunction' | 'conjunction'; visibilityRules: VisibilityRule[]; } @@ -120,5 +125,6 @@ function isValid(blueprint?: SectionProperties): boolean { blueprint.visibilityDelay !== undefined && blueprint.animatedVisibility !== undefined && blueprint.enableReHide !== undefined && + blueprint.logicalConnectiveOfRules !== undefined && blueprint.visibilityRules !== undefined; } diff --git a/projects/common/models/unit.ts b/projects/common/models/unit.ts index b0beed434..3b70dc4ac 100644 --- a/projects/common/models/unit.ts +++ b/projects/common/models/unit.ts @@ -4,6 +4,7 @@ import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; import { StateVariable } from 'common/models/state-variable'; import { environment } from 'common/environment'; import { VersionManager } from 'common/services/version-manager'; +import { InstantiationEror } from 'common/util/errors'; import { ArrayUtils } from 'common/util/array'; export class Unit implements UnitProperties { @@ -19,7 +20,7 @@ export class Unit implements UnitProperties { this.pages = unit.pages.map(page => new Page(page)); } else { if (environment.strictInstantiation) { - throw Error('Error at unit instantiation'); + throw new InstantiationEror('Error at unit instantiation'); } this.version = VersionManager.getCurrentVersion(); if (unit?.stateVariables !== undefined) this.stateVariables = unit.stateVariables; diff --git a/projects/editor/src/app/components/canvas/section-menu.component.ts b/projects/editor/src/app/components/canvas/section-menu.component.ts index 8bd8e1e81..098dd4f04 100644 --- a/projects/editor/src/app/components/canvas/section-menu.component.ts +++ b/projects/editor/src/app/components/canvas/section-menu.component.ts @@ -293,6 +293,7 @@ export class SectionMenuComponent implements OnDestroy { this.dialogService .showVisibilityRulesDialog( this.section.visibilityRules, + this.section.logicalConnectiveOfRules, this.getControlIds(), this.section.visibilityDelay, this.section.animatedVisibility, @@ -301,6 +302,7 @@ export class SectionMenuComponent implements OnDestroy { .subscribe(visibilityConfig => { if (visibilityConfig) { this.updateModel('visibilityRules', visibilityConfig.visibilityRules); + this.updateModel('logicalConnectiveOfRules', visibilityConfig.logicalConnectiveOfRules); this.updateModel('visibilityDelay', visibilityConfig.visibilityDelay); this.updateModel('animatedVisibility', visibilityConfig.animatedVisibility); this.updateModel('enableReHide', visibilityConfig.enableReHide); diff --git a/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.html b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.html index 901ddfcdf..1f8055eff 100644 --- a/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.html +++ b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.html @@ -2,6 +2,11 @@ <div mat-dialog-content class="fx-column-start-stretch fx-gap-20"> <div *ngIf="controlIds.length && visibilityRules.length" class="fx-column-start-start"> + <label>{{'section.logicalConnectiveOfRules' | translate}}</label> + <mat-radio-group [(ngModel)]="logicalConnectiveOfRules"> + <mat-radio-button value="disjunction">{{'section.rulesDisjunction' | translate}}</mat-radio-button> + <mat-radio-button value="conjunction">{{'section.rulesConjunction' | translate}}</mat-radio-button> + </mat-radio-group> <mat-checkbox [(ngModel)]="enableReHide"> {{'section.enableReHide' | translate}} @@ -47,7 +52,9 @@ <div mat-dialog-actions> <button *ngIf="controlIds.length" mat-button - [mat-dialog-close]="{visibilityRules, visibilityDelay, animatedVisibility, enableReHide}"> + [mat-dialog-close]="{ + visibilityRules, logicalConnectiveOfRules, visibilityDelay, animatedVisibility, enableReHide + }"> {{'save' | translate}} </button> <button mat-button diff --git a/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.ts b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.ts index 78fa0f594..1d59a88ec 100644 --- a/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.ts +++ b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.ts @@ -19,6 +19,7 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog'; }) export class VisibilityRulesDialogComponent { visibilityRules!: VisibilityRule[]; + logicalConnectiveOfRules!: 'disjunction' | 'conjunction'; controlIds!: string[]; visibilityDelay!: number; animatedVisibility!: boolean; @@ -27,6 +28,7 @@ export class VisibilityRulesDialogComponent { constructor( @Inject(MAT_DIALOG_DATA) private data: { visibilityRules: VisibilityRule[], + logicalConnectiveOfRules: 'disjunction' | 'conjunction', visibilityDelay: number, animatedVisibility: boolean, controlIds: string[], @@ -34,6 +36,7 @@ export class VisibilityRulesDialogComponent { } ) { this.visibilityRules = [...data.visibilityRules]; + this.logicalConnectiveOfRules = data.logicalConnectiveOfRules; this.visibilityDelay = data.visibilityDelay; this.animatedVisibility = data.animatedVisibility; this.enableReHide = data.enableReHide; diff --git a/projects/editor/src/app/services/dialog.service.ts b/projects/editor/src/app/services/dialog.service.ts index 00c7c8afc..b990658ef 100644 --- a/projects/editor/src/app/services/dialog.service.ts +++ b/projects/editor/src/app/services/dialog.service.ts @@ -163,12 +163,14 @@ export class DialogService { } showVisibilityRulesDialog(visibilityRules: VisibilityRule[], + logicalConnectiveOfRules: 'disjunction' | 'conjunction', controlIds: string[], visibilityDelay: number, animatedVisibility: boolean, enableReHide: boolean ): Observable<{ visibilityRules: VisibilityRule[], + logicalConnectiveOfRules: 'disjunction' | 'conjunction', visibilityDelay: number, animatedVisibility: boolean, enableReHide: boolean @@ -177,6 +179,7 @@ export class DialogService { .open(VisibilityRulesDialogComponent, { data: { visibilityRules, + logicalConnectiveOfRules, controlIds, visibilityDelay, animatedVisibility, diff --git a/projects/editor/src/assets/i18n/de.json b/projects/editor/src/assets/i18n/de.json index 5a3ab1e71..c2b52d150 100644 --- a/projects/editor/src/assets/i18n/de.json +++ b/projects/editor/src/assets/i18n/de.json @@ -40,14 +40,17 @@ "stateVariableId": "ID", "manageStateVariables": "Zustandsvariablen verwalten", "section": { - "manageVisibilityRules": "Sichtbarkeit verwalten", + "manageVisibilityRules": "Bedingungen zur Sichtbarkeit von Abschnitten bearbeiten", "animatedVisibility": "Einblenden mit Scroll-Animation", "visibilityDelay": "Verzögertes Einblenden (in ms)", "enableReHide": "Erneutes Ausblenden erlauben, wenn die Bedingungen nicht mehr erfüllt sind", "controlId": "Variablen/Element-ID", "operator": "Operator", "addElementsOrStateVariables": "Bitte zuerst Elemente oder Zustandsvariablen anlegen", - "value": "Wert" + "value": "Wert", + "rulesDisjunction": "ODER", + "rulesConjunction": "UND", + "logicalConnectiveOfRules": "Logische Verknüpfung der Bedingungen:" }, "visibilityRule": { "contains": "Enthält", diff --git a/projects/player/src/app/components/unit/unit.component.ts b/projects/player/src/app/components/unit/unit.component.ts index 47913142c..b0ac32a2c 100644 --- a/projects/player/src/app/components/unit/unit.component.ts +++ b/projects/player/src/app/components/unit/unit.component.ts @@ -82,8 +82,8 @@ export class UnitComponent implements OnInit { } private checkUnitDefinitionVersion(unitDefinition: Record<string, unknown>): void { - if (!VersionManager.hasCompatibleVersion(unitDefinition)) { - if (VersionManager.isNewer(unitDefinition)) { + if (unitDefinition.version !== VersionManager.getCurrentVersion()) { + if (!VersionManager.hasCompatibleVersion(unitDefinition) && VersionManager.isNewer(unitDefinition)) { throw Error(this.translateService.instant('errorMessage.unitDefinitionIsNewer')); } throw Error(this.translateService.instant('errorMessage.unitDefinitionIsOutdated')); diff --git a/projects/player/src/app/directives/section-visibility-handling.directive.ts b/projects/player/src/app/directives/section-visibility-handling.directive.ts index 72a3540f6..5523d3a4d 100644 --- a/projects/player/src/app/directives/section-visibility-handling.directive.ts +++ b/projects/player/src/app/directives/section-visibility-handling.directive.ts @@ -10,6 +10,7 @@ import { ValueChangeElement } from 'common/models/elements/element'; import { ElementCode } from 'player/modules/verona/models/verona'; import { Storable } from 'player/src/app/classes/storable'; import { StateVariableStateService } from 'player/src/app/services/state-variable-state.service'; +import { VisibilityRule } from 'common/models/visibility-rule'; @Directive({ selector: '[aspectSectionVisibilityHandling]' @@ -140,39 +141,56 @@ export class SectionVisibilityHandlingDirective implements OnInit, OnDestroy { } private areVisibilityRulesFulfilled(): boolean { - return this.section.visibilityRules.some(rule => { + const methodName = this.section.logicalConnectiveOfRules === 'disjunction' ? 'some' : 'every'; + return this.section.visibilityRules[methodName](rule => { if (this.getAnyElementCodeById(rule.id)) { - const codeValue = this.getAnyElementCodeById(rule.id)?.value; - const value = codeValue || codeValue === 0 ? codeValue : ''; - switch (rule.operator) { - case '=': - return value.toString() === rule.value; - case '≠': - return value.toString() !== rule.value; - case '>': - return Number(value) > Number(rule.value); - case '<': - return Number(value) < Number(rule.value); - case '≥': - return Number(value) >= Number(rule.value); - case '≤': - return Number(value) <= Number(rule.value); - case 'contains': - return value.toString().includes(rule.value); - case 'pattern': - return SectionVisibilityHandlingDirective.isPatternMatching(value.toString(), rule.value); - case 'minLength': - return value.toString().length >= Number(rule.value); - case 'maxLength': - return value.toString().length <= Number(rule.value); - default: - return false; - } + return this.isRuleFullFilled(rule); } return false; }); } + isRuleFullFilled(rule: VisibilityRule): boolean { + let isFullFilled; + const codeValue = this.getAnyElementCodeById(rule.id)?.value; + const value = codeValue || codeValue === 0 ? codeValue : ''; + switch (rule.operator) { + case '=': + isFullFilled = value.toString() === rule.value; + break; + case '≠': + isFullFilled = value.toString() !== rule.value; + break; + case '>': + isFullFilled = Number(value) > Number(rule.value); + break; + case '<': + isFullFilled = Number(value) < Number(rule.value); + break; + case '≥': + isFullFilled = Number(value) >= Number(rule.value); + break; + case '≤': + isFullFilled = Number(value) <= Number(rule.value); + break; + case 'contains': + isFullFilled = value.toString().includes(rule.value); + break; + case 'pattern': + isFullFilled = SectionVisibilityHandlingDirective.isPatternMatching(value.toString(), rule.value); + break; + case 'minLength': + isFullFilled = value.toString().length >= Number(rule.value); + break; + case 'maxLength': + isFullFilled = value.toString().length <= Number(rule.value); + break; + default: + isFullFilled = false; + } + return isFullFilled; + } + private static isPatternMatching(value: string, ruleValue: string): boolean { // We use a similar implementation to Angular's PatternValidator let regexStr = (ruleValue.charAt(0) !== '^') ? `^${ruleValue}` : ruleValue; -- GitLab