diff --git a/src/app/services/removeTranslationsService.spec.ts b/src/app/services/removeTranslationsService.spec.ts new file mode 100644 index 00000000000..eecd905a141 --- /dev/null +++ b/src/app/services/removeTranslationsService.spec.ts @@ -0,0 +1,49 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { TeacherProjectService } from '../../assets/wise5/services/teacherProjectService'; +import { StudentTeacherCommonServicesModule } from '../student-teacher-common-services.module'; +import { RemoveTranslationsService } from '../../assets/wise5/services/removeTranslationsService'; +import { ProjectLocale } from '../domain/projectLocale'; +import { ComponentContent } from '../../assets/wise5/common/ComponentContent'; +import { ConfigService } from '../../assets/wise5/services/configService'; + +let configService: ConfigService; +let http: HttpTestingController; +let projectService: TeacherProjectService; +let service: RemoveTranslationsService; +describe('RemoveTranslationsService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, StudentTeacherCommonServicesModule], + providers: [RemoveTranslationsService, TeacherProjectService], + teardown: { destroyAfterEach: false } + }); + configService = TestBed.inject(ConfigService); + http = TestBed.inject(HttpTestingController); + projectService = TestBed.inject(TeacherProjectService); + service = TestBed.inject(RemoveTranslationsService); + }); + removeComponent(); +}); + +function removeComponent() { + describe('removeComponent()', () => { + it('fetches all supported translations', () => { + spyOn(projectService, 'getLocale').and.returnValue( + new ProjectLocale({ default: 'en_us', supported: ['es', 'ja'] }) + ); + spyOn(configService, 'getProjectId').and.returnValue('123'); + spyOn(configService, 'getConfigParam').and.returnValue('/123/project.json'); + service.removeComponents([ + { + id: 'abc', + type: 'OpenResponse', + prompt: 'hello', + 'prompt.i18n': { id: 'xyz' } + } as ComponentContent + ]); + http.expectOne(`/123/translations.es.json`).flush({ xyz: {} }); + http.expectOne(`/123/translations.ja.json`).flush({ xyz: {} }); + }); + }); +} diff --git a/src/app/teacher/teacher-authoring.module.ts b/src/app/teacher/teacher-authoring.module.ts index e84a8cb1f6f..4073f6b8da5 100644 --- a/src/app/teacher/teacher-authoring.module.ts +++ b/src/app/teacher/teacher-authoring.module.ts @@ -33,6 +33,7 @@ import { AuthoringRoutingModule } from './authoring-routing.module'; import { RouterModule } from '@angular/router'; import { ComponentInfoService } from '../../assets/wise5/services/componentInfoService'; import { TeacherProjectTranslationService } from '../../assets/wise5/services/teacherProjectTranslationService'; +import { RemoveTranslationsService } from '../../assets/wise5/services/removeTranslationsService'; @NgModule({ imports: [StudentTeacherCommonModule, AuthoringToolModule, RouterModule, AuthoringRoutingModule], @@ -55,6 +56,7 @@ import { TeacherProjectTranslationService } from '../../assets/wise5/services/te { provide: NodeService, useExisting: TeacherNodeService }, ProjectAssetService, SpaceService, + RemoveTranslationsService, { provide: PeerGroupService, useExisting: TeacherPeerGroupService }, { provide: ProjectService, useExisting: TeacherProjectService }, TeacherDataService, diff --git a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.spec.ts b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.spec.ts index 8e2523cf879..e97cb395503 100644 --- a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.spec.ts +++ b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.spec.ts @@ -28,6 +28,7 @@ import { CopyComponentButtonComponent } from '../copy-component-button/copy-comp import { ProjectLocale } from '../../../../../app/domain/projectLocale'; import { TeacherProjectTranslationService } from '../../../services/teacherProjectTranslationService'; import { ComponentTypeServiceModule } from '../../../services/componentTypeService.module'; +import { RemoveTranslationsService } from '../../../services/removeTranslationsService'; let component: NodeAuthoringComponent; let component1: any; @@ -39,6 +40,7 @@ let node1Components = []; const nodeId1 = 'node1'; let teacherDataService: TeacherDataService; let teacherProjectService: TeacherProjectService; +let saveProjectSpy: jasmine.Spy; describe('NodeAuthoringComponent', () => { beforeEach(async () => { @@ -68,6 +70,7 @@ describe('NodeAuthoringComponent', () => { ClassroomStatusService, TeacherProjectTranslationService, ProjectAssetService, + RemoveTranslationsService, TeacherDataService, TeacherNodeService, TeacherProjectService, @@ -106,7 +109,8 @@ describe('NodeAuthoringComponent', () => { teacherProjectService.idToNode = { node1: node1 }; teacherProjectService.project = { nodes: [{ id: nodeId1, components: node1Components }], - inactiveNodes: [] + inactiveNodes: [], + metadata: { locale: { default: 'en_US', supported: ['es'] } } }; spyOn(teacherProjectService, 'getNodeById').and.returnValue(node1); teacherDataService = TestBed.inject(TeacherDataService); @@ -114,8 +118,10 @@ describe('NodeAuthoringComponent', () => { return Promise.resolve(); }); spyOn(teacherProjectService, 'getLocale').and.returnValue( - new ProjectLocale({ default: 'en-US' }) + new ProjectLocale({ default: 'en-US', supported: ['es'] }) ); + spyOn(TestBed.inject(TeacherProjectService), 'isDefaultLocale').and.returnValue(true); + saveProjectSpy = spyOn(teacherProjectService, 'saveProject').and.returnValue(Promise.resolve()); fixture = TestBed.createComponent(NodeAuthoringComponent); component = fixture.componentInstance; component.nodeId = nodeId1; @@ -156,6 +162,7 @@ function deleteComponent() { expect(confirmSpy).toHaveBeenCalledWith( `Are you sure you want to delete this component?\n2. MultipleChoice` ); + expect(saveProjectSpy).toHaveBeenCalled(); expect(teacherProjectService.idToNode[nodeId1].components).toEqual([component1, component3]); }); }); diff --git a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts index b0b91890366..244aee18def 100644 --- a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts +++ b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts @@ -10,6 +10,7 @@ import { scrollToTopOfPage, temporarilyHighlightElement } from '../../../common/ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { ActivatedRoute, Router } from '@angular/router'; import { TeacherNodeService } from '../../../services/teacherNodeService'; +import { RemoveTranslationsService } from '../../../services/removeTranslationsService'; @Component({ selector: 'node-authoring', @@ -35,6 +36,7 @@ export class NodeAuthoringComponent implements OnInit { private nodeService: TeacherNodeService, private projectService: TeacherProjectService, private dataService: TeacherDataService, + private removeTranslationsService: RemoveTranslationsService, private route: ActivatedRoute, private router: Router ) {} @@ -172,10 +174,9 @@ export class NodeAuthoringComponent implements OnInit { protected deleteComponents(): void { scrollToTopOfPage(); if (this.confirmDeleteComponent(this.getSelectedComponentNumbersAndTypes())) { - const componentIdAndTypes = this.getSelectedComponents() - .map((component) => this.node.deleteComponent(component.id)) - .map((component) => ({ componentId: component.id, type: component.type })); - this.afterDeleteComponent(componentIdAndTypes); + this.deleteComponentsOnServer( + this.getSelectedComponents().map((component) => this.node.deleteComponent(component.id)) + ); } } @@ -186,10 +187,7 @@ export class NodeAuthoringComponent implements OnInit { ): void { event.stopPropagation(); if (this.confirmDeleteComponent([`${componentNumber}. ${component.type}`])) { - const deletedComponent = this.node.deleteComponent(component.id); - this.afterDeleteComponent([ - { componentId: deletedComponent.id, type: deletedComponent.type } - ]); + this.deleteComponentsOnServer([this.node.deleteComponent(component.id)]); } } @@ -202,13 +200,15 @@ export class NodeAuthoringComponent implements OnInit { return confirm(confirmMessage); } - private afterDeleteComponent(componentIdAndTypes: any[]): void { - for (const componentIdAndType of componentIdAndTypes) { - this.componentsToChecked.mutate((obj) => delete obj[componentIdAndType.componentId]); - delete this.componentsToExpanded[componentIdAndType.componentId]; - } + private deleteComponentsOnServer(components: ComponentContent[]): void { this.checkIfNeedToShowNodeSaveOrNodeSubmitButtons(); - this.projectService.saveProject(); + this.projectService.saveProject().then(() => { + for (const component of components) { + this.componentsToChecked.mutate((obj) => delete obj[component.id]); + delete this.componentsToExpanded[component.id]; + } + this.removeTranslations(components); + }); } private checkIfNeedToShowNodeSaveOrNodeSubmitButtons(): void { @@ -224,6 +224,12 @@ export class NodeAuthoringComponent implements OnInit { } } + private removeTranslations(components: ComponentContent[]): void { + if (this.projectService.getLocale().hasTranslations()) { + this.removeTranslationsService.removeComponents(components); + } + } + /** * Temporarily highlight the specified components and show the component * authoring views. Used to bring user's attention to new changes. diff --git a/src/assets/wise5/services/removeTranslationsService.ts b/src/assets/wise5/services/removeTranslationsService.ts new file mode 100644 index 00000000000..73074f5c3e1 --- /dev/null +++ b/src/assets/wise5/services/removeTranslationsService.ts @@ -0,0 +1,66 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, forkJoin, lastValueFrom } from 'rxjs'; +import { Translations } from '../../../app/domain/translations'; +import { ComponentContent } from '../common/ComponentContent'; +import { ConfigService } from './configService'; +import { Language } from '../../../app/domain/language'; +import { ProjectTranslationService } from './projectTranslationService'; +import { TeacherProjectService } from './teacherProjectService'; + +@Injectable() +export class RemoveTranslationsService extends ProjectTranslationService { + constructor( + protected configService: ConfigService, + protected http: HttpClient, + protected projectService: TeacherProjectService + ) { + super(configService, http, projectService); + } + + async removeComponents(components: ComponentContent[]): Promise { + const allTranslations = await this.fetchAllTranslations(); + const i18nKeys = components.flatMap((component) => this.getI18NKeys(component)); + const saveTranslationRequests: Observable[] = []; + allTranslations.forEach((translations, language) => { + i18nKeys.forEach((i18nKey) => delete translations[i18nKey]); + saveTranslationRequests.push( + this.http.post( + `/api/author/project/translate/${this.configService.getProjectId()}/${language.locale}`, + translations + ) + ); + }); + forkJoin(saveTranslationRequests).subscribe(); + } + + private async fetchAllTranslations(): Promise> { + const allTranslations = new Map(); + await Promise.all( + this.projectService + .getLocale() + .getSupportedLanguages() + .map(async (language) => { + allTranslations.set( + language, + await lastValueFrom(this.fetchTranslations(language.locale)) + ); + }) + ); + return allTranslations; + } + + private getI18NKeys(componentElement: object): string[] { + let i18nKeys = Object.keys(componentElement) + .filter((key) => key.endsWith('.i18n')) + .map((key) => componentElement[key].id); + Object.values(componentElement).forEach((value) => { + if (Array.isArray(value)) { + i18nKeys = i18nKeys.concat(...value.map((val) => this.getI18NKeys(val))); + } else if (typeof value === 'object' && value != null) { + i18nKeys = i18nKeys.concat(this.getI18NKeys(value)); + } + }); + return i18nKeys; + } +} diff --git a/src/messages.xlf b/src/messages.xlf index ca1cab6324b..6d166f20d92 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -11827,7 +11827,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts - 199 + 197 @@ -11835,7 +11835,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts - 200 + 198