Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(MultipleLanguages): Delete translations when components are deleted #1756

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/app/services/removeTranslationsService.spec.ts
Original file line number Diff line number Diff line change
@@ -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: {} });
});
});
}
2 changes: 2 additions & 0 deletions src/app/teacher/teacher-authoring.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,6 +40,7 @@ let node1Components = [];
const nodeId1 = 'node1';
let teacherDataService: TeacherDataService;
let teacherProjectService: TeacherProjectService;
let saveProjectSpy: jasmine.Spy;

describe('NodeAuthoringComponent', () => {
beforeEach(async () => {
Expand Down Expand Up @@ -68,6 +70,7 @@ describe('NodeAuthoringComponent', () => {
ClassroomStatusService,
TeacherProjectTranslationService,
ProjectAssetService,
RemoveTranslationsService,
TeacherDataService,
TeacherNodeService,
TeacherProjectService,
Expand Down Expand Up @@ -106,16 +109,19 @@ 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);
spyOn(teacherDataService, 'saveEvent').and.callFake(() => {
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;
Expand Down Expand Up @@ -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]);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
) {}
Expand Down Expand Up @@ -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))
);
}
}

Expand All @@ -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)]);
}
}

Expand All @@ -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 {
Expand All @@ -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.
Expand Down
66 changes: 66 additions & 0 deletions src/assets/wise5/services/removeTranslationsService.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const allTranslations = await this.fetchAllTranslations();
const i18nKeys = components.flatMap((component) => this.getI18NKeys(component));
const saveTranslationRequests: Observable<Object>[] = [];
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<Map<Language, Translations>> {
const allTranslations = new Map<Language, Translations>();
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;
}
}
4 changes: 2 additions & 2 deletions src/messages.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -11827,15 +11827,15 @@ Click &quot;Cancel&quot; to keep the invalid JSON open so you can fix it.</sourc
</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts</context>
<context context-type="linenumber">199</context>
<context context-type="linenumber">197</context>
</context-group>
</trans-unit>
<trans-unit id="5170405945214763287" datatype="html">
<source>Are you sure you want to delete these components?
</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts</context>
<context context-type="linenumber">200</context>
<context context-type="linenumber">198</context>
</context-group>
</trans-unit>
<trans-unit id="bb39841df2c03f771748c1f986e615bffa7f2593" datatype="html">
Expand Down