Skip to content

Commit

Permalink
feat(Authoring): Add JSON authoring view for when project is broken (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
geoffreykwan authored Jan 19, 2023
1 parent cb30282 commit 9dbcfc9
Show file tree
Hide file tree
Showing 9 changed files with 521 additions and 23 deletions.
30 changes: 30 additions & 0 deletions src/app/domain/nodeRecoveryAnalysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export class NodeRecoveryAnalysis {
hasTransitionToNull: boolean = false;
nodeId: string;
referencedIdsThatAreDuplicated: string[] = [];
referencedIdsThatDoNotExist: string[] = [];

constructor(nodeId: string) {
this.nodeId = nodeId;
}

addReferencedIdThatIsDuplicated(nodeId: string): void {
this.referencedIdsThatAreDuplicated.push(nodeId);
}

addReferencedIdThatDoesNotExist(nodeId: string): void {
this.referencedIdsThatDoNotExist.push(nodeId);
}

setHasTransitionToNull(value: boolean): void {
this.hasTransitionToNull = value;
}

hasProblem(): boolean {
return (
this.referencedIdsThatAreDuplicated.length > 0 ||
this.referencedIdsThatDoNotExist.length > 0 ||
this.hasTransitionToNull
);
}
}
2 changes: 2 additions & 0 deletions src/app/teacher/authoring-tool.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ComponentAuthoringModule } from './component-authoring.module';
import { ComponentStudentModule } from '../../assets/wise5/components/component/component-student.module';
import { PreviewComponentModule } from '../../assets/wise5/authoringTool/components/preview-component/preview-component.module';
import { StudentTeacherCommonModule } from '../student-teacher-common.module';
import { RecoveryAuthoringComponent } from '../../assets/wise5/authoringTool/recovery-authoring/recovery-authoring.component';

