diff --git a/frontend/src/app/shared/upload-json-button/upload-json-button.component.html b/frontend/src/app/shared/upload-json-button/upload-json-button.component.html index ee9a97f5a..f4b3b2ce7 100644 --- a/frontend/src/app/shared/upload-json-button/upload-json-button.component.html +++ b/frontend/src/app/shared/upload-json-button/upload-json-button.component.html @@ -1,4 +1,8 @@ - + + diff --git a/frontend/src/app/shared/upload-json-button/upload-json-button.component.ts b/frontend/src/app/shared/upload-json-button/upload-json-button.component.ts index abb790e48..9a5b3a490 100644 --- a/frontend/src/app/shared/upload-json-button/upload-json-button.component.ts +++ b/frontend/src/app/shared/upload-json-button/upload-json-button.component.ts @@ -7,6 +7,8 @@ import { MatButton } from '@angular/material/button'; styleUrls: ['./upload-json-button.component.scss'], }) export class UploadJsonButtonComponent { + + @Input() inline = false; /** The display name for the button. Defaults to 'Upload' */ @Input() name = 'Upload'; /** If multi-select files are enabled. Defaults to false (only single file) */ diff --git a/frontend/src/app/template/template-builder/components/template-import-dialog/template-import-dialog.component.html b/frontend/src/app/template/template-builder/components/template-import-dialog/template-import-dialog.component.html new file mode 100644 index 000000000..1129c4367 --- /dev/null +++ b/frontend/src/app/template/template-builder/components/template-import-dialog/template-import-dialog.component.html @@ -0,0 +1,9 @@ +

Import {{title()}}

+
+ + +
+
+ + +
diff --git a/frontend/src/app/template/template-builder/components/template-import-dialog/template-import-dialog.component.scss b/frontend/src/app/template/template-builder/components/template-import-dialog/template-import-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/template/template-builder/components/template-import-dialog/template-import-dialog.component.ts b/frontend/src/app/template/template-builder/components/template-import-dialog/template-import-dialog.component.ts new file mode 100644 index 000000000..1a1c43ddd --- /dev/null +++ b/frontend/src/app/template/template-builder/components/template-import-dialog/template-import-dialog.component.ts @@ -0,0 +1,56 @@ +import { Component, Inject, ViewChild } from "@angular/core"; +import { ApiEvaluationTemplate } from "../../../../../../openapi"; +import { TemplateImportTreeBranch, TemplateImportTreeComponent } from "../template-import-tree/template-import-tree.component"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; + +export interface TemplateImportDialogData{ + templates: ApiEvaluationTemplate[]; + branches: TemplateImportTreeBranch; +} + +@Component({ + selector: 'app-template-import-dialog', + templateUrl: './template-import-dialog.component.html', + styleUrls: ['./template-import-dialog.component.scss'] +}) +export class TemplateImportDialogComponent { + + @ViewChild('templateImportTree',{static: true}) importTree: TemplateImportTreeComponent + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: TemplateImportDialogData + ) { + } + + title(){ + switch(this.data.branches){ + case TemplateImportTreeBranch.NONE: + return "---NONE -- Programmer's Error---"; + case TemplateImportTreeBranch.TASK_TYPES: + return "Task Types" + case TemplateImportTreeBranch.TASK_GROUPS: + return "Task Groups" + case TemplateImportTreeBranch.TASK_TEMPLATES: + return "Task Templates" + case TemplateImportTreeBranch.TEAMS: + return "Teams" + case TemplateImportTreeBranch.TEAM_GROUPS: + return "Team Groups" + case TemplateImportTreeBranch.JUDGES: + return "Judges" + case TemplateImportTreeBranch.ALL: + return "Evaluation Templates" + } + } + + public save(){ + this.dialogRef.close(this.importTree.getImportTemplate()) + } + + public close(){ + this.dialogRef.close(); + } + + +} diff --git a/frontend/src/app/template/template-builder/components/template-import-tree/template-import-tree.component.html b/frontend/src/app/template/template-builder/components/template-import-tree/template-import-tree.component.html index 4d385d802..fed42af1d 100644 --- a/frontend/src/app/template/template-builder/components/template-import-tree/template-import-tree.component.html +++ b/frontend/src/app/template/template-builder/components/template-import-tree/template-import-tree.component.html @@ -3,7 +3,7 @@ {{node.item}} + (change)="leafItemSelectionToggle(node)">{{node.label}} @@ -15,6 +15,6 @@ {{node.item}} + (change)="itemSelectionToggle(node)">{{node.label}} diff --git a/frontend/src/app/template/template-builder/components/template-import-tree/template-import-tree.component.ts b/frontend/src/app/template/template-builder/components/template-import-tree/template-import-tree.component.ts index 21e63d770..7fcd9cf96 100644 --- a/frontend/src/app/template/template-builder/components/template-import-tree/template-import-tree.component.ts +++ b/frontend/src/app/template/template-builder/components/template-import-tree/template-import-tree.component.ts @@ -1,6 +1,7 @@ -import { Component, Input } from "@angular/core"; +import { Component, Input, OnInit } from "@angular/core"; import { - ApiEvaluationTemplate, + ApiEvaluationStatus, + ApiEvaluationTemplate, ApiEvaluationTemplateOverview, ApiTaskGroup, ApiTaskTemplate, ApiTaskType, @@ -14,7 +15,7 @@ import { SelectionModel } from "@angular/cdk/collections"; /* See https://v15.material.angular.io/components/tree/examples */ -export enum TemplateImportTreeBranches { +export enum TemplateImportTreeBranch { NONE = 0, // 000000 TASK_TYPES = 1 << 0, // 000001 TASK_GROUPS = 1 << 1, // 000010 @@ -33,12 +34,15 @@ export class TemplateTreeFlatNode { expandable: boolean; item: T; label: string; + branch: TemplateImportTreeBranch } export class TemplateTreeNode { children: TemplateTreeNode[] | null; item: T; label: string; + branch: TemplateImportTreeBranch; + origin: string; // TemplateID } @Component({ @@ -46,11 +50,13 @@ export class TemplateTreeNode { templateUrl: "./template-import-tree.component.html", styleUrls: ["./template-import-tree.component.scss"] }) -export class TemplateImportTreeComponent { +export class TemplateImportTreeComponent implements OnInit{ flatNodeMap = new Map, TemplateTreeNode>(); nestedNodeMap = new Map, TemplateTreeFlatNode>(); + templatesMap = new Map(); + selectedParent: TemplateTreeFlatNode | null = null; treeControl: FlatTreeControl> @@ -60,9 +66,9 @@ export class TemplateImportTreeComponent { selection = new SelectionModel>(true); @Input() - template: ApiEvaluationTemplate; + templates: ApiEvaluationTemplate[]; @Input() - branches: TemplateImportTreeBranches; + branches: TemplateImportTreeBranch; constructor() { this.treeFlattener = new MatTreeFlattener, TemplateTreeFlatNode>( @@ -70,9 +76,14 @@ export class TemplateImportTreeComponent { ) this.treeControl = new FlatTreeControl>(this.getLevel, this.isExpandable); this.dataSource = new MatTreeFlatDataSource, TemplateTreeFlatNode>(this.treeControl, this.treeFlattener); - this.dataSource.data = TemplateImportTreeComponent.buildTree(this.template, this.branches); + } + ngOnInit(): void { + this.dataSource.data = TemplateImportTreeComponent.buildTrees(this.templates, this.branches); + this.templates.forEach(it => this.templatesMap.set(it.id, it)); + } + getLevel = (node: TemplateTreeFlatNode) => node.level; isExpandable = (node: TemplateTreeFlatNode) => node.expandable; getChildren = (node: TemplateTreeNode) => node.children; @@ -84,6 +95,8 @@ export class TemplateImportTreeComponent { flatNode.item = node.item; flatNode.level = level; flatNode.expandable = !!node.children?.length; + flatNode.branch = node.branch; + flatNode.label = node.label; this.flatNodeMap.set(flatNode, node); this.nestedNodeMap.set(node, flatNode); return flatNode; @@ -106,7 +119,7 @@ export class TemplateImportTreeComponent { } /** Toggle the to-do item selection. Select/deselect all the descendants node */ - todoItemSelectionToggle(node: TemplateTreeFlatNode): void { + itemSelectionToggle(node: TemplateTreeFlatNode): void { this.selection.toggle(node); const descendants = this.treeControl.getDescendants(node); this.selection.isSelected(node) @@ -119,7 +132,7 @@ export class TemplateImportTreeComponent { } /** Toggle a leaf to-do item selection. Check all the parents to see if they changed */ - todoLeafItemSelectionToggle(node: TemplateTreeFlatNode): void { + leafItemSelectionToggle(node: TemplateTreeFlatNode): void { this.selection.toggle(node); this.checkAllParentsSelection(node); } @@ -169,61 +182,148 @@ export class TemplateImportTreeComponent { return null; } - public static buildTree(template: ApiEvaluationTemplate, branches: TemplateImportTreeBranches): TemplateTreeNode[] { + public getImportTemplate(){ + const template = { + name: "", + description: "---Automatically generated template whose elements get imported. If this is seen, there was a programmer's error somewhere---", + taskTypes: this.getAllSelectedTaskTypes(), + taskGroups: this.getAllSelectedTaskGroups(), + tasks: this.getAllSelectedTaskTemplates(), + teams: this.getAllSelectedTeams(), + teamGroups: this.getAllSelectedTeamGroups(), + judges: this.getAllSelectedJudges(), + id:"---IMPORT_TEMPLATE_NO_ID---" + } as ApiEvaluationTemplate + + /* Sanitisation: For each task, the group and type is required */ + return template; + } + + public getAllSelectedTaskTypes(){ + return this.getSelectedItemsForBranch(TemplateImportTreeBranch.TASK_TYPES) as ApiTaskType[] + } + + public getAllSelectedTaskGroups(){ + return this.getSelectedItemsForBranch(TemplateImportTreeBranch.TASK_GROUPS) as ApiTaskGroup[]; + } + + public getAllSelectedTaskTemplates(){ + return this.getSelectedItemsForBranch(TemplateImportTreeBranch.TASK_TEMPLATES) as ApiTaskTemplate[] + } + + public getAllSelectedTeams(){ + return this.getSelectedItemsForBranch(TemplateImportTreeBranch.TEAMS) as ApiTeam[] + } + + public getAllSelectedTeamGroups(){ + return this.getSelectedItemsForBranch(TemplateImportTreeBranch.TEAM_GROUPS) as ApiTeamGroup[] + } + + public getAllSelectedJudges(){ + return this.getSelectedItemsForBranch(TemplateImportTreeBranch.JUDGES) as ApiUser[] + } + + /** + * + * @param branch A single branch, do not use ALL or NONE here (or any combination) + * @private + */ + private getSelectedItemsForBranch(branch: TemplateImportTreeBranch){ + /* Filter appropriately */ + const items = this.selection.selected.filter(it => TemplateImportTreeComponent.checkForBranch(it.branch, branch)).map(it => this.flatNodeMap.get(it)) + switch(branch){ + case TemplateImportTreeBranch.NONE: + case TemplateImportTreeBranch.ALL: + throw new Error("Cannot type set for TemplateImportTreeBanches ALL and NONE. This is a programmer's error") + case TemplateImportTreeBranch.TASK_TYPES: + return items.map(it => it.item) + case TemplateImportTreeBranch.TASK_GROUPS: + return items.map(it => it.item) + case TemplateImportTreeBranch.TASK_TEMPLATES: + return items.map(it => { + /* Warning: collectionId remains and therefore must exist */ + const newItem = it.item as ApiTaskTemplate; + newItem.id = undefined; + return newItem + }) + case TemplateImportTreeBranch.TEAMS: + return items.map(it => { + const newItem = it.item as ApiTeam + newItem.id = undefined; + return newItem + }) + case TemplateImportTreeBranch.TEAM_GROUPS: + return items.map(it => { + const newItem = it.item as ApiTeamGroup + newItem.id = undefined + return newItem + }) + case TemplateImportTreeBranch.JUDGES: + return items.map(it => it.item) + } + } + + + + public static buildTrees(templates: ApiEvaluationTemplate[], branches: TemplateImportTreeBranch): TemplateTreeNode[]{ + return templates.map(it => this.buildTree(it, branches)); + } + + public static buildTree(template: ApiEvaluationTemplate, branches: TemplateImportTreeBranch): TemplateTreeNode { const root = new TemplateTreeNode(); root.item = template; root.label = template.name; root.children = [] as TemplateTreeNode[]; - if(this.checkForBranch(branches, TemplateImportTreeBranches.TASK_TYPES)){ + if(this.checkForBranch(branches, TemplateImportTreeBranch.TASK_TYPES)){ root.children.push(this.buildTaskTypesBranch(template)); } - if(this.checkForBranch(branches, TemplateImportTreeBranches.TASK_GROUPS)){ + if(this.checkForBranch(branches, TemplateImportTreeBranch.TASK_GROUPS)){ root.children.push(this.buildTaskGroupsBranch(template)); } - if(this.checkForBranch(branches, TemplateImportTreeBranches.TASK_TEMPLATES)){ + if(this.checkForBranch(branches, TemplateImportTreeBranch.TASK_TEMPLATES)){ root.children.push(this.buildTaskTemplatesBranch(template)); } - if(this.checkForBranch(branches, TemplateImportTreeBranches.TEAMS)){ + if(this.checkForBranch(branches, TemplateImportTreeBranch.TEAMS)){ root.children.push(this.buildTeamsBranch(template)); } - if(this.checkForBranch(branches, TemplateImportTreeBranches.TEAM_GROUPS)){ + if(this.checkForBranch(branches, TemplateImportTreeBranch.TEAM_GROUPS)){ root.children.push(this.buildTeamGroupsBranch(template)); } - if(this.checkForBranch(branches, TemplateImportTreeBranches.TEAM_GROUPS)){ + if(this.checkForBranch(branches, TemplateImportTreeBranch.TEAM_GROUPS)){ root.children.push(this.buildJudgesBranch(template)); } - return [root]; + return root; } - private static checkForBranch(branches: TemplateImportTreeBranches, test: TemplateImportTreeBranches): boolean{ + public static checkForBranch(branches: TemplateImportTreeBranch, test: TemplateImportTreeBranch): boolean{ return (branches & test) === test } public static buildTaskTypesBranch(template: ApiEvaluationTemplate): TemplateTreeNode { - return this.buildBranch(template, "taskTypes", "Task Types"); + return this.buildBranch(template, "taskTypes", "Task Types", TemplateImportTreeBranch.TASK_TYPES); } public static buildTaskGroupsBranch(template: ApiEvaluationTemplate): TemplateTreeNode { - return this.buildBranch(template, "taskGroups", "Task Groups"); + return this.buildBranch(template, "taskGroups", "Task Groups", TemplateImportTreeBranch.TASK_GROUPS); } public static buildTaskTemplatesBranch(template: ApiEvaluationTemplate): TemplateTreeNode { - return this.buildBranch(template, "tasks", "Task Templates"); + return this.buildBranch(template, "tasks", "Task Templates", TemplateImportTreeBranch.TASK_TEMPLATES); } public static buildTeamsBranch(template: ApiEvaluationTemplate): TemplateTreeNode { - return this.buildBranch(template, "teams", "Teams"); + return this.buildBranch(template, "teams", "Teams", TemplateImportTreeBranch.TEAMS); } public static buildTeamGroupsBranch(template: ApiEvaluationTemplate): TemplateTreeNode { - return this.buildBranch(template, "teamGroups", "Team Groups"); + return this.buildBranch(template, "teamGroups", "Team Groups", TemplateImportTreeBranch.TEAM_GROUPS); } public static buildJudgesBranch(template: ApiEvaluationTemplate): TemplateTreeNode { - return this.buildBranch(template, "judges", "Judges"); + return this.buildBranch(template, "judges", "Judges", TemplateImportTreeBranch.JUDGES); } - public static buildBranch(template: ApiEvaluationTemplate, key: string, rootLabel: string): TemplateTreeNode { + public static buildBranch(template: ApiEvaluationTemplate, key: string, rootLabel: string, branch: TemplateImportTreeBranch): TemplateTreeNode { const root = new TemplateTreeNode(); root.label = rootLabel; root.item = template[key]; @@ -232,6 +332,8 @@ export class TemplateImportTreeComponent { item.label = it["name"]; item.item = it; item.children = null; + item.branch = branch; + item.origin = template.id return item; }); return root; diff --git a/frontend/src/app/template/template-builder/template-builder.component.html b/frontend/src/app/template/template-builder/template-builder.component.html index 95590d592..6023d3cb3 100644 --- a/frontend/src/app/template/template-builder/template-builder.component.html +++ b/frontend/src/app/template/template-builder/template-builder.component.html @@ -29,6 +29,17 @@

Edit evaluation template {{(builderService.templateAsObservable() | async)?. [inline]="true" matTooltip="Download the entire evaluation template as JSON"> +
+ +
+
+ +
diff --git a/frontend/src/app/template/template-builder/template-builder.component.ts b/frontend/src/app/template/template-builder/template-builder.component.ts index 4dca67b58..fce4e5fd3 100644 --- a/frontend/src/app/template/template-builder/template-builder.component.ts +++ b/frontend/src/app/template/template-builder/template-builder.component.ts @@ -1,14 +1,28 @@ import { Component, HostListener, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import {AbstractTemplateBuilderComponent} from './components/abstract-template-builder.component'; -import {DeactivationGuarded} from '../../services/can-deactivate.guard'; -import {Observable, Subscription} from 'rxjs'; -import { ApiTaskGroup, ApiTaskTemplate, ApiTaskType, DownloadService, TemplateService, UserService } from "../../../../openapi"; -import {ActivatedRoute, Router, RouterStateSnapshot} from '@angular/router'; -import {MatSnackBar} from '@angular/material/snack-bar'; -import {TemplateBuilderService} from './template-builder.service'; -import {take} from 'rxjs/operators'; +import { AbstractTemplateBuilderComponent } from "./components/abstract-template-builder.component"; +import { DeactivationGuarded } from "../../services/can-deactivate.guard"; +import { forkJoin, Observable, Subscription } from "rxjs"; +import { + ApiEvaluationTemplate, ApiEvaluationTemplateOverview, + ApiTaskGroup, + ApiTaskTemplate, + ApiTaskType, + DownloadService, + TemplateService, + UserService +} from "../../../../openapi"; +import { ActivatedRoute, Router, RouterStateSnapshot } from "@angular/router"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { TemplateBuilderService } from "./template-builder.service"; +import { map, switchMap, take } from "rxjs/operators"; import { TaskTemplateEditorLauncher } from "./components/tasks-list/task-templates-list.component"; import { TaskTemplateEditorComponent } from "./components/task-template-editor/task-template-editor.component"; +import { MatDialog } from "@angular/material/dialog"; +import { + TemplateImportDialogComponent, + TemplateImportDialogData +} from "./components/template-import-dialog/template-import-dialog.component"; +import { TemplateImportTreeBranch } from "./components/template-import-tree/template-import-tree.component"; @Component({ selector: 'app-template-builder', @@ -32,6 +46,7 @@ export class TemplateBuilderComponent extends AbstractTemplateBuilderComponent i private downloadService: DownloadService, route: ActivatedRoute, private router: Router, + private dialg: MatDialog, snackBar: MatSnackBar, public builderService: TemplateBuilderService ) { @@ -71,6 +86,33 @@ export class TemplateBuilderComponent extends AbstractTemplateBuilderComponent i } } + public onUpload(contents: string){ + console.log("Uploaded "+contents.length+" characters") + } + + public import(){ + console.log("Import open") + let templateList : Observable; + templateList = this.templateService.getApiV2TemplateList().pipe( + map(overviews => overviews.map(o => this.templateService.getApiV2TemplateByTemplateId(o.id))), + switchMap(templateList => forkJoin(...templateList)) + ); + templateList.subscribe(templates => { + console.log("Templates ", templates) + const ownIdx = templates.indexOf(this.builderService.getTemplate()) + templates.splice(ownIdx,1) + const dialogref = this.dialg.open(TemplateImportDialogComponent, {width: '800px', data: {templates: templates, branches: TemplateImportTreeBranch.ALL} as TemplateImportDialogData}) + dialogref.afterClosed().subscribe( d => { + this.onImport(d) + }) + }) + + } + + public onImport(templateToImportFrom: ApiEvaluationTemplate){ + console.log("Importing...", templateToImportFrom) + } + public save(){ // FIXME re-enable form validation. possibly on the form-builder? this.isSaving = true; diff --git a/frontend/src/app/template/template-builder/template-builder.module.ts b/frontend/src/app/template/template-builder/template-builder.module.ts index 1943f3b13..ce27855e5 100644 --- a/frontend/src/app/template/template-builder/template-builder.module.ts +++ b/frontend/src/app/template/template-builder/template-builder.module.ts @@ -19,6 +19,7 @@ import { MatChipsModule } from "@angular/material/chips"; import { TemplateImportTreeComponent } from './components/template-import-tree/template-import-tree.component'; import { MatTreeModule } from "@angular/material/tree"; import { MatCheckboxModule } from "@angular/material/checkbox"; +import { TemplateImportDialogComponent } from './components/template-import-dialog/template-import-dialog.component'; @NgModule({ @@ -26,7 +27,8 @@ import { MatCheckboxModule } from "@angular/material/checkbox"; TemplateBuilderComponent, TeamgroupsListComponent, TeamgroupsDialogComponent, - TemplateImportTreeComponent + TemplateImportTreeComponent, + TemplateImportDialogComponent ], imports: [