Skip to content
Snippets Groups Projects
Commit bbffab06 authored by rhenck's avatar rhenck
Browse files

[editor] Add undo function

Only works for a few commands (mainly page and section stuff).

- Adds new menu to page-bar, which shows issued change commands with a 
button to rollback the latest change.
- Added warning to the UI element that this is a experimental feature 
and can destroy units.
- Undo is done by having the normal function to apply the change to the 
unit-object and another to undo this change. The HistoryService manages 
the list and calling rollack (via UnitService).
- Further refactiring to the unit services.
parent b0780ef1
No related branches found
No related tags found
No related merge requests found
Pipeline #59605 failed
Showing
with 198 additions and 91 deletions
......@@ -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 {
......
......@@ -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 {
......
......@@ -126,7 +126,7 @@ export class ElementPropertiesPanelComponent implements OnInit, OnDestroy {
}
duplicateElement(): void {
this.sectionService.duplicateSelectedElements();
this.elementService.duplicateSelectedElements();
}
ngOnDestroy(): void {
......
......@@ -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();
}
......
......@@ -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;*/
}
......@@ -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>
......
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>
}
......@@ -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) -
......
......@@ -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'
);
}
});
}
}
......@@ -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();
}
}
......@@ -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') {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment