diff --git a/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.html b/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.html new file mode 100644 index 000000000..65d51795b --- /dev/null +++ b/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.html @@ -0,0 +1,131 @@ + +
{{selectast}}
+
+ + +
+
+ +

{{assetsCount}}

+
+
{{emptySearchMessage}}
+
+
+
+ {{data?.name}} +
+
+ +
+
+ +
+
+ +

{{assetsCount}}

+
+
{{emptySearchMessage}}
+
+
+
+ {{data?.name}} +
+
+
+ +
+
+
+
+ +
+
+ + + +
{{configService.labelConfig?.lbl?.uploadAndUse}}
+
+
+
+
+
{{chooseOrDragAst}}*
+
+ + +
{{assetName}}
+ {{configService.labelConfig?.lbl?.upload}} {{acceptedFileType}} ({{configService.labelConfig?.lbl?.maxFileSize}} + {{astSize}}{{astSizeType}}) +
+

{{errorMsg}}

+
+
+
+
+
    +
  • {{configService.labelConfig?.lbl?.allowedFileTypes}} {{acceptedFileType}}
  • +
  • {{configService.labelConfig?.lbl?.maximumAllowedFileSize}} {{astSize}}{{astSizeType}}
  • +
+
+
{{configService.labelConfig?.lbl?.copyRightsAndLicense}}*
+

{{termsAndCondition}}

+
+
+
+
+
    +
  • + + {{configService.labelConfig?.lbl?.dropChooseFile}}
  • +
