diff --git a/src/app/components/multiple-draft-generator/multiple-draft-generator.component.css b/src/app/components/multiple-draft-generator/multiple-draft-generator.component.css new file mode 100644 index 00000000..00a27f06 --- /dev/null +++ b/src/app/components/multiple-draft-generator/multiple-draft-generator.component.css @@ -0,0 +1,17 @@ +.toggle-drafts { + width: 100%; + display: flex; + margin-bottom: 15px; +} + +.toggle-drafts label.btn { + flex: 0 1 50%; +} + +.translation-btn { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 350px; + margin: 0 auto 8px; +} diff --git a/src/app/components/multiple-draft-generator/multiple-draft-generator.component.html b/src/app/components/multiple-draft-generator/multiple-draft-generator.component.html index c4fc4aa5..af2051fc 100644 --- a/src/app/components/multiple-draft-generator/multiple-draft-generator.component.html +++ b/src/app/components/multiple-draft-generator/multiple-draft-generator.component.html @@ -1,35 +1,92 @@ + + + diff --git a/src/app/components/multiple-draft-generator/multiple-draft-generator.component.spec.ts b/src/app/components/multiple-draft-generator/multiple-draft-generator.component.spec.ts index b835168d..b0f3b4b5 100644 --- a/src/app/components/multiple-draft-generator/multiple-draft-generator.component.spec.ts +++ b/src/app/components/multiple-draft-generator/multiple-draft-generator.component.spec.ts @@ -1,4 +1,10 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + TestBed, + discardPeriodicTasks, + fakeAsync, + tick, +} from '@angular/core/testing'; import { NgbActiveModal, NgbAlert, @@ -7,20 +13,26 @@ import { import { MultipleDraftGeneratorComponent } from './multiple-draft-generator.component'; import { FormsModule } from '@angular/forms'; import { DraftService } from '../../service/draft.service'; +import { LanguageService } from '../../service/language.service'; +import { ResourceService } from '../../service/resource/resource.service'; import { Resource } from '../../models/resource'; import { Translation } from '../../models/translation'; import { By } from '@angular/platform-browser'; import { NgbButtonLabel } from '@ng-bootstrap/ng-bootstrap'; import { Language } from '../../models/language'; import { DebugElement } from '@angular/core'; +import { TranslationVersionBadgeComponent } from '../translation/translation-version-badge/translation-version-badge.component'; +import { MessageType } from '../../models/message'; describe('MultipleDraftGeneratorComponent', () => { let comp: MultipleDraftGeneratorComponent; let fixture: ComponentFixture; + let customResourceServiceStub; + let customDraftServiceStub; const buildTranslation = ( isPublished: boolean, - generateDraft: boolean, + selectedForAction: boolean, language: string, ) => { const l = new Language(); @@ -29,15 +41,57 @@ describe('MultipleDraftGeneratorComponent', () => { const t = new Translation(); t.language = l; t.is_published = isPublished; - t.generateDraft = generateDraft; + t['is-published'] = isPublished; + t.selectedForAction = selectedForAction; return t; }; beforeEach(() => { + customResourceServiceStub = { + getResource() {}, + }; + customDraftServiceStub = { + createDraft() {}, + publishDraft() {}, + }; + + spyOn(customResourceServiceStub, 'getResource').and.returnValue( + Promise.resolve({ + 'latest-drafts-translations': [ + { + language: { id: 1 }, + 'publishing-errors': null, + 'is-published': false, + }, + ], + }), + ); + spyOn(customDraftServiceStub, 'createDraft').and.returnValue( + Promise.resolve(), + ); + spyOn(customDraftServiceStub, 'publishDraft').and.returnValue( + Promise.resolve([ + { + 'publishing-errors': null, + 'is-published': false, + }, + ]), + ); + + customResourceServiceStub.getResource(); + TestBed.configureTestingModule({ - declarations: [MultipleDraftGeneratorComponent], + declarations: [ + MultipleDraftGeneratorComponent, + TranslationVersionBadgeComponent, + ], imports: [NgbModule.forRoot(), FormsModule], - providers: [{ provide: DraftService }, { provide: NgbActiveModal }], + providers: [ + { provide: DraftService, useValue: customDraftServiceStub }, + { provide: NgbActiveModal }, + { provide: ResourceService, useValue: customResourceServiceStub }, + { provide: LanguageService }, + ], }).compileComponents(); fixture = TestBed.createComponent(MultipleDraftGeneratorComponent); @@ -53,17 +107,18 @@ describe('MultipleDraftGeneratorComponent', () => { const r = new Resource(); r['latest-drafts-translations'] = translations; comp.resource = r; + comp.actionType = 'publish'; fixture.detectChanges(); }); - it('only shows languages without drafts', () => { + it('shows languages with and without drafts', () => { expect( fixture.debugElement.queryAll(By.directive(NgbButtonLabel)).length, - ).toBe(3); + ).toBe(4); }); - it('confirm message lists all languages', () => { + it('shows confirm message to publish selected languages', () => { comp.showConfirmAlert(); fixture.detectChanges(); @@ -71,7 +126,78 @@ describe('MultipleDraftGeneratorComponent', () => { By.directive(NgbAlert), ); expect(alert.nativeElement.textContent).toContain( - `${comp.baseConfirmMessage} Chinese, French?`, + `Are you sure you want to publish these languages: Chinese, French?`, ); }); + + it('shows confirm message to create a draft for selected languages', () => { + comp.actionType = 'createDrafts'; + comp.showConfirmAlert(); + fixture.detectChanges(); + + const alert: DebugElement = fixture.debugElement.query( + By.directive(NgbAlert), + ); + expect(alert.nativeElement.textContent).toContain( + `Are you sure you want to generate a draft for these languages: Chinese, French?`, + ); + }); + + describe('publishOrCreateDrafts() Publish', () => { + it('should send publish 2 languages, and call isPublished() every 5 seconds ', fakeAsync(() => { + comp.showConfirmAlert(); + fixture.detectChanges(); + spyOn(comp, 'renderMessage'); + spyOn(comp, 'isPublished'); + comp.publishOrCreateDrafts(); + expect(comp.renderMessage).toHaveBeenCalledWith( + MessageType.success, + 'Publishing translations...', + ); + + tick(5500); + fixture.detectChanges(); + discardPeriodicTasks(); + + fixture.whenStable().then(() => { + expect(customDraftServiceStub.publishDraft).toHaveBeenCalledTimes(2); + expect(comp.errorMessage).toEqual([]); + expect(comp.isPublished).toHaveBeenCalledTimes(1); + + tick(5500); + fixture.detectChanges(); + discardPeriodicTasks(); + + expect(comp.isPublished).toHaveBeenCalledTimes(2); + }); + })); + + it('should return publishing errors and warn the user.', fakeAsync(() => { + customDraftServiceStub.publishDraft.and.returnValue( + Promise.resolve([ + { + 'publishing-errors': 'Error publishing...', + 'is-published': false, + }, + ]), + ); + spyOn(comp, 'renderMessage'); + spyOn(comp, 'isPublished'); + + comp.showConfirmAlert(); + fixture.detectChanges(); + comp.publishOrCreateDrafts(); + + tick(5500); + fixture.detectChanges(); + discardPeriodicTasks(); + + fixture.whenStable().then(() => { + expect(comp.errorMessage).toEqual([ + 'Error publishing...', + 'Error publishing...', + ]); + }); + })); + }); }); diff --git a/src/app/components/multiple-draft-generator/multiple-draft-generator.component.ts b/src/app/components/multiple-draft-generator/multiple-draft-generator.component.ts index 1947d11e..aea296aa 100644 --- a/src/app/components/multiple-draft-generator/multiple-draft-generator.component.ts +++ b/src/app/components/multiple-draft-generator/multiple-draft-generator.component.ts @@ -1,59 +1,275 @@ -import { Component } from '@angular/core'; +import { Component, OnDestroy } from '@angular/core'; import { Resource } from '../../models/resource'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { DraftService } from '../../service/draft.service'; +import { ResourceService } from '../../service/resource/resource.service'; +import { LanguageService } from '../../service/language.service'; import { Translation } from '../../models/translation'; +import { MessageType } from '../../models/message'; + +enum LanguageTypeEnum { + draft = 'draft', + publish = 'publish', +} + +interface PromisePayload { + success: boolean; + type: LanguageTypeEnum; + value?: Translation; + error?: string; +} +interface APICall { + type: LanguageTypeEnum; + translation: Translation; +} + +type ActionType = 'publish' | 'createDrafts'; @Component({ selector: 'admin-multiple-draft-generator', templateUrl: './multiple-draft-generator.component.html', + styleUrls: ['./multiple-draft-generator.component.css'], }) -export class MultipleDraftGeneratorComponent { +export class MultipleDraftGeneratorComponent implements OnDestroy { resource: Resource; translations: Translation[]; - + actionType: ActionType = 'publish'; confirmMessage: string; - saving: boolean; - errorMessage: string; - - readonly baseConfirmMessage = - 'Are you sure you want to generate a draft for these languages:'; + errorMessage: string[]; + sucessfulMessages: string[]; + alertMessage: string; + sucessfulMessage: string; + checkToEnsureTranslationIsPublished: number; + disableButtons: boolean; constructor( private ngbActiveModal: NgbActiveModal, private draftService: DraftService, + private resourceService: ResourceService, + private languageService: LanguageService, ) {} + ngOnDestroy(): void { + clearInterval(this.checkToEnsureTranslationIsPublished); + } + + renderMessage(type: MessageType, text: string, time?: number) { + if (type === MessageType.error) { + this.errorMessage = [text]; + return; + } else if (type === MessageType.success) { + this.sucessfulMessages = [text]; + } else { + this[`${type}Message`] = text; + if (time) { + setTimeout(() => { + this[`${type}Message`] = ''; + }, time); + } + } + } + + switchActionType(type: ActionType) { + this.actionType = type; + this.translations = []; + this.confirmMessage = ''; + this.sucessfulMessages = []; + this.alertMessage = ''; + this.disableButtons = false; + this.resource['latest-drafts-translations'].forEach((translation) => { + delete translation.selectedForAction; + }); + } + showConfirmAlert(): void { this.translations = this.resource['latest-drafts-translations'].filter( - (translation) => translation.generateDraft, + (translation) => translation.selectedForAction, ); if (this.translations.length === 0) { return; } - const message = this.translations + const selectedTranslations = this.translations .map((translation) => translation.language.name) .join(', '); - this.confirmMessage = `${this.baseConfirmMessage} ${message}?`; + if (this.actionType === 'publish') { + this.confirmMessage = `Are you sure you want to publish these languages: ${selectedTranslations}?`; + } else { + this.confirmMessage = `Are you sure you want to generate a draft for these languages: ${selectedTranslations}?`; + } } - generateDrafts(): void { - this.saving = true; - this.errorMessage = null; + async publishOrCreateDrafts(): Promise { + this.confirmMessage = null; + this.errorMessage = []; + const promises: APICall[] = []; + this.disableButtons = true; - this.translations.forEach((translation, index) => { - this.draftService - .createDraft(translation) - .then(() => { - if (index === this.translations.length - 1) { - this.ngbActiveModal.close(); + // Define what promises we will call + this.translations.forEach((translation) => { + if (this.actionType === 'publish') { + promises.push({ + type: LanguageTypeEnum.publish, + translation, + }); + } else { + promises.push({ + type: LanguageTypeEnum.draft, + translation, + }); + } + }); + + // Call promises + if (promises.length) { + if (this.actionType === 'publish') { + this.renderMessage(MessageType.success, 'Publishing translations...'); + } else { + this.renderMessage(MessageType.alert, 'Creating drafts...'); + } + + const results: PromisePayload[] = await Promise.all( + promises.map(({ type, translation }) => { + if (type === LanguageTypeEnum.draft) { + return this.draftService + .createDraft(translation) + .then( + () => + ({ + success: true, + type, + } as PromisePayload), + ) + .catch( + (error) => + ({ + success: false, + type, + error, + } as PromisePayload), + ); + } else { + return this.draftService + .publishDraft(this.resource, translation) + .then( + (value) => + ({ + success: true, + type, + value, + } as PromisePayload), + ) + .catch( + (error) => + ({ + success: false, + type, + error, + } as PromisePayload), + ); } - }) - .catch((message) => { - this.saving = false; - this.errorMessage = message; + }), + ); + + // Determine results + const invalidResults = results.filter((result) => !result.success); + if (invalidResults.length) { + invalidResults.forEach((invalidResult) => { + this.errorMessage = [...this.errorMessage, invalidResult.error]; + }); + this.disableButtons = false; + } else { + if (this.actionType === 'publish') { + const publishingErrors = results + .filter((result) => result.value[0]['publishing-errors']) + .map((result) => result.value[0]['publishing-errors']); + if (publishingErrors.length) { + publishingErrors.forEach((publishingError) => { + this.errorMessage = [...this.errorMessage, publishingError]; + }); + } + this.checkToEnsureTranslationIsPublished = window.setInterval(() => { + this.isPublished(); + }, 5000); + } else { + this.renderMessage(MessageType.alert, ''); + this.renderMessage( + MessageType.success, + 'Drafts created. Ready for you to publish.', + ); + this.disableButtons = false; + // Update languages + this.resourceService + .getResources('latest-drafts-translations') + .then((resources) => { + const resource = resources.find((r) => r.id === this.resource.id); + this.setResourceAndLoadTranslations(resource); + }); + setTimeout(() => { + this.renderMessage(MessageType.success, ''); + }, 5000); + } + } + } + } + + isPublished() { + this.renderMessage(MessageType.success, 'Publishing translations...'); + this.resourceService + .getResource(this.resource.id, 'latest-drafts-translations') + .then((resource) => { + let numberpublished = 0; + this.translations.forEach((translation) => { + const updatedTranslation = resource[ + 'latest-drafts-translations' + ].find( + (draftTranslation) => + draftTranslation.language.id === translation.language.id, + ); + if (updatedTranslation['is-published']) { + numberpublished++; + this.sucessfulMessages = [ + ...this.sucessfulMessages, + `${translation.language.name} version ${updatedTranslation.version} has been published`, + ]; + } + if (updatedTranslation['publishing-errors']) { + clearInterval(this.checkToEnsureTranslationIsPublished); + this.errorMessage = [ + ...this.errorMessage, + updatedTranslation['publishing-errors'], + ]; + this.disableButtons = false; + } + }); + + if (numberpublished === this.translations.length) { + clearInterval(this.checkToEnsureTranslationIsPublished); + this.renderMessage( + MessageType.success, + 'All Languages are successfully published.', + ); + this.disableButtons = false; + this.setResourceAndLoadTranslations(resource); + } + }) + .catch((err) => { + console.log('ERROR', err); + clearInterval(this.checkToEnsureTranslationIsPublished); + this.errorMessage = [...this.errorMessage, err]; + this.disableButtons = false; + }); + } + + private setResourceAndLoadTranslations(resource: Resource): void { + this.resource = resource; + this.resource['latest-drafts-translations'].forEach((translation) => { + this.languageService + .getLanguage(translation.language.id, 'custom_pages,custom_tips') + .then((language) => { + translation.language = language; + translation.is_published = translation['is-published']; }); }); } diff --git a/src/app/components/resource/resource.component.html b/src/app/components/resource/resource.component.html index f2a020de..0674456d 100644 --- a/src/app/components/resource/resource.component.html +++ b/src/app/components/resource/resource.component.html @@ -32,7 +32,7 @@ class="btn btn-secondary" *ngIf="!isMetaTool()" > - Generate multiple drafts + Bulk Actions diff --git a/src/app/components/translation/translation-version-badge/translation-version-badge.component.html b/src/app/components/translation/translation-version-badge/translation-version-badge.component.html index b315a268..7cd84fa2 100644 --- a/src/app/components/translation/translation-version-badge/translation-version-badge.component.html +++ b/src/app/components/translation/translation-version-badge/translation-version-badge.component.html @@ -1,9 +1,16 @@ {{ translation.version }} | Draft {{ translation.version }} | Live +{{ translation.version }} | Error None diff --git a/src/app/components/translation/translation.component.html b/src/app/components/translation/translation.component.html index 34bcdb90..0d00c9be 100644 --- a/src/app/components/translation/translation.component.html +++ b/src/app/components/translation/translation.component.html @@ -38,7 +38,7 @@

-
+
@@ -49,30 +49,20 @@

> Download -

+
+ -
- - Publishing... + + {{ sucessfulMessage }} - - Saving... + + {{ alertMessage }} {{ errorMessage }} diff --git a/src/app/components/translation/translation.component.spec.ts b/src/app/components/translation/translation.component.spec.ts index 24c9f8f5..c2377003 100644 --- a/src/app/components/translation/translation.component.spec.ts +++ b/src/app/components/translation/translation.component.spec.ts @@ -1,4 +1,11 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { + async, + ComponentFixture, + discardPeriodicTasks, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; import { TranslationComponent } from './translation.component'; import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DraftService } from '../../service/draft.service'; @@ -12,10 +19,12 @@ import { Page } from '../../models/page'; import { CustomPage } from '../../models/custom-page'; import { ResourceComponent } from '../resource/resource.component'; import anything = jasmine.anything; +import { ResourceService } from '../../service/resource/resource.service'; import { CustomPageService } from '../../service/custom-page.service'; import { CustomManifestService } from '../../service/custom-manifest.service'; -import { CustomManifest } from '../../models/custom-manifest'; import { CustomTipService } from '../../service/custom-tip.service'; +import { CustomManifest } from '../../models/custom-manifest'; +import { MessageType } from '../../models/message'; import { TranslationVersionBadgeComponent } from './translation-version-badge/translation-version-badge.component'; describe('TranslationComponent', () => { @@ -26,6 +35,8 @@ describe('TranslationComponent', () => { let customTipsServiceStub; let modalServiceStub; let customManifestServiceStub; + let customDraftServiceStub; + let customResourceServiceStub; let resourceComponent: ResourceComponent; let language: Language; @@ -73,6 +84,12 @@ describe('TranslationComponent', () => { modalServiceStub = { open() {}, }; + customDraftServiceStub = { + publishDraft() {}, + }; + customResourceServiceStub = { + getResource() {}, + }; const modalRef = { componentInstance: {}, result: Promise.resolve(), @@ -87,20 +104,41 @@ describe('TranslationComponent', () => { spyOn(customManifestServiceStub, 'delete').and.returnValue( Promise.resolve(), ); + spyOn(customDraftServiceStub, 'publishDraft').and.returnValue( + Promise.resolve([ + { + 'publishing-errors': null, + }, + ]), + ); + spyOn(customResourceServiceStub, 'getResource').and.returnValue( + Promise.resolve({ + 'latest-drafts-translations': [ + { + language: { id: 1 }, + 'publishing-errors': null, + 'is-published': false, + }, + ], + }), + ); customPageServiceStub.delete(); modalServiceStub.open(); customManifestServiceStub.delete(); + customDraftServiceStub.publishDraft(); + customResourceServiceStub.getResource(); TestBed.configureTestingModule({ declarations: [TranslationComponent, TranslationVersionBadgeComponent], imports: [NgbModule.forRoot()], providers: [ - { provide: DraftService }, + { provide: DraftService, useValue: customDraftServiceStub }, { provide: CustomPageService, useValue: customPageServiceStub }, { provide: CustomTipService, useValue: customTipsServiceStub }, { provide: CustomManifestService, useValue: customManifestServiceStub }, { provide: NgbModal, useValue: modalServiceStub }, + { provide: ResourceService, useValue: customResourceServiceStub }, ], }).compileComponents(); })); @@ -111,6 +149,9 @@ describe('TranslationComponent', () => { resourceComponent = new ResourceComponent(null, null); comp.translationLoaded = resourceComponent.translationLoaded$; + comp.errorMessage = ''; + comp.alertMessage = ''; + comp.sucessfulMessage = ''; const pageWithCustomPage = buildPage(2); @@ -124,6 +165,7 @@ describe('TranslationComponent', () => { comp.language = language; const resource = new Resource(); + resource.id = 15; resource.pages = [buildPage(1), pageWithCustomPage]; resource.tips = []; resource['custom-manifests'] = [ @@ -139,11 +181,11 @@ describe('TranslationComponent', () => { fixture.detectChanges(); }); - it(`should show action button with 'New Draft'`, () => { + it(`should show action button with 'Publish'`, () => { const element: DebugElement = fixture.debugElement - .queryAll(By.css('.btn.btn-secondary')) + .queryAll(By.css('.btn.btn-success')) .pop(); - expect(element.nativeElement.textContent.trim()).toBe('New Draft'); + expect(element.nativeElement.textContent.trim()).toBe('Publish'); }); it(`should show status badge with 'None'`, () => { @@ -152,6 +194,140 @@ describe('TranslationComponent', () => { ); expect(element.nativeElement.textContent).toBe('None'); }); + + describe('publish a new translation (Server creates draft)', () => { + let translation: Translation; + + beforeEach(() => { + translation = new Translation(); + translation.none = true; + translation.language = language; + translation.resource = comp.resource; + + comp.resource['latest-drafts-translations'] = [translation]; + comp.reloadTranslation(); + fixture.detectChanges(); + }); + + it('should git resource endpoint', fakeAsync(() => { + spyOn(comp, 'renderMessage'); + spyOn(comp, 'isPublished'); + comp.publish(); + + expect(comp.renderMessage).toHaveBeenCalledWith(MessageType.error, ''); + expect(comp.renderMessage).toHaveBeenCalledWith( + MessageType.success, + 'Publishing...', + ); + + tick(5500); + fixture.detectChanges(); + + discardPeriodicTasks(); + fixture.whenStable().then(() => { + expect(comp.isPublished).toHaveBeenCalled(); + }); + })); + + it('should clear the interval on destroy', fakeAsync(() => { + spyOn(comp, 'renderMessage'); + spyOn(comp, 'isPublished'); + spyOn(global, 'clearInterval'); + comp.publish(); + tick(5500); + fixture.detectChanges(); + discardPeriodicTasks(); + + comp.ngOnDestroy(); + fixture.whenStable().then(() => { + expect(global.clearInterval).toHaveBeenCalled(); + }); + })); + }); + }); + + describe('isPublished()', () => { + let translation: Translation; + + beforeEach(() => { + translation = new Translation(); + translation.language = language; + translation.none = true; + translation.resource = comp.resource; + comp.translation = translation; + comp.resource['latest-drafts-translations'] = [translation]; + comp.reloadTranslation(); + fixture.detectChanges(); + }); + + it('should not run clearInterval as it is not published and had no errors', () => { + spyOn(global, 'clearInterval'); + comp.isPublished(); + + expect(customResourceServiceStub.getResource).toHaveBeenCalledWith( + 15, + 'latest-drafts-translations', + ); + expect(global.clearInterval).not.toHaveBeenCalled(); + }); + + it('should run clearInterval and report pubslishing error to user', () => { + customResourceServiceStub.getResource.and.returnValue( + Promise.resolve({ + 'latest-drafts-translations': [ + { + language: { id: 1 }, + 'publishing-errors': 'Error while saving', + 'is-published': false, + }, + ], + }), + ); + spyOn(global, 'clearInterval'); + spyOn(comp, 'renderMessage'); + comp.isPublished(); + + fixture.whenStable().then(() => { + expect(global.clearInterval).toHaveBeenCalled(); + expect(comp.renderMessage).toHaveBeenCalledWith( + MessageType.success, + null, + ); + expect(comp.renderMessage).toHaveBeenCalledWith( + MessageType.error, + 'Error while saving', + ); + }); + }); + + it('should run clearInterval and report success to user', () => { + customResourceServiceStub.getResource.and.returnValue( + Promise.resolve({ + 'latest-drafts-translations': [ + { + language: { id: 1 }, + 'publishing-errors': null, + 'is-published': true, + }, + ], + }), + ); + spyOn(global, 'clearInterval'); + spyOn(comp, 'renderMessage'); + comp.isPublished(); + + fixture.whenStable().then(() => { + expect(global.clearInterval).toHaveBeenCalled(); + expect(comp.renderMessage).toHaveBeenCalledWith( + MessageType.error, + null, + ); + expect(comp.renderMessage).toHaveBeenCalledWith( + MessageType.success, + comp.successfullyPublishedMessage, + ); + }); + }); }); describe('language has existing translation(s)', () => { @@ -286,15 +462,15 @@ describe('TranslationComponent', () => { }); describe('action button', () => { - it(`should say 'New Draft' for published translations`, () => { + it(`should say 'Publish' for published translations`, () => { translation.is_published = true; fixture.detectChanges(); const element: DebugElement = fixture.debugElement - .queryAll(By.css('.btn.btn-secondary')) + .queryAll(By.css('.btn.btn-success')) .pop(); - expect(element.nativeElement.textContent.trim()).toBe('New Draft'); + expect(element.nativeElement.textContent.trim()).toBe('Publish'); }); it(`should say 'Publish' for drafts`, () => { diff --git a/src/app/components/translation/translation.component.ts b/src/app/components/translation/translation.component.ts index 46b3cb6c..72b3f261 100644 --- a/src/app/components/translation/translation.component.ts +++ b/src/app/components/translation/translation.component.ts @@ -6,6 +6,7 @@ import { OnChanges, Output, EventEmitter, + OnDestroy, } from '@angular/core'; import { Translation } from '../../models/translation'; import { DraftService } from '../../service/draft.service'; @@ -29,12 +30,14 @@ import { Resource } from '../../models/resource'; import { Observable } from 'rxjs'; import { getLatestTranslation } from './utilities'; import { environment } from '../../../environments/environment'; +import { MessageType } from '../../models/message'; +import { ResourceService } from '../../service/resource/resource.service'; @Component({ selector: 'admin-translation', templateUrl: './translation.component.html', }) -export class TranslationComponent implements OnInit, OnChanges { +export class TranslationComponent implements OnInit, OnChanges, OnDestroy { @Input() language: Language; @Input() resource: Resource; @Input() translationLoaded: Observable; @@ -43,10 +46,11 @@ export class TranslationComponent implements OnInit, OnChanges { translation: Translation; customManifest: CustomManifest; baseDownloadUrl = environment.base_url + 'translations/'; - - saving = false; - publishing = false; errorMessage: string; + alertMessage: string; + sucessfulMessage: string; + checkToEnsureDraftIsPublished: number; + successfullyPublishedMessage = 'Language has been successfully published.'; constructor( private customPageService: CustomPageService, @@ -54,6 +58,7 @@ export class TranslationComponent implements OnInit, OnChanges { private draftService: DraftService, private customManifestService: CustomManifestService, private modalService: NgbModal, + private resourceService: ResourceService, ) {} ngOnInit(): void { @@ -76,6 +81,10 @@ export class TranslationComponent implements OnInit, OnChanges { } } + ngOnDestroy() { + clearInterval(this.checkToEnsureDraftIsPublished); + } + getPages(): AbstractPage[] { const _tPages = this.translation.resource.pages.map((page) => { const customPage: CustomPage = this.translation.language[ @@ -138,29 +147,65 @@ export class TranslationComponent implements OnInit, OnChanges { return tip as Tip; } - publishDraft(): void { - this.publishing = true; - this.errorMessage = null; - - const t = Translation.copy(this.translation); - t.is_published = true; + renderMessage(type: MessageType, text: string, time?: number) { + this[`${type}Message`] = text; + if (time) { + setTimeout(() => { + this[`${type}Message`] = ''; + }, time); + } + } + async publish(): Promise { + this.renderMessage(MessageType.error, ''); + this.renderMessage(MessageType.success, 'Publishing...'); this.draftService - .updateDraft(t) - .then(() => this.loadAllResources()) - .catch(this.handleError.bind(this)) - .then(() => (this.publishing = false)); + .publishDraft(this.resource, this.translation) + .then((data) => { + const publishingError = data[0]['publishing-errors']; + if (publishingError) { + this.renderMessage(MessageType.success, publishingError); + } + this.checkToEnsureDraftIsPublished = window.setInterval(() => { + this.isPublished(); + }, 5000); + }) + .catch(this.handleError.bind(this)); } - createDraft(): void { - this.saving = true; - this.errorMessage = null; - - this.draftService - .createDraft(this.translation) - .then(() => this.loadAllResources()) - .catch(this.handleError.bind(this)) - .then(() => (this.saving = false)); + isPublished() { + try { + this.resourceService + .getResource(this.resource.id, 'latest-drafts-translations') + .then((resource) => { + const translation = resource['latest-drafts-translations'].find( + (draftTranslation) => + draftTranslation.language.id === this.translation.language.id, + ); + if (translation['publishing-errors']) { + clearInterval(this.checkToEnsureDraftIsPublished); + this.renderMessage(MessageType.success, null); + this.renderMessage( + MessageType.error, + translation['publishing-errors'], + ); + } + if (translation['is-published']) { + clearInterval(this.checkToEnsureDraftIsPublished); + this.renderMessage(MessageType.error, null); + this.renderMessage( + MessageType.success, + this.successfullyPublishedMessage, + ); + this.loadAllResources(); + } + }); + } catch (err) { + console.log('ERROR', err); + clearInterval(this.checkToEnsureDraftIsPublished); + this.renderMessage(MessageType.success, null); + this.renderMessage(MessageType.error, err.message); + } } createCustomPage(page: Page): void { diff --git a/src/app/models/message.ts b/src/app/models/message.ts new file mode 100644 index 00000000..1726c44e --- /dev/null +++ b/src/app/models/message.ts @@ -0,0 +1,5 @@ +export enum MessageType { + success = 'sucessful', + alert = 'alert', + error = 'error', +} diff --git a/src/app/models/translation.ts b/src/app/models/translation.ts index 5d12769a..736ae170 100644 --- a/src/app/models/translation.ts +++ b/src/app/models/translation.ts @@ -9,7 +9,7 @@ export class Translation { resource: Resource; version: number; - generateDraft: boolean; + selectedForAction: boolean; none: boolean; static copy(translation: Translation): Translation { diff --git a/src/app/service/draft.service.ts b/src/app/service/draft.service.ts index 807a0a3c..f97f4e2b 100644 --- a/src/app/service/draft.service.ts +++ b/src/app/service/draft.service.ts @@ -7,10 +7,12 @@ import { Page } from '../models/page'; import { Tip } from '../models/tip'; import { environment } from '../../environments/environment'; import { AbstractService } from './abstract.service'; +import { Resource } from '../models/resource'; @Injectable() export class DraftService extends AbstractService { private readonly draftsUrl = environment.base_url + 'drafts'; + private readonly resourcesUrl = environment.base_url + 'resources'; constructor(private http: Http, private authService: AuthService) { super(); @@ -59,19 +61,29 @@ export class DraftService extends AbstractService { .catch(this.handleError); } - updateDraft(translation: Translation): Promise { + publishDraft( + resource: Resource, + translation: Translation, + ): Promise { const payload = { data: { - type: 'translation', - attributes: { - is_published: translation.is_published, + type: 'publish-translations', + relationships: { + languages: { + data: [ + { + id: translation.language.id, + type: 'language', + }, + ], + }, }, }, }; return this.http - .put( - `${this.draftsUrl}/${translation.id}`, + .post( + `${this.resourcesUrl}/${resource.id}/translations/publish`, payload, this.authService.getAuthorizationAndOptions(), ) diff --git a/src/app/service/resource/resource.service.spec.ts b/src/app/service/resource/resource.service.spec.ts index f1cb5c06..bc2d26a0 100644 --- a/src/app/service/resource/resource.service.spec.ts +++ b/src/app/service/resource/resource.service.spec.ts @@ -4,6 +4,8 @@ import { Http, RequestOptionsArgs } from '@angular/http'; import { AuthService } from '../auth/auth.service'; import { Resource } from '../../models/resource'; import { Observable } from 'rxjs/Observable'; +import { environment } from '../../../environments/environment'; + import anything = jasmine.anything; const headers: RequestOptionsArgs = {}; @@ -22,6 +24,7 @@ describe('ResourceService', () => { const service = new ResourceService(mockHttp, mockAuthService); const resource = new Resource(); + resource.id = 13; beforeEach(() => { spyOn(mockHttp, 'post').and.returnValue( @@ -38,6 +41,10 @@ describe('ResourceService', () => { spyOn(mockHttp, 'put').and.returnValue( new Observable((observer) => observer.complete()), ); + + spyOn(mockHttp, 'get').and.returnValue( + new Observable((observer) => observer.complete()), + ); }); it('creating uses authorization code', () => { @@ -51,4 +58,36 @@ describe('ResourceService', () => { expect(mockHttp.put).toHaveBeenCalledWith(anything(), anything(), headers); }); + + describe('GetResources()', () => { + it('should include "include"', () => { + service.getResources('test-data'); + expect(mockHttp.get).toHaveBeenCalledWith( + `${environment.base_url}resources?include=test-data`, + ); + }); + + it('should not include "include"', () => { + service.getResource(resource.id); + expect(mockHttp.get).toHaveBeenCalledWith( + `${environment.base_url}resources/${resource.id}`, + ); + }); + }); + + describe('GetResource()', () => { + it('should include "include"', () => { + service.getResource(resource.id, 'test-data'); + expect(mockHttp.get).toHaveBeenCalledWith( + `${environment.base_url}resources/${resource.id}?include=test-data`, + ); + }); + + it('should not include "include"', () => { + service.getResource(resource.id); + expect(mockHttp.get).toHaveBeenCalledWith( + `${environment.base_url}resources/${resource.id}`, + ); + }); + }); }); diff --git a/src/app/service/resource/resource.service.ts b/src/app/service/resource/resource.service.ts index 9f4ec616..22efc690 100644 --- a/src/app/service/resource/resource.service.ts +++ b/src/app/service/resource/resource.service.ts @@ -27,6 +27,20 @@ export class ResourceService extends AbstractService { .catch(this.handleError); } + getResource(resourceId: number, include?: string): Promise { + return this.http + .get( + include + ? `${this.resourcesUrl}/${resourceId}?include=${include}` + : `${this.resourcesUrl}/${resourceId}`, + ) + .toPromise() + .then((response) => { + return new JsonApiDataStore().sync(response.json()); + }) + .catch(this.handleError); + } + create(resource: Resource): Promise { return this.http .post(