From 989ec4588ac91eab132a7d4e8d168c1998963f9d Mon Sep 17 00:00:00 2001 From: Shivam Chaudhary Date: Wed, 19 Oct 2022 04:32:48 +0530 Subject: [PATCH] Fix part of #9749: Migration of ParamChangesEditorComponent from angularJs to Angular. (#16335) * paramChanges Editor migration * done * done * two testing files * value generator editor * done * keytones * text space * changes for pass * pass * testing update * done * upstred --- core/controllers/resources_test.py | 2 +- core/domain/value_generators_domain.py | 2 +- .../components/shared-component.module.ts | 8 + .../state-param-changes-editor.component.html | 2 +- .../exploration-editor-page.module.ts | 8 +- ...s-guiding-responses-task.component.spec.ts | 17 +- .../lost-changes-modal.component.spec.ts | 16 +- .../param-changes-editor.component.html | 93 ++-- .../param-changes-editor.component.spec.ts | 481 +++++++++-------- .../param-changes-editor.component.ts | 486 +++++++++--------- .../value-generator-editor.component.html | 1 + .../value-generator-editor.component.spec.ts | 82 +++ .../value-generator-editor.component.ts | 94 ++++ .../value-generator-editor.directive.spec.ts | 55 -- .../value-generator-editor.directive.ts | 65 --- .../services/exploration-save.service.spec.ts | 81 ++- .../services/exploration-save.service.ts | 3 +- .../settings-tab/settings-tab.component.html | 24 +- .../models/generators_test.py | 6 +- .../templates/Copier.component.html | 4 + .../value_generators/templates/Copier.html | 4 - .../templates/RandomSelector.component.html | 2 + .../templates/RandomSelector.html | 2 - .../templates/copier.component.spec.ts | 57 ++ .../templates/copier.component.ts | 42 ++ .../templates/copier.directive.ts | 60 --- .../templates/dynamic-component.module.ts | 49 ++ .../random-selector.component.spec.ts | 78 +++ .../templates/random-selector.component.ts | 58 +++ .../random-selector.directive.spec.ts | 72 --- .../templates/random-selector.directive.ts | 62 --- .../valueGeneratorsRequires.ts | 4 +- scripts/typescript_checks.py | 10 +- tsconfig.json | 3 +- 34 files changed, 1117 insertions(+), 916 deletions(-) create mode 100644 core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.html create mode 100644 core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.spec.ts create mode 100644 core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.ts delete mode 100644 core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.spec.ts delete mode 100644 core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.ts create mode 100644 extensions/value_generators/templates/Copier.component.html delete mode 100644 extensions/value_generators/templates/Copier.html create mode 100644 extensions/value_generators/templates/RandomSelector.component.html delete mode 100644 extensions/value_generators/templates/RandomSelector.html create mode 100644 extensions/value_generators/templates/copier.component.spec.ts create mode 100644 extensions/value_generators/templates/copier.component.ts delete mode 100644 extensions/value_generators/templates/copier.directive.ts create mode 100644 extensions/value_generators/templates/dynamic-component.module.ts create mode 100644 extensions/value_generators/templates/random-selector.component.spec.ts create mode 100644 extensions/value_generators/templates/random-selector.component.ts delete mode 100644 extensions/value_generators/templates/random-selector.directive.spec.ts delete mode 100644 extensions/value_generators/templates/random-selector.directive.ts diff --git a/core/controllers/resources_test.py b/core/controllers/resources_test.py index 12733ef80b6d..78a79df053ce 100644 --- a/core/controllers/resources_test.py +++ b/core/controllers/resources_test.py @@ -920,4 +920,4 @@ def test_html_response(self): response = self.get_html_response( '/value_generator_handler/' + copier_id ) - self.assertIn(b' str: """ return utils.get_file_contents(os.path.join( os.getcwd(), feconf.VALUE_GENERATORS_DIR, 'templates', - '%s.html' % cls.__name__)) + '%s.component.html' % cls.__name__)) # Here we use type Any because child classes of BaseValueGenerator can use # the 'generate_value' function with different types of arguments, 'args', diff --git a/core/templates/components/shared-component.module.ts b/core/templates/components/shared-component.module.ts index 5ea899d028c9..803d06e6c494 100644 --- a/core/templates/components/shared-component.module.ts +++ b/core/templates/components/shared-component.module.ts @@ -159,6 +159,8 @@ import { VisualizationSortedTilesComponent } from 'visualizations/oppia-visualiz import { OppiaVisualizationClickHexbinsComponent } from 'visualizations/oppia-visualization-click-hexbins.directive'; import { OppiaVisualizationFrequencyTableComponent } from 'visualizations/oppia-visualization-frequency-table.directive'; import { OppiaVisualizationEnumeratedFrequencyTableComponent } from 'visualizations/oppia-visualization-enumerated-frequency-table.directive'; +import { RandomSelectorComponent } from 'value_generators/templates/random-selector.component'; +import { CopierComponent } from 'value_generators/templates/copier.component'; // Pipes. import { StringUtilityPipesModule } from 'filters/string-utility-filters/string-utility-pipes.module'; @@ -174,6 +176,8 @@ import { SmartRouterModule } from 'hybrid-router-module-provider'; import { StaleTabInfoModalComponent } from './stale-tab-info/stale-tab-info-modal.component'; import { UnsavedChangesStatusInfoModalComponent } from './unsaved-changes-status-info/unsaved-changes-status-info-modal.component'; import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; +import { MatMenuModule} from '@angular/material/menu'; +import { DynamicComponentModule } from 'value_generators/templates/dynamic-component.module'; @NgModule({ imports: [ @@ -181,6 +185,7 @@ import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; BaseModule, CommonModule, DragDropModule, + MatMenuModule, CustomFormsComponentsModule, CommonElementsModule, CodeMirrorModule, @@ -205,6 +210,7 @@ import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; AngularFireAuthModule, MatProgressSpinnerModule, NgbModalModule, + DynamicComponentModule ], providers: [ @@ -465,6 +471,8 @@ import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; OppiaVisualizationFrequencyTableComponent, ReviewTestPageComponent, VisualizationSortedTilesComponent, + CopierComponent, + RandomSelectorComponent ], exports: [ diff --git a/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.html b/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.html index cd256435e0db..7f5aab0883e0 100644 --- a/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.html +++ b/core/templates/pages/exploration-editor-page/editor-tab/state-param-changes-editor/state-param-changes-editor.component.html @@ -2,7 +2,7 @@
-
diff --git a/core/templates/pages/exploration-editor-page/exploration-editor-page.module.ts b/core/templates/pages/exploration-editor-page/exploration-editor-page.module.ts index 9d37298836dd..1f34b51483cf 100644 --- a/core/templates/pages/exploration-editor-page/exploration-editor-page.module.ts +++ b/core/templates/pages/exploration-editor-page/exploration-editor-page.module.ts @@ -36,7 +36,6 @@ import { platformFeatureInitFactory, PlatformFeatureService } from import { RequestInterceptor } from 'services/request-interceptor.service'; import { StateParamChangesEditorComponent } from './editor-tab/state-param-changes-editor/state-param-changes-editor.component'; import { DeleteStateSkillModalComponent } from './editor-tab/templates/modal-templates/delete-state-skill-modal.component'; -import { ParamChangesEditorDirective } from './param-changes-editor/param-changes-editor.component'; import { SwitchContentLanguageRefreshRequiredModalComponent } from 'pages/exploration-player-page/switch-content-language-refresh-required-modal.component'; import { InteractionExtensionsModule } from 'interactions/interactions.module'; import { SaveVersionMismatchModalComponent } from './modal-templates/save-version-mismatch-modal.component'; @@ -99,6 +98,8 @@ import { StateTranslationComponent } from './translation-tab/state-translation/s import { TranslatorOverviewComponent } from './translation-tab/translator-overview/translator-overview.component'; import { StateTranslationStatusGraphComponent } from './translation-tab/state-translation-status-graph/state-translation-status-graph.component'; import { TranslationTabComponent } from './translation-tab/translation-tab.component'; +import { ValueGeneratorEditorComponent } from './param-changes-editor/value-generator-editor.component'; +import { ParamChangesEditorComponent } from './param-changes-editor/param-changes-editor.component'; import { ExplorationEditorPageComponent } from './exploration-editor-page.component'; @NgModule({ @@ -123,7 +124,6 @@ import { ExplorationEditorPageComponent } from './exploration-editor-page.compon declarations: [ CkEditorCopyToolbarComponent, DeleteStateSkillModalComponent, - ParamChangesEditorDirective, StateParamChangesEditorComponent, SwitchContentLanguageRefreshRequiredModalComponent, SaveVersionMismatchModalComponent, @@ -178,6 +178,8 @@ import { ExplorationEditorPageComponent } from './exploration-editor-page.compon AddAudioTranslationModalComponent, AudioTranslationBarComponent, StateTranslationEditorComponent, + ValueGeneratorEditorComponent, + ParamChangesEditorComponent, StateTranslationComponent, TranslatorOverviewComponent, StateTranslationStatusGraphComponent, @@ -241,6 +243,8 @@ import { ExplorationEditorPageComponent } from './exploration-editor-page.compon AddAudioTranslationModalComponent, AudioTranslationBarComponent, StateTranslationEditorComponent, + ValueGeneratorEditorComponent, + ParamChangesEditorComponent, StateTranslationComponent, TranslatorOverviewComponent, StateTranslationStatusGraphComponent, diff --git a/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.spec.ts b/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.spec.ts index 6b9f41b76c1a..88c24f1057da 100644 --- a/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/improvements-tab/needs-guiding-responses-task.component.spec.ts @@ -34,7 +34,20 @@ describe('NeedsGuidingResponsesTask component', function() { const stateName = 'Introduction'; const totalAnswersCount = 50; let task = {targetId: stateName}; - let stats = {answerStats: [], stateStats: {totalAnswersCount}}; + let stats = { + answerStats: [], + stateStats: { + totalAnswersCount, + usefulFeedbackCount: null, + totalHitCount: null, + firstHitCount: null, + numTimesSolutionViewed: null, + numCompletions: null + }, + cstPlaythroughIssues: null, + eqPlaythroughIssues: null, + misPlaythroughIssues: null + } as SupportingStateStats; class MockNgbModal { open() { @@ -71,7 +84,7 @@ describe('NeedsGuidingResponsesTask component', function() { routerService = TestBed.inject(RouterService); component.task = task as NeedsGuidingResponsesTask; - component.stats = stats as unknown as SupportingStateStats; + component.stats = stats as SupportingStateStats; component.ngOnInit(); }); diff --git a/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.spec.ts b/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.spec.ts index 130bb607d442..4a45fb689dcf 100644 --- a/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/modal-templates/lost-changes-modal.component.spec.ts @@ -25,6 +25,7 @@ import { LostChange, LostChangeObjectFactory } from import { LostChangesModalComponent } from './lost-changes-modal.component'; import { LoggerService } from 'services/contextual/logger.service'; +import { UtilsService } from 'services/utils.service'; @Component({ selector: 'oppia-changes-in-human-readable-form', @@ -52,7 +53,20 @@ describe('Lost Changes Modal Component', () => { const lostChanges = [{ cmd: 'add_state', state_name: 'State name', - } as unknown as LostChange]; + utilsService: new UtilsService, + isEndingExploration: () => false, + isAddingInteraction: () => false, + isOldValueEmpty: () => false, + isNewValueEmpty: () => false, + isOutcomeFeedbackEqual: () => false, + isOutcomeDestEqual: () => false, + isDestEqual: () => false, + isFeedbackEqual: () => false, + isRulesEqual: () => false, + getRelativeChangeToGroups: () => 'string', + getLanguage: () => 'en', + getStatePropertyValue: (value1) => 'string' + } as LostChange]; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.html b/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.html index c2aae2462a18..dc6e8e17a672 100644 --- a/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.html +++ b/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.html @@ -1,18 +1,19 @@ + -
-
+
-
- + - - - + + + Change - - - - + - - +
+ +
-
+
- <[warningText]> + {{ warningText }}
- +
-
- +
- + No parameter changes. -
- <[$index + 1]>. Change <[paramChange.name]> - <[HUMAN_READABLE_ARGS_RENDERERS[paramChange.generatorId](paramChange.customizationArgs)]> +
+ {{ index + 1 }}. Change {{ paramChange.name }} + {{ HUMAN_READABLE_ARGS_RENDERERS[paramChange.generatorId](paramChange.customizationArgs) }}
diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.spec.ts b/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.spec.ts index 6bf4f3ddc764..6952e41ec951 100644 --- a/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.spec.ts +++ b/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.spec.ts @@ -16,55 +16,49 @@ * @fileoverview Unit tests for paramChangesEditor. */ -import { EventEmitter, destroyPlatform } from '@angular/core'; -import { async, TestBed } from '@angular/core/testing'; -import { StateCustomizationArgsService } from - // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-customization-args.service'; -import { StateInteractionIdService } from - // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-interaction-id.service'; -import { StateParamChangesService } from - // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-param-changes.service'; -import { StateSolutionService } from - // eslint-disable-next-line max-len - 'components/state-editor/state-editor-properties-services/state-solution.service'; -import { OutcomeObjectFactory } from 'domain/exploration/OutcomeObjectFactory'; -import { ParamChangeObjectFactory } from - 'domain/exploration/ParamChangeObjectFactory'; -import { ParamSpecsObjectFactory } from - 'domain/exploration/ParamSpecsObjectFactory'; -import { TextInputRulesService } from - 'interactions/TextInput/directives/text-input-rules.service'; -import { AngularNameService } from - 'pages/exploration-editor-page/services/angular-name.service'; -import { StateEditorRefreshService } from - 'pages/exploration-editor-page/services/state-editor-refresh.service'; +import { CdkDragSortEvent } from '@angular/cdk/drag-drop'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { StateParamChangesService } from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; +import { ParamChange, ParamChangeObjectFactory } from 'domain/exploration/ParamChangeObjectFactory'; +import { ParamSpecs, ParamSpecsObjectFactory } from 'domain/exploration/ParamSpecsObjectFactory'; import { AlertsService } from 'services/alerts.service'; -import { importAllAngularServices, setupAndGetUpgradedComponentAsync } from 'tests/unit-test-utils.ajs'; +import { EditabilityService } from 'services/editability.service'; +import { ExternalSaveService } from 'services/external-save.service'; import { ExplorationDataService } from '../services/exploration-data.service'; -import { ParamChangesEditorDirective } from './param-changes-editor.component'; - -describe('Param Changes Editor Component', function() { - var ctrl = null; - var $rootScope = null; - var $scope = null; - var alertsService = null; - var editabilityService = null; - var explorationParamSpecsService = null; - var explorationStatesService = null; - var paramChangeObjectFactory = null; - var paramSpecsObjectFactory = null; - var externalSaveService = null; - var stateParamChangesService = null; - - var postSaveHookSpy = jasmine.createSpy('postSaveHook', () => {}); - - var mockExternalSaveEventEmitter = null; - - beforeEach(() => { +import { ExplorationParamSpecsService } from '../services/exploration-param-specs.service'; +import { ExplorationStatesService } from '../services/exploration-states.service'; +import { ParamChangesEditorComponent } from './param-changes-editor.component'; + +class MockNgbModal { + open() { + return { + result: Promise.resolve() + }; + } +} + +describe('Param Changes Editor Component', () => { + let component: ParamChangesEditorComponent; + let fixture: ComponentFixture; + let alertsService: AlertsService; + let editabilityService: EditabilityService; + let explorationParamSpecsService: ExplorationParamSpecsService; + let explorationStatesService: ExplorationStatesService; + let paramChangeObjectFactory: ParamChangeObjectFactory; + let paramSpecsObjectFactory: ParamSpecsObjectFactory; + let stateParamChangesService: StateParamChangesService; + let postSaveHookSpy = jasmine.createSpy('postSaveHook', () => {}); + let mockExternalSaveEventEmitter = new EventEmitter(); + + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [ + ParamChangesEditorComponent + ], providers: [ { provide: ExplorationDataService, @@ -74,59 +68,35 @@ describe('Param Changes Editor Component', function() { return; } } + }, + { + provide: NgbModal, + useClass: MockNgbModal + }, + { + provide: ExternalSaveService, + useValue: { + onExternalSave: mockExternalSaveEventEmitter + } } - ] - }); - }); - - importAllAngularServices(); - - beforeEach(angular.mock.module('oppia', function($provide) { - $provide.value('NgbModal', { - open: () => { - return { - result: Promise.resolve() - }; - } - }); + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); })); - beforeEach(function() { - alertsService = TestBed.get(AlertsService); - paramChangeObjectFactory = TestBed.get(ParamChangeObjectFactory); - paramSpecsObjectFactory = TestBed.get(ParamSpecsObjectFactory); - stateParamChangesService = TestBed.get(StateParamChangesService); - }); - - beforeEach(angular.mock.module('oppia', function($provide) { - $provide.value('AngularNameService', TestBed.get(AngularNameService)); - $provide.value( - 'TextInputRulesService', - TestBed.get(TextInputRulesService)); - $provide.value( - 'OutcomeObjectFactory', TestBed.get(OutcomeObjectFactory)); - mockExternalSaveEventEmitter = new EventEmitter(); - $provide.value('ExternalSaveService', { - onExternalSave: mockExternalSaveEventEmitter - }); - $provide.value( - 'StateCustomizationArgsService', - TestBed.get(StateCustomizationArgsService)); - $provide.value( - 'StateEditorRefreshService', TestBed.get(StateEditorRefreshService)); - $provide.value( - 'StateInteractionIdService', TestBed.get(StateInteractionIdService)); - $provide.value( - 'StateSolutionService', TestBed.get(StateSolutionService)); - })); - beforeEach(angular.mock.inject(function($injector, $componentController) { - $rootScope = $injector.get('$rootScope'); - editabilityService = $injector.get('EditabilityService'); - explorationParamSpecsService = $injector.get( - 'ExplorationParamSpecsService'); - explorationStatesService = $injector.get('ExplorationStatesService'); - externalSaveService = $injector.get('ExternalSaveService'); + beforeEach(() => { + fixture = TestBed.createComponent(ParamChangesEditorComponent); + component = fixture.componentInstance; + + alertsService = TestBed.inject(AlertsService); + paramChangeObjectFactory = TestBed.inject(ParamChangeObjectFactory); + paramSpecsObjectFactory = TestBed.inject(ParamSpecsObjectFactory); + stateParamChangesService = TestBed.inject(StateParamChangesService); + editabilityService = TestBed.inject(EditabilityService); + explorationParamSpecsService = TestBed.inject( + ExplorationParamSpecsService); + explorationStatesService = TestBed.inject(ExplorationStatesService); explorationParamSpecsService.init( paramSpecsObjectFactory.createFromBackendDict({ y: { @@ -135,37 +105,34 @@ describe('Param Changes Editor Component', function() { a: { obj_type: 'UnicodeString' } - })); + }) as ParamSpecs); + stateParamChangesService.init('', []); - $scope = $rootScope.$new(); - ctrl = $componentController('paramChangesEditor', { - $scope: $scope, - AlertsService: alertsService, - ParamChangeObjectFactory: paramChangeObjectFactory, - ExternalSaveService: externalSaveService - }, { - paramChangesService: stateParamChangesService, - postSaveHook: postSaveHookSpy, - isCurrentlyInSettingsTab: false - }); - ctrl.$onInit(); - })); + component.paramChangesServiceName = 'explorationParamChangesService'; + component.postSaveHook = postSaveHookSpy; + component.currentlyInSettingsTab = false; + + fixture.detectChanges(); + component.ngOnInit(); + + component.paramChangesService.displayed = []; + }); afterEach(() => { - ctrl.$onDestroy(); + component.ngOnDestroy(); }); - it('should initialize $scope properties after controller is initialized', - function() { - expect($scope.isParamChangesEditorOpen).toBe(false); - expect($scope.warningText).toBe(''); - expect($scope.paramNameChoices).toEqual([]); + it('should initialize component properties after controller is initialized', + () => { + expect(component.isParamChangesEditorOpen).toBe(false); + expect(component.warningText).toBe(''); + expect(component.paramNameChoices).toEqual([]); }); it('should reset customization args from param change when changing' + - ' generator type', function() { - var paramChange = paramChangeObjectFactory.createFromBackendDict({ + ' generator type', () => { + let paramChange = paramChangeObjectFactory.createFromBackendDict({ customization_args: { list_of_values: ['first value', 'second value'] }, @@ -173,7 +140,7 @@ describe('Param Changes Editor Component', function() { name: 'a' }); - $scope.onChangeGeneratorType(paramChange); + component.onChangeGeneratorType(paramChange); expect(paramChange.customizationArgs).toEqual({ list_of_values: ['sample value'] @@ -181,30 +148,39 @@ describe('Param Changes Editor Component', function() { }); it('should get complete image path corresponding to a given relative path', - function() { - expect($scope.getStaticImageUrl('/path/to/image.png')).toBe( + () => { + expect(component.getStaticImageUrl('/path/to/image.png')).toBe( '/assets/images/path/to/image.png'); }); - it('should save param changes when externalSave is broadcasted', function() { - spyOn(editabilityService, 'isEditable').and.returnValue(true); - var saveParamChangesSpy = spyOn( - explorationStatesService, 'saveStateParamChanges').and.callFake(() => {}); - $scope.addParamChange(); - $scope.openParamChangesEditor(); - - mockExternalSaveEventEmitter.emit(); - - expect(saveParamChangesSpy).toHaveBeenCalled(); - expect(postSaveHookSpy).toHaveBeenCalled(); - }); + it('should save param changes when externalSave is broadcasted', + fakeAsync(() => { + component.paramChangesService.displayed = []; + spyOn(component, 'generateParamNameChoices').and.stub(); + spyOn(component.paramChangesService, 'saveDisplayedValue').and.stub(); + spyOn(explorationParamSpecsService, 'saveDisplayedValue').and.stub(); + spyOn(editabilityService, 'isEditable').and.returnValue(true); + let saveParamChangesSpy = spyOn( + explorationStatesService, 'saveStateParamChanges') + .and.callFake(() => {}); + component.addParamChange(); + component.openParamChangesEditor(); + + mockExternalSaveEventEmitter.emit(); + tick(); + + expect(saveParamChangesSpy).toHaveBeenCalled(); + expect(postSaveHookSpy).toHaveBeenCalled(); + })); it('should add a new param change when there are no param changes displayed', - function() { - expect(ctrl.paramChangesService.displayed.length).toBe(0); - $scope.addParamChange(); + () => { + expect(( + component.paramChangesService.displayed as ParamChange[] + ).length).toBe(0); + component.addParamChange(); - expect($scope.paramNameChoices).toEqual([{ + expect(component.paramNameChoices).toEqual([{ id: 'a', text: 'a' }, { @@ -214,106 +190,121 @@ describe('Param Changes Editor Component', function() { id: 'y', text: 'y' }]); - expect(ctrl.paramChangesService.displayed.length).toBe(1); + expect(( + component.paramChangesService.displayed as ParamChange[] + ).length).toBe(1); }); it('should not open param changes editor when it is not editable', - function() { + () => { spyOn(editabilityService, 'isEditable').and.returnValue(false); - expect(ctrl.paramChangesService.displayed.length).toBe(0); - $scope.openParamChangesEditor(); + expect(( + component.paramChangesService.displayed as ParamChange[] + ).length).toBe(0); + component.openParamChangesEditor(); - expect($scope.isParamChangesEditorOpen).toBe(false); - expect(ctrl.paramChangesService.displayed.length).toBe(0); + expect(component.isParamChangesEditorOpen).toBe(false); + expect(( + component.paramChangesService.displayed as ParamChange[] + ).length).toBe(0); }); - it('should open param changes editor and cancel edit', function() { + it('should open param changes editor and cancel edit', fakeAsync(() => { + component.paramChangesService.displayed = []; + spyOn(component, 'generateParamNameChoices').and.returnValue([]); + spyOn(component.paramChangesService, 'restoreFromMemento').and.stub(); + spyOn(component.paramChangesService, 'saveDisplayedValue').and.stub(); + spyOn(explorationParamSpecsService, 'saveDisplayedValue').and.stub(); spyOn(editabilityService, 'isEditable').and.returnValue(true); - expect(ctrl.paramChangesService.displayed.length).toBe(0); - $scope.openParamChangesEditor(); + component.openParamChangesEditor(); + tick(); - expect($scope.isParamChangesEditorOpen).toBe(true); - expect(ctrl.paramChangesService.displayed.length).toBe(1); + expect(component.isParamChangesEditorOpen).toBe(true); + expect(( + component.paramChangesService.displayed as ParamChange[]).length).toBe(1); - $scope.cancelEdit(); + component.cancelEdit(); + tick(); - expect($scope.isParamChangesEditorOpen).toBe(false); - expect(ctrl.paramChangesService.displayed.length).toBe(0); - }); + expect(component.isParamChangesEditorOpen).toBe(false); + expect(( + component.paramChangesService.displayed as ParamChange[]).length).toBe(1); + })); - it('should open param changes editor and add a param change', function() { + it('should open param changes editor and add a param change', () => { spyOn(editabilityService, 'isEditable').and.returnValue(true); - expect(ctrl.paramChangesService.displayed.length).toBe(0); - $scope.openParamChangesEditor(); + component.openParamChangesEditor(); - expect($scope.isParamChangesEditorOpen).toBe(true); - expect($scope.paramNameChoices).toEqual([{ + expect(component.isParamChangesEditorOpen).toBe(true); + expect(component.paramNameChoices).toEqual([{ id: 'a', text: 'a' }, { id: 'y', text: 'y' }]); - expect(ctrl.paramChangesService.displayed.length).toBe(1); + expect(( + component.paramChangesService.displayed as ParamChange[]).length).toBe(1); }); - it('should check whenever param changes are valid', function() { - $scope.addParamChange(); + it('should check whenever param changes are valid', () => { + component.addParamChange(); - expect($scope.areDisplayedParamChangesValid()).toBe(true); - expect($scope.warningText).toBe(''); + expect(component.areDisplayedParamChangesValid()).toBe(true); + expect(component.warningText).toBe(''); }); it('should check param changes as invalid when it has an empty parameter' + - ' name', function() { - ctrl.paramChangesService.displayed = [ + ' name', () => { + component.paramChangesService.displayed = [ paramChangeObjectFactory.createDefault('')]; - expect($scope.areDisplayedParamChangesValid()).toBe(false); - expect($scope.warningText).toBe('Please pick a non-empty parameter name.'); + expect(component.areDisplayedParamChangesValid()).toBe(false); + expect(component.warningText).toBe( + 'Please pick a non-empty parameter name.'); }); it('should check param changes as invalid when it has a reserved parameter' + - ' name', function() { - ctrl.paramChangesService.displayed = [ + ' name', () => { + component.paramChangesService.displayed = [ paramChangeObjectFactory.createDefault('answer')]; - expect($scope.areDisplayedParamChangesValid()).toBe(false); - expect($scope.warningText).toBe( + expect(component.areDisplayedParamChangesValid()).toBe(false); + expect(component.warningText).toBe( 'The parameter name \'answer\' is reserved.'); }); it('should check param changes as invalid when it has non alphabetic' + - ' characters in parameter name', function() { - ctrl.paramChangesService.displayed = [ + ' characters in parameter name', () => { + component.paramChangesService.displayed = [ paramChangeObjectFactory.createDefault('123')]; - expect($scope.areDisplayedParamChangesValid()).toBe(false); - expect($scope.warningText).toBe( + expect(component.areDisplayedParamChangesValid()).toBe(false); + expect(component.warningText).toBe( 'Parameter names should use only alphabetic characters.'); }); it('should check param changes as invalid when it has no default' + - ' generator id', function() { - ctrl.paramChangesService.displayed = [ + ' generator id', () => { + component.paramChangesService.displayed = [ paramChangeObjectFactory.createFromBackendDict({ customization_args: {}, generator_id: '', name: 'a' })]; - $scope.areDisplayedParamChangesValid(); - expect($scope.areDisplayedParamChangesValid()).toBe(false); - expect($scope.warningText).toBe( + component.areDisplayedParamChangesValid(); + expect(component.areDisplayedParamChangesValid()).toBe(false); + expect(component.warningText).toBe( 'Each parameter should have a generator id.'); }); it('should check param changes as invalid when it has no values and its' + - ' generator id is RandomSelector', function() { - ctrl.paramChangesService.displayed = [ + ' generator id is RandomSelector', () => { + component.paramChangesService.displayed = [ paramChangeObjectFactory.createFromBackendDict({ customization_args: { list_of_values: [] @@ -322,116 +313,104 @@ describe('Param Changes Editor Component', function() { name: 'a' })]; - $scope.areDisplayedParamChangesValid(); - expect($scope.areDisplayedParamChangesValid()).toBe(false); - expect($scope.warningText).toBe( + component.areDisplayedParamChangesValid(); + expect(component.areDisplayedParamChangesValid()).toBe(false); + expect(component.warningText).toBe( 'Each parameter should have at least one possible value.'); }); - it('should not save param changes when it is invalid', function() { + it('should not save param changes when it is invalid', fakeAsync(() => { spyOn(alertsService, 'addWarning'); - ctrl.paramChangesService.displayed = [ + component.paramChangesService.displayed = [ paramChangeObjectFactory.createDefault('123')]; - $scope.saveParamChanges(); + + component.postSaveHook = () => { + let value = 'value'; + return value; + }; + + component.currentlyInSettingsTab = false; + component.saveParamChanges(); + tick(); expect(alertsService.addWarning).toHaveBeenCalledWith( 'Invalid parameter changes.'); - }); + })); - it('should save param changes when it is valid', function() { - var saveParamChangesSpy = spyOn( + it('should save param changes when it is valid', fakeAsync(() => { + spyOn(component, 'generateParamNameChoices').and.stub(); + spyOn(component.paramChangesService, 'saveDisplayedValue').and.stub(); + spyOn(explorationParamSpecsService, 'saveDisplayedValue').and.stub(); + + let saveParamChangesSpy = spyOn( explorationStatesService, 'saveStateParamChanges').and.callFake(() => {}); - $scope.addParamChange(); - $scope.saveParamChanges(); + + component.currentlyInSettingsTab = false; + component.addParamChange(); + component.saveParamChanges(); + tick(); expect(saveParamChangesSpy).toHaveBeenCalled(); expect(postSaveHookSpy).toHaveBeenCalled(); - }); + })); - it('should not delete a param change when index is less than 0', function() { - $scope.addParamChange(); - expect(ctrl.paramChangesService.displayed.length).toBe(1); + it('should not delete a param change when index is less than 0', () => { + component.addParamChange(); + expect(( + component.paramChangesService.displayed as ParamChange[]).length).toBe(1); spyOn(alertsService, 'addWarning'); - $scope.deleteParamChange(-1); + component.deleteParamChange(-1); expect(alertsService.addWarning).toHaveBeenCalledWith( 'Cannot delete parameter change at position -1: index out of range'); }); it('should not delete a param change when index is greather than param' + - ' changes length', function() { - $scope.addParamChange(); - expect(ctrl.paramChangesService.displayed.length).toBe(1); + ' changes length', () => { + component.addParamChange(); + expect(( + component.paramChangesService.displayed as ParamChange[]).length).toBe(1); spyOn(alertsService, 'addWarning'); - $scope.deleteParamChange(5); + component.deleteParamChange(5); expect(alertsService.addWarning).toHaveBeenCalledWith( 'Cannot delete parameter change at position 5: index out of range'); }); - it('should delete a param change', function() { - $scope.addParamChange(); - expect(ctrl.paramChangesService.displayed.length).toBe(1); + it('should delete a param change', () => { + component.addParamChange(); + expect(( + component.paramChangesService.displayed as ParamChange[] + ).length).toBe(1); - $scope.deleteParamChange(0); - expect(ctrl.paramChangesService.displayed.length).toBe(0); + component.deleteParamChange(0); + expect(( + component.paramChangesService.displayed as ParamChange[] + ).length).toBe(0); }); it('should change customization args values to be human readable', - function() { - expect($scope.HUMAN_READABLE_ARGS_RENDERERS.Copier({ + () => { + expect(component.HUMAN_READABLE_ARGS_RENDERERS.Copier({ value: 'Copier value' })).toBe('to Copier value'); - expect($scope.HUMAN_READABLE_ARGS_RENDERERS.RandomSelector({ + expect(component.HUMAN_READABLE_ARGS_RENDERERS.RandomSelector({ list_of_values: ['first value', 'second value'] })).toBe('to one of [first value, second value] at random'); }); - it('should start param change list to be sortable', function() { - var pladeholderHeightSpy = jasmine.createSpy('placeholderHeight', () => {}); - var itemHeightSpy = jasmine.createSpy('itemHeight', () => {}); - var ui = { - placeholder: { - height: pladeholderHeightSpy - }, - item: { - height: itemHeightSpy - } - }; - $scope.PARAM_CHANGE_LIST_SORTABLE_OPTIONS.start(null, ui); - - expect(pladeholderHeightSpy).toHaveBeenCalled(); - expect(itemHeightSpy).toHaveBeenCalled(); - }); + it('should change list order properly', () => { + jasmine.createSpy('moveItemInArray').and.stub(); - it('should stop param change list to be sortable', function() { - $scope.addParamChange(); + component.paramChangesService.displayed = [ + new ParamChangeObjectFactory(), + new ParamChangeObjectFactory() + ]; - $scope.PARAM_CHANGE_LIST_SORTABLE_OPTIONS.stop(); - expect($scope.paramNameChoices).toEqual([{ - id: 'a', - text: 'a' - }, { - id: 'x', - text: 'x' - }, { - id: 'y', - text: 'y' - }]); + component.drop({ + previousIndex: 1, + currentIndex: 2 + } as CdkDragSortEvent); }); }); - -describe('Upgraded component', () => { - beforeEach(() => destroyPlatform()); - afterEach(() => destroyPlatform()); - it('should create the upgraded component', async(() => { - setupAndGetUpgradedComponentAsync( - 'param-changes-editor', - 'paramChangesEditor', - [ParamChangesEditorDirective] - ).then( - async(textContext) => expect(textContext).toBe('Hello Oppia!') - ); - })); -}); diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.ts b/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.ts index 2473c607ec7c..dc4e6f275ef4 100644 --- a/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.ts +++ b/core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.ts @@ -13,275 +13,273 @@ // limitations under the License. /** - * @fileoverview Directive for the parameter changes editor (which is shown in + * @fileoverview Component for the parameter changes editor (which is shown in * both the exploration settings tab and the state editor page). */ -require( - 'components/forms/custom-forms-directives/select2-dropdown.directive.ts'); -require( - 'pages/exploration-editor-page/param-changes-editor/' + - 'value-generator-editor.directive.ts'); - -require('domain/exploration/ParamChangeObjectFactory.ts'); -require('domain/utilities/url-interpolation.service.ts'); -require( - 'pages/exploration-editor-page/services/exploration-param-specs.service.ts'); -require('pages/exploration-editor-page/services/exploration-states.service.ts'); -require( - 'components/state-editor/state-editor-properties-services/' + - 'state-editor.service.ts'); -require('services/alerts.service.ts'); -require('services/editability.service.ts'); -require('services/external-save.service.ts'); - +import { Component, Injector, Input, OnDestroy, OnInit } from '@angular/core'; +import { downgradeComponent } from '@angular/upgrade/static'; +import { UrlInterpolationService } from 'domain/utilities/url-interpolation.service'; import { Subscription } from 'rxjs'; -import { Directive, ElementRef, Injector, Input } from '@angular/core'; -import { UpgradeComponent } from '@angular/upgrade/static'; - - -angular.module('oppia').component('paramChangesEditor', { - bindings: { - paramChangesService: '<', - postSaveHook: '=', - isCurrentlyInSettingsTab: '<' - }, - template: require('./param-changes-editor.component.html'), - controller: [ - '$scope', 'AlertsService', 'EditabilityService', - 'ExplorationParamSpecsService', 'ExplorationStatesService', - 'ExternalSaveService', 'ParamChangeObjectFactory', - 'UrlInterpolationService', 'INVALID_PARAMETER_NAMES', - function( - $scope, AlertsService, EditabilityService, - ExplorationParamSpecsService, ExplorationStatesService, - ExternalSaveService, ParamChangeObjectFactory, - UrlInterpolationService, INVALID_PARAMETER_NAMES) { - var ctrl = this; - ctrl.directiveSubscriptions = new Subscription(); - var generateParamNameChoices = function() { - return ExplorationParamSpecsService.displayed.getParamNames().sort() - .map(function(paramName) { - return { - id: paramName, - text: paramName - }; - }); - }; - - $scope.addParamChange = function() { - var newParamName = ( - $scope.paramNameChoices.length > 0 ? - $scope.paramNameChoices[0].id : 'x'); - var newParamChange = ParamChangeObjectFactory.createDefault( - newParamName); - // Add the new param name to $scope.paramNameChoices, if necessary, - // so that it shows up in the dropdown. - if (ExplorationParamSpecsService.displayed.addParamIfNew( - newParamChange.name)) { - $scope.paramNameChoices = generateParamNameChoices(); - } - ctrl.paramChangesService.displayed.push(newParamChange); - }; +import { ExplorationParamSpecsService } from '../services/exploration-param-specs.service'; +import { ParamChange, ParamChangeObjectFactory } from 'domain/exploration/ParamChangeObjectFactory'; +import { EditabilityService } from 'services/editability.service'; +import { AlertsService } from 'services/alerts.service'; +import { ExplorationStatesService } from '../services/exploration-states.service'; +import { ExternalSaveService } from 'services/external-save.service'; +import { AppConstants } from 'app.constants'; +import { StateParamChangesService } from 'components/state-editor/state-editor-properties-services/state-param-changes.service'; +import { ExplorationParamChangesService } from '../services/exploration-param-changes.service'; +import cloneDeep from 'lodash/cloneDeep'; +import { ParamSpecs } from 'domain/exploration/ParamSpecsObjectFactory'; +import { CdkDragSortEvent, moveItemInArray} from '@angular/cdk/drag-drop'; - $scope.openParamChangesEditor = function() { - if (!EditabilityService.isEditable()) { - return; - } +@Component({ + selector: 'param-changes-editor', + templateUrl: './param-changes-editor.component.html' +}) +export class ParamChangesEditorComponent implements OnInit, OnDestroy { + @Input() paramChangesServiceName: string; + @Input() postSaveHook: () => void; + @Input() currentlyInSettingsTab: boolean; - $scope.isParamChangesEditorOpen = true; - $scope.paramNameChoices = generateParamNameChoices(); + SERVICE_MAPPING = { + explorationParamChangesService: ExplorationParamChangesService, + stateParamChangesService: StateParamChangesService, + }; - if (ctrl.paramChangesService.displayed.length === 0) { - $scope.addParamChange(); - } - }; + directiveSubscriptions = new Subscription(); + isParamChangesEditorOpen: boolean; + paramNameChoices: { id: string; text: string }[]; + warningText: string; + HUMAN_READABLE_ARGS_RENDERERS: { + Copier: (value) => void; + RandomSelector: (value) => void; + }; - $scope.onChangeGeneratorType = function(paramChange) { - paramChange.resetCustomizationArgs(); - }; + PREAMBLE_TEXT = { + Copier: 'to', + RandomSelector: 'to one of' + }; - $scope.areDisplayedParamChangesValid = function() { - var paramChanges = ctrl.paramChangesService.displayed; + paramChangesService: ( + ExplorationParamChangesService | StateParamChangesService); - for (var i = 0; i < paramChanges.length; i++) { - var paramName = paramChanges[i].name; - if (paramName === '') { - $scope.warningText = 'Please pick a non-empty parameter name.'; - return false; - } + constructor( + private alertsService: AlertsService, + private externalSaveService: ExternalSaveService, + private explorationStatesService: ExplorationStatesService, + private explorationParamSpecsService: ExplorationParamSpecsService, + private paramChangeObjectFactory: ParamChangeObjectFactory, + private editabilityService: EditabilityService, + private urlInterpolationService: UrlInterpolationService, + private injector: Injector, + ) {} - if (INVALID_PARAMETER_NAMES.indexOf(paramName) !== -1) { - $scope.warningText = ( - 'The parameter name \'' + paramName + '\' is reserved.'); - return false; - } + drop(event: CdkDragSortEvent): void { + moveItemInArray( + this.paramChangesService.displayed as ParamChange[], event.previousIndex, + event.currentIndex); + } - var ALPHA_CHARS_REGEX = /^[A-Za-z]+$/; - if (!ALPHA_CHARS_REGEX.test(paramName)) { - $scope.warningText = ( - 'Parameter names should use only alphabetic characters.'); - return false; - } + openParamChangesEditor(): void { + if (!this.editabilityService.isEditable()) { + return; + } - var generatorId = paramChanges[i].generatorId; - var customizationArgs = paramChanges[i].customizationArgs; + this.isParamChangesEditorOpen = true; + this.paramNameChoices = this.generateParamNameChoices(); - if (!$scope.PREAMBLE_TEXT.hasOwnProperty(generatorId)) { - $scope.warningText = - 'Each parameter should have a generator id.'; - return false; - } + if ((this.paramChangesService.displayed as ParamChange[]).length === 0) { + this.addParamChange(); + } + } - if (generatorId === 'RandomSelector' && - customizationArgs.list_of_values.length === 0) { - $scope.warningText = ( - 'Each parameter should have at least one possible value.'); - return false; - } - } + addParamChange(): void { + let newParamName = ( + this.paramNameChoices.length > 0 ? + this.paramNameChoices[0].id : 'x'); + let newParamChange = this.paramChangeObjectFactory.createDefault( + newParamName); + // Add the new param name to this.paramNameChoices, if necessary, + // so that it shows up in the dropdown. + if (( + this.explorationParamSpecsService.displayed as ParamSpecs).addParamIfNew( + newParamChange.name, null)) { + this.paramNameChoices = this.generateParamNameChoices(); + } + (this.paramChangesService.displayed as ParamChange[]).push(newParamChange); + } + + generateParamNameChoices(): {id: string; text: string}[] { + return (this.explorationParamSpecsService.displayed as { + getParamNames: () => {sort: () => []}; + }).getParamNames().sort() + .map((paramName) => { + return { + id: paramName, + text: paramName + }; + }); + } + + onChangeGeneratorType(paramChange: ParamChange): void { + paramChange.resetCustomizationArgs(); + } - $scope.warningText = ''; - return true; - }; + areDisplayedParamChangesValid(): boolean { + let paramChanges = this.paramChangesService.displayed; - $scope.saveParamChanges = function() { - // Validate displayed value. - if (!$scope.areDisplayedParamChangesValid()) { - AlertsService.addWarning('Invalid parameter changes.'); - return; + if (paramChanges && (paramChanges as ParamChange[]).length) { + for (let i = 0; i < (paramChanges as ParamChange[]).length; i++) { + let paramName = paramChanges[i].name; + if (paramName === '') { + this.warningText = 'Please pick a non-empty parameter name.'; + return false; } - $scope.isParamChangesEditorOpen = false; - - // Update paramSpecs manually with newly-added param names. - ExplorationParamSpecsService.restoreFromMemento(); - ctrl.paramChangesService.displayed.forEach(function(paramChange) { - ExplorationParamSpecsService.displayed.addParamIfNew( - paramChange.name); - }); - - ExplorationParamSpecsService.saveDisplayedValue(); - ctrl.paramChangesService.saveDisplayedValue(); - if (!ctrl.isCurrentlyInSettingsTab) { - ExplorationStatesService.saveStateParamChanges( - ctrl.paramChangesService.stateName, - angular.copy(ctrl.paramChangesService.displayed)); + if (AppConstants.INVALID_PARAMETER_NAMES.indexOf(paramName) !== -1) { + this.warningText = ( + 'The parameter name \'' + paramName + '\' is reserved.'); + return false; } - if (ctrl.postSaveHook) { - ctrl.postSaveHook(); + + let ALPHA_CHARS_REGEX = /^[A-Za-z]+$/; + if (!ALPHA_CHARS_REGEX.test(paramName)) { + this.warningText = ( + 'Parameter names should use only alphabetic characters.'); + return false; } - }; - - $scope.deleteParamChange = function(index) { - if (index < 0 || - index >= ctrl.paramChangesService.displayed.length) { - AlertsService.addWarning( - 'Cannot delete parameter change at position ' + index + - ': index out of range'); + + let generatorId = paramChanges[i].generatorId; + let customizationArgs = paramChanges[i].customizationArgs; + + if (!this.PREAMBLE_TEXT.hasOwnProperty(generatorId)) { + this.warningText = + 'Each parameter should have a generator id.'; + return false; } - // This ensures that any new parameter names that have been added - // before the deletion are added to the list of possible names in - // the select2 dropdowns. Otherwise, after the deletion, the - // dropdowns may turn blank. - ctrl.paramChangesService.displayed.forEach(function(paramChange) { - ExplorationParamSpecsService.displayed.addParamIfNew( - paramChange.name); - }); - $scope.paramNameChoices = generateParamNameChoices(); - - ctrl.paramChangesService.displayed.splice(index, 1); - }; - - $scope.cancelEdit = function() { - ctrl.paramChangesService.restoreFromMemento(); - $scope.isParamChangesEditorOpen = false; - }; - - ctrl.$onInit = function() { - $scope.EditabilityService = EditabilityService; - $scope.isParamChangesEditorOpen = false; - $scope.warningText = ''; - $scope.PREAMBLE_TEXT = { - Copier: 'to', - RandomSelector: 'to one of' - }; - ctrl.directiveSubscriptions.add( - ExternalSaveService.onExternalSave.subscribe( - () => { - if ($scope.isParamChangesEditorOpen) { - $scope.saveParamChanges(); - } - })); - $scope.getStaticImageUrl = function(imagePath) { - return UrlInterpolationService.getStaticImageUrl(imagePath); - }; - // This is a local variable that is used by the select2 dropdowns - // for choosing parameter names. It may not accurately reflect the - // content of ExplorationParamSpecsService, since it's possible that - // temporary parameter names may be added and then deleted within - // the course of a single "parameter changes" edit. - $scope.paramNameChoices = []; - $scope.HUMAN_READABLE_ARGS_RENDERERS = { - Copier: function(customizationArgs) { - return 'to ' + customizationArgs.value; - }, - RandomSelector: function(customizationArgs) { - var result = 'to one of ['; - for ( - var i = 0; i < customizationArgs.list_of_values.length; i++) { - if (i !== 0) { - result += ', '; - } - result += String(customizationArgs.list_of_values[i]); - } - result += '] at random'; - return result; + if (generatorId === 'RandomSelector' && + customizationArgs.list_of_values.length === 0) { + this.warningText = ( + 'Each parameter should have at least one possible value.'); + return false; + } + } + } + + this.warningText = ''; + return true; + } + + saveParamChanges(): void { + // Validate displayed value. + if (!this.areDisplayedParamChangesValid()) { + this.alertsService.addWarning('Invalid parameter changes.'); + return; + } + + this.isParamChangesEditorOpen = false; + + // Update paramSpecs manually with newly-added param names. + this.explorationParamSpecsService.restoreFromMemento(); + (this.paramChangesService.displayed as ParamChange[]).forEach(( + paramChange) => { + (this.explorationParamSpecsService.displayed as ParamSpecs).addParamIfNew( + paramChange.name, null); + }); + + this.explorationParamSpecsService.saveDisplayedValue(); + + this.paramChangesService.saveDisplayedValue(); + if (!this.currentlyInSettingsTab) { + this.explorationStatesService.saveStateParamChanges( + (this.paramChangesService as StateParamChangesService).stateName, + cloneDeep(this.paramChangesService.displayed as ParamChange[])); + } + if (this.postSaveHook) { + this.postSaveHook(); + } + } + + deleteParamChange(index: number): void { + if (index < 0 || + index >= (this.paramChangesService.displayed as []).length) { + this.alertsService.addWarning( + 'Cannot delete parameter change at position ' + index + + ': index out of range'); + } + + // This ensures that any new parameter names that have been added + // before the deletion are added to the list of possible names in + // the select2 dropdowns. Otherwise, after the deletion, the + // dropdowns may turn blank. + (this.paramChangesService.displayed as ParamChange[]).forEach( + (paramChange) => { + (this.explorationParamSpecsService.displayed as { + addParamIfNew: (value) => void;}).addParamIfNew( + paramChange.name); + }); + this.paramNameChoices = this.generateParamNameChoices(); + + (this.paramChangesService.displayed as []).splice(index, 1); + } + + cancelEdit(): void { + this.paramChangesService.restoreFromMemento(); + this.isParamChangesEditorOpen = false; + } + + getStaticImageUrl(imagePath: string): string { + return this.urlInterpolationService.getStaticImageUrl(imagePath); + } + + ngOnInit(): void { + this.paramChangesService = ( + this.injector.get(this.SERVICE_MAPPING[this.paramChangesServiceName])); + + this.isParamChangesEditorOpen = false; + this.warningText = ''; + this.directiveSubscriptions.add( + this.externalSaveService.onExternalSave.subscribe( + () => { + if (this.isParamChangesEditorOpen) { + this.saveParamChanges(); } - }; - $scope.PARAM_CHANGE_LIST_SORTABLE_OPTIONS = { - axis: 'y', - containment: '.oppia-param-change-draggable-area', - cursor: 'move', - handle: '.oppia-param-change-sort-handle', - items: '.oppia-param-editor-row', - tolerance: 'pointer', - start: function(e, ui) { - $scope.$apply(); - ui.placeholder.height(ui.item.height()); - }, - stop: function() { - // This ensures that any new parameter names that have been - // added before the swap are added to the list of possible names - // in the select2 dropdowns. Otherwise, after the swap, the - // dropdowns may turn blank. - ctrl.paramChangesService.displayed.forEach( - function(paramChange) { - ExplorationParamSpecsService.displayed.addParamIfNew( - paramChange.name); - } - ); - $scope.paramNameChoices = generateParamNameChoices(); - $scope.$apply(); + })); + + // This is a local letiable that is used by the select2 dropdowns + // for choosing parameter names. It may not accurately reflect the + // content of ExplorationParamSpecsService, since it's possible that + // temporary parameter names may be added and then deleted within + // the course of a single "parameter changes" edit. + this.paramNameChoices = []; + this.HUMAN_READABLE_ARGS_RENDERERS = { + Copier: (customizationArgs) => { + return 'to ' + customizationArgs.value; + }, + RandomSelector: (customizationArgs) => { + let result = 'to one of ['; + for ( + let i = 0; i < customizationArgs.list_of_values.length; i++) { + if (i !== 0) { + result += ', '; } - }; - }; - ctrl.$onDestroy = function() { - ctrl.directiveSubscriptions.unsubscribe(); - }; - } - ] -}); + result += String(customizationArgs.list_of_values[i]); + } + result += '] at random'; + return result; + } + }; + } -@Directive({ - selector: 'param-changes-editor' -}) -export class ParamChangesEditorDirective extends UpgradeComponent { - @Input() paramChangesService: unknown; - @Input() postSaveHook: () => void; - @Input() currentlyInSettingsTab: boolean; - constructor(elementRef: ElementRef, injector: Injector) { - super('paramChangesEditor', elementRef, injector); + ngOnDestroy(): void { + this.directiveSubscriptions.unsubscribe(); } } + +angular.module('oppia').directive('paramChangesEditor', + downgradeComponent({ + component: ParamChangesEditorComponent + }) as angular.IDirectiveFactory); diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.html b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.html new file mode 100644 index 000000000000..484c584c21eb --- /dev/null +++ b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.html @@ -0,0 +1 @@ + diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.spec.ts b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.spec.ts new file mode 100644 index 000000000000..e6aa4d85e7c4 --- /dev/null +++ b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.spec.ts @@ -0,0 +1,82 @@ +// Copyright 2014 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Unit tests for valueGeneratorEditor. + */ + +import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core'; +import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; +import { CopierComponent } from 'value_generators/templates/copier.component'; +import { RandomSelectorComponent } from 'value_generators/templates/random-selector.component'; +import { ValueGeneratorEditorComponent } from './value-generator-editor.component'; + +describe('Value Generator Editor Component', function() { + let component: ValueGeneratorEditorComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + ValueGeneratorEditorComponent, + RandomSelectorComponent, + CopierComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideModule(BrowserDynamicTestingModule, { + set: { + entryComponents: [ + CopierComponent, + RandomSelectorComponent], + } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ValueGeneratorEditorComponent); + component = fixture.componentInstance; + + component.generatorId = 'copier'; + component.initArgs = 'initArgs'; + component.objType = 'objType'; + component.customizationArgs = { + value: 'value', + list_of_values: ['list_of_values'] + }; + + fixture.detectChanges(); + component.ngAfterViewInit(); + }); + + it('should initialize the component', () => { + component.generatorId = 'random-selector'; + component.ngOnChanges({ + generatorId: { + currentValue: 'currentValue', + previousValue: 'previousValue' + } + } as { generatorId: SimpleChange}); + + expect(component).toBeDefined(); + }); + + it('should render RandomSelectorComponent', () => { + component.generatorId = 'random-selector'; + component.ngAfterViewInit(); + + const bannerElement: HTMLElement = fixture.nativeElement; + expect(bannerElement.querySelector('random-selector')).toBeDefined(); + }); +}); diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.ts b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.ts new file mode 100644 index 000000000000..e1fff48ac5dc --- /dev/null +++ b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.ts @@ -0,0 +1,94 @@ +// Copyright 2014 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Component for the parameter generator editors. + */ + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + Input, + OnChanges, + SimpleChange, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { downgradeComponent } from '@angular/upgrade/static'; +import { CopierComponent } from 'value_generators/templates/copier.component'; +import { RandomSelectorComponent } from 'value_generators/templates/random-selector.component'; + +@Component({ + selector: 'oppia-value-generator-editor', + templateUrl: './value-generator-editor.component.html' +}) +export class ValueGeneratorEditorComponent implements OnChanges, AfterViewInit { + @Input() generatorId: string; + @Input() initArgs: string; + @Input() objType: string; + @Input() customizationArgs: { + value: string; + list_of_values: string[]; + }; + + @ViewChild('interactionContainer', { + read: ViewContainerRef}) viewContainerRef!: ViewContainerRef; + + TAG_TO_INTERACTION_MAPPING = { + copier: CopierComponent, + 'random-selector': RandomSelectorComponent + }; + + constructor( + private componentFactoryResolver: ComponentFactoryResolver, + private changeDetectorRef: ChangeDetectorRef + ) {} + + ngAfterViewInit(): void { + let componentName = this.generatorId.replace( + /([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + + const componentFactory = this.componentFactoryResolver + .resolveComponentFactory( + this.TAG_TO_INTERACTION_MAPPING[componentName]); + + const componentRef = this.viewContainerRef.createComponent< + CopierComponent | RandomSelectorComponent>( + componentFactory); + + componentRef.instance.customizationArgs = this.customizationArgs; + componentRef.instance.generatorId = this.generatorId; + componentRef.instance.initArgs = this.initArgs; + componentRef.instance.objType = this.objType; + + componentRef.changeDetectorRef.detectChanges(); + this.changeDetectorRef.detectChanges(); + } + + ngOnChanges(changes: { generatorId: SimpleChange }): void { + if ((changes.generatorId.currentValue !== + changes.generatorId.previousValue) && + this.viewContainerRef) { + this.viewContainerRef.clear(); + this.ngAfterViewInit(); + } + } +} + +angular.module('oppia').directive('oppiaValueGeneratorEditor', + downgradeComponent({ + component: ValueGeneratorEditorComponent + }) as angular.IDirectiveFactory); diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.spec.ts b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.spec.ts deleted file mode 100644 index a2fc6164c59c..000000000000 --- a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2014 The Oppia Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS-IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview Unit tests for valueGeneratorEditor. - */ - -describe('Value Generator Editor directive', function() { - var $scope = null; - var elem = null; - - var compiledElement = null; - - beforeEach(angular.mock.module('directiveTemplates')); - beforeEach(angular.mock.module('oppia')); - - beforeEach(angular.mock.inject(function($compile, $injector) { - var $rootScope = $injector.get('$rootScope'); - - $scope = $rootScope.$new(); - $scope.generatorId = 'Copier'; - $scope.customizationArgs = []; - $scope.initArgs = []; - $scope.objType = 'UnicodeString'; - - elem = angular.element( - ''); - - compiledElement = $compile(elem)($scope); - $rootScope.$digest(); - })); - - it('should add new attributes in the compiled one', function() { - expect(compiledElement.html()).toContain( - 'get-generator-id="getGeneratorId()"'); - expect(compiledElement.html()).toContain( - 'get-init-args="getInitArgs()"'); - expect(compiledElement.html()).toContain( - 'get-generator-id="getGeneratorId()"'); - expect(compiledElement.html()).toContain('get-obj-type="getObjType()"'); - }); -}); diff --git a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.ts b/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.ts deleted file mode 100644 index 46ff23fdb605..000000000000 --- a/core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2014 The Oppia Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS-IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview Directives for the parameter generator editors. - */ - -// Individual value generator directives can be found in -// extensions/value_generators/templates. - -interface ValueGeneratorEditorCustomScope extends ng.IScope { - objType?: string; - generatorId?: string; - getObjType?: (() => string); - getGeneratorId?: (() => string); - initArgs?: Object; - getInitArgs?: (() => Object); -} - -angular.module('oppia').directive('valueGeneratorEditor', [ - '$compile', function($compile) { - return { - restrict: 'E', - scope: { - customizationArgs: '=', - generatorId: '=', - initArgs: '=', - objType: '=' - }, - link: function(scope: ValueGeneratorEditorCustomScope, element) { - scope.$watch('generatorId', function() { - var directiveName = scope.generatorId.replace( - /([a-z])([A-Z])/g, '$1-$2').toLowerCase(); - scope.getGeneratorId = function() { - return scope.generatorId; - }; - scope.getInitArgs = function() { - return scope.initArgs; - }; - scope.getObjType = function() { - return scope.objType; - }; - element.html( - '<' + directiveName + - ' customization-args="customizationArgs"' + - ' get-generator-id="getGeneratorId()"' + - ' get-init-args="getInitArgs()"' + - ' get-obj-type="getObjType()"' + - '>'); - $compile(element.contents())(scope); - }); - } - }; - }]); diff --git a/core/templates/pages/exploration-editor-page/services/exploration-save.service.spec.ts b/core/templates/pages/exploration-editor-page/services/exploration-save.service.spec.ts index d8eaadbd145a..b43f7134fcfe 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-save.service.spec.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-save.service.spec.ts @@ -863,12 +863,22 @@ describe('Exploration save service ' + .and.returnValue(sampleStates); spyOn(explorationDiffService, 'getDiffGraphData') .and.returnValue({ - nodes: 'nodes', - links: ['links'], + nodes: { + nodes: { + newestStateName: 'newestStateName', + originalStateName: 'originalStateName', + stateProperty: 'stateProperty', + } + }, + links: [{ + source: 0, + target: 0, + linkProperty: 'links' + }], finalStateIds: ['finalStaeIds'], - originalStateIds: ['Hola'], - stateIds: [], - } as unknown as ProcessedStateIdsAndData); + originalStateIds: {Hola: 0}, + stateIds: {Hola: 0}, + } as ProcessedStateIdsAndData); let modalSpy = spyOn(ngbModal, 'open').and.returnValue( { componentInstance: { @@ -920,12 +930,23 @@ describe('Exploration save service ' + .and.returnValue(sampleStates); spyOn(explorationDiffService, 'getDiffGraphData') .and.returnValue({ - nodes: 'nodes', - links: ['links'], + nodes: { + nodes: { + newestStateName: 'nodes', + originalStateName: 'originalStateName', + stateProperty: 'stateProperty', + } + }, + links: [{ + source: 0, + target: 0, + linkProperty: 'links' + }], finalStateIds: ['finalStaeIds'], - originalStateIds: ['Hola'], - stateIds: [], - } as unknown as ProcessedStateIdsAndData); + originalStateIds: {Hola: 0}, + stateIds: {Hola: 0}, + } as ProcessedStateIdsAndData); + let modalSpy = spyOn(ngbModal, 'open').and.returnValue( { componentInstance: { @@ -961,12 +982,22 @@ describe('Exploration save service ' + .and.returnValue(sampleStates); spyOn(explorationDiffService, 'getDiffGraphData') .and.returnValue({ - nodes: 'nodes', - links: ['links'], + nodes: { + nodes: { + newestStateName: 'newestStateName', + originalStateName: 'originalStateName', + stateProperty: 'stateProperty', + } + }, + links: [{ + source: 0, + target: 0, + linkProperty: 'links' + }], finalStateIds: ['finalStaeIds'], - originalStateIds: ['Hola'], - stateIds: [], - } as unknown as ProcessedStateIdsAndData); + originalStateIds: {Hola: 0}, + stateIds: {Hola: 0}, + } as ProcessedStateIdsAndData); changeListServiceSpy.and.returnValue(Promise.resolve(null)); let modalSpy = spyOn(ngbModal, 'open').and.returnValue( { @@ -1000,12 +1031,22 @@ describe('Exploration save service ' + .and.returnValue(sampleStates); spyOn(explorationDiffService, 'getDiffGraphData') .and.returnValue({ - nodes: 'nodes', - links: ['links'], + nodes: { + nodes: { + newestStateName: 'newestStateName', + originalStateName: 'originalStateName', + stateProperty: 'stateProperty', + } + }, + links: [{ + source: 0, + target: 0, + linkProperty: 'links' + }], finalStateIds: ['finalStaeIds'], - originalStateIds: ['Hola'], - stateIds: [], - } as unknown as ProcessedStateIdsAndData); + originalStateIds: {Hola: 0}, + stateIds: {Hola: 0}, + } as ProcessedStateIdsAndData); let modalSpy = spyOn(ngbModal, 'open').and.returnValue( { componentInstance: { diff --git a/core/templates/pages/exploration-editor-page/services/exploration-save.service.ts b/core/templates/pages/exploration-editor-page/services/exploration-save.service.ts index 3b3e16aa7f52..dbd7b69c8164 100644 --- a/core/templates/pages/exploration-editor-page/services/exploration-save.service.ts +++ b/core/templates/pages/exploration-editor-page/services/exploration-save.service.ts @@ -45,7 +45,6 @@ import { ExplorationTitleService } from './exploration-title.service'; import { ExplorationWarningsService } from './exploration-warnings.service'; import { RouterService } from './router.service'; import { StatesObjectFactory } from 'domain/exploration/StatesObjectFactory'; -import { LostChange } from 'domain/exploration/LostChangeObjectFactory'; import { WindowRef } from 'services/contextual/window-ref.service'; import { LoggerService } from 'services/contextual/logger.service'; @@ -157,7 +156,7 @@ export class ExplorationSaveService { draftChanges !== null && draftChanges.length > 0) { this.autosaveInfoModalsService.showVersionMismatchModal( - changeList as unknown as LostChange[]); + changeList); return; } diff --git a/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.html b/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.html index c8fe70bb716b..232d0844ef89 100644 --- a/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.html +++ b/core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.html @@ -291,7 +291,7 @@

Advanced Features

-
@@ -312,7 +312,7 @@

Roles

-
+
Managers
  • @@ -325,7 +325,7 @@

    Roles

-
+
Collaborators
  • @@ -338,7 +338,7 @@

    Roles

-
+
Playtesters
  • @@ -391,7 +391,7 @@

    Roles

-
@@ -407,7 +407,7 @@

Voice Artists

-
+
No voice artists are assigned to this exploration.
@@ -417,7 +417,7 @@

Voice Artists

-
+
  • @@ -493,7 +493,7 @@

    Permissions

    It is available in the Oppia library.

    -
    +

    Permissions

    This exploration is public and community-editable. @@ -659,10 +659,10 @@

    Admin Controls

    Parameters used in this exploration

    -
    +
    No parameters used.
    -
      +
      1. {{item.key}} ({{item.value.getType().getName()}})
      2. @@ -680,8 +680,8 @@

        -
        diff --git a/extensions/value_generators/models/generators_test.py b/extensions/value_generators/models/generators_test.py index 3d3bfb6e1485..bc9e595c4d1e 100644 --- a/extensions/value_generators/models/generators_test.py +++ b/extensions/value_generators/models/generators_test.py @@ -29,7 +29,7 @@ def test_copier(self) -> None: generator = generators.Copier() self.assertEqual(generator.generate_value(None, 'a'), 'a') self.assertIn( - 'init-args="initArgs" value="customizationArgs.value"', + '[initArgs]="initArgs" [(value)]="customizationArgs.value"', generator.get_html_template()) def test_random_selector(self) -> None: @@ -37,6 +37,6 @@ def test_random_selector(self) -> None: self.assertIn(generator.generate_value( {}, ['a', 'b', 'c']), ['a', 'b', 'c']) self.assertIn( - '[schema]="$ctrl.SCHEMA" ' + - 'ng-model="$ctrl.customizationArgs.list_of_values"', + '[schema]="SCHEMA" ' + + '[(ngModel)]="customizationArgs.list_of_values"', generator.get_html_template()) diff --git a/extensions/value_generators/templates/Copier.component.html b/extensions/value_generators/templates/Copier.component.html new file mode 100644 index 000000000000..16a764f9ce02 --- /dev/null +++ b/extensions/value_generators/templates/Copier.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/value_generators/templates/Copier.html b/extensions/value_generators/templates/Copier.html deleted file mode 100644 index 20c331a7cf41..000000000000 --- a/extensions/value_generators/templates/Copier.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/extensions/value_generators/templates/RandomSelector.component.html b/extensions/value_generators/templates/RandomSelector.component.html new file mode 100644 index 000000000000..d2eb2d0318e6 --- /dev/null +++ b/extensions/value_generators/templates/RandomSelector.component.html @@ -0,0 +1,2 @@ + + diff --git a/extensions/value_generators/templates/RandomSelector.html b/extensions/value_generators/templates/RandomSelector.html deleted file mode 100644 index 7cf404cc532b..000000000000 --- a/extensions/value_generators/templates/RandomSelector.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/extensions/value_generators/templates/copier.component.spec.ts b/extensions/value_generators/templates/copier.component.spec.ts new file mode 100644 index 000000000000..b5033ea24aec --- /dev/null +++ b/extensions/value_generators/templates/copier.component.spec.ts @@ -0,0 +1,57 @@ +// Copyright 2022 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Unit test for copier value generator. + */ + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; +import { CopierComponent } from './copier.component'; + +describe('copier value generator component', function() { + let component: CopierComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + CopierComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CopierComponent); + component = fixture.componentInstance; + + component.generatorId = 'generatorId'; + component.initArgs = 'initArgs'; + component.objType = 'objType'; + component.customizationArgs = { + value: 'value', + list_of_values: ['list_of_values'] + }; + + fixture.detectChanges(); + }); + + it('should initialize the component', () => { + expect(component).toBeDefined(); + expect(component.getTemplateUrl()).toEqual( + '/value_generator_handler/generatorId' + ); + }); +}); diff --git a/extensions/value_generators/templates/copier.component.ts b/extensions/value_generators/templates/copier.component.ts new file mode 100644 index 000000000000..ed53cce6c28d --- /dev/null +++ b/extensions/value_generators/templates/copier.component.ts @@ -0,0 +1,42 @@ +// Copyright 2014 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Component for copier value generator. + */ + +import { Component, Input } from '@angular/core'; +import { downgradeComponent } from '@angular/upgrade/static'; + +@Component({ + selector: 'copier', + templateUrl: './Copier.component.html' +}) +export class CopierComponent { + @Input() generatorId: string; + @Input() initArgs: string; + @Input() objType: string; + @Input() customizationArgs: { + value: string; + list_of_values: string[]; + }; + + getTemplateUrl(): string { + return '/value_generator_handler/' + this.generatorId; + } +} + +angular.module('oppia').directive( + 'copier', downgradeComponent({ + component: CopierComponent})); diff --git a/extensions/value_generators/templates/copier.directive.ts b/extensions/value_generators/templates/copier.directive.ts deleted file mode 100644 index e550faead48e..000000000000 --- a/extensions/value_generators/templates/copier.directive.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2014 The Oppia Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS-IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview Directive for copier value generator. - */ - -// TODO(sll): Remove this directive (as well as the whole of the value -// generators framework). -require('components/forms/custom-forms-directives/object-editor.directive.ts'); - -interface CopierCustomScope extends ng.IScope { - generatorId?: string; - getTemplateUrl?: (() => string); -} - -angular.module('oppia').directive('copier', ['$compile', function($compile) { - return { - link: function(scope: CopierCustomScope, element) { - scope.getTemplateUrl = function() { - return '/value_generator_handler/' + scope.generatorId; - }; - $compile(element.contents())(scope); - }, - restrict: 'E', - scope: { - customizationArgs: '=', - getGeneratorId: '&', - getInitArgs: '&', - getObjType: '&', - }, - template: '', - controller: ['$scope', function($scope) { - var ctrl = this; - ctrl.$onInit = function() { - $scope.generatorId = $scope.getGeneratorId(); - $scope.initArgs = $scope.getInitArgs(); - $scope.objType = $scope.getObjType(); - $scope.$watch('initArgs', function() { - $scope.initArgs = $scope.getInitArgs(); - }, true); - - $scope.$watch('objType', function() { - $scope.objType = $scope.getObjType(); - }, true); - }; - }] - }; -}]); diff --git a/extensions/value_generators/templates/dynamic-component.module.ts b/extensions/value_generators/templates/dynamic-component.module.ts new file mode 100644 index 000000000000..685f20fc9fe8 --- /dev/null +++ b/extensions/value_generators/templates/dynamic-component.module.ts @@ -0,0 +1,49 @@ +// Copyright 2022 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Module for the Dynamic Component. + */ + +import 'core-js/es7/reflect'; +import 'zone.js'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { CopierComponent } from './copier.component'; +import { RandomSelectorComponent } from './random-selector.component'; +import { SharedFormsModule } from 'components/forms/shared-forms.module'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + SharedFormsModule + ], + providers: [ + RandomSelectorComponent, + CopierComponent + ], + declarations: [ + RandomSelectorComponent, + CopierComponent + ], + exports: [ + RandomSelectorComponent, + CopierComponent + ], +}) + +export class DynamicComponentModule { } diff --git a/extensions/value_generators/templates/random-selector.component.spec.ts b/extensions/value_generators/templates/random-selector.component.spec.ts new file mode 100644 index 000000000000..edb66a8d6c6f --- /dev/null +++ b/extensions/value_generators/templates/random-selector.component.spec.ts @@ -0,0 +1,78 @@ +// Copyright 2021 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Unit tests for random selector value generator. + */ + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; +import { RandomSelectorComponent } from './random-selector.component'; + +describe('RandomSelector component', function() { + let component: RandomSelectorComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + RandomSelectorComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RandomSelectorComponent); + component = fixture.componentInstance; + + component.generatorId = 'generatorId'; + component.initArgs = 'generatorId'; + component.objType = 'objType'; + component.customizationArgs = { + list_of_values: null, + value: null, + }; + }); + + it('should initialise component', () => { + component.customizationArgs = { + list_of_values: ['test'], + value: null, + }; + + component.ngOnInit(); + + expect(component.SCHEMA).toEqual({ + type: 'list', + items: { + type: 'unicode' + }, + ui_config: { + add_element_text: 'Add New Choice' + } + }); + expect(component.getTemplateUrl()).toBe( + '/value_generator_handler/generatorId'); + expect(component.generatorId).toBe('generatorId'); + expect(component.customizationArgs.list_of_values).toEqual(['test']); + }); + + it('should initialise list_of_values as an empty array when list_of_values' + + ' is not defined', () => { + component.ngOnInit(); + + expect(component.customizationArgs.list_of_values).toEqual([]); + }); +}); diff --git a/extensions/value_generators/templates/random-selector.component.ts b/extensions/value_generators/templates/random-selector.component.ts new file mode 100644 index 000000000000..43bf601780fd --- /dev/null +++ b/extensions/value_generators/templates/random-selector.component.ts @@ -0,0 +1,58 @@ +// Copyright 2014 The Oppia Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Component for random selector value generator. + */ + +import { Component, Input, OnInit } from '@angular/core'; +import { downgradeComponent } from '@angular/upgrade/static'; + +@Component({ + selector: 'random-selector', + templateUrl: './RandomSelector.component.html' +}) +export class RandomSelectorComponent implements OnInit { + @Input() generatorId: string; + @Input() initArgs: string; + @Input() objType: string; + @Input() customizationArgs: { + value: string; + list_of_values: string[]; + }; + + SCHEMA = { + type: 'list', + items: { + type: 'unicode' + }, + ui_config: { + add_element_text: 'Add New Choice' + } + }; + + getTemplateUrl(): string { + return '/value_generator_handler/' + this.generatorId; + } + + ngOnInit(): void { + if (!this.customizationArgs.list_of_values) { + this.customizationArgs.list_of_values = []; + } + } +} + +angular.module('oppia').directive( + 'randomSelector', downgradeComponent({ + component: RandomSelectorComponent})); diff --git a/extensions/value_generators/templates/random-selector.directive.spec.ts b/extensions/value_generators/templates/random-selector.directive.spec.ts deleted file mode 100644 index d56086f979b9..000000000000 --- a/extensions/value_generators/templates/random-selector.directive.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2021 The Oppia Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS-IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview Unit tests for random selector value generator. - */ - -describe('randomSelector', () => { - let ctrl = null; - let $rootScope = null; - let $scope = null; - - beforeEach(angular.mock.module('oppia')); - beforeEach(angular.mock.inject(function( - $injector, $componentController, $compile) { - $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - ctrl = $componentController('randomSelector', {}, { - getGeneratorId: () => { - return 'generatorId'; - } - }); - - let elem = angular.element( - ''); - $compile(elem)($scope); - $rootScope.$digest(); - })); - - it('should initialise component', () => { - ctrl.customizationArgs = { - list_of_values: ['test'] - }; - - ctrl.$onInit(); - - expect(ctrl.SCHEMA).toEqual({ - type: 'list', - items: { - type: 'unicode' - }, - ui_config: { - add_element_text: 'Add New Choice' - } - }); - expect(ctrl.generatorId).toBe('generatorId'); - expect(ctrl.customizationArgs.list_of_values).toEqual(['test']); - }); - - it('should initialise list_of_values as an empty array when list_of_values' + - ' is not defined', () => { - ctrl.customizationArgs = { - list_of_values: undefined - }; - - ctrl.$onInit(); - - expect(ctrl.customizationArgs.list_of_values).toEqual([]); - }); -}); diff --git a/extensions/value_generators/templates/random-selector.directive.ts b/extensions/value_generators/templates/random-selector.directive.ts deleted file mode 100644 index 261313015930..000000000000 --- a/extensions/value_generators/templates/random-selector.directive.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2014 The Oppia Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS-IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview Directive for random selector value generator. - */ - -interface RandomSelectorCustomScope extends ng.IScope { - $ctrl?: { - generatorId?: string; - }; - getTemplateUrl?: (() => string); -} - -angular.module('oppia').directive('randomSelector', [ - '$compile', function($compile) { - return { - link: function(scope: RandomSelectorCustomScope, element) { - scope.getTemplateUrl = function() { - return '/value_generator_handler/' + scope.$ctrl.generatorId; - }; - $compile(element.contents())(scope); - }, - restrict: 'E', - scope: {}, - bindToController: { - customizationArgs: '=', - getGeneratorId: '&' - }, - template: '
        ', - controllerAs: '$ctrl', - controller: function() { - var ctrl = this; - ctrl.$onInit = function() { - ctrl.SCHEMA = { - type: 'list', - items: { - type: 'unicode' - }, - ui_config: { - add_element_text: 'Add New Choice' - } - }; - ctrl.generatorId = ctrl.getGeneratorId(); - if (!ctrl.customizationArgs.list_of_values) { - ctrl.customizationArgs.list_of_values = []; - } - }; - } - }; - }]); diff --git a/extensions/value_generators/valueGeneratorsRequires.ts b/extensions/value_generators/valueGeneratorsRequires.ts index 46b7a39d0243..589591c09aab 100644 --- a/extensions/value_generators/valueGeneratorsRequires.ts +++ b/extensions/value_generators/valueGeneratorsRequires.ts @@ -16,5 +16,5 @@ * @fileoverview Requires for all the value generators directives. */ -require('value_generators/templates/copier.directive.ts'); -require('value_generators/templates/random-selector.directive.ts'); +require('value_generators/templates/copier.component.ts'); +require('value_generators/templates/random-selector.component.ts'); diff --git a/scripts/typescript_checks.py b/scripts/typescript_checks.py index 8f7808198c00..6c1fe7ffd8f0 100644 --- a/scripts/typescript_checks.py +++ b/scripts/typescript_checks.py @@ -273,8 +273,8 @@ 'core/templates/pages/exploration-editor-page/modal-templates/exploration-save-modal.component.ts', 'core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.spec.ts', 'core/templates/pages/exploration-editor-page/param-changes-editor/param-changes-editor.component.ts', - 'core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.spec.ts', - 'core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.directive.ts', + 'core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.spec.ts', + 'core/templates/pages/exploration-editor-page/param-changes-editor/value-generator-editor.component.ts', 'core/templates/pages/exploration-editor-page/preview-tab/preview-tab.component.spec.ts', 'core/templates/pages/exploration-editor-page/preview-tab/preview-tab.component.ts', 'core/templates/pages/exploration-editor-page/services/change-list.service.spec.ts', @@ -504,9 +504,9 @@ 'extensions/rich_text_components/Image/directives/oppia-noninteractive-image.component.ts', 'extensions/rich_text_components/Math/directives/oppia-noninteractive-math.component.ts', 'extensions/rich_text_components/rte-output-display.component.ts', - 'extensions/value_generators/templates/copier.directive.ts', - 'extensions/value_generators/templates/random-selector.directive.spec.ts', - 'extensions/value_generators/templates/random-selector.directive.ts', + 'extensions/value_generators/templates/copier.component.ts', + 'extensions/value_generators/templates/random-selector.component.spec.ts', + 'extensions/value_generators/templates/random-selector.component.ts', 'extensions/visualizations/oppia-visualization-click-hexbins.directive.spec.ts', 'extensions/visualizations/oppia-visualization-click-hexbins.directive.ts', 'extensions/visualizations/oppia-visualization-enumerated-frequency-table.directive.spec.ts', diff --git a/tsconfig.json b/tsconfig.json index e287636e27bb..31d2324b1152 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -41,7 +41,8 @@ "static/*": ["third_party/static/*"], "tests/*": ["core/templates/tests/*"], "utility/*": ["core/templates/utility/*"], - "visualizations/*": ["extensions/visualizations/*"] + "visualizations/*": ["extensions/visualizations/*"], + "value_generators/*": ["extensions/value_generators/*"] } }, "files": [