+
+
+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+
+
diff --git a/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.scss b/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.scss new file mode 100644 index 000000000..799c9782c --- /dev/null +++ b/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.scss @@ -0,0 +1,374 @@ +.editorWrapper { + border: 0px solid white; + } + + .editorWrapper.hasError { + border: 1px solid red; + border-radius: 2px; + } + + .characterCount { + text-align: right; + /* background-color: #f2f2f2; + border: 1px solid #c4c4c4; */ + border-top: 0; + padding-right: 7px; + font-size: 11px; + font-weight: bold; + margin-top: -16px; + position: relative + } + + .custom-image img { + border: 1px dotted; + padding: 7px; + margin: 6px; + cursor: pointer; + } + + .resource-image { + height: 180px !important; + } + + .asset_container { + overflow-y: auto; + overflow-x: hidden; + min-height: 300px !important; + max-height: 300px !important; + padding: 5px; + } + + .insert-image-btn { + position: absolute; + z-index: 1; + left: 653px; + background-color: transparent; + padding: 12px 14px !important; + margin-left: 6px; + } + + .insert-image-btn>.icon { + opacity: 1; + } + + .insert-image-btn:active { + background-color: transparent; + } + + .upload-file-section { + display: flex; + height: 240px; + width: 100%; + max-width: 800px; + align-items: center; + justify-content: center; + flex-direction: column; + margin: 0 auto; + background-color: #F5F9FC; + border: 1px dashed #80a7ce; + } + + .upload-file-description p { + color: #999999; + } + + .upload-file-description ul { + margin: 0; + list-style: disc; + } + + .upload-file-description ul li { + margin-bottom: 8px; + } + + .upload-file-description ul li a { + cursor: pointer; + font-size: 12px; + } + + + /* Grid Layout CSS for uploaded image & video section */ + .sb-grid-layout { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); + grid-gap: 16px; + } + + .sb-grid-layout.image { + grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); + } + + .sb-grid-layout.video { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + } + + .sb-grid-layout .sb-video-content .sb-image-section { + height: 96px; + overflow: hidden; + border-radius: 4px; + box-shadow: inset 0 1px 3px 0 rgba(0, 0, 0, 0.5); + } + + .sb-image-section { + .selected-video { + display: none; + } + + &.active { + .selected-video { + position: absolute; + right: 4px; + top: 4px; + color: #07bc81; + font-size: 20px; + display: block; + } + } + } + .sb-grid-layout .sb-video-content .sb-image-section img { + width: 100%; + height: 100%; + cursor: pointer; + } + + .overlay-image { + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.3); + position: absolute; + left: 0; + right: 0; + cursor: pointer; + } + + .overlay-image .play.icon { + position: absolute; + transform: translate(-50%, -50%); + top: 50%; + left: 50%; + color: rgba(255, 255, 255, .6); + font-size: 32px; + cursor: pointer; + } + + + + ::ng-deep { + + .ck-rounded-corners .ck.ck-editor__top .ck-sticky-panel .ck-toolbar, .ck.ck-editor__top .ck-sticky-panel .ck-toolbar.ck-rounded-corners { + border-left: none; + border-right: none; + border-bottom: 1px solid #c4c4c4; + border-radius: 0; + } + + .ck-rounded-corners .ck.ck-editor__main>.ck-editor__editable, .ck.ck-editor__main>.ck-editor__editable.ck-rounded-corners { + border: none; + } + + .ck-dropdown.ck-font-size-dropdown .ck-dropdown__panel { + height: 150px; + overflow-y: scroll; + } + + figure.image.ck-widget { + width: auto; + height: auto; + overflow: visible; + } + + figure.image img { + width: 100%; + } + + figure.image.resize-25 { + width: 25%; + height: auto; + } + figure.image.resize-50 { + width: 50%; + height: auto; + } + figure.image.resize-75 { + width: 75%; + height: auto; + } + figure.image.resize-100 { + width: 100%; + height: auto; + } + figure.table{ + margin : 2.2rem 0 1.5rem 1rem!important; + } + .text-center { text-align: center } + .text-left { text-align: left } + .text-right { text-align: right } + + .fs-8 { font-size: 0.5rem; } + .fs-10 { font-size: 0.625rem; } + .fs-12 { font-size: 0.75rem; } + .fs-14 { font-size: 0.875rem; } + .fs-16 { font-size: 1rem; } + .fs-18 { font-size: 1.125rem; } + .fs-20 { font-size: 1.25rem; } + .fs-22 { font-size: 1.375rem; } + .fs-24 { font-size: 1.5rem; } + .fs-26 { font-size: 1.625rem; } + .fs-28 { font-size: 1.75rem; } + .fs-30 { font-size: 1.875rem; } + .fs-36 { font-size: 2.25rem; } + } + + .upload-image-modal-section { + border-radius: 4px; + width: 100%; + min-height: 130px; + background: #F5F9FC; + border: 1px dashed #024f9d; + } + + .upload-input-button { + input[type="file"] { + position: absolute; + right: 0px; + top: 0px; + font-size: 118px; + margin: 0px; + padding: 0px; + cursor: pointer; + opacity: 0; + height: 100%; + } + } + + .qq-uploader.qq-uploader-selector.custom-qq-uploader { + background: inherit; + border-color: none; + border: none; + max-height: inherit; + min-height: inherit; + overflow-y: inherit; + width: 688px; + height: 240px; + display: flex; + justify-content: center; + align-items: center; + } + + .terms-and-condition { + line-height: 14px; + } + + .sb-info-bx{ + padding: 0.4rem !important; + li{ + margin: 0 !important; + padding: 0 !important; + &::before{ + content: "" !important; + } + } + } + + .red{ + color: red; + } + .b-bl{ + border-left: solid 1px #e4e1e1; + } + .sb-form-fields{ + .sb-field-group{ + margin: 1rem 0; + } + .sb-field{ + position: relative; + margin-bottom: 0.5rem; + .sb-form-control{ + border: 1px solid rgba(34,36,38,.15); + width: 100%; + border-radius: .28571429rem; + box-shadow: 0 0 0 0 transparent inset; + padding: .25rem .5rem!important; + } + } + } + @keyframes spinner-border { + to { + transform: rotate(360deg); + } + } + .sb-loading-spinner { + width: 1rem; + height: 1rem; + margin-right: 8px; + border-width: 0.2em; + display: inline-block; + vertical-align: text-bottom; + border: 0.15em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + -webkit-animation: spinner-border 0.75s linear infinite; + animation: spinner-border 0.75s linear infinite; + } + .sb-btn-loading:before { + position: absolute; + content: ""; + top: 50%; + left: 50%; + margin: -0.64285714em 0 0 -0.64285714em; + width: 1.28571429em; + height: 1.28571429em; + border-radius: 500rem; + border: 0.2em solid rgba(0, 0, 0, 0.15); + } + .sb-btn-loading:after { + position: absolute; + content: ""; + top: 50%; + left: 50%; + margin: -0.64285714em 0 0 -0.64285714em; + width: 1.28571429em; + height: 1.28571429em; + -webkit-animation: button-spin 0.6s linear; + animation: button-spin 0.6s linear; + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; + border-radius: 500rem; + border-color: #fff transparent transparent; + border-style: solid; + border-width: 0.2em; + -webkit-box-shadow: 0 0 0 1px transparent; + box-shadow: 0 0 0 1px transparent; + } + .sb-btn-outline-disabled { + background-color: #ffffff; + border: 1px solid #cccccc; + color: #999999; + cursor: default; + font-weight: 400; + } + .sb-color-grey{color:#666;} + + .flex-jc-space-end { + justify-content: flex-end !important; + } + .fs-0785{ + font-size: 0.785rem !important; + } + .ui.info.message{ + color: #276f86 !important; + } + .sb-textbox[disabled="true"]{ + opacity: 0.3 !important; + font-weight: 500 !important; + } + .sb-tabset-segment { + min-height: 288px; + max-height: 288px; + overflow-y: auto; + } + + input:focus-visible{ + border: none !important; + } + \ No newline at end of file diff --git a/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.spec.ts b/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.spec.ts new file mode 100644 index 000000000..a8aadb1de --- /dev/null +++ b/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.spec.ts @@ -0,0 +1,728 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { QuestionService } from './../../services/question/question.service'; +import { AssetsBrowserComponent } from './assets-browser.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { EditorService } from '../../services/editor/editor.service'; +import { of, throwError } from 'rxjs'; +import * as _ from 'lodash-es'; +import { mockData } from '../asset-browser/asset-browser.component.spec.data'; +import { ConfigService } from '../../services/config/config.service'; +import { ToasterService } from '../../services/toaster/toaster.service'; + +const mockEditorService = { + editorConfig: { + config: { + assetConfig: { + video: { + size: '50', + accepted: 'mp4, webm' + } + } + }, + context: { + user: { + id: 123, + fullName: 'Ram Gopal' + }, + channel: 'sunbird' + } + }, + apiErrorHandling: () => {}, + appendCloudStorageHeaders: (config) => { + return {...config, headers: {'x-ms-blob-type': 'BlockBlob'}}; + } +}; + +describe('AssetsBrowserComponent', () => { + let component: AssetsBrowserComponent; + let fixture: ComponentFixture; + let editorService + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [InfiniteScrollModule, HttpClientTestingModule, FormsModule], + declarations: [AssetsBrowserComponent], + providers: [{ provide: EditorService, useValue: mockEditorService }, QuestionService, ConfigService, ToasterService], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AssetsBrowserComponent); + component = fixture.componentInstance; + editorService = TestBed.inject(EditorService); + component.assetType = "video"; + fixture.detectChanges(); + }) + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('#ngOnInit() should call #getAcceptType()', () => { + spyOn(component, 'ngOnInit').and.callThrough(); + component.assetType="video"; + spyOn(editorService.editorConfig.config.assetConfig, 'video').and.returnValue({ + size: '50', + sizeType: 'MB', + }); + component.ngOnInit(); + // expect(component.astSize).toEqual(mockEditorService.editorConfig.config.assetConfig.video.size); + // expect(component.astSizeType).toEqual('MB'); + // expect(component.getAcceptType).toHaveBeenCalledWith(mockEditorService.editorConfig.config.assetConfig.video.accepted, 'video'); + }); + + it("#getAcceptType should return accepted content types", () => { + const typeList = "mp4, webm"; + const type = "video"; + spyOn(component, 'getAcceptType').and.callThrough(); + const result = component.getAcceptType(typeList, type); + expect(result).toEqual("video/mp4,video/webm"); + }); + + + it('should update showAssetPicker when ngOnChanges is called', () => { + component.assetShow = true; + component.ngOnChanges(); + expect(component.showAssetPicker).toBe(true); + }); + + it('#initializeAssetPicker() should set showAssetPicker to true', () => { + spyOn(component, 'initializeAssetPicker').and.callThrough(); + component.initializeAssetPicker(); + expect(component.showAssetPicker).toBeTruthy(); + }); + + it('#outputEventHandler() should log event', () => { + spyOn(component, 'outputEventHandler').and.callThrough(); + component.outputEventHandler({}); + expect(component.outputEventHandler).toHaveBeenCalled(); + }); + + it('#getMyAssets() should return assets on API success', + async () => { + const response = mockData.serverResponse; + response.result = { + count: 1, + content: [{ + downloadUrl: '/test' + }] + } + component.assetType = "video"; + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService, 'getAssetMedia').and.returnValue(of(response)); + const offset = 0; + component.getMyAssets(offset); + expect(component.assetsCount).toEqual(1); + }); + + it('#getMyAssets() should return assets on API success', + async () => { + const response = mockData.serverResponse; + response.result = { + count: 1, + content: [{ + downloadUrl: '/test' + }] + } + component.assetType = "video"; + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService, 'getAssetMedia').and.returnValue(of(response)); + const offset = 0; + const query = "test"; + component.getMyAssets(offset, query); + expect(component.assetsCount).toEqual(1); + }); + + it('#getAllAssets() should return assets on API success', async () => { + const response = mockData.serverResponse; + response.result = { + count: 1, + content: [{ + downloadUrl: '/test' + }] + } + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService, 'getAssetMedia').and.returnValue(of(response)); + const offset = 0; + component.getAllAssets(offset); + spyOn(component.allAssets, 'push'); + expect(component.assetsCount).toEqual(1); + }); + + it('#getAllAssets() should return assets on API success', async () => { + const response = mockData.serverResponse; + response.result = { + count: 1, + content: [{ + downloadUrl: '/test' + }] + } + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService, 'getAssetMedia').and.returnValue(of(response)); + const offset = 0; + const query = "test"; + component.getAllAssets(offset, query); + let modal = undefined; + spyOn(component, 'addAssetInEditor').and.callThrough(); + // spyOn(component.assetDataOutput, 'emit').and.callFake(() => {}); + component.addAssetInEditor(modal); + spyOn(component.allAssets, 'push'); + expect(component.assetsCount).toEqual(1); + }); + it('should handle API error gracefully', () => { + const mockError = { status: 500, message: 'Server Error' }; + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService, 'getAssetMedia').and.returnValue(throwError(mockError)); + spyOn(editorService, "apiErrorHandling").and.callFake(() => {}); + }); + + it('#resetFormData() should reset the form data', () => { + component.resetFormData(); + expect(component.assetUploadLoader).toEqual(false); + expect(component.assetFormValid).toEqual(false); + expect(component.formConfig).toBeTruthy(); + }) + + it('#uploadAndUseAsset should upload asset on API success', async () => { + const createMediaAssetResponse = mockData.serverResponse; + createMediaAssetResponse.result = { + node_id: 'do_123' + } + const preSignedResponse = mockData.serverResponse; + preSignedResponse.result = { + node_id: 'do_234', + pre_signed_url: '/test' + } + expect(component.loading).toEqual(false); + expect(component.isClosable).toEqual(true); + expect(component.assetFormValid).toEqual(false); + spyOn(component, 'uploadAndUseAsset').and.callThrough(); + let questionService: QuestionService = TestBed.inject(QuestionService); + let modal = true; + spyOn(questionService, 'createMediaAsset').and.returnValue(of(createMediaAssetResponse)); + spyOn(questionService, 'generatePreSignedUrl').and.returnValue(of(preSignedResponse)); + const editorService = TestBed.inject(EditorService); + spyOn(editorService, 'appendCloudStorageHeaders').and.callThrough(); + spyOn(component, 'dismissPops').and.callThrough(); + component.uploadAndUseAsset(modal); + expect(questionService.createMediaAsset).toHaveBeenCalled(); + }); + it('#updateContentWithURL should update asset with url', async () => { + let fileURL = 'video/webm'; + let mimeType = 'video'; + let contentId = 'do_123'; + const data = new FormData(); + data.append('fileUrl', fileURL); + data.append('mimeType', mimeType); + + const conf = { + enctype: 'multipart/form-data', + processData: false, + contentType: false, + cache: false + }; + const option = { + data, + param: conf + }; + const createMediaAssetResponse = mockData.serverResponse; + createMediaAssetResponse.result = { + node_id: 'do_123' + } + const preSignedResponse = mockData.serverResponse; + preSignedResponse.result = { + node_id: 'do_234', + pre_signed_url: '/test' + } + spyOn(component, 'updateContentWithURL').and.callThrough(); + component.updateContentWithURL(fileURL, mimeType, contentId); + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService, 'getQuestionList').and.returnValue(throwError({})); + let modal = true; + spyOn(questionService, 'uploadMedia').and.returnValue(of(createMediaAssetResponse)); + component.getUploadAsset(createMediaAssetResponse.result['node_id'], modal); + }); + it('should handle error correctly', () => { + const fileURL = 'mockFileURL'; + const mimeType = 'mockMimeType'; + const contentId = 'mockContentId'; + const modal = 'mockModal'; + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService, 'uploadMedia').and.returnValue( + throwError({ message: 'Mock error' }) + ); + let configService: ConfigService = TestBed.inject(ConfigService); + spyOn(configService, 'labelConfig').and.returnValue({ messages: { error: { '027': 'MockErrorMessage' } } }); + + spyOn(editorService, "apiErrorHandling").and.callFake(() => {}); + + component.updateContentWithURL(fileURL, mimeType, contentId, modal); + + expect(questionService.uploadMedia).toHaveBeenCalledOnceWith(jasmine.anything(), contentId); + expect(component.isClosable).toBe(true); + expect(component.loading).toBe(false); + expect(component.assetFormValid).toBe(true); + }); + it('#updateContentWithURL should update asset with url', async () => { + let fileURL = 'video/webm'; + let mimeType = 'video'; + let contentId = 'do_123'; + const data = new FormData(); + data.append('fileUrl', fileURL); + data.append('mimeType', mimeType); + const conf = { + enctype: 'multipart/form-data', + processData: false, + contentType: false, + cache: false + }; + const option = { + data, + param: conf + }; + const createMediaAssetResponse = mockData.serverResponse; + createMediaAssetResponse.result = { + node_id: 'do_123' + } + const preSignedResponse = mockData.serverResponse; + preSignedResponse.result = { + node_id: 'do_234', + pre_signed_url: '/test' + } + spyOn(component, 'updateContentWithURL').and.callThrough(); + component.updateContentWithURL(fileURL, mimeType, contentId); + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService, 'getQuestionList').and.returnValue(throwError({})); + spyOn(questionService, "uploadMedia").and.returnValue( + throwError("error") + ); + }); + it('#getUploadAsset should get asset', async () => { + const createMediaAssetResponse = mockData.serverResponse; + createMediaAssetResponse.result = { + node_id: 'do_123' + } + const preSignedResponse = mockData.serverResponse; + preSignedResponse.result = { + node_id: 'do_234', + pre_signed_url: '/test' + } + spyOn(component, 'getUploadAsset').and.callThrough(); + let questionService: QuestionService = TestBed.inject(QuestionService); + let modal = undefined; + spyOn(questionService, 'getVideo').and.returnValue(of(createMediaAssetResponse)); + expect(component.loading).toEqual(false); + expect(component.isClosable).toEqual(true); + expect(component.assetFormValid).toEqual(false); + spyOn(component, 'addAssetInEditor').and.callThrough(); + component.addAssetInEditor(modal); + }); + + it('#getUploadAsset should get asset', async () => { + const createMediaAssetResponse = mockData.serverResponse; + createMediaAssetResponse.result = { + node_id: 'do_123' + } + const preSignedResponse = mockData.serverResponse; + preSignedResponse.result = { + node_id: 'do_234', + pre_signed_url: '/test' + } + }); + it('#addAssetInEditor() should emit proper event', () => { let modal = undefined; + spyOn(component, 'addAssetInEditor').and.callThrough(); + // spyOn(component.assetDataOutput, 'emit').and.callFake(() => {}); + component.addAssetInEditor(modal); + // expect(component.assetDataOutput.emit).toHaveBeenCalledWith(mockData.assetBrowserEvent); + }); + + it('#addAssetInEditor() should emit proper event', () => { let modal = undefined; + component.url = '/test'; + spyOn(component, 'addAssetInEditor').and.callThrough(); + // spyOn(component.assetDataOutput, 'emit').and.callFake(() => {}); + component.addAssetInEditor(modal); + // expect(component.assetDataOutput.emit).toHaveBeenCalledWith(mockData.assetBrowserEvent); + }); + + it('#getMediaOriginURL() should emit media origin url', () => { + let src = '/test'; + spyOn(component, 'getMediaOriginURL').and.callThrough(); + // spyOn(component.assetDataOutput, 'emit').and.callFake(() => {}); + component.getMediaOriginURL(src); + // expect(component.assetDataOutput.emit).toHaveBeenCalledWith(mockData.assetBrowserEvent); + }); + it('#getMediaOriginURL() should emit media origin url', () => { + let url = '/test'; + spyOn(component, 'getMediaOriginURL').and.callThrough(); + const src = 'https://example.com/image.jpg'; + + const result = component.getMediaOriginURL(src); + + expect(result).toEqual(src); // No replacement should occur + // expect(component.assetDataOutput.emit).toHaveBeenCalledWith(mockData.assetBrowserEvent); + }); + + it('#getMediaOriginURL() should replace cloud storage URL with assetProxyUrl', () => { + component.assetProxyUrl = 'https://asset-proxy.com/'; + editorService.editorConfig.context.cloudStorageUrls = [ + 'https://storage-url1.com/', + 'https://storage-url2.com/' + ]; + const src = 'https://storage-url1.com/video.mp3'; + + // Act + const result = component.getMediaOriginURL(src); + + // Assert + expect(result).toEqual('https://asset-proxy.com/video.mp3'); + }); + + it('#getMediaOriginURL() should handle no matches', () => { + component.assetProxyUrl = 'https://asset-proxy.com/'; + editorService.editorConfig.context.cloudStorageUrls = [ + 'https://storage-url1.com/', + 'https://storage-url2.com/' + ]; + const src = 'https://unrelated-url.com/video.mp3'; + // Assert + const result = component.getMediaOriginURL(src); + expect(result).toEqual('https://unrelated-url.com/video.mp3'); + }); + + it('#getMediaOriginURL() should handle empty cloudStorageUrls', () => { + component.assetProxyUrl = 'https://asset-proxy.com/'; + editorService.editorConfig.context.cloudStorageUrls = []; + const src = 'https://storage-url1.com/video.mp3'; + + // Act + const result = component.getMediaOriginURL(src); + + // Assert + expect(result).toEqual('https://storage-url1.com/video.mp3'); + }); + + it('#uploadAsset() should create asset on API success', () => { + const file = new File([''], 'filename', { type: 'video' }); + const event = { + target: { + files: [ + file + ] + } + } + component.assetConfig = { + "video": { + "size": "50", + "sizeType": "MB", + "accepted": "mp4, webm" + } + } + spyOn(component, 'uploadAsset').and.callThrough(); + component.uploadAsset(event); + expect(component.assetUploadLoader).toEqual(true); + expect(component.assetFormValid).toEqual(true); + }) + it('#generateAssetCreateRequest() should return asset create request', () => { + let fileName = 'test'; + let fileType = 'video/webm'; + let mediaType = 'video'; + const result = component.generateAssetCreateRequest(fileName, fileType, mediaType); + expect(result).toEqual({ + name: fileName, + mediaType, + mimeType: fileType, + createdBy: _.get(mockEditorService.editorConfig, 'context.user.id'), + creator: _.get(mockEditorService.editorConfig, 'context.user.fullName'), + channel: _.get(mockEditorService.editorConfig, 'context.channel') + }) + }); + + xit('#uploadAndUseAsset should upload asset and call upload to blob', + async () => { + const createMediaAssetResponse = mockData.serverResponse; + createMediaAssetResponse.result = { + node_id: 'do_123' + } + const preSignedResponse = mockData.serverResponse; + preSignedResponse.result = { + node_id: 'do_234', + pre_signed_url: '/test?' + } + const uploadMediaResponse = mockData.serverResponse; + uploadMediaResponse.result = { + node_id: 'do_234', + content_url: '/test' + } + component.showAssetUploadModal = false; + let questionService: QuestionService = TestBed.inject(QuestionService); + let modal = true; + spyOn(questionService, 'createMediaAsset').and.returnValue(of(createMediaAssetResponse)); + spyOn(component, 'uploadToBlob').and.returnValue(of(true)); + spyOn(questionService, 'uploadMedia').and.returnValue(of(uploadMediaResponse)); + spyOn(component, 'addAssetInEditor').and.callThrough(); + spyOn(component, 'dismissPops').and.callFake(()=> {}); + spyOn(component, 'uploadAndUseAsset').and.callThrough(); + component.uploadAndUseAsset(modal); + expect(questionService.createMediaAsset).toHaveBeenCalled(); + expect(questionService.generatePreSignedUrl).toHaveBeenCalled(); + expect(component.uploadToBlob).toHaveBeenCalled(); + }); + it('should handle a valid file upload', () => { + // Prepare a mock event + const event = { + target: { + files: [new File(['test-content'], 'filename.mp3', { type: 'video' })] + } + } as any; + + component.assetType = 'video'; // Replace with your asset type + component.assetConfig = { + video: { + size: 50, + sizeType: 'MB' + } + }; + + component.uploadAsset(event); + + expect(component.assetFile).toBeTruthy(); + expect(component.assetName).toBe('filename.mp3'); + }); + + it('should handle an invalid file type', () => { + const event = { + target: { + files: [new File(['test-content'], 'test-file.exe', { type: 'txt' })] + } + } as any; + component.assetType = 'video'; + component.assetConfig = { + video: { + size: 50, + sizeType: 'MB' + } + }; + + component.uploadAsset(event); + + expect(component.showErrorMsg).toBe(true); + }); + it('#uploadToBlob() should upload blob on API success', () => { + let signedURL = '/test'; + let file = new File([], 'fileName'); + let config = {}; + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService.http, 'put').and.returnValue(of({"responseCode": "OK"})); + component.uploadToBlob(signedURL, file, config).subscribe(data => { + expect(data.responseCode).toEqual('OK'); + }) + }); + + it('should upload to blob and return data', () => { + const signedURL = 'mockedSignedURL'; + const file = 'mockedFile'; + const config = {}; + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService.http, 'put').and.returnValue(of({ mockData: 'data' })); + + component.uploadToBlob(signedURL, file, config).subscribe((data) => { + expect(data).toEqual({ mockData: 'data' }); // Assert the expected response + }); + }); + + it('should handle API error and throw an error', () => { + const signedURL = 'mockedSignedURL'; + const file = 'mockedFile'; + const config = {}; + let questionService: QuestionService = TestBed.inject(QuestionService); + spyOn(questionService.http, 'put').and.returnValue(throwError({ errorMessage: 'API error' })); + + component.uploadToBlob(signedURL, file, config).subscribe( + () => { + // This should not be called since it's an error case + fail('Expected error to be thrown'); + }, + (error) => { + spyOn(editorService, "apiErrorHandling").and.callFake(() => {}); + } + ); + }); + + it('#dismissAssetUploadModal() should set showAssetPicker to true', () => { + component.showAssetPicker = true; + spyOn(component, 'dismissAssetUploadModal').and.callThrough(); + component.dismissAssetUploadModal(); + expect(component.showAssetPicker).toBeTruthy(); + }); + + it('#dismissAssetUploadModal() should set showAssetUploadModal to false', () => { + spyOn(component, 'dismissAssetUploadModal').and.callThrough(); + component.dismissAssetUploadModal(); + expect(component.showAssetUploadModal).toBeFalsy(); + }); + + it('#initiateAssetUploadModal() should set showAssetUploadModal to false', () => { + spyOn(component, 'initiateAssetUploadModal').and.callThrough(); + component.initiateAssetUploadModal(); + expect(component.showAssetPicker).toBeFalsy(); + expect(component.showAssetUploadModal).toBeTruthy(); + expect(component.loading).toBeFalsy(); + expect(component.isClosable).toBeTruthy(); + }); + + it('#resetFormConfig() should reset form', () => { + spyOn(component, 'resetFormConfig').and.callThrough(); + component.resetFormConfig(); + expect(component.assetUploadLoader).toBeFalsy(); + expect(component.assetFormValid).toBeFalsy(); + component.formConfig = component.initialFormConfig; + }); + + it('#lazyloadMyAssets() should get my assets', () => { + spyOn(component, 'getMyAssets'); + component.lazyloadMyAssets(); + expect(component.getMyAssets).toHaveBeenCalledWith(0, undefined, true); + }); + it('#lazyloadAllAssets() should get all assets', () => { + spyOn(component, 'getAllAssets'); + component.lazyloadAllAssets(); + expect(component.getAllAssets).toHaveBeenCalledWith(0, undefined, true); + }); + + + + +it('#dismissAssetPicker() should emit modalDismissEmitter ', () => { + component.showAssetPicker = false; + component.assetShow = false; + spyOn(component, 'getMyAssets'); + spyOn(component.assetDataOutput, 'emit'); + component.dismissAssetPicker(); + expect(component.showAssetPicker).toBeFalsy(); + expect(component.assetDataOutput.emit).toHaveBeenCalledWith(false); +}); + +it('#ngOnDestroy() should call modal deny ', () => { + component['modal'] = { + deny: jasmine.createSpy('deny') + }; + component.ngOnDestroy(); + expect(component['modal'].deny).toHaveBeenCalled(); +}); +it('#searchAsset() should call getMyAssets for my videos', () => { + spyOn(component, 'getMyAssets'); + component.searchAsset('clearInput', 'myAssets'); + expect(component.query).toEqual(''); + expect(component.searchMyInput).toEqual(''); + expect(component.getMyAssets).toHaveBeenCalledWith(0, '', true); +}); + +it('#searchAsset() should call allVideos for all videos ', () => { + spyOn(component, 'getAllAssets'); + component.searchAsset('clearInput', 'allAssets'); + expect(component.query).toEqual(''); + expect(component.searchAllInput).toEqual(''); + expect(component.getAllAssets).toHaveBeenCalledWith(0, '', true); +}); + +it('#searchAsset() should call getMyAssets for my videos', () => { + // spyOn(component, 'getMyAssets'); + const event = { + target: { + value:"testing" + } + } + component.searchAsset(event, 'myAssets'); + expect(component.query).toEqual('testing'); + expect(component.searchMyInput).toEqual(''); + // expect(component.getMyAssets).toHaveBeenCalledWith(0, '', true); +}); +it('#ngOnInit() should call ngOnInit and define formConfig', () => { + component.assetType = "video"; + component.assetConfig = { + "video": { + "size": "50", + "sizeType": "MB", + "accepted": "mp4, webm" + } + } + component.ngOnInit(); + expect(component.formConfig).toBeDefined(); +}); +it('#onStatusChanges() should call onStatusChanges and assetUploadLoader is false', () => { + component.assetUploadLoader = true; + const data = { + controls: [], + isDirty: true, + isInvalid: true, + isPristine: false, + isValid: true + }; + component.onStatusChanges(data); + expect(component.assetFormValid).toBeTruthy(); +}); +it('#onStatusChanges() should call onStatusChanges and assetUploadLoader is true and is form valid false', () => { + component.assetUploadLoader = true; + const data = { + controls: [], + isDirty: true, + isInvalid: false, + isPristine: false, + isValid: false + }; + component.onStatusChanges(data); + expect(component.assetFormValid).toBeFalsy(); +}); +it('#onStatusChanges() should call onStatusChanges and assetUploadLoader is true and is form valid true', () => { + component.assetUploadLoader = true; + const data = { + controls: [], + isDirty: true, + isInvalid: false, + isPristine: false, + isValid: true + }; + component.onStatusChanges(data); + expect(component.assetFormValid).toBeTruthy(); +}); +it('#valueChanges() should define assetRequestBody ', () => { + component.assetUploadLoader = true; + component.assetData = mockData.formData; + const data = { + creator: 'Vaibahv Bhuva', + keywords: undefined, + name: 'logo' + }; + component.valueChanges(data); + expect(component.assetData).toBeDefined(); +}); +it('#openAssetUploadModal() should reset upload video form ', () => { + component.openAssetUploadModal(); + expect(component.assetUploadLoader).toBeFalsy(); + expect(component.assetFormValid).toBeFalsy(); + expect(component.showAssetUploadModal).toBeTruthy(); + expect(component.formData).toBeNull(); + +}); +it('#resetFormData() should reset form ', () => { + component.openAssetUploadModal(); + expect(component.assetUploadLoader).toBeFalsy(); + expect(component.assetFormValid).toBeFalsy(); + expect(component.showAssetUploadModal).toBeTruthy(); + expect(component.formData).toBeNull(); + expect(component.isClosable).toBeTruthy(); +}); +it('#dismissPops() should close both pops ', () => { + spyOn(component, 'dismissAssetPicker'); + const modal = { + deny: jasmine.createSpy('deny') + }; + component.dismissPops(modal); + expect(component.dismissAssetPicker).toHaveBeenCalled(); + expect(modal.deny).toHaveBeenCalled(); +}); + +}); \ No newline at end of file diff --git a/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.ts b/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.ts new file mode 100644 index 000000000..6a7a42817 --- /dev/null +++ b/projects/questionset-editor-library/src/lib/components/assets-browser/assets-browser.component.ts @@ -0,0 +1,451 @@ +import { Component, OnInit, Output, Input, EventEmitter, OnChanges, ViewChild, ElementRef, OnDestroy } from '@angular/core'; +import * as _ from 'lodash-es'; +import { catchError, map } from 'rxjs/operators'; +import { throwError, Observable } from 'rxjs'; +import { EditorService } from '../../services/editor/editor.service'; +import { ConfigService } from '../../services/config/config.service'; +import { QuestionService } from '../../services/question/question.service'; +import { config } from '../asset-browser/asset-browser.data'; +import { ToasterService } from '../../services/toaster/toaster.service'; +@Component({ + selector: 'lib-assets-browser', + templateUrl: './assets-browser.component.html', + styleUrls: ['./assets-browser.component.scss'] +}) +export class AssetsBrowserComponent implements OnInit, OnChanges, OnDestroy { + @ViewChild('editor') public editorRef: ElementRef; + @Output() assetDataOutput = new EventEmitter(); + @Input() assetShow; + @Input() assetType; + @Input() showAssetPicker; + @ViewChild('modal') private modal; + myAssets = []; + allAssets = []; + selectedAsset = {}; + loading = false; + isClosable = true; + assetConfig: any = {}; + acceptAssetType: any; + initialized = false; + selectedAssetId: string; + showAddButton: boolean; + showErrorMsg: boolean; + errorMsg: string; + query: string; + showAssetUploadModal: boolean; + assetFormValid = false; + selectast: string; + myast: string; + allast: string; + emptySearchMessage: string; + chooseOrDragAst: string; + astSize: string; + astSizeType: string; + acceptedFileType: string; + url: any; + fileType: any; + public assetData = {}; + public assetFile: any; + public formData: any; + public mediaobj; + public assetsCount: any; + public editorInstance: any; + public editorConfig: any; + public isAssetBrowserReadOnly = false; + public assetProxyUrl: any; + public initialFormConfig: any; + public formConfig: any; + public termsAndCondition: any; + public assetName: any; + public searchMyInput = ''; + public searchAllInput: any; + public assetUploadLoader = false; + constructor(private editorService: EditorService, public configService: ConfigService, + private questionService: QuestionService, public toasterService: ToasterService) { } + + ngOnInit() { + this.assetProxyUrl = _.get(this.editorService.editorConfig, 'config.assetProxyUrl') || _.get(this.configService.urlConFig, 'URLS.assetProxyUrl'); + this.initialFormConfig = _.get(config, 'uploadIconFormConfig'); + this.formConfig = _.get(config, 'uploadIconFormConfig'); + this.termsAndCondition = _.get(this.configService.labelConfig, 'termsAndConditions.001'); + this.assetConfig = this.editorService.editorConfig.config.assetConfig; + this.initialized = true; + this.selectast = this.configService.labelConfig?.lbl?.selectAsset[this.assetType]; + this.emptySearchMessage = _.get(this.configService.labelConfig?.emptySearchMessage[this.assetType]); + this.myast = this.configService.labelConfig?.lbl?.myAssets[this.assetType]; + this.allast = this.configService.labelConfig?.lbl?.allAssets[this.assetType]; + this.chooseOrDragAst = this.configService.labelConfig?.lbl?.chooseOrDragAsset[this.assetType]; + this.astSize = this.assetConfig[this.assetType].size; + this.astSizeType = this.assetConfig[this.assetType].sizeType; + this.acceptedFileType = this.assetConfig[this.assetType].accepted; + this.acceptAssetType = this.getAcceptType(this.assetConfig[this.assetType].accepted, this.assetType); + } + + ngOnChanges() { + if (this.assetShow) { + this.showAssetPicker = true; + } + } + + initializeAssetPicker() { + this.showAssetPicker = true; + } + + outputEventHandler(event) { + console.log(JSON.stringify(event)); + } + + getAcceptType(typeList, type) { + const acceptTypeList = typeList.split(', '); + const result = []; + _.forEach(acceptTypeList, (content) => { + result.push(`${type}/${content}`); + }); + return result.toString(); + } + + dismissAssetPicker() { + this.showAssetPicker = false; + this.assetShow=false; + this.showAssetPicker = false; + this.assetDataOutput.emit(false); + } + + + getMyAssets(offset, query?, search?) { + this.assetsCount = 0; + if (!search) { + this.searchMyInput = ''; + } + if (offset === 0) { + this.myAssets.length = 0; + } + let req; + req = { + filters: { + mediaType: [this.assetType], + createdBy: _.get(this.editorService.editorConfig, 'context.user.id') + }, + offset + }; + if (query) { + req.query = query; + } + this.questionService.getAssetMedia(req).pipe(catchError(err => { + let errInfo; + errInfo = _.get(this.configService.labelConfig?.assetSearchFailed[this.assetType]); + return throwError(this.editorService.apiErrorHandling(err, errInfo)); + })).subscribe((res) => { + this.assetsCount = res.result.count; + _.map(res.result.content, (item) => { + if (item.downloadUrl) { + this.myAssets.push(item); + } + }); + }); + } + + getAllAssets(offset, query?, search?) { + this.assetsCount = 0; + if (!search) { + this.searchAllInput = ''; + } + if (offset === 0) { + this.allAssets.length = 0; + } + let req; + req = { + filters: { + mediaType: [this.assetType] + }, + offset + }; + if (query) { + req.query = query; + } + this.questionService.getAssetMedia(req).pipe(catchError(err => { + let errInfo; + errInfo = _.get(this.configService.labelConfig?.assetSearchFailed[this.assetType]); + return throwError(this.editorService.apiErrorHandling(err, errInfo)); + })).subscribe((res) => { + this.assetsCount = res.result.count; + _.map(res.result.content, (item) => { + if (item.downloadUrl) { + this.allAssets.push(item); + } + }) + }); + } + + /** + * function to lazy load my assets + */ + lazyloadMyAssets() { + const offset = this.myAssets.length; + this.getMyAssets(offset, this.query, true); + } + + // search feature for images + searchAsset(event, type) { + if (event === 'clearInput' && type === 'myAssets') { + this.query = ''; + this.searchMyInput = ''; + } else if (event === 'clearInput' && type === 'allAssets') { + this.query = ''; + this.searchAllInput = ''; + } else { + this.query = event.target.value; + } + if (type === 'myAssets') { + this.getMyAssets(0, this.query, true); + } else { + this.getAllAssets(0, this.query, true); + } + } + + addAssetInEditor(videoModal?, assetUrl?, assetId?, assetName?) { + const assetData: any = _.cloneDeep(this.selectedAsset); + if(this.url!=undefined){ + assetData.downloadUrl = this.url; + } + assetData.src = this.getMediaOriginURL(assetData.downloadUrl); + assetData.thumbnail = (assetData.thumbnail) && this.getMediaOriginURL(assetData.thumbnail); + this.showAssetPicker = false; + this.assetDataOutput.emit(assetData); + if (videoModal) { + videoModal.deny(); + } + } + + getMediaOriginURL(src) { + const replaceText = this.assetProxyUrl; + const cloudStorageUrls = _.compact(_.get(this.editorService.editorConfig, 'context.cloudStorageUrls') || []); + _.forEach(cloudStorageUrls, url => { + if (src.indexOf(url) !== -1) { + src = src.replace(url, replaceText); + } + }); + return src; + } + + /** + * function to lazy load all images + */ + lazyloadAllAssets() { + const offset = this.allAssets.length; + this.getAllAssets(offset, this.query, true); + } + + openAssetUploadModal() { + this.showAssetUploadModal = true; + this.resetFormData(); + } + + resetFormData() { + this.showErrorMsg = false; + this.formData = null; + this.formConfig = this.initialFormConfig; + this.assetUploadLoader = false; + this.assetFormValid = false; + this.loading = false; + this.isClosable = true; + } + + dismissAssetUploadModal() { + if (this.isClosable) { + this.showAssetUploadModal = false; + } + } + + initiateAssetUploadModal() { + this.showAssetPicker = false; + this.showAssetUploadModal = true; + this.loading = false; + this.isClosable = true; + } + + uploadAsset(event) { + this.assetFile = event.target.files[0]; + this.assetName = this.assetFile.name; + const reader = new FileReader(); + this.formData = new FormData(); + this.formData.append('file', this.assetFile); + const fileType = this.assetFile.type; + const fileName = this.assetFile.name.split('.').slice(0, -1).join('.'); + const fileSize = this.assetFile.size / 1024 / 1024; + if (fileType.split('/')[0] === this.assetType) { + this.showErrorMsg = false; + if (fileSize > this.assetConfig[this.assetType].size) { + this.showErrorMsg = true; + this.errorMsg = _.get(this.configService.labelConfig, 'messages.error.021') + + this.assetConfig[this.assetType].size + this.assetConfig[this.assetType].sizeType; + this.resetFormConfig(); + } else { + this.errorMsg = ''; + this.showErrorMsg = false; + reader.readAsDataURL(this.assetFile); + } + } else { + this.showErrorMsg = true; + this.errorMsg = _.get(this.configService.labelConfig?.chooseFileMsg[this.assetType]); + } + if (!this.showErrorMsg) { + this.assetUploadLoader = true; + this.assetFormValid = true; + this.assetData = this.generateAssetCreateRequest(fileName, fileType, this.assetType); + this.populateFormData(this.assetData); + } + } + + generateAssetCreateRequest(fileName, fileType, mediaType) { + return { + name: fileName, + mediaType, + mimeType: fileType, + createdBy: _.get(this.editorService.editorConfig, 'context.user.id'), + creator: _.get(this.editorService.editorConfig, 'context.user.fullName'), + channel: _.get(this.editorService.editorConfig, 'context.channel') + }; + } + + populateFormData(formData) { + const formvalue = _.cloneDeep(this.formConfig); + this.formConfig = null; + _.forEach(formvalue, (formFieldCategory) => { + formFieldCategory.default = formData[formFieldCategory.code]; + }); + this.formConfig = formvalue; + } + + resetFormConfig() { + this.assetUploadLoader = false; + this.assetFormValid = false; + this.formConfig = this.initialFormConfig; + } + + onStatusChanges(event) { + if (event.isValid && this.assetUploadLoader) { + this.assetFormValid = true; + } else { + this.assetFormValid = false; + } + } + + valueChanges(event) { + this.assetData = _.merge({}, this.assetData, event); + } + + dismissPops(modal) { + this.dismissAssetPicker(); + modal.deny(); + } + + uploadAndUseAsset(modal) { + this.isClosable = false; + this.loading = true; + this.showErrorMsg = false; + this.assetFormValid = false; + if (!this.showErrorMsg) { + this.questionService.createMediaAsset({ asset: this.assetData }).pipe(catchError(err => { + this.loading = false; + this.isClosable = true; + this.assetFormValid = true; + let errInfo; + errInfo = { errorMsg: _.get(this.configService.labelConfig?.chooseFileMsg[this.assetType]) }; + return throwError(this.editorService.apiErrorHandling(err, errInfo)); + })).subscribe((res) => { + const contentId = res.result.node_id; + const request = { + content: { + fileName: this.assetName + } + }; + this.questionService.generatePreSignedUrl(request, contentId).pipe(catchError(err => { + let errInfo; + errInfo = { errorMsg: _.get(this.configService.labelConfig, 'messages.error.026') }; + this.loading = false; + this.isClosable = true; + this.assetFormValid = true; + return throwError(this.editorService.apiErrorHandling(err, errInfo)); + })).subscribe((response) => { + const signedURL = response.result.pre_signed_url; + let blobConfig = { + processData: false, + contentType: 'Asset' + }; + blobConfig = this.editorService.appendCloudStorageHeaders(blobConfig); + this.uploadToBlob(signedURL, this.assetFile, blobConfig).subscribe(() => { + const fileURL = signedURL.split('?')[0]; + let fileType; + if (this.assetFile!==undefined) { + fileType = this.assetFile.type; + } else { + fileType = this.fileType; + } + this.updateContentWithURL(fileURL, fileType, contentId, modal); + }) + }) + }) + } + } + + updateContentWithURL(fileURL, mimeType, contentId, modal?) { + const data = new FormData(); + data.append('fileUrl', fileURL); + data.append('mimeType', mimeType); + const conf = { + enctype: 'multipart/form-data', + processData: false, + contentType: false, + cache: false + }; + const option = { + data, + param: conf + }; + this.questionService.uploadMedia(option, contentId).pipe(catchError(err => { + const errInfo = { errorMsg: _.get(this.configService.labelConfig, 'messages.error.027') }; + this.isClosable = true; + this.loading = false; + this.assetFormValid = true; + return throwError(this.editorService.apiErrorHandling(err, errInfo)); + })).subscribe(res => { + // Read upload asset data + this.getUploadAsset(res.result.node_id, modal); + }); + } + + getUploadAsset(assetId, modal?) { + this.questionService.getVideo(assetId).pipe(map((data: any) => data.result.content), catchError(err => { + const errInfo = { errorMsg: _.get(this.configService, 'labelConfig.messages.error.011') }; + this.loading = false; + this.isClosable = true; + this.assetFormValid = true; + return throwError(this.editorService.apiErrorHandling(err, errInfo)); + })).subscribe(res => { + this.toasterService.success(_.get(this.configService, 'labelConfig.messages.success.006')); + this.selectedAsset = res; + this.showAddButton = true; + this.loading = false; + this.isClosable = true; + this.assetFormValid = true; + this.addAssetInEditor(modal); + }); + } + + uploadToBlob(signedURL, file, config): Observable { + return this.questionService.http.put(signedURL, file, config).pipe(catchError(err => { + const errInfo = { errorMsg: _.get(this.configService.labelConfig, 'messages.error.018') }; + this.isClosable = true; + this.loading = false; + this.assetFormValid = true; + return throwError(this.editorService.apiErrorHandling(err, errInfo)); + }), map(data => data)); + } + + ngOnDestroy() { + if (this.modal?.deny) { + this.modal.deny(); + } + } +} \ No newline at end of file diff --git a/projects/questionset-editor-library/src/lib/components/collection-icon/collection-icon.component.ts b/projects/questionset-editor-library/src/lib/components/collection-icon/collection-icon.component.ts index b34da1cec..28fa7cb20 100644 --- a/projects/questionset-editor-library/src/lib/components/collection-icon/collection-icon.component.ts +++ b/projects/questionset-editor-library/src/lib/components/collection-icon/collection-icon.component.ts @@ -11,7 +11,6 @@ export class CollectionIconComponent { @Input() appIconConfig; @Output() iconEmitter = new EventEmitter(); public showImagePicker = false; - constructor(public configService: ConfigService) { } initializeImagePicker() { diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.html b/projects/questionset-editor-library/src/lib/components/question/question.component.html index 1d9d78527..7f789fd0f 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.html +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.html @@ -34,10 +34,15 @@
+ *ngIf="!isReadOnlyMode" + (editorDataOutput)="editorDataHandler($event, 'question')" + [editorDataInput]="editorState.question" + > +
@@ -96,6 +101,10 @@ [telemetryInteractEdata]="telemetryService.getTelemetryInteractEdata('solution_type','select','single_select', telemetryService.telemetryPageId, {solution_type:'video'})"> {{configService.labelConfig?.lbl?.video}}
+
+ {{configService.labelConfig?.lbl?.audio}} +
@@ -113,31 +122,30 @@ -
+
+ libTelemetryInteract + [telemetryInteractEdata]="telemetryService.getTelemetryInteractEdata('delete_solution','click','cancel', this.telemetryService.telemetryPageId, {solution_type: selectedSolutionType})"> + +
- -
-
-
- {{ videoSolutionName - }} + assetThumbnail + ? { background: 'url(' + assetThumbnail + ')' } + : { background: 'rgba(0,0,0,0.3)' } + "> +
+
+ {{ assetSolutionName }} +
+
@@ -149,13 +157,13 @@

-
+
-
+
- {{videoSolutionName}} + {{assetSolutionName}}
diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.data.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.data.ts index f5529c520..0f2cf8f62 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.data.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.data.ts @@ -3764,6 +3764,24 @@ export const videoSolutionObject= { "value": "do_2137972441518325761398" }; +export const audioSolutionObject= { + "id": "4772d9da-569f-46bb-a8b1-9faf742d0640", + "type": "audio", + "value": "do_2137972441518325761398" +}; + +export const mediaAudioArray = [ + { + "id": "do_2137972441518325761398", + "src": "/assets/public/content/assets/do_2137972441518325761398/earth.mp3", + "type": "audio", + "assetId": "do_2137972441518325761398", + "name": "earth", + "baseUrl": "https://dev.inquiry.sunbird.org", + "thubmnail": "/assets/public/content/assets/do_21379724415183257613675/earth.png" + } +]; + export const mediaVideoArray = [ { "id": "do_2137972441518325761398", diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts index a102e1fc2..75881dbf3 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts @@ -28,7 +28,9 @@ import { interactionMatchEditorState, RubricData, videoSolutionObject, - mediaVideoArray + mediaVideoArray, + audioSolutionObject, + mediaAudioArray } from "./question.component.spec.data"; import { of, throwError } from "rxjs"; import * as urlConfig from "../../services/config/url.config.json"; @@ -802,13 +804,24 @@ describe("QuestionComponent", () => { it('#getQuestionSolution() should return video solution', () => { component.mediaArr = mediaVideoArray; + component.selectedSolutionType = "video"; spyOn(component, 'getQuestionSolution').and.callThrough(); spyOn(component, 'getMediaById').and.callThrough(); - spyOn(component, 'getVideoSolutionHtml').and.callThrough(); + spyOn(component, 'getAssetSolutionHtml').and.callThrough(); const solution = component.getQuestionSolution(videoSolutionObject); expect(solution).toBeDefined(); }) + it('#getQuestionSolution() should return audio solution', () => { + component.mediaArr = mediaAudioArray; + component.selectedSolutionType = "audio"; + spyOn(component, 'getQuestionSolution').and.callThrough(); + spyOn(component, 'getMediaById').and.callThrough(); + spyOn(component, 'getAssetSolutionHtml').and.callThrough(); + const solution = component.getQuestionSolution(audioSolutionObject); + expect(solution).toBeDefined(); + }) + it('#getQuestionSolution() should return html solution', () => { const solutionObject = { "id": "4772d9da-569f-46bb-a8b1-9faf742d0640", @@ -827,10 +840,25 @@ describe("QuestionComponent", () => { expect(mediaobj).toBeDefined(); }); - it('#getVideoSolutionHtml() should return videoSolutionHtml', () => { - spyOn(component, 'getVideoSolutionHtml').and.callThrough(); - const videoSolutionHtml = component.getVideoSolutionHtml(mediaVideoArray[0].thubmnail, mediaVideoArray[0].src, mediaVideoArray[0].id); - expect(videoSolutionHtml).toBeDefined(); + it('#getMediaById() should return audio object', () => { + component.mediaArr = mediaAudioArray; + spyOn(component, 'getMediaById').and.callThrough(); + const mediaobj = component.getMediaById(mediaAudioArray[0].id); + expect(mediaobj).toBeDefined(); + }); + + it('#getAssetSolutionHtml() should return assetSolutionHtml', () => { + spyOn(component, 'getAssetSolutionHtml').and.callThrough(); + component.selectedSolutionType = "video"; + const assetSolutionHtml = component.getAssetSolutionHtml(mediaVideoArray[0].thubmnail, mediaVideoArray[0].src, mediaVideoArray[0].id); + expect(assetSolutionHtml).toBeDefined(); + }); + + it('#getAssetSolutionHtml() should return assetSolutionHtml', () => { + spyOn(component, 'getAssetSolutionHtml').and.callThrough(); + component.selectedSolutionType = "audio"; + const assetSolutionHtml = component.getAssetSolutionHtml(mediaAudioArray[0].thubmnail, mediaAudioArray[0].src, mediaAudioArray[0].id); + expect(assetSolutionHtml).toBeDefined(); }); it("call #getMcqQuestionHtmlBody() to verify questionBody", () => { @@ -1467,6 +1495,12 @@ describe("QuestionComponent", () => { component.deleteSolution(); expect(component.mediaArr).toBeDefined(); }); + it("#deleteSolution() should call deleteSolution and define mediaArr for audio type", () => { + component.editorState = mockData.editorState; + component.selectedSolutionType = "audio"; + component.deleteSolution(); + expect(component.mediaArr).toBeDefined(); + }); it("#validateQuestionData() should call validateQuestionData and question is undefined", () => { component.editorState = mockData.editorState; component.editorState.question = undefined; @@ -1739,31 +1773,34 @@ describe("QuestionComponent", () => { component.addResourceToQuestionset(); }); - it("#videoDataOutput() should call videoDataOutput and event data is empty", () => { + it("#assetDataOutput() should call assetDataOutput and event data is empty", () => { const event = ""; spyOn(component, "deleteSolution"); - component.videoDataOutput(event); + component.assetDataOutput(event); expect(component.deleteSolution).toHaveBeenCalled(); }); - it("#videoDataOutput() should call videoDataOutput and event data is not empty", () => { + it("#assetDataOutput() should call assetDataOutput and event data is not empty", () => { const event = { name: "event name", identifier: "1234" }; - component.videoDataOutput(event); + component.assetDataOutput(event); + expect(component.assetSolutionData).toBeDefined(); }); - it("#videoDataOutput() should call videoDataOutput for thumbnail", () => { + it("#assetDataOutput() should call assetDataOutput for thumbnail", () => { const event = { name: "event name", identifier: "1234", thumbnail: "sample data", }; - component.videoDataOutput(event); + component.assetDataOutput(event); + expect(component.assetSolutionData).toBeDefined(); }); - it("#videoDataOutput() should call videoDataOutput for thumbnail", () => { + it("#assetDataOutput() should call assetDataOutput for thumbnail", () => { const event = { name: "event name", identifier: "1234", thumbnail: "sample data", }; - component.videoDataOutput(event); + component.assetDataOutput(event); + expect(component.assetSolutionData).toBeDefined(); }); it("#subMenuChange() should set the sub-menu value ", () => { spyOn(component,'subMenuChange').and.callThrough(); diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.ts index 52114917b..f51adc709 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.ts @@ -37,17 +37,21 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { @Output() questionEmitter = new EventEmitter(); private onComponentDestroy$ = new Subject(); toolbarConfig: any = {}; + public showAssetPicker = false; + public terms = false; public editorState: any = {}; public showPreview = false; public mediaArr: any = []; - public videoShow = false; + public assetShow = false; public showFormError = false; public actionType: string; + assetType: string; selectedSolutionType: string; showSolutionDropDown = true; showSolution = false; - videoSolutionName: string; - videoThumbnail: string; + assetSolutionName: string; + assetSolutionData: any; + assetThumbnail: string; solutionUUID: string; solutionTypes: any = [{ type: 'html', @@ -56,6 +60,10 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { { type: 'video', value: 'video' + }, + { + type: 'audio', + value: 'audio' }]; questionMetaData: any; questionInteractionType; @@ -270,13 +278,13 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { this.solutionUUID = this.editorState.solutions[0].id; this.showSolutionDropDown = false; this.showSolution = true; - if (this.selectedSolutionType === 'video') { + if (this.selectedSolutionType === 'video' || this.selectedSolutionType === 'audio') { const index = _.findIndex(this.questionMetaData.media, (o) => { - return o.type === 'video' && o.id === this.editorState.solutions[0].value; + return o.type === this.selectedSolutionType && o.id === this.editorState.solutions[0].value; }); - this.videoSolutionName = this.questionMetaData.media[index].name; - this.videoThumbnail = this.questionMetaData.media[index].thumbnail; - } + this.assetSolutionName = this.questionMetaData.media[index].name; + this.assetThumbnail = this.questionMetaData.media[index].thumbnail; + } if (this.selectedSolutionType === 'html') { this.editorState.solutions = this.editorState.solutions[0].value; } @@ -733,58 +741,62 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } } - videoDataOutput(event) { + assetDataOutput(event) { if (event) { - this.videoSolutionName = event.name; + this.assetSolutionData = event; + this.assetSolutionName = event.name; this.editorState.solutions = event.identifier; - this.videoThumbnail = event.thumbnail; - const videoMedia: any = {}; - videoMedia.id = event.identifier; - videoMedia.src = event.src; - videoMedia.type = 'video'; - videoMedia.assetId = event.identifier; - videoMedia.name = event.name; - videoMedia.thumbnail = this.videoThumbnail; - videoMedia.baseUrl = _.get(this.editorService.editorConfig, 'context.host') || document.location.origin; - if (videoMedia.thumbnail) { + this.assetThumbnail = event.thumbnail; + const assetMedia: any = {}; + assetMedia.id = event.identifier; + assetMedia.src = event.src; + assetMedia.type = this.assetType; + assetMedia.assetId = event.identifier; + assetMedia.name = event.name; + assetMedia.thumbnail = this.assetThumbnail; + assetMedia.baseUrl = _.get(this.editorService.editorConfig, 'context.host') || document.location.origin; + if (assetMedia.thumbnail) { const thumbnailMedia: any = {}; - thumbnailMedia.src = this.videoThumbnail; + thumbnailMedia.src = this.assetThumbnail; thumbnailMedia.type = 'image'; - thumbnailMedia.id = `video_${event.identifier}`; + thumbnailMedia.id = `${this.assetType}_${event.identifier}`; thumbnailMedia.baseUrl = _.get(this.editorService.editorConfig, 'context.host') || document.location.origin; this.mediaArr.push(thumbnailMedia); } - this.mediaArr.push(videoMedia); + this.mediaArr.push(assetMedia); this.showSolutionDropDown = false; this.showSolution = true; } else { this.deleteSolution(); } - this.videoShow = false; + this.assetShow = false; } selectSolutionType(data: any) { const index = _.findIndex(this.solutionTypes, (sol: any) => { return sol.value === data; }); + this.assetType = data; this.selectedSolutionType = this.solutionTypes[index].type; - if (this.selectedSolutionType === 'video') { - const showVideo = true; - this.videoShow = showVideo; - } else { + if (this.selectedSolutionType === 'video' || this.selectedSolutionType === 'audio') { + const showAsset = true; + this.assetShow = showAsset; + } + else { this.showSolutionDropDown = false; } } deleteSolution() { - if (this.selectedSolutionType === 'video') { + if (this.selectedSolutionType === 'video' || this.selectedSolutionType === 'audio') { this.mediaArr = _.filter(this.mediaArr, (item: any) => item.id !== this.editorState.solutions); - } + } this.showSolutionDropDown = true; this.selectedSolutionType = ''; - this.videoSolutionName = ''; + this.assetType = ''; + this.assetSolutionName = ''; this.editorState.solutions = ''; - this.videoThumbnail = ''; + this.assetThumbnail = ''; this.showSolution = false; } @@ -928,12 +940,12 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { getQuestionSolution(solutionObj) { if (solutionObj?.type === 'html') { return {[solutionObj?.id]: solutionObj.value}; - } else if (solutionObj?.type === 'video') { - const videoMedia = this.getMediaById(solutionObj?.value); - const videoThumbnail = videoMedia?.thumbnail ? videoMedia?.thumbnail : ''; - const videoSolution = this.getVideoSolutionHtml(videoThumbnail, videoMedia?.src, videoMedia.id); - return {[solutionObj.id]: videoSolution}; - } + } else if (solutionObj?.type === 'video' || solutionObj?.type === 'audio') { + const assetMedia = this.getMediaById(solutionObj?.value); + const assetThumbnail = assetMedia?.thumbnail ? assetMedia?.thumbnail : ''; + const assetSolution = this.getAssetSolutionHtml(assetThumbnail, assetMedia?.src, assetMedia.id); + return {[solutionObj.id]: assetSolution}; + } } getMediaById(mediaId) { @@ -949,10 +961,15 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { return responseDeclaration; } - getVideoSolutionHtml(posterURL, srcUrl, solutionMediaId) { - const videoSolutionHtml = '' - const videoSolutionValue = videoSolutionHtml.replace('{posterUrl}', posterURL).replace('{sourceURL}', srcUrl).replace('{sourceURL}', srcUrl).replace('{solutionMediaId}', solutionMediaId); - return videoSolutionValue; + getAssetSolutionHtml(posterURL, srcUrl, solutionMediaId) { + let assetSolutionHtml + if (this.selectedSolutionType === 'video') { + assetSolutionHtml = '' + } else if(this.selectedSolutionType === 'audio') { + assetSolutionHtml = '' + } + const assetSolutionValue = assetSolutionHtml.replace('{posterUrl}', posterURL).replace('{sourceURL}', srcUrl).replace('{sourceURL}', srcUrl).replace('{solutionMediaId}', solutionMediaId); + return assetSolutionValue; } getMtfQuestionHtmlBody(question, templateId) { @@ -1338,7 +1355,6 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } onStatusChanges(event) { - console.log(event); if (_.has(event, 'isValid')) { this.questionMetadataFormStatus = event.isValid; } diff --git a/projects/questionset-editor-library/src/lib/questionset-editor-library.module.ts b/projects/questionset-editor-library/src/lib/questionset-editor-library.module.ts index b5e69138e..043562902 100644 --- a/projects/questionset-editor-library/src/lib/questionset-editor-library.module.ts +++ b/projects/questionset-editor-library/src/lib/questionset-editor-library.module.ts @@ -38,6 +38,7 @@ import {TermAndConditionComponent} from './components/term-and-condition/term-an import { MatchComponent } from './components/match/match.component'; import { QualityParamsModalComponent } from './components/quality-params-modal/quality-params-modal.component'; +import { AssetsBrowserComponent } from './components/assets-browser/assets-browser.component'; @NgModule({ declarations: [ QuestionsetEditorLibraryComponent, @@ -68,6 +69,7 @@ import { QualityParamsModalComponent } from './components/quality-params-modal/q ProgressStatusComponent, TermAndConditionComponent, QualityParamsModalComponent, + AssetsBrowserComponent, MatchComponent ], imports: [CommonModule, FormsModule, ReactiveFormsModule.withConfig({callSetDisabledState: 'whenDisabledForLegacyCode'}), RouterModule.forChild([]), SuiModule, diff --git a/projects/questionset-editor-library/src/lib/services/config/editor.config.json b/projects/questionset-editor-library/src/lib/services/config/editor.config.json index ca85ae50f..333399224 100644 --- a/projects/questionset-editor-library/src/lib/services/config/editor.config.json +++ b/projects/questionset-editor-library/src/lib/services/config/editor.config.json @@ -14,6 +14,11 @@ "size": "50", "sizeType": "MB", "accepted": "mp4, webm" + }, + "audio": { + "size": "50", + "sizeType": "MB", + "accepted": "mp3, wav" } }, "questionPrimaryCategories": ["Multiple Choice Question", "Subjective Question", "Match The Following Question"], diff --git a/projects/questionset-editor-library/src/lib/services/config/label.config.json b/projects/questionset-editor-library/src/lib/services/config/label.config.json index 7b1ea5962..e3682af77 100644 --- a/projects/questionset-editor-library/src/lib/services/config/label.config.json +++ b/projects/questionset-editor-library/src/lib/services/config/label.config.json @@ -53,7 +53,7 @@ "answers":"Answers", "answersRequired":"Answer is required", "answersPopupText":"Please provide an answer for the question. Check preview to understand how it would look.", - "selectImage":"Select Image", + "selectImage":"Select Image", "myImages":"My Images", "allImage":"All Image", "uploadAndUse":"Upload and Use", @@ -67,9 +67,32 @@ "copyRightsAndLicense":"Copyright & License", "dropChooseFile":"Drop or choose file to upload before entering the details", "charactersLeft":"Characters left:", + "myAssets": { + "image": "My Images", + "video": "My Video(s)", + "audio": "My Audio" + }, + "allAssets": { + "image": "All Image", + "video": "All Video(s)", + "audio": "All Audio" + }, + "selectAsset": { + "image":"Select Image", + "video":"Select Video", + "audio":"Select Audio" + }, + "chooseOrDragAsset": { + "image": "Choose or drag and drop your image here", + "video": "Choose or drag and drop your video here", + "audio": "Choose or drag and drop your audio here" + }, "myVideos":"My Video(s)", "allVideos":"All Video(s)", "selectVideo":"Select Video", + "myAudios": "My Audio(s)", + "allAudios":"All Audio(s)", + "selectAudio":"Select Audio", "searchPlaceholder":"Search...", "addAnImage":"Add an image", "confirmDeleteContent":"Confirm Delete Content", @@ -101,6 +124,7 @@ "pageNumber":"Page No", "confirmQuestionNotSaved":"This question will not be saved, are you sure you want to go back to questionset?", "video":"Video", + "audio":"Audio", "textImage":"Text+Image", "chooseType":"Choose type", "solution":"Solution", @@ -145,6 +169,26 @@ "termsAndConditions": { "001": "I understand and confirm that all resources and assets created through the content editor or uploaded on the platform shall be available for free and public use without limitations on the platform (web portal, applications and any other end user interface that the platform would enable) and will be licensed under terms & conditions and policy guidelines of the platform. In doing so, the copyright and license of the original author is not infringed." }, + "emptySearchMessage": { + "image": "No images found, please try searching for something else", + "video": "No videos found, please try searching for something else", + "audio": "No audios found, please try searching for something else" + }, + "chooseFileMsg": { + "image": "Please choose an image file", + "video": "Please choose a video file", + "audio": "Please choose a audio file" + }, + "assetSearchFailed": { + "image": "Image search failed", + "video": "Video search failed", + "audio": "Audio search falied" + }, + "assetUploadFailed": { + "image": "Image upload failed", + "audio": "Video upload failed", + "video": "Audio upload failed" + }, "messages": { "error": { "001": "Something went wrong, Please try later",