@NgModule({
declarations: [
Expand All @@ -39,6 +40,7 @@ import { StudentTeacherCommonModule } from '../student-teacher-common.module';
NodeAdvancedJsonAuthoringComponent,
NodeAdvancedPathAuthoringComponent,
NodeIconChooserDialog,
RecoveryAuthoringComponent,
RequiredErrorLabelComponent,
RubricAuthoringComponent
],
Expand Down
30 changes: 30 additions & 0 deletions src/assets/wise5/authoringTool/project/project-authoring.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ProjectAssetService } from '../../../../app/services/projectAssetServic
import { ProjectAuthoringComponent } from '../../authoringTool/project/projectAuthoringComponent';
import { ProjectInfoAuthoringComponent } from '../../authoringTool/info/projectInfoAuthoringComponent';
import { RubricAuthoringComponent } from '../../authoringTool/rubric/rubric-authoring.component';
import { RecoveryAuthoringComponent } from '../recovery-authoring/recovery-authoring.component';

export default angular
.module('projectAuthoringModule', [])
Expand All @@ -30,6 +31,10 @@ export default angular
'rubricAuthoringComponent',
downgradeComponent({ component: RubricAuthoringComponent }) as angular.IDirectiveFactory
)
.directive(
'recoveryAuthoringComponent',
downgradeComponent({ component: RecoveryAuthoringComponent })
)
.factory('ProjectAssetService', downgradeInjectable(ProjectAssetService))
.config([
'$stateProvider',
Expand Down Expand Up @@ -106,6 +111,31 @@ export default angular
.state('root.at.project.milestones', {
url: '/milestones',
component: 'milestonesAuthoringComponent'
})
.state('root.at.recovery', {
url: '/unit/:projectId/recovery',
component: 'recoveryAuthoringComponent',
resolve: {
projectConfig: [
'ConfigService',
'SessionService',
'$stateParams',
(ConfigService, SessionService, $stateParams) => {
return ConfigService.retrieveConfig(
`/api/author/config/${$stateParams.projectId}`
).then(() => {
SessionService.initializeSession();
});
}
],
project: [
'ProjectService',
'projectConfig',
(ProjectService, projectConfig) => {
return ProjectService.retrieveProject(false);
}
]
}
});
}
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<div id="top" class="view-content view-content--with-sidemenu">
<div class="l-constrained" layout="column">
<div class="node-content md-whiteframe-1dp main-box">
<div class="top-div">
<div class="save-bar" fxLayout="row" fxLayoutAlign="space-between center">
<div fxLayoutGap="20px">
<button mat-raised-button
color="primary"
(click)="save()"
[disabled]="!saveButtonEnabled"
i18n>
Save
</button>
<button mat-raised-button
color="primary"
(click)="goToAuthoringView()"
i18n>
Go to Authoring View
</button>
</div>
<div fxLayout="row">
<div *ngIf="jsonIsValid" class="valid" i18n>JSON Valid</div>
<div *ngIf="!jsonIsValid" class="invalid" i18n>JSON Invalid</div>
<div *ngIf="globalMessage != null">
<div class="component__actions__info md-caption global-message">
{{ globalMessage.text }} {{ globalMessage.time | date:'medium' }}
</div>
</div>
</div>
</div>
<div class="warning-div" i18n>Warning: Modifying the JSON may break the project. Please make a backup copy of the JSON before you modify it.</div>
<div *ngIf="badNodes.length > 0" class="potential-problems-div">
<div class="potential-problems-header" i18n>Potential Problems</div>
<div *ngFor="let badNode of badNodes" class="bad-node">
<div class="node-id">{{ badNode.nodeId }}</div>
<div *ngIf="badNode.referencedIdsThatDoNotExist.length > 0" i18n>
This group references the node ID but the node does not exist: {{ badNode.referencedIdsThatDoNotExist }}
</div>
<div *ngIf="badNode.referencedIdsThatAreDuplicated.length > 0" i18n>
This group references the same node ID multiple times: {{ badNode.referencedIdsThatAreDuplicated }}
</div>
<div *ngIf="badNode.hasTransitionToNull" i18n>This node has a transition to null</div>
</div>
</div>
</div>
<div>
<mat-form-field fxFlex appearance="outline">
<mat-label i18n>Edit Unit JSON</mat-label>
<textarea class="mat-body-1"
matInput
cdkTextareaAutosize
[(ngModel)]="projectJSONString"
(ngModelChange)="projectJSONChanged()">
</textarea>
</mat-form-field>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.main-box {
position: relative;
padding: 16px !important;
}

.top-div {
background-color: white;
position: sticky;
top: 0px;
z-index: 2;
padding-top: 4px;
padding-bottom: 4px;
}

.save-bar {
margin-bottom: 20px;
}

.valid {
color: green;
}

.invalid {
color: red;
}

.warning-div {
margin-bottom: 20px;
color: red;
}

.potential-problems-div {
color: red;
}

.potential-problems-header {
text-decoration: underline;
margin-bottom: 8px;
}

.bad-node {
border: 1px solid red;
border-radius: 8px;
padding: 8px;
margin-bottom: 4px;
}

.node-id {
font-weight: bold;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { UpgradeModule } from '@angular/upgrade/static';
import { StudentTeacherCommonServicesModule } from '../../../../app/student-teacher-common-services.module';
import { TeacherProjectService } from '../../services/teacherProjectService';
import { RecoveryAuthoringComponent } from './recovery-authoring.component';

class MockTeacherProjectService {
project = {
nodes: []
};

saveProject() {}
}

class Node {
id: string;
ids: string[];
transitionLogic: any;

constructor(id: string, ids: string[], transitions: any[]) {
this.id = id;
this.ids = ids;
this.transitionLogic = {
transitions: transitions
};
}
}

let component: RecoveryAuthoringComponent;
let fixture: ComponentFixture<RecoveryAuthoringComponent>;
const groupId1 = 'group1';
const nodeId1 = 'node1';
const nodeId2 = 'node2';

describe('RecoveryAuthoringComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RecoveryAuthoringComponent],
imports: [
BrowserAnimationsModule,
FormsModule,
HttpClientTestingModule,
MatDialogModule,
MatInputModule,
StudentTeacherCommonServicesModule,
UpgradeModule
],
providers: [{ provide: TeacherProjectService, useClass: MockTeacherProjectService }]
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(RecoveryAuthoringComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

projectJSONChanged();
save();
});

function projectJSONChanged() {
describe('projectJSONChanged()', () => {
detectJSONValidity();
detectPotentialProblems();
});
}

function detectJSONValidity() {
it('should detect json is invalid and disable save button', () => {
setJSONAndExpect('abc', false, false);
});

it('should detect json is valid and enable save button', () => {
setJSONAndExpect('{ "nodes": [] }', true, true);
});
}

function setJSONAndExpect(json: string, jsonIsValid: boolean, saveButtonEnabled: boolean) {
setProjectJSONStringAndTriggerChange(json);
expect(component.jsonIsValid).toEqual(jsonIsValid);
expect(component.saveButtonEnabled).toEqual(saveButtonEnabled);
}

function detectPotentialProblems() {
it('should detect potential problem transition to null', () => {
const projectJSON = {
nodes: [new Node(nodeId1, null, [{ to: null }])]
};
setProjectJSONStringAndTriggerChange(JSON.stringify(projectJSON));
expect(component.badNodes.length).toEqual(1);
expect(component.badNodes[0].hasTransitionToNull).toEqual(true);
});

it('should detect potential problem reference to node id that does not exist', () => {
const projectJSON = {
nodes: [new Node(groupId1, [nodeId1, nodeId2], []), new Node(nodeId1, null, [])]
};
setProjectJSONStringAndTriggerChange(JSON.stringify(projectJSON));
expect(component.badNodes.length).toEqual(1);
expect(component.badNodes[0].referencedIdsThatDoNotExist).toEqual([nodeId2]);
});

it('should detect potential problem reference to node id duplicate', () => {
const projectJSON = {
nodes: [
new Node(groupId1, [nodeId1, nodeId1], []),
new Node(nodeId1, null, [{ to: nodeId2 }]),
new Node(nodeId2, null, [])
]
};
setProjectJSONStringAndTriggerChange(JSON.stringify(projectJSON));
expect(component.badNodes.length).toEqual(1);
expect(component.badNodes[0].referencedIdsThatAreDuplicated).toEqual([nodeId1]);
});
}

function setProjectJSONStringAndTriggerChange(jsonString: string): void {
component.projectJSONString = jsonString;
component.projectJSONChanged();
}

function save() {
describe('save()', () => {
it('should save and disable save button', () => {
component.saveButtonEnabled = true;
component.save();
expect(component.saveButtonEnabled).toBeFalsy();
});
});
}
Loading

0 comments on commit 9dbcfc9

Please sign in to comment.