diff --git a/projects/common/models/page.ts b/projects/common/models/page.ts index 551799b452dde332d2bf2f089b5e6b173118cb51..d1c1d729b9002f3eb3e1820cfeae71efe792d19d 100644 --- a/projects/common/models/page.ts +++ b/projects/common/models/page.ts @@ -48,6 +48,18 @@ export class Page { getVariableInfos(dropLists: DropListElement[]): VariableInfo[] { return this.sections.map(section => section.getVariableInfos(dropLists)).flat(); } + + addSection(section?: Section, sectionIndex?: number): void { + if (sectionIndex !== undefined) { + this.sections.splice(sectionIndex, 0, section || new Section()); + } else { + this.sections.push(section || new Section()); + } + } + + deleteSection(sectionIndex: number){ + this.sections.splice(sectionIndex, 1); + } } export interface PageProperties { diff --git a/projects/common/models/unit.ts b/projects/common/models/unit.ts index ed96d47f6bc6064bfea54f45604b0bb4ad4020b5..2606b9c5bb49e2009aaa13edf306ab7bef7d0009 100644 --- a/projects/common/models/unit.ts +++ b/projects/common/models/unit.ts @@ -39,25 +39,6 @@ export class Unit implements UnitProperties { ]; return this.pages.map(page => page.getVariableInfos(dropLists)).flat(); } - - /* check if movement is allowed - * - alwaysVisible has to be index 0 - * - don't move left when already the leftmost - * - don't move right when already the last - */ - canPageBeMoved(pageIndex: number, direction: 'left' | 'right'): boolean { - return !((direction === 'left' && pageIndex === 1 && this.pages[0].alwaysVisible) || - (direction === 'left' && pageIndex === 0) || - (direction === 'right' && pageIndex === this.pages.length - 1)); - } - - movePage(pageIndex: number, direction: 'left' | 'right'): void { - ArrayUtils.moveArrayItem( - this.pages[pageIndex], - this.pages, - direction === 'left' ? 'up' : 'down' - ); - } } function isValid(blueprint?: UnitProperties): boolean { diff --git a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts index fb926868a0f389ce31b774839e84357fe02e4499..d95f88cd2194ab3dbe7e49ae0085c137b7c25ff4 100644 --- a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts +++ b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts @@ -126,7 +126,7 @@ export class ElementPropertiesPanelComponent implements OnInit, OnDestroy { } duplicateElement(): void { - this.sectionService.duplicateSelectedElements(); + this.elementService.duplicateSelectedElements(); } ngOnDestroy(): void { diff --git a/projects/editor/src/app/components/unit-view/page-menu.component.ts b/projects/editor/src/app/components/unit-view/page-menu.component.ts index e4512511633147987429668dd3b9d734a8beae02..4dec3f9eb3938a6f30f622d24101e78e34b6462b 100644 --- a/projects/editor/src/app/components/unit-view/page-menu.component.ts +++ b/projects/editor/src/app/components/unit-view/page-menu.component.ts @@ -77,7 +77,7 @@ export class PageMenu implements OnDestroy { private messageService: MessageService) {} movePage(direction: 'left' | 'right'): void { - this.pageService.moveSelectedPage(direction); + this.pageService.moveSelectedPage(this.selectionService.selectedPageIndex, direction); this.pageOrderChanged.emit(); } diff --git a/projects/editor/src/app/components/unit-view/unit-view.component.css b/projects/editor/src/app/components/unit-view/unit-view.component.css index ca1b1b2943871853c2d699e875fcf9cd5758b17a..7bee92160c89c166c1464d55838b9732486e67ec 100644 --- a/projects/editor/src/app/components/unit-view/unit-view.component.css +++ b/projects/editor/src/app/components/unit-view/unit-view.component.css @@ -59,3 +59,13 @@ aspect-page-canvas { :host ::ng-deep div.mat-mdc-tab * { pointer-events: auto; } + +/* History-button tab label */ +:host ::ng-deep .mat-mdc-tab-labels>div:last-child { + margin-left: auto !important; + /*background-color: red !important;*/ +} + +.history-button { + /*margin-left: auto;*/ +} diff --git a/projects/editor/src/app/components/unit-view/unit-view.component.html b/projects/editor/src/app/components/unit-view/unit-view.component.html index 06ea1ffaf18d25c58c0c92e09aa80f8232984477..d543893d7833c47bd4b9b7bd7b22ba8161daec1d 100644 --- a/projects/editor/src/app/components/unit-view/unit-view.component.html +++ b/projects/editor/src/app/components/unit-view/unit-view.component.html @@ -54,6 +54,24 @@ </button> </ng-template> </mat-tab> + + <mat-tab disabled> + <ng-template mat-tab-label> + <button mat-icon-button class="history-button" [matMenuTriggerFor]="historyMenu"> + <mat-icon>history</mat-icon> + </button> + <mat-menu #historyMenu="matMenu"> + <h3>Änderungshistorie</h3> + <p [style.color]="'red'">Achtung experimentelle Funktion!<br> + Benutzung kann Fehler verursachen bis hin zu kaputter Unit.</p> + <div *ngFor="let command of historyService.commandList"> + <p>{{ command.title }}</p> + </div> + <button *ngIf="historyService.commandList.length > 0" (click)="unitService.rollback()">Rollback</button> + </mat-menu> + </ng-template> + </mat-tab> + </mat-tab-group> </mat-drawer-content> diff --git a/projects/editor/src/app/services/history.service.ts b/projects/editor/src/app/services/history.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b20af79357edcf4abb1af7c470c1888d736d399 --- /dev/null +++ b/projects/editor/src/app/services/history.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class HistoryService { + commandList: HistoryEntry[] = []; + + addCommand(command: UnitUpdateCommand, deletedData: Record<string, unknown>): void { + this.commandList.push({ ...command, deletedData: deletedData }); + console.log('HISTORY', this.commandList); + } + + rollback(): void { + const lastCommand = this.commandList[this.commandList.length - 1]; + lastCommand.rollback(lastCommand.deletedData); + this.commandList.splice(this.commandList.length - 1, 1); + } +} + +export interface UnitUpdateCommand { + title: string; + command: () => Record<string, unknown>; + rollback: (deletedData: Record<string, unknown>) => void; +} + +interface HistoryEntry extends UnitUpdateCommand { + deletedData: Record<string, unknown> +} diff --git a/projects/editor/src/app/services/unit-services/element.service.ts b/projects/editor/src/app/services/unit-services/element.service.ts index 41a9554b42ffc32ba2f57eef3033c0068b5e2a0a..0f0e1c209320dc1fc309beaf4ab646f4f85fb8f2 100644 --- a/projects/editor/src/app/services/unit-services/element.service.ts +++ b/projects/editor/src/app/services/unit-services/element.service.ts @@ -35,8 +35,6 @@ import { DragNDropValueObject } from 'common/models/elements/label-interfaces'; providedIn: 'root' }) export class ElementService { - unit = this.unitService.unit; - constructor(private unitService: UnitService, private selectionService: SelectionService, private dialogService: DialogService, @@ -47,7 +45,7 @@ export class ElementService { addElementToSectionByIndex(elementType: UIElementType, pageIndex: number, sectionIndex: number): void { - this.addElementToSection(elementType, this.unit.pages[pageIndex].sections[sectionIndex]); + this.addElementToSection(elementType, this.unitService.unit.pages[pageIndex].sections[sectionIndex]); } async addElementToSection(elementType: UIElementType, section: Section, @@ -111,7 +109,7 @@ export class ElementService { if (result) { ReferenceManager.deleteReferences(refs); this.unitService.unregisterIDs(elements); - this.unit.pages[this.selectionService.selectedPageIndex].sections.forEach(section => { + this.unitService.unit.pages[this.selectionService.selectedPageIndex].sections.forEach(section => { section.elements = section.elements.filter(element => !elements.includes(element)); }); this.unitService.updateUnitDefinition(); @@ -124,7 +122,7 @@ export class ElementService { .subscribe((result: boolean) => { if (result) { this.unitService.unregisterIDs(elements); - this.unit.pages[this.selectionService.selectedPageIndex].sections.forEach(section => { + this.unitService.unit.pages[this.selectionService.selectedPageIndex].sections.forEach(section => { section.elements = section.elements.filter(element => !elements.includes(element)); }); this.unitService.updateUnitDefinition(); @@ -325,6 +323,15 @@ export class ElementService { this.unitService.updateUnitDefinition(); } + duplicateSelectedElements(): void { + const selectedSection = + this.unitService.unit.pages[this.selectionService.selectedPageIndex].sections[this.selectionService.selectedSectionIndex]; + this.selectionService.getSelectedElements().forEach((element: UIElement) => { + selectedSection.elements.push(this.duplicateElement(element, true) as PositionedUIElement); + }); + this.unitService.updateUnitDefinition(); + } + /* - Also changes position of the element to not cover copied element. - Also changes and registers all copied IDs. */ duplicateElement(element: UIElement, adjustPosition: boolean = false): UIElement { @@ -393,10 +400,10 @@ export class ElementService { /* Reorder elements by their position properties, so the tab order is correct */ reorderElements() { - const sectionElementList = this.unit.pages[this.selectionService.selectedPageIndex] - .sections[this.selectionService.selectedPageSectionIndex].elements; - const isDynamicPositioning = this.unit.pages[this.selectionService.selectedPageIndex] - .sections[this.selectionService.selectedPageSectionIndex].dynamicPositioning; + const sectionElementList = this.unitService.unit.pages[this.selectionService.selectedPageIndex] + .sections[this.selectionService.selectedSectionIndex].elements; + const isDynamicPositioning = this.unitService.unit.pages[this.selectionService.selectedPageIndex] + .sections[this.selectionService.selectedSectionIndex].dynamicPositioning; const sortDynamicPositioning = (a: PositionedUIElement, b: PositionedUIElement) => { const rowSort = (a.position.gridRow !== null ? a.position.gridRow : Infinity) - diff --git a/projects/editor/src/app/services/unit-services/page.service.ts b/projects/editor/src/app/services/unit-services/page.service.ts index 28a7b82b8817bfa259cad42ee84e1830f18ee66c..cd0553a24c3141f987955bcd49789b8191d82f52 100644 --- a/projects/editor/src/app/services/unit-services/page.service.ts +++ b/projects/editor/src/app/services/unit-services/page.service.ts @@ -3,13 +3,12 @@ import { Page } from 'common/models/page'; import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; import { MessageService } from 'common/services/message.service'; import { SelectionService } from 'editor/src/app/services/selection.service'; +import { ArrayUtils } from 'common/util/array'; @Injectable({ providedIn: 'root' }) export class PageService { - unit = this.unitService.unit; - constructor(private unitService: UnitService, private messageService: MessageService, private selectionService: SelectionService) { } @@ -18,12 +17,12 @@ export class PageService { this.unitService.updateUnitDefinition({ title: 'Seite hinzugefügt', command: () => { - this.unit.pages.push(new Page()); - this.selectionService.selectedPageIndex = this.unit.pages.length - 1; // TODO selection stuff here is not good + this.unitService.unit.pages.push(new Page()); + this.selectionService.selectedPageIndex = this.unitService.unit.pages.length - 1; // TODO selection stuff here is not good return {}; }, rollback: () => { - this.unit.pages.splice(this.unit.pages.length - 1, 1); + this.unitService.unit.pages.splice(this.unitService.unit.pages.length - 1, 1); this.selectionService.selectPreviousPage(); } }); @@ -40,17 +39,29 @@ export class PageService { }; }, rollback: (deletedData: Record<string, unknown>) => { - this.unit.pages.splice(deletedData['pageIndex'] as number, 0, deletedData['deletedpage'] as Page); + this.unitService.unit.pages.splice(deletedData['pageIndex'] as number, 0, deletedData['deletedpage'] as Page); } }); } - moveSelectedPage(direction: 'left' | 'right') { - if (this.unit.canPageBeMoved(this.selectionService.selectedPageIndex, direction)) { - this.unit.movePage(this.selectionService.selectedPageIndex, direction); - this.unitService.updateUnitDefinition(); - } else { - this.messageService.showWarning('Seite kann nicht verschoben werden.'); - } + moveSelectedPage(pageIndex: number, direction: 'left' | 'right') { + this.unitService.updateUnitDefinition({ + title: 'Seite verschoben', + command: () => { + ArrayUtils.moveArrayItem( + this.unitService.unit.pages[pageIndex], + this.unitService.unit.pages, + direction === 'left' ? 'up' : 'down' + ); + return {direction}; + }, + rollback: (deletedData: Record<string, unknown>) => { + ArrayUtils.moveArrayItem( + this.unitService.unit.pages[pageIndex], + this.unitService.unit.pages, + direction === 'left' ? 'up' : 'down' + ); + } + }); } } diff --git a/projects/editor/src/app/services/unit-services/section.service.ts b/projects/editor/src/app/services/unit-services/section.service.ts index c2cc6a70b2a8e636b444130efb57c527d3c99b10..efe2c0f14833be8ae6b5843fbf7e6500287f4811 100644 --- a/projects/editor/src/app/services/unit-services/section.service.ts +++ b/projects/editor/src/app/services/unit-services/section.service.ts @@ -3,8 +3,7 @@ import { UnitService } from 'editor/src/app/services/unit-services/unit.service' import { SelectionService } from 'editor/src/app/services/selection.service'; import { Page } from 'common/models/page'; import { Section } from 'common/models/section'; -import { PositionedUIElement, UIElement } from 'common/models/elements/element'; -import { DropListElement } from 'common/models/elements/input-elements/drop-list'; +import { PositionedUIElement, UIElement, UIElementValue } from 'common/models/elements/element'; import { ArrayUtils } from 'common/util/array'; import { IDService } from 'editor/src/app/services/id.service'; import { VisibilityRule } from 'common/models/visibility-rule'; @@ -14,71 +13,103 @@ import { ElementService } from 'editor/src/app/services/unit-services/element.se providedIn: 'root' }) export class SectionService { - unit = this.unitService.unit; - constructor(private unitService: UnitService, private elementService: ElementService, private selectionService: SelectionService, private idService: IDService) { } updateSectionProperty(section: Section, property: string, value: string | number | boolean | VisibilityRule[] | { value: number; unit: string }[]): void { - section.setProperty(property, value); - this.unitService.elementPropertyUpdated.next(); - this.unitService.updateUnitDefinition(); + this.unitService.updateUnitDefinition({ + title: 'Abschnittseigenschaft geändert', + command: () => { + const oldValue = section[property]; + section.setProperty(property, value); + this.unitService.elementPropertyUpdated.next(); + return {oldValue}; + }, + rollback: (deletedData: Record<string, unknown>) => { + section.setProperty(property, deletedData.oldValue as UIElementValue); + this.unitService.elementPropertyUpdated.next(); + } + }); } - addSection(page: Page, section?: Section): void { - // register section IDs - if (section) { - section.elements.forEach(element => { - if (['drop-list', 'drop-list-simple'].includes((element as UIElement).type as string)) { - (element as DropListElement).value.forEach(value => this.idService.addID(value.id)); + addSection(page: Page, section?: Section, sectionIndex?: number): void { + this.unitService.updateUnitDefinition({ + title: 'Abschnitt hinzugefügt', + command: () => { + const newSection = section; + if (section) { + this.unitService.registerIDs(section.elements); } - if (['likert', 'cloze'].includes((element as UIElement).type as string)) { - element.getChildElements().forEach(el => { - this.idService.addID(el.id); - if ((element as UIElement).type === 'drop-list') { - (element as DropListElement).value.forEach(value => this.idService.addID(value.id)); - } - }); + page.addSection(section, sectionIndex); + this.selectionService.selectedSectionIndex = + Math.max(0, this.selectionService.selectedSectionIndex - 1); + return {section, sectionIndex}; + }, + rollback: (deletedData: Record<string, unknown>) => { + if (deletedData.section) { + this.unitService.unregisterIDs((deletedData.section as Section).elements); } - this.idService.addID(element.id); - }); - } - page.sections.push( - section || new Section() - ); - this.unitService.updateUnitDefinition(); + const sectionIndex: number = (deletedData.sectionIndex as number) !== undefined ? + (deletedData.sectionIndex as number) : + page.sections.length - 1; + page.deleteSection(sectionIndex); + this.selectionService.selectedSectionIndex = + Math.max(0, this.selectionService.selectedSectionIndex - 1); + } + }); } deleteSection(pageIndex: number, sectionIndex: number): void { - this.unitService.unregisterIDs(this.unit.pages[pageIndex].sections[sectionIndex].getAllElements()); - this.unit.pages[pageIndex].sections.splice(sectionIndex, 1); - this.unitService.updateUnitDefinition(); + this.unitService.updateUnitDefinition({ + title: `Abschnitt gelöscht - Seite ${pageIndex + 1}, Abschnitt ${sectionIndex + 1}`, + command: () => { + const deletedSection = this.unitService.unit.pages[pageIndex].sections[sectionIndex]; + this.unitService.unregisterIDs(this.unitService.unit.pages[pageIndex].sections[sectionIndex].getAllElements()); + this.unitService.unit.pages[pageIndex].sections.splice(sectionIndex, 1); + return {deletedSection, pageIndex, sectionIndex}; + }, + rollback: (deletedData: Record<string, unknown>) => { + this.unitService.registerIDs((deletedData.deletedSection as Section).getAllElements()); + this.unitService.unit.pages[deletedData.pageIndex as number].addSection(deletedData.deletedSection as Section, sectionIndex) + } + }); } duplicateSection(section: Section, page: Page, sectionIndex: number): void { - const newSection: Section = new Section({ - ...section, - elements: section.elements.map(element => this.elementService.duplicateElement(element) as PositionedUIElement) + this.unitService.updateUnitDefinition({ + title: `Abschnitt dupliziert`, + command: () => { + const newSection: Section = new Section({ + ...section, + elements: section.elements.map(element => this.elementService.duplicateElement(element) as PositionedUIElement) + }); + page.addSection(newSection, sectionIndex + 1); + this.selectionService.selectedSectionIndex += 1; + return {}; + }, + rollback: (deletedData: Record<string, unknown>) => { + this.unitService.unregisterIDs(page.sections[sectionIndex + 1].getAllElements()); + page.deleteSection(sectionIndex + 1); + this.selectionService.selectedSectionIndex -= 1; + } }); - page.sections.splice(sectionIndex + 1, 0, newSection); - this.unitService.updateUnitDefinition(); } moveSection(section: Section, page: Page, direction: 'up' | 'down'): void { ArrayUtils.moveArrayItem(section, page.sections, direction); - if (direction === 'up' && this.selectionService.selectedPageSectionIndex > 0) { - this.selectionService.selectedPageSectionIndex -= 1; + if (direction === 'up' && this.selectionService.selectedSectionIndex > 0) { + this.selectionService.selectedSectionIndex -= 1; } else if (direction === 'down') { - this.selectionService.selectedPageSectionIndex += 1; + this.selectionService.selectedSectionIndex += 1; } this.unitService.updateUnitDefinition(); } replaceSection(pageIndex: number, sectionIndex: number, newSection: Section): void { this.deleteSection(pageIndex, sectionIndex); - this.addSection(this.unit.pages[pageIndex], newSection); + this.addSection(this.unitService.unit.pages[pageIndex], newSection, sectionIndex); } /* Move element between sections */ @@ -89,13 +120,4 @@ export class SectionService { }); this.unitService.updateUnitDefinition(); } - - duplicateSelectedElements(): void { - const selectedSection = - this.unit.pages[this.selectionService.selectedPageIndex].sections[this.selectionService.selectedPageSectionIndex]; - this.selectionService.getSelectedElements().forEach((element: UIElement) => { - selectedSection.elements.push(this.elementService.duplicateElement(element, true) as PositionedUIElement); - }); - this.unitService.updateUnitDefinition(); - } } diff --git a/projects/editor/src/app/services/unit-services/unit.service.ts b/projects/editor/src/app/services/unit-services/unit.service.ts index 7ee604b7c7418e24c3dad7e826357fb491c7ac3c..889d923b67ea04883f4980b952378e1def789545 100644 --- a/projects/editor/src/app/services/unit-services/unit.service.ts +++ b/projects/editor/src/app/services/unit-services/unit.service.ts @@ -97,6 +97,23 @@ export class UnitService { this.veronaApiService.sendChanged(this.unit); } + registerIDs(elements: UIElement[]): void { + elements.forEach(element => { + if (['drop-list', 'drop-list-simple'].includes((element as UIElement).type as string)) { + (element as DropListElement).value.forEach(value => this.idService.addID(value.id)); + } + if (['likert', 'cloze'].includes((element as UIElement).type as string)) { + element.getChildElements().forEach(el => { + this.idService.addID(el.id); + if ((element as UIElement).type === 'drop-list') { + (element as DropListElement).value.forEach(value => this.idService.addID(value.id)); + } + }); + } + this.idService.addID(element.id); + }); + } + unregisterIDs(elements: UIElement[]): void { elements.forEach(element => { if (element.type === 'drop-list') {