diff --git a/src/app/client/src/app/modules/workspace/classes/workspace.ts b/src/app/client/src/app/modules/workspace/classes/workspace.ts index d990781164073336f93cebf35e63cdb33eb2fe8b..f71225006dae4a49773ba396c5e37c3386fef1b5 100644 --- a/src/app/client/src/app/modules/workspace/classes/workspace.ts +++ b/src/app/client/src/app/modules/workspace/classes/workspace.ts @@ -4,6 +4,7 @@ import { ResourceService, ServerResponse } from '@sunbird/shared'; import * as _ from 'lodash-es'; import { mergeMap, catchError, map } from 'rxjs/operators'; import { throwError as observableThrowError, of as observableOf, Observable } from 'rxjs'; +import {ContentIDParam } from '../../workspace/interfaces/delteparam'; /** * Base class for workspace module */ @@ -126,4 +127,17 @@ export class WorkSpace { return errorMessage; } + /** + * search collection Api call + */ + getLinkedCollections(contentId: ContentIDParam): any { + return this.workSpaceService.searchContent(contentId); + } + + /** + * get channel Api call + */ + getChannelDetails(channelId) { + return this.workSpaceService.getChannel(channelId); + } } diff --git a/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.html b/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.html index 8799fdf84cda5b05418bf4ec736d0fd389d4b522..5a588142fd1d9a5916271b5c5ba45a60b293fc76 100644 --- a/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.html +++ b/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.html @@ -48,7 +48,7 @@ <td class="UpForReviewStatusOrange">{{content.status}}</td> <td> <i *ngIf="!(content.pkgVersion && content.pkgVersion > 0 && (content.status === 'Review' || content.status === 'Draft'))" appTelemetryInteract [telemetryInteractObject]="{id:content.identifier,type:content.contentType,ver:content.pkgVersion ? content.pkgVersion.toString(): '1.0' }" - [telemetryInteractEdata]="{id:'deleteContent',type:'click',pageid:'AllContent'}" class="trash large icon" (click)="deleteConfirmModal(content.identifier); $event.stopPropagation()"> + [telemetryInteractEdata]="{id:'deleteContent',type:'click',pageid:'AllContent'}" class="trash large icon" (click)="deleteConfirmModal(content.identifier, content?.contentType); $event.stopPropagation()"> </i> </td> <td> @@ -86,32 +86,77 @@ <!--Content--> <div class="sb-modal-content"> - <p>{{resourceService?.frmelmnts?.lbl?.deleteconfirm}}</p> + <p [class.sb-color-gray-100]="showCollectionLoader">{{resourceService?.frmelmnts?.lbl?.deleteconfirm}}</p> + <div class="py-30"> + <div *ngIf="showCollectionLoader" class="sb-loader sb-gray-loader pb-20"></div> + </div> </div> <!--/Content--> <!--Actions--> - <div class="sb-modal-actions"> - <button - class="sb-btn sb-btn-normal sb-btn-error" - (click)="modal.approve('approved')" - autofocus - > - {{resourceService?.frmelmnts?.btn?.yes}} - </button> - <button - class="sb-btn sb-btn-normal sb-btn-outline-error" + <div class="sb-modal-actions flex-jc-space-between"> + <div> + <button + class="sb-btn sb-btn-normal sb-btn-outline-error mr-10" + [class.fadInButton]="showCollectionLoader" (click)="modal.deny('denied')" + [disabled]="showCollectionLoader" > {{resourceService?.frmelmnts?.btn?.no}} </button> + <button + class="sb-btn sb-btn-normal sb-btn-error ui disabled" + [class.fadInButton]="showCollectionLoader" + (click)="checkLinkedCollections(modal)" + autofocus + [disabled]="showCollectionLoader" + > + {{resourceService?.frmelmnts?.btn?.yes}} + </button> + + </div> + <div *ngIf="showCollectionLoader">Scanning for linked content</div> </div> <!--/Actions--> </div> </div> </ng-template> - - <app-contentlock-info-popup [content]="lockPopupData" (closeEvent)="onCloseLockInfoPopup($event)" *ngIf="showLockedContentModal"></app-contentlock-info-popup> - - \ No newline at end of file + <sui-modal *ngIf="collectionListModal" [mustScroll]="true" [isClosable]="true" [transitionDuration]="0" [size]="'large'" + class="sb-modal custom-lg-modal" (dismissed)="this.collectionListModal = false" #modal> + <div class="sb-modal-header"> + </div> + <div class="sb-modal-content"> + <h5 class="font-weight-bold sb-color-red">{{resourceService?.frmelmnts?.lbl?.linkedContentErrorMessage}}</h5> + <div class="sb-table-responsive"> + <table aria-hidden="true" class="sb-table sb-table-striped sb-table-sortable sb-table-blue-strip"> + <thead> + <th class="w-10" id="contentType">{{headers.type}}</th> + <th class="w-30" id="contentName">{{headers.name}}</th> + <th class="w-10" id="contentSubject">{{headers.subject}}</th> + <th class="w-10" id="contentGrade">{{headers.grade}}</th> + <th class="w-10" id="contentMedium">{{headers.medium}}</th> + <th class="w-10" id="contentBoard">{{headers.board}}</th> + <th class="w-20" id="contentchannel">{{headers.channel}}</th> + </thead> + <tbody> + <tr *ngFor = "let row of collectionData"> + <td class="white-space-inherit">{{row['contentType']}}</td> + <td class="white-space-inherit font-weight-bold">{{row['name']}}</td> + <td class="white-space-inherit">{{row['subject']?.join(", ") || '-'}}</td> + <td class="white-space-inherit">{{row['gradeLevel']?.join(", ") || '-'}}</td> + <td class="white-space-inherit">{{row['medium']?.join(", ") || '-'}}</td> + <td class="white-space-inherit">{{row['board']}}</td> + <td class="white-space-inherit">{{row['channel']}}</td> + </tr> + </tbody> + </table> + </div> + </div> + <div class="sb-modal-actions"> + <button class="sb-btn sb-btn-normal sb-btn-primary" (click)="modal.deny('denied')"> + {{resourceService?.frmelmnts?.btn?.close}} + </button> + </div> +</sui-modal> +<app-contentlock-info-popup [content]="lockPopupData" (closeEvent)="onCloseLockInfoPopup($event)" *ngIf="showLockedContentModal"></app-contentlock-info-popup> diff --git a/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.scss b/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..af6f2839f8db4edf503db5aa481d9b62ffd470f2 --- /dev/null +++ b/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.scss @@ -0,0 +1,61 @@ +.sb-gray-loader{ + &:after{ + content: ''; + width: 2rem; + height: 2rem; + border-radius: 50%; + border: 0.125rem solid; + border-color: var(--gray-100) var(--gray-100) transparent; + -webkit-animation: loader 0.5s infinite; + animation: loader 0.5s infinite; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} + +.sb-table-blue-strip{ + background: none; + border: none; + border-bottom: solid 1px var(--primary-400); + th{ + background: none; + color: var(--primary-400); + border-bottom: solid 1px var(--primary-400); + } + tbody{ + tr:nth-child(odd){ + background: var(--primary-100); + } + tr:nth-child(even){ + background: white; + } + tr{ + td:first-child{ + border-left: solid 1px var(--primary-400); + } + td:last-child{ + border-right: solid 1px var(--primary-400); + } + } + } +} + +.bt-0{ + border-top: none; +} + +.fadInButton{ + opacity:0.5 !important; +} + +.white-space-inherit { + white-space: inherit !important; + word-wrap: break-word; + max-width: 1px; +} + +.max-h-360 { + max-height:360px; +} \ No newline at end of file diff --git a/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.spec.data.ts b/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.spec.data.ts index 7750f511b6f3b07e95f3f8bb8301748a2e0cff90..4fc35af3cdc8005e1cf7cbcb90a484c1082d2ed7 100644 --- a/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.spec.data.ts +++ b/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.spec.data.ts @@ -1753,5 +1753,60 @@ export const Response = { }, 'responseCode': 'OK', 'result': [] - } + }, + searchedCollection: { + 'id': 'api.search-service.search', + 'ver': '3.0', + 'ts': '2020-09-07T13:44:30ZZ', + 'params': { + 'resmsgid': '15ce98a8-4b85-4b2a-bef8-57e0422d121c', + 'msgid': null, + 'err': null, + 'status': 'successful', + 'errmsg': null + }, + 'responseCode': 'OK', + 'result': { + 'count': 1, + 'content': [ + { + 'subject': [ + 'Mathematics' + ], + 'channel': '0124784842112040965', + 'gradeLevel': [ + 'Class 10' + ], + 'contentType': 'TextBook', + 'name': 'book1', + 'medium': [ + 'Hindi' + ], + 'board': 'State (Rajasthan)' + } + ] + } + }, + channelDetail: { + 'id': 'api.search-service.search', + 'ver': '3.0', + 'ts': '2020-09-07T13:44:30ZZ', + 'params': { + 'resmsgid': '15ce98a8-4b85-4b2a-bef8-57e0422d121c', + 'msgid': null, + 'err': null, + 'status': 'successful', + 'errmsg': null + }, + 'responseCode': 'OK', + 'result': { + 'count': 1, + 'channel': [ + { + 'code': '0124784842112040965', + 'name': 'Sunbird' + } + ] + } + } }; diff --git a/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.spec.ts b/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.spec.ts index e70a0bf13854dacf23d5e9a8418807c47745bc62..9fdfe383ab0cd549d41b7ab9ca9bfcd175b07d69 100644 --- a/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.spec.ts +++ b/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.spec.ts @@ -149,14 +149,14 @@ describe('AllContentComponent', () => { it('should call delete api and get success response', inject([SuiModalService, WorkSpaceService, ActivatedRoute], (modalService, workSpaceService, activatedRoute, http) => { spyOn(workSpaceService, 'deleteContent').and.callFake(() => observableOf(Response.deleteSuccess)); - spyOn(component, 'deleteConfirmModal').and.callThrough(); + spyOn(component, 'deleteContent').and.callThrough(); spyOn(modalService, 'open').and.callThrough(); spyOn(component, 'delete').and.callThrough(); const DeleteParam = { contentIds: ['do_2124645735080755201259'] }; - component.deleteConfirmModal('do_2124645735080755201259'); - expect(component.deleteConfirmModal).toHaveBeenCalledWith('do_2124645735080755201259'); + component.deleteContent('do_2124645735080755201259'); + expect(component.deleteContent).toHaveBeenCalledWith('do_2124645735080755201259'); workSpaceService.deleteContent(DeleteParam).subscribe( apiResponse => { expect(apiResponse.responseCode).toBe('OK'); @@ -164,4 +164,13 @@ describe('AllContentComponent', () => { } ); })); + it('should call search content and get channel and get success response', inject([SuiModalService, WorkSpaceService], + (modalService, workSpaceService) => { + spyOn(workSpaceService, 'searchContent').and.callFake(() => observableOf(Response.searchedCollection)); + spyOn(workSpaceService, 'getChannel').and.callFake(() => observableOf(Response.channelDetail)); + spyOn(component, 'checkLinkedCollections').and.callThrough(); + spyOn(modalService, 'open').and.callThrough(); + component.checkLinkedCollections(undefined); + expect(component.checkLinkedCollections).toHaveBeenCalledWith(undefined); + })); }); diff --git a/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.ts b/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.ts index d5fa5012e066b3562544c95da3c4a67d66ee6034..5791375f0e3c1d4efd398189965a273f51728bef 100644 --- a/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.ts +++ b/src/app/client/src/app/modules/workspace/components/all-content/all-content.component.ts @@ -1,5 +1,5 @@ -import {combineLatest as observableCombineLatest, Observable } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, forkJoin } from 'rxjs'; import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { WorkSpace } from '../../classes/workspace'; @@ -14,10 +14,12 @@ import * as _ from 'lodash-es'; import { IImpressionEventInput } from '@sunbird/telemetry'; import { SuiModalService, TemplateModalConfig, ModalTemplate } from 'ng2-semantic-ui'; import { debounceTime, map } from 'rxjs/operators'; +import { ContentIDParam } from '../../interfaces/delteparam'; @Component({ selector: 'app-all-content', - templateUrl: './all-content.component.html' + templateUrl: './all-content.component.html', + styleUrls: ['./all-content.component.scss'] }) export class AllContentComponent extends WorkSpace implements OnInit, AfterViewInit { @@ -162,6 +164,41 @@ export class AllContentComponent extends WorkSpace implements OnInit, AfterViewI */ public resourceService: ResourceService; + /** + * To store all the collection details to be shown in collection modal + */ + public collectionData: Array<any>; + + /** + * Flag to show/hide loader on first modal + */ + private showCollectionLoader: boolean; + + /** + * To define collection modal table header + */ + private headers: any; + + /** + * To store deleting content id + */ + private currentContentId: ContentIDParam; + + /** + * To store deleteing content type + */ + private deletingContentType: string; + + /** + * To store modal object of first yes/No modal + */ + private deleteModal: any; + + /** + * To show/hide collection modal + */ + public collectionListModal = false; + /** * Constructor to create injected service(s) object Default method of Draft Component class @@ -268,36 +305,113 @@ export class AllContentComponent extends WorkSpace implements OnInit, AfterViewI } ); } - public deleteConfirmModal(contentIds) { + + public deleteConfirmModal(contentIds, contentType) { + this.currentContentId = contentIds; + this.deletingContentType = contentType; + this.showCollectionLoader = false; const config = new TemplateModalConfig<{ data: string }, string, string>(this.modalTemplate); config.isClosable = false; config.size = 'small'; config.transitionDuration = 0; config.mustScroll = true; this.modalService - .open(config) - .onApprove(result => { - this.showLoader = true; - this.loaderMessage = { - 'loaderMessage': this.resourceService.messages.stmsg.m0034, - }; - this.delete(contentIds).subscribe( - (data: ServerResponse) => { - this.showLoader = false; - this.allContent = this.removeAllMyContent(this.allContent, contentIds); - if (this.allContent.length === 0) { - this.ngOnInit(); + .open(config); + } + + /** + * This method checks whether deleting content is linked to any collections, if linked to collection displays collection list pop modal. + */ + public checkLinkedCollections(modal) { + if (!_.isUndefined(modal)) { + this.deleteModal = modal; + } + this.showCollectionLoader = false; + if (['Course', 'TextBook', 'Collection', 'LessonPlan'].includes(this.deletingContentType)) { + this.deleteContent(this.currentContentId); + return; + } + + this.getLinkedCollections(this.currentContentId) + .subscribe((response) => { + const count = _.get(response, 'result.count'); + if (!count) { + this.deleteContent(this.currentContentId); + return; + } + this.showCollectionLoader = true; + const collections = _.get(response, 'result.content', []); + + const channels = _.map(collections, (collection) => { + return _.get(collection, 'channel'); + }); + const channelMapping = {}; + forkJoin(_.map(channels, (channel: string) => { + return this.getChannelDetails(channel); + })).subscribe((forkResponse) => { + this.collectionData = []; + _.forEach(forkResponse, channelResponse => { + const channelId = _.get(channelResponse, 'result.channel.code'); + const channelName = _.get(channelResponse, 'result.channel.name'); + channelMapping[channelId] = channelName; + }); + + _.forEach(collections, collection => { + const obj = _.pick(collection, ['contentType', 'board', 'medium', 'name', 'gradeLevel', 'subject', 'channel']); + obj['channel'] = channelMapping[obj.channel]; + this.collectionData.push(obj); + }); + + this.headers = { + type: 'Type', + name: 'Name', + subject: 'Subject', + grade: 'Grade', + medium: 'Medium', + board: 'Board', + channel: 'Tenant Name' + }; + if (!_.isUndefined(modal)) { + this.deleteModal.deny(); } - this.toasterService.success(this.resourceService.messages.smsg.m0006); + this.collectionListModal = true; }, - (err: ServerResponse) => { - this.showLoader = false; - this.toasterService.error(this.resourceService.messages.fmsg.m0022); - } - ); - }) - .onDeny(result => { - }); + (error) => { + this.toasterService.error(_.get(this.resourceService, 'messages.emsg.m0014')); + console.log(error); + }); + }, + (error) => { + this.toasterService.error(_.get(this.resourceService, 'messages.emsg.m0015')); + console.log(error); + }); + } + + /** + * This method deletes content using the content id. + */ + deleteContent(contentId) { + this.showLoader = true; + this.loaderMessage = { + 'loaderMessage': this.resourceService.messages.stmsg.m0034, + }; + this.delete(contentId).subscribe( + (data: ServerResponse) => { + this.showLoader = false; + this.allContent = this.removeAllMyContent(this.allContent, contentId); + if (this.allContent.length === 0) { + this.ngOnInit(); + } + this.toasterService.success(this.resourceService.messages.smsg.m0006); + }, + (err: ServerResponse) => { + this.showLoader = false; + this.toasterService.error(this.resourceService.messages.fmsg.m0022); + } + ); + if (!_.isUndefined(this.deleteModal)) { + this.deleteModal.deny(); + } } /** diff --git a/src/app/client/src/app/modules/workspace/components/published/published.component.html b/src/app/client/src/app/modules/workspace/components/published/published.component.html index 81c5e51c94ea3769fc19dfc3206433a431bdd706..e7b3f3a9cb18f311a650f4f50cd481cf06b9255a 100644 --- a/src/app/client/src/app/modules/workspace/components/published/published.component.html +++ b/src/app/client/src/app/modules/workspace/components/published/published.component.html @@ -17,7 +17,7 @@ <div class="twelve wide column" [appTelemetryImpression]="telemetryImpression"> <div class="ui stackable three column grid" in-view-container (inview)="inview($event)" [throttle]="[1000]" [trigger]="publishedContent"> <div in-view-item *ngFor="let content of publishedContent;let i = index;" [id]="i" [data]="content" class="column py-5 pl-10"> - <app-card-creation [data]="content" (clickEvent)="contentClick($event)"></app-card-creation> + <app-card-creation [data]="content" (clickEvent)="contentClick($event,content)"></app-card-creation> </div> </div> <div *ngIf="noResult"> @@ -56,28 +56,74 @@ <!--Content--> <div class="sb-modal-content"> - <p>{{resourceService?.frmelmnts?.lbl?.deleteconfirm}}</p> + <p [class.sb-color-gray-100]="showCollectionLoader">{{resourceService?.frmelmnts?.lbl?.deleteconfirm}}</p> + <div class="py-30"> + <div *ngIf="showCollectionLoader" class="sb-loader sb-gray-loader pb-20"></div> + </div> </div> <!--/Content--> <!--Actions--> - <div class="sb-modal-actions"> - <button - class="sb-btn sb-btn-normal sb-btn-error" - (click)="modal.approve('approved')" - autofocus - > - {{resourceService?.frmelmnts?.btn?.yes}} - </button> - <button - class="sb-btn sb-btn-normal sb-btn-outline-error" + <div class="sb-modal-actions flex-jc-space-between"> + <div> + <button + class="sb-btn sb-btn-normal sb-btn-outline-error mr-10" + [class.fadInButton]="showCollectionLoader" (click)="modal.deny('denied')" - > + [disabled]="showCollectionLoader" + > {{resourceService?.frmelmnts?.btn?.no}} - </button> - + </button> + <button + class="sb-btn sb-btn-normal sb-btn-error ui disabled" + [class.fadInButton]="showCollectionLoader" + (click)="checkLinkedCollections(modal)" + autofocus + [disabled]="showCollectionLoader" + > + {{resourceService?.frmelmnts?.btn?.yes}} + </button> + </div> + <div *ngIf="showCollectionLoader">{{resourceService?.frmelmnts?.lbl?.scanningLinkedCollection}}</div> </div> <!--/Actions--> </div> </div> -</ng-template> \ No newline at end of file +</ng-template> +<sui-modal *ngIf="collectionListModal" [mustScroll]="true" [isClosable]="true" [transitionDuration]="0" [size]="'large'" +class="sb-modal custom-lg-modal" (dismissed)="this.collectionListModal = false" #modal> + <div class="sb-modal-header"> + </div> + <div class="sb-modal-content"> + <h5 class="font-weight-bold sb-color-red">{{resourceService?.frmelmnts?.lbl?.linkedContentErrorMessage}}</h5> + <div class="sb-table-responsive"> + <table aria-hidden="true" class="sb-table sb-table-striped sb-table-sortable sb-table-blue-strip"> + <thead> + <th class="w-10" id="contentType">{{headers.type}}</th> + <th class="w-30" id="contentName">{{headers.name}}</th> + <th class="w-10" id="contentSubject">{{headers.subject}}</th> + <th class="w-10" id="contentGrade">{{headers.grade}}</th> + <th class="w-10" id="contentMedium">{{headers.medium}}</th> + <th class="w-10" id="contentBoard">{{headers.board}}</th> + <th class="w-20" id="contentchannel">{{headers.channel}}</th> + </thead> + <tbody> + <tr *ngFor = "let row of collectionData"> + <td class="white-space-inherit">{{row['contentType']}}</td> + <td class="white-space-inherit font-weight-bold">{{row['name']}}</td> + <td class="white-space-inherit">{{row['subject']?.join(", ") || '-'}}</td> + <td class="white-space-inherit">{{row['gradeLevel']?.join(", ") || '-'}}</td> + <td class="white-space-inherit">{{row['medium']?.join(", ") || '-'}}</td> + <td class="white-space-inherit">{{row['board']}}</td> + <td class="white-space-inherit">{{row['channel']}}</td> + </tr> + </tbody> + </table> + </div> + </div> + <div class="sb-modal-actions"> + <button class="sb-btn sb-btn-normal sb-btn-primary" (click)="modal.deny('denied')"> + {{resourceService?.frmelmnts?.btn?.close}} + </button> + </div> +</sui-modal> diff --git a/src/app/client/src/app/modules/workspace/components/published/published.component.scss b/src/app/client/src/app/modules/workspace/components/published/published.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..6da9a5b6a372b591e7a7f21edb1ee9598971aed4 --- /dev/null +++ b/src/app/client/src/app/modules/workspace/components/published/published.component.scss @@ -0,0 +1,61 @@ +.sb-gray-loader{ + &:after{ + content: ''; + width: 2rem; + height: 2rem; + border-radius: 50%; + border: 0.125rem solid; + border-color: var(--gray-100) var(--gray-100) transparent; + -webkit-animation: loader 0.5s infinite; + animation: loader 0.5s infinite; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + + .sb-table-blue-strip{ + background: none; + border: none; + border-bottom: solid 1px var(--primary-400); + th{ + background: none; + color: var(--primary-400); + border-bottom: solid 1px var(--primary-400); + } + tbody{ + tr:nth-child(odd){ + background: var(--primary-100); + } + tr:nth-child(even){ + background: white; + } + tr{ + td:first-child{ + border-left: solid 1px var(--primary-400); + } + td:last-child{ + border-right: solid 1px var(--primary-400); + } + } + } + } + + .bt-0{ + border-top: none; + } + + .fadInButton{ + opacity:0.5 !important; + } + + .white-space-inherit { + white-space: inherit !important; + word-wrap: break-word; + max-width: 1px; +} + +.max-h-360 { + max-height:360px; +} \ No newline at end of file diff --git a/src/app/client/src/app/modules/workspace/components/published/published.component.spec.data.ts b/src/app/client/src/app/modules/workspace/components/published/published.component.spec.data.ts index fbb63f93fbe79de585e89b6bf4d800a780d640b1..54ff78457760906b7b9bd80c490d5f6d22ed1f65 100644 --- a/src/app/client/src/app/modules/workspace/components/published/published.component.spec.data.ts +++ b/src/app/client/src/app/modules/workspace/components/published/published.component.spec.data.ts @@ -112,5 +112,60 @@ export const mockRes = { } }, pager: { 'totalItems': 72, 'currentPage': 3, 'pageSize': 9, 'totalPages': 8, - 'startPage': 1, 'endPage': 8, 'startIndex': 1, 'endIndex': 72, 'pages': [1, 2, 3, 4, 5]} + 'startPage': 1, 'endPage': 8, 'startIndex': 1, 'endIndex': 72, 'pages': [1, 2, 3, 4, 5]}, + searchedCollection: { + 'id': 'api.search-service.search', + 'ver': '3.0', + 'ts': '2020-09-07T13:44:30ZZ', + 'params': { + 'resmsgid': '15ce98a8-4b85-4b2a-bef8-57e0422d121c', + 'msgid': null, + 'err': null, + 'status': 'successful', + 'errmsg': null + }, + 'responseCode': 'OK', + 'result': { + 'count': 1, + 'content': [ + { + 'subject': [ + 'Mathematics' + ], + 'channel': '0124784842112040965', + 'gradeLevel': [ + 'Class 10' + ], + 'contentType': 'TextBook', + 'name': 'book1', + 'medium': [ + 'Hindi' + ], + 'board': 'State (Rajasthan)' + } + ] + } + }, + channelDetail: { + 'id': 'api.search-service.search', + 'ver': '3.0', + 'ts': '2020-09-07T13:44:30ZZ', + 'params': { + 'resmsgid': '15ce98a8-4b85-4b2a-bef8-57e0422d121c', + 'msgid': null, + 'err': null, + 'status': 'successful', + 'errmsg': null + }, + 'responseCode': 'OK', + 'result': { + 'count': 1, + 'channel': [ + { + 'code': '0124784842112040965', + 'name': 'Sunbird' + } + ] + } + } }; diff --git a/src/app/client/src/app/modules/workspace/components/published/published.component.spec.ts b/src/app/client/src/app/modules/workspace/components/published/published.component.spec.ts index 0f90f8f15fb13eddb14af9c7e2231869b10276b5..a45189c95cfaf904593b71073e48329798a142cd 100644 --- a/src/app/client/src/app/modules/workspace/components/published/published.component.spec.ts +++ b/src/app/client/src/app/modules/workspace/components/published/published.component.spec.ts @@ -16,7 +16,7 @@ import * as mockData from './published.component.spec.data'; const testData = mockData.mockRes; import { TelemetryModule } from '@sunbird/telemetry'; import { NgInviewModule } from 'angular-inport'; -import { SuiModule } from 'ng2-semantic-ui'; +import { SuiModule, SuiModalService } from 'ng2-semantic-ui'; import { CoreModule } from '@sunbird/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { configureTestSuite } from '@sunbird/test-util'; @@ -100,7 +100,12 @@ describe('PublishedComponent', () => { eventName: 'delete' }, data: { metaData: { identifier: 'do_2124341006465925121871' } } }; - component.contentClick(params); + const content = { + metaData: { + contentType: 'Resource' + } + }; + component.contentClick(params, content); const DeleteParam = { contentIds: ['do_2124341006465925121871'] }; @@ -166,4 +171,13 @@ describe('PublishedComponent', () => { component.getCourseQRCsv(); expect(window.open).toHaveBeenCalledWith(returnData.result.fileUrl, '_blank'); }); + it('should call search content and get channel and get success response', inject([SuiModalService, WorkSpaceService], + (modalService, workSpaceService) => { + spyOn(workSpaceService, 'searchContent').and.callFake(() => observableOf(testData.searchedCollection)); + spyOn(workSpaceService, 'getChannel').and.callFake(() => observableOf(testData.channelDetail)); + spyOn(component, 'checkLinkedCollections').and.callThrough(); + spyOn(modalService, 'open').and.callThrough(); + component.checkLinkedCollections(undefined); + expect(component.checkLinkedCollections).toHaveBeenCalledWith(undefined); + })); }); diff --git a/src/app/client/src/app/modules/workspace/components/published/published.component.ts b/src/app/client/src/app/modules/workspace/components/published/published.component.ts index 0d57e74150f5613e28e6d59c4ae40c6a99359b4e..daa9b94b601a7b0e09c3edb1b27d72700afea49e 100644 --- a/src/app/client/src/app/modules/workspace/components/published/published.component.ts +++ b/src/app/client/src/app/modules/workspace/components/published/published.component.ts @@ -11,7 +11,8 @@ import { import { WorkSpaceService } from '../../services'; import * as _ from 'lodash-es'; import { IImpressionEventInput } from '@sunbird/telemetry'; -import {combineLatest } from 'rxjs'; +import { combineLatest, forkJoin } from 'rxjs'; +import { ContentIDParam } from '../../interfaces/delteparam'; /** * Interface for passing the configuartion for modal @@ -25,7 +26,8 @@ import { SuiModalService, TemplateModalConfig, ModalTemplate } from 'ng2-semanti @Component({ selector: 'app-published', - templateUrl: './published.component.html' + templateUrl: './published.component.html', + styleUrls: ['./published.component.scss'] }) export class PublishedComponent extends WorkSpace implements OnInit, AfterViewInit { @ViewChild('modalTemplate') @@ -128,6 +130,41 @@ export class PublishedComponent extends WorkSpace implements OnInit, AfterViewIn query: string; sort: object; + /** + * To store all the collection details to be shown in collection modal + */ + public collectionData: Array<any>; + + /** + * Flag to show/hide loader on first modal + */ + private showCollectionLoader: boolean; + + /** + * To define collection modal table header + */ + private headers: any; + + /** + * To store deleting content id + */ + private currentContentId: ContentIDParam; + + /** + * To store deleteing content type + */ + private deletingContentType: string; + + /** + * To store modal object of first yes/No modal + */ + private deleteModal: any; + + /** + * To show/hide collection modal + */ + public collectionListModal = false; + /** * Constructor to create injected service(s) object Default method of Draft Component class @@ -271,23 +308,94 @@ export class PublishedComponent extends WorkSpace implements OnInit, AfterViewIn /** * This method launch the content editior */ - contentClick(param) { + contentClick(param, content) { + this.deletingContentType = content.metaData.contentType; if (param.action.eventName === 'delete') { - this.deleteConfirmModal(param.data.metaData.identifier); + this.currentContentId = param.data.metaData.identifier; + const config = new TemplateModalConfig<{ data: string }, string, string>(this.modalTemplate); + config.isClosable = false; + config.size = 'small'; + config.transitionDuration = 0; + config.mustScroll = true; + this.modalService + .open(config); + this.showCollectionLoader = false; } else { this.workSpaceService.navigateToContent(param.data.metaData, this.state); } } - public deleteConfirmModal(contentIds) { - const config = new TemplateModalConfig<{ data: string }, string, string>(this.modalTemplate); - config.isClosable = false; - config.size = 'small'; - config.transitionDuration = 0; - config.mustScroll = true; - this.modalService - .open(config) - .onApprove(result => { + /** + * This method checks whether deleting content is linked to any collections, if linked to collection displays collection list pop modal. + */ + public checkLinkedCollections(modal) { + if (!_.isUndefined(modal)) { + this.deleteModal = modal; + } + this.showCollectionLoader = false; + if (['Course', 'TextBook', 'Collection', 'LessonPlan'].includes(this.deletingContentType)) { + this.deleteContent(this.currentContentId); + return; + } + this.getLinkedCollections(this.currentContentId) + .subscribe((response) => { + const count = _.get(response, 'result.count'); + if (!count) { + this.deleteContent(this.currentContentId); + return; + } + this.showCollectionLoader = true; + const collections = _.get(response, 'result.content', []); + + const channels = _.map(collections, (collection) => { + return _.get(collection, 'channel'); + }); + const channelMapping = {}; + forkJoin(_.map(channels, (channel: string) => { + return this.getChannelDetails(channel); + })).subscribe((forkResponse) => { + this.collectionData = []; + _.forEach(forkResponse, channelResponse => { + const channelId = _.get(channelResponse, 'result.channel.code'); + const channelName = _.get(channelResponse, 'result.channel.name'); + channelMapping[channelId] = channelName; + }); + + _.forEach(collections, collection => { + const obj = _.pick(collection, ['contentType', 'board', 'medium', 'name', 'gradeLevel', 'subject', 'channel']); + obj['channel'] = channelMapping[obj.channel]; + this.collectionData.push(obj); + }); + + this.headers = { + type: 'Type', + name: 'Name', + subject: 'Subject', + grade: 'Grade', + medium: 'Medium', + board: 'Board', + channel: 'Tenant Name' + }; + if (!_.isUndefined(this.deleteModal)) { + this.deleteModal.deny(); + } + this.collectionListModal = true; + }, + (error) => { + this.toasterService.error(_.get(this.resourceService, 'messages.emsg.m0014')); + console.log(error); + }); + }, + (error) => { + this.toasterService.error(_.get(this.resourceService, 'messages.emsg.m0015')); + console.log(error); + }); + } + + /** + * This method deletes content using the content id. + */ + public deleteContent(contentIds) { this.showLoader = true; this.loaderMessage = { 'loaderMessage': this.resourceService.messages.stmsg.m0034, @@ -306,9 +414,9 @@ export class PublishedComponent extends WorkSpace implements OnInit, AfterViewIn this.toasterService.success(this.resourceService.messages.fmsg.m0022); } ); - }) - .onDeny(result => { - }); + if (!_.isUndefined(this.deleteModal)) { + this.deleteModal.deny(); + } } /** diff --git a/src/app/client/src/app/modules/workspace/interfaces/delteparam.ts b/src/app/client/src/app/modules/workspace/interfaces/delteparam.ts index ab9865f80bd72c0e5f2d391a61b74ebb0fb655c7..4acc282911290ee5eb090190e6a8d348dd5f189b 100644 --- a/src/app/client/src/app/modules/workspace/interfaces/delteparam.ts +++ b/src/app/client/src/app/modules/workspace/interfaces/delteparam.ts @@ -1,3 +1,7 @@ export interface IDeleteParam { contentIds ?: string[]; } + +export interface ContentIDParam { + contentId ?: string; +} diff --git a/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.spec.data.ts b/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.spec.data.ts index 4705a7d99ad2ea881f63aa803d71c1dfbd3d8d1d..1341d142c9e45fa28543ef5fbd941ca589b5a426 100644 --- a/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.spec.data.ts +++ b/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.spec.data.ts @@ -300,5 +300,60 @@ export const mockRes = { 'expires': 1534405277659, 'maxAge': 600 } - } + }, + searchedCollection: { + 'id': 'api.search-service.search', + 'ver': '3.0', + 'ts': '2020-09-07T13:44:30ZZ', + 'params': { + 'resmsgid': '15ce98a8-4b85-4b2a-bef8-57e0422d121c', + 'msgid': null, + 'err': null, + 'status': 'successful', + 'errmsg': null + }, + 'responseCode': 'OK', + 'result': { + 'count': 1, + 'content': [ + { + 'subject': [ + 'Mathematics' + ], + 'channel': '0124784842112040965', + 'gradeLevel': [ + 'Class 10' + ], + 'contentType': 'TextBook', + 'name': 'book1', + 'medium': [ + 'Hindi' + ], + 'board': 'State (Rajasthan)' + } + ] + } + }, + channelDetail: { + 'id': 'api.search-service.search', + 'ver': '3.0', + 'ts': '2020-09-07T13:44:30ZZ', + 'params': { + 'resmsgid': '15ce98a8-4b85-4b2a-bef8-57e0422d121c', + 'msgid': null, + 'err': null, + 'status': 'successful', + 'errmsg': null + }, + 'responseCode': 'OK', + 'result': { + 'count': 1, + 'channel': [ + { + 'code': '0124784842112040965', + 'name': 'Sunbird' + } + ] + } + } }; diff --git a/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.spec.ts b/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.spec.ts index e9016986255f8fcae208b058a76223db1ec50296..bddcfc2d292443c065a07386cca5044ab443bb76 100644 --- a/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.spec.ts +++ b/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.spec.ts @@ -6,7 +6,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { WorkSpaceService } from './workspace.service'; import { SharedModule } from '@sunbird/shared'; -import { CoreModule } from '@sunbird/core'; +import { CoreModule, ContentService, PublicDataService } from '@sunbird/core'; import { CacheService } from 'ng2-cache-service'; import * as mockData from './workspace.service.spec.data'; import { configureTestSuite } from '@sunbird/test-util'; @@ -99,4 +99,18 @@ describe('WorkSpaceService', () => { expect(workSpaceService.getFormData).toHaveBeenCalledWith(param); expect(workSpaceService).toBeTruthy(); })); + it('should call contentService post', inject([ContentService, WorkSpaceService], + ( contentService, workSpaceService) => { + spyOn(contentService, 'post').and.callFake(() => observableOf(testData.searchedCollection)); + spyOn(workSpaceService, 'searchContent').and.callThrough(); + workSpaceService.searchContent('do_2131027620732764161258'); + expect(contentService.post).toHaveBeenCalled(); + })); + it('should call publicDataservice get', inject([PublicDataService, WorkSpaceService], + ( publicDataService, workSpaceService) => { + spyOn(publicDataService, 'get').and.callFake(() => observableOf(testData.channelDetail)); + spyOn(workSpaceService, 'getChannel').and.callThrough(); + workSpaceService.getChannel('0124784842112040965'); + expect(publicDataService.get).toHaveBeenCalled(); + })); }); diff --git a/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.ts b/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.ts index f686b6337c1be86a42d986a72d6418827b9e4cd0..ef6486a53365a4701aa71ef0edc6c4c967730172 100644 --- a/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.ts +++ b/src/app/client/src/app/modules/workspace/services/work-space/workspace.service.ts @@ -5,7 +5,7 @@ import { ConfigService, ServerResponse, ICard, NavigationHelperService, ResourceService, BrowserCacheTtlService } from '@sunbird/shared'; import { ContentService, PublicDataService, UserService } from '@sunbird/core'; -import { IDeleteParam } from '../../interfaces/delteparam'; +import { IDeleteParam, ContentIDParam } from '../../interfaces/delteparam'; import { Router } from '@angular/router'; import * as _ from 'lodash-es'; import { CacheService } from 'ng2-cache-service'; @@ -41,6 +41,7 @@ export class WorkSpaceService { this.content = content; this.config = config; this.route = route; + this.publicDataService = publicDataService; } /** * deleteContent @@ -259,4 +260,34 @@ export class WorkSpaceService { maxAge: this.browserCacheTtlService.browserCacheTtl }); } + +/** + * Search Content which are used in some other content/collection + * @param {ContentID} requestParam + */ + searchContent(requestparam: ContentIDParam): Observable<ServerResponse> { + const option = { + url: `${this.config.urlConFig.URLS.COMPOSITE.SEARCH}`, + 'data': { + 'request': { + 'filters': { + 'childNodes': [requestparam] + } + } + } + }; + return this.content.post(option); + } + +/** + * To get channel details + * @param {channelId} id required for read API + */ +getChannel(channelId): Observable<ServerResponse> { + const option = { + url: `${this.config.urlConFig.URLS.CHANNEL.READ}` + '/' + channelId + }; + return this.publicDataService.get(option); +} + } diff --git a/src/app/client/src/assets/styles/base/_typography.scss b/src/app/client/src/assets/styles/base/_typography.scss index 74c6a8e7e9dcc51f914ecdd7cd0287cadb8c34d1..2a02b956c0bee1869649bfe9c827897275764427 100644 --- a/src/app/client/src/assets/styles/base/_typography.scss +++ b/src/app/client/src/assets/styles/base/_typography.scss @@ -200,4 +200,10 @@ a { line-height: normal !important; } -/* typography styles ends */ \ No newline at end of file +/* typography styles ends */ + +.sb-modal.custom-lg-modal .modal.large{max-width: 70.25rem !important;} + +.w-10{width:10% !important;max-width:10% !important;} +.w-20{width:20% !important;max-width:20% !important;} +.w-30{width:30% !important;max-width:30% !important;} diff --git a/src/app/resourcebundles/data/creation/en.properties b/src/app/resourcebundles/data/creation/en.properties index 55a1afb6a375506c12a087c21136bd747c17ebd0..91a42eba832a091e9b6741880016af345c5b739b 100644 --- a/src/app/resourcebundles/data/creation/en.properties +++ b/src/app/resourcebundles/data/creation/en.properties @@ -140,6 +140,9 @@ frmelmnts.lbl.publhwarng=You have given some review comments or suggestions, the frmelmnts.lbl.disablePopupText=This content can not be deleted frmelmnts.lbl.contactStateAdminToAdd = Please contact your state admin to add more participants to this batch frmelmnts.lbl.whatToCreate = What type of course do you want to create ? +frmelmnts.lbl.scanningLinkedContent=Scanning for linked content +frmelmnts.lbl.linkedContentErrorMessage=This content cannot be deleted as it is used in the following + # prmpt frmelmnts.prmpt.search=Search @@ -229,6 +232,8 @@ messages.emsg.m0011=Fetching review comments failed messages.emsg.m0010=Creating review comments failed messages.emsg.m0012=Something went wrong while saving your preferences. Please go to your profile to save your preferences messages.emsg.m0013=You don't have permission to edit this content +messages.emsg.m0014=Unable to get channel details +messages.emsg.m0015=Unable to get collection details # information messages messages.imsg.m0020=location is removed sucessfully.