From 57687196ae26c640c6cb54ac643d295cd28e3934 Mon Sep 17 00:00:00 2001 From: Caleb Alldrin Date: Mon, 26 Feb 2024 08:28:04 -0800 Subject: [PATCH 1/9] GT-1646 Provide several filters for the overall list of tools (#702) Add filter and sort --- .../resource/resource.component.html | 10 +- .../resource/resource.component.spec.ts | 8 +- .../resources/resources.component.html | 144 +++++++++++++-- .../resources/resources.component.spec.ts | 168 ++++++++++++++++++ .../resources/resources.component.ts | 110 +++++++++++- src/app/models/filters.ts | 5 + src/styles.css | 86 +++++++++ tslint.json | 3 - 8 files changed, 514 insertions(+), 20 deletions(-) create mode 100644 src/app/models/filters.ts diff --git a/src/app/components/resource/resource.component.html b/src/app/components/resource/resource.component.html index 1f327609..f2a020de 100644 --- a/src/app/components/resource/resource.component.html +++ b/src/app/components/resource/resource.component.html @@ -1,13 +1,17 @@
{{ resource.name }} -
+
-
-
+
+
Instructions
  • @@ -64,11 +67,132 @@
    Instructions

-
- +
+
+ { let comp: ResourcesComponent; @@ -26,6 +28,71 @@ describe('ResourcesComponent', () => { } as unknown) as Language; const resource: Resource = new Resource(); + const unfilteredResources = [ + { + name: 'GodTools CYOA Tool - Hidden', + abbreviation: 'emojitool2', + description: 'Emoji survey for the holiday season', + id: 123, + system: { id: 1, name: 'GodTools' }, + manifest: '', + showTranslations: false, + oneskyProjectId: 78097, + resourceType: { id: 2, name: 'cyoa' }, + variants: [], + 'latest-drafts-translations': [], + attachments: [], + pages: [], + tips: [], + latest: [], + data: { id: 22 }, + customManifests: [], + 'attr-default-order': 2, + 'attr-hidden': true, + 'resource-type': 'cyoa', + }, + { + name: 'GodTools Tract Tool - Not Hidden', + abbreviation: 'emojitool2', + description: 'Tract for all purposes', + id: 123, + system: { id: 1, name: 'GodTools' }, + manifest: '', + showTranslations: false, + oneskyProjectId: 385532, + resourceType: { id: 1, name: 'tract' }, + variants: [], + 'latest-drafts-translations': [], + attachments: [], + pages: [], + tips: [], + latest: [], + data: { id: 13 }, + customManifests: [], + 'attr-default-order': 1, + 'resource-type': 'tract', + }, + { + name: 'Test System Lesson Tool - Not Hidden', + abbreviation: 'lesson2', + description: 'Lesson for the holiday season', + id: 123, + system: { id: 99, name: 'Test' }, + manifest: '', + showTranslations: false, + oneskyProjectId: 456858656, + resourceType: { id: 3, name: 'lesson' }, + variants: [], + 'latest-drafts-translations': [], + attachments: [], + pages: [], + tips: [], + latest: [], + data: { id: 77 }, + customManifests: [], + 'resource-type': 'lesson', + }, + ]; beforeEach(async(() => { spyOn(resourceServiceStub, 'getResources').and.returnValue( @@ -48,13 +115,31 @@ describe('ResourcesComponent', () => { { provide: LanguageService, useValue: languageServiceStub }, { provide: NgbModal }, { provide: DraftService }, + { provide: ResourceTypeService }, + { provide: SystemService }, ], }).compileComponents(); })); + let localStore; + beforeEach(() => { fixture = TestBed.createComponent(ResourcesComponent); comp = fixture.componentInstance; + + localStore = {}; + + spyOn(window.localStorage, 'getItem').and.callFake((key) => + key in localStore ? localStore[key] : null, + ); + spyOn(window.localStorage, 'setItem').and.callFake( + (key, value) => (localStore[key] = value + ''), + ); + spyOn(window.localStorage, 'clear').and.callFake(() => (localStore = {})); + }); + + afterEach(() => { + comp.clearFilters(); }); it('should include pages when loading resources', (done) => { @@ -64,7 +149,90 @@ describe('ResourcesComponent', () => { expect(resourceServiceStub.getResources).toHaveBeenCalledWith( 'latest-drafts-translations,pages,custom-manifests,tips,attachments,variants', ); + done(); + }); + }); + + it('should filter resources by hidden', (done) => { + comp.unfilteredResources = unfilteredResources; + comp.filterAndSort(); + setTimeout(() => { + comp.toggleResources('other', 'hidden'); + }); + + setTimeout(() => { + expect( + comp.resources.find( + (r) => r.name === 'GodTools Tract Tool - Not Hidden', + ), + ).toEqual(undefined); + expect( + comp.resources.find((r) => r.name === 'GodTools CYOA Tool - Hidden'), + ).toBeTruthy(); + done(); + }); + }); + + it('should filter resources by type', (done) => { + comp.unfilteredResources = unfilteredResources; + comp.filterAndSort(); + comp.toggleResources('type', 'tract'); + + setTimeout(() => { + expect( + comp.resources.find( + (r) => r.name === 'GodTools Tract Tool - Not Hidden', + ), + ).toBeTruthy(); + expect( + comp.resources.find((r) => r.name === 'GodTools CYOA Tool - Hidden'), + ).toEqual(undefined); + done(); + }); + }); + + it('should filter resources by system', (done) => { + comp.unfilteredResources = unfilteredResources; + comp.filterAndSort(); + comp.toggleResources('system', 99); + + setTimeout(() => { + expect( + comp.resources.find( + (r) => r.name === 'Test System Lesson Tool - Not Hidden', + ), + ).toBeTruthy(); + expect( + comp.resources.find((r) => r.name === 'GodTools CYOA Tool - Hidden'), + ).toEqual(undefined); + done(); + }); + }); + it('should set local storage', (done) => { + comp.unfilteredResources = unfilteredResources; + comp.filterAndSort(); + comp.toggleResources('system', 99); + const expectedData = { + type: [], + system: [99], + other: [], + }; + + setTimeout(() => { + expect(localStorage.getItem('filters')).toEqual( + JSON.stringify(expectedData), + ); + done(); + }); + }); + it('should sort by tool order', (done) => { + comp.unfilteredResources = unfilteredResources; + comp.updateSort('tool'); + setTimeout(() => { + expect(comp.resources[0].name).toEqual( + 'GodTools Tract Tool - Not Hidden', + ); done(); }); }); diff --git a/src/app/components/resources/resources.component.ts b/src/app/components/resources/resources.component.ts index 62e53f64..481af48a 100644 --- a/src/app/components/resources/resources.component.ts +++ b/src/app/components/resources/resources.component.ts @@ -5,6 +5,11 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { CreateResourceComponent } from '../edit-resource/create-resource/create-resource.component'; import { Language } from '../../models/language'; import { LanguageService } from '../../service/language.service'; +import { ResourceType } from '../../../../src/app/models/resource-type'; +import { System } from '../../../../src/app/models/system'; +import { Filters } from '../../../../src/app/models/filters'; +import { SystemService } from '../../../../src/app/service/system.service'; +import { ResourceTypeService } from '../../../../src/app/service/resource-type.service'; @Component({ selector: 'admin-resources', @@ -12,45 +17,144 @@ import { LanguageService } from '../../service/language.service'; }) export class ResourcesComponent implements OnInit { resources: Resource[]; + unfilteredResources: Resource[]; languages: Language[]; + resourceTypes: ResourceType[]; + systems: System[]; showInstructions = false; loadingResources = false; loadingLanguages = false; errorMessage: string; + filters: Filters; + totalFilters: number; + sortOrder: string; constructor( private resourceService: ResourceService, private languageService: LanguageService, private modalService: NgbModal, + protected systemService: SystemService, + protected resourceTypeService: ResourceTypeService, ) {} ngOnInit(): void { this.loadResources(); this.loadLanguages(); + this.loadFilters(); } - loadResources(): void { - this.loadingResources = true; + getBlankFilters(): Filters { + return { + type: [], + system: [], + other: [], + }; + } + loadResources(): void { this.resourceService .getResources( 'latest-drafts-translations,pages,custom-manifests,tips,attachments,variants', ) .then((resources) => { - this.resources = resources; + this.unfilteredResources = resources; + this.filterAndSort(); }) .catch(this.handleError.bind(this)) .then(() => (this.loadingResources = false)); } + filterAndSort(): void { + const localFilters = localStorage.getItem('filters'); + this.sortOrder = localStorage.getItem('sortOrder'); + this.filters = localFilters + ? JSON.parse(localFilters) + : this.getBlankFilters(); + this.totalFilters = Object.keys(this.filters).reduce( + (acc, item) => acc + this.filters[item].length, + 0, + ); + + this.resources = this.filters + ? this.unfilteredResources.filter((resource) => { + if (this.filters['type'].length) { + if (!this.filters['type'].includes(resource['resource-type'])) { + return false; + } + } + if (this.filters['system'].length) { + if (!this.filters['system'].includes(resource['system']['id'])) { + return false; + } + } + if (this.filters['other'].length) { + if ( + !resource['attr-hidden'] || + (resource['attr-hidden'] && + !this.filters['other'].includes('hidden')) + ) { + return false; + } + } + return true; + }) + : this.unfilteredResources; + + if (this.sortOrder === 'tool') { + this.resources.sort( + (a, b) => + (a['attr-default-order'] || 100) - (b['attr-default-order'] || 100), + ); + } + } + loadFilters(): void { + this.resourceTypeService.getResourceTypes().then((types) => { + this.resourceTypes = types; + }); + this.systemService.getSystems().then((systems) => { + this.systems = systems; + }); + } openCreateModal(): void { const modalRef: NgbModalRef = this.modalService.open( CreateResourceComponent, + { size: 'lg' }, ); modalRef.result.then(() => this.loadResources(), console.log); } + toggleResources = function ( + category: string, + optionId: string | number, + ): void { + const updatedFilters = this.filters[category] + ? this.filters[category].includes(optionId) + ? { + ...this.filters, + [category]: this.filters[category].filter((e) => e !== optionId), + } + : { + ...this.filters, + [category]: [...this.filters[category], optionId], + } + : { + ...this.filters, + [category]: [optionId], + }; + localStorage.setItem('filters', JSON.stringify(updatedFilters)); + this.filterAndSort(); + }; + + clearFilters = function (): void { + localStorage.setItem('filters', JSON.stringify(this.getBlankFilters())); + this.filterAndSort(); + }; + updateSort = function (order: string): void { + localStorage.setItem('sortOrder', order); + this.filterAndSort(); + }; + trackByFunction(pIx: number, pItem: Resource) { if (!pItem || pIx < 0) { return null; diff --git a/src/app/models/filters.ts b/src/app/models/filters.ts new file mode 100644 index 00000000..048312da --- /dev/null +++ b/src/app/models/filters.ts @@ -0,0 +1,5 @@ +export class Filters { + type: string[] | null; + system: number[] | null; + other: string[] | null; +} diff --git a/src/styles.css b/src/styles.css index 3aedd71b..b487aebd 100644 --- a/src/styles.css +++ b/src/styles.css @@ -6,3 +6,89 @@ max-height: 500px; overflow-y: auto; } +/* Filter Sort Bar */ +#filterSortBar { + padding: 0 0 0 3px; +} +#filterSortBar > div > span { + padding-right: 0.75rem; + font-size: 0.75rem; + float: right; + vertical-align: middle; + margin: auto 0; + font-style: italic; + color: #6c757d; +} +#filterSortBar .btn-group { + vertical-align: inherit; +} +#filterSortBar .dropdown-header { + padding: 0.3rem 0.5rem; + color: #000; +} +/* Filter/Sort Button (Within Dropdown Menu) */ +#filterSortBar .dropdown > button { + font-size: 0.75rem; + border: 0; + padding: 0.15rem 0.4rem; + background-color: rgba(0, 0, 0, 0); + color: #6c757d; + margin-right: 5px; +} +#filterSortBar .dropdown > button:hover { + background-color: rgba(0, 0, 0, 0.05); +} +#filterSortBar .dropdown > button:focus { + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 0.05); +} +#filterSortBar .dropdown > button:active { + background-color: rgba(0, 0, 0, 0.1); +} +/* Filter/Sort NavBar Buttons */ +.filter-dropdown-button { + cursor: pointer; + display: block; + width: 100%; + padding: 0.25rem 1rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; +} +.filter-dropdown-button:hover { + background-color: rgba(0, 0, 0, 0.05); +} +.filter-dropdown-button:active { + background-color: rgba(0, 0, 0, 0.1); +} +span.sortLabel { + font-size: 0.75rem; + color: #6c757d; + margin-left: 7px; +} +/* Tool Cards */ +.card-header.pointer:hover { + cursor: pointer; + background-color: rgba(52, 58, 64, 0.9) !important; +} +.card-header.pointer:active span.h4 { + color: rgba(255, 255, 255, 0.5); +} +/* Other */ +#filterBadge { + translate: 0 -50%; + top: 50%; + width: 15px; +} +@media (min-width: 576px) { + .modal-dialog.modal-lg { + max-width: 750px; + } +} diff --git a/tslint.json b/tslint.json index 4c7e7d0c..c1e3e50c 100644 --- a/tslint.json +++ b/tslint.json @@ -56,8 +56,6 @@ "no-trailing-whitespace": true, "no-unnecessary-initializer": true, "no-unused-expression": true, - "no-unused-variable": [true, "check-parameters"], - "no-use-before-declare": true, "no-var-keyword": true, "object-literal-sort-keys": false, "one-line": [ @@ -82,7 +80,6 @@ "variable-declaration": "nospace" } ], - "typeof-compare": true, "unified-signatures": true, "variable-name": false, "whitespace": [ From a7c975e2602fc33df47803f9f412cbdace0f6116 Mon Sep 17 00:00:00 2001 From: Caleb Alldrin Date: Tue, 27 Feb 2024 16:01:56 -0800 Subject: [PATCH 2/9] Rename tools to content and update the readme (#703) Rename tools to content and update readme --- README.md | 10 +++++----- .../components/resources/resources.component.html | 14 +++++++------- .../components/resources/resources.component.ts | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 478fd9ab..378f5596 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # MobileContentAdmin [![Build Status](https://travis-ci.org/CruGlobal/mobile-content-admin.svg?branch=master)](https://travis-ci.org/CruGlobal/mobile-content-admin) [![codecov](https://codecov.io/gh/CruGlobal/mobile-content-admin/branch/master/graph/badge.svg)](https://codecov.io/gh/CruGlobal/mobile-content-admin) - This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.2.7. ## Development server -Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. +Run `yarn start` for a dev server. Navigate to `http://localhost:8080/`. The app will automatically reload if you change any of the source files. ## Code scaffolding @@ -13,15 +12,16 @@ Run `ng generate component component-name` to generate a new component. You can ## Build -Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. +Run `yarn build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. ## Running unit tests -Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). +Run `yarn test` to execute the unit tests via [Karma](https://karma-runner.github.io). +If you want to only run a single test on a file, change the file's root `describe` to `fdescribe` and Jamsine will only test that file. ## Running end-to-end tests -Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). +Run `yarn e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). Before running the tests make sure you are serving the app via `ng serve`. ## Further help diff --git a/src/app/components/resources/resources.component.html b/src/app/components/resources/resources.component.html index 1de55148..8f91d45f 100644 --- a/src/app/components/resources/resources.component.html +++ b/src/app/components/resources/resources.component.html @@ -1,9 +1,9 @@
- Tools + Content
diff --git a/src/app/components/resources/resources.component.ts b/src/app/components/resources/resources.component.ts index 481af48a..87666dde 100644 --- a/src/app/components/resources/resources.component.ts +++ b/src/app/components/resources/resources.component.ts @@ -100,10 +100,10 @@ export class ResourcesComponent implements OnInit { }) : this.unfilteredResources; - if (this.sortOrder === 'tool') { + if (this.sortOrder === 'content') { this.resources.sort( (a, b) => - (a['attr-default-order'] || 100) - (b['attr-default-order'] || 100), + (a['attr-default-order'] || 1000) - (b['attr-default-order'] || 1000), ); } } From 4da3df92a1282a5de300a7809bc39e804a0b0707 Mon Sep 17 00:00:00 2001 From: Bizz <56281168+dr-bizz@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:04:14 -0500 Subject: [PATCH 3/9] Revert "Rename tools to content and update the readme (#703)" This reverts commit a7c975e2602fc33df47803f9f412cbdace0f6116. --- README.md | 10 +++++----- .../components/resources/resources.component.html | 14 +++++++------- .../components/resources/resources.component.ts | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 378f5596..478fd9ab 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # MobileContentAdmin [![Build Status](https://travis-ci.org/CruGlobal/mobile-content-admin.svg?branch=master)](https://travis-ci.org/CruGlobal/mobile-content-admin) [![codecov](https://codecov.io/gh/CruGlobal/mobile-content-admin/branch/master/graph/badge.svg)](https://codecov.io/gh/CruGlobal/mobile-content-admin) + This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.2.7. ## Development server -Run `yarn start` for a dev server. Navigate to `http://localhost:8080/`. The app will automatically reload if you change any of the source files. +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. ## Code scaffolding @@ -12,16 +13,15 @@ Run `ng generate component component-name` to generate a new component. You can ## Build -Run `yarn build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. ## Running unit tests -Run `yarn test` to execute the unit tests via [Karma](https://karma-runner.github.io). -If you want to only run a single test on a file, change the file's root `describe` to `fdescribe` and Jamsine will only test that file. +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests -Run `yarn e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). Before running the tests make sure you are serving the app via `ng serve`. ## Further help diff --git a/src/app/components/resources/resources.component.html b/src/app/components/resources/resources.component.html index 8f91d45f..1de55148 100644 --- a/src/app/components/resources/resources.component.html +++ b/src/app/components/resources/resources.component.html @@ -1,9 +1,9 @@
- Content + Tools
diff --git a/src/app/components/resources/resources.component.ts b/src/app/components/resources/resources.component.ts index 87666dde..481af48a 100644 --- a/src/app/components/resources/resources.component.ts +++ b/src/app/components/resources/resources.component.ts @@ -100,10 +100,10 @@ export class ResourcesComponent implements OnInit { }) : this.unfilteredResources; - if (this.sortOrder === 'content') { + if (this.sortOrder === 'tool') { this.resources.sort( (a, b) => - (a['attr-default-order'] || 1000) - (b['attr-default-order'] || 1000), + (a['attr-default-order'] || 100) - (b['attr-default-order'] || 100), ); } } From 45becd018d0dc95c2f9fa5577588cfd80b41f55c Mon Sep 17 00:00:00 2001 From: Caleb Alldrin Date: Fri, 1 Mar 2024 11:20:19 -0800 Subject: [PATCH 4/9] No Jira Rename tools to content and update the readme (#705) Rename tools to content and update the readme --- README.md | 10 +++++----- .../components/resources/resources.component.html | 14 +++++++------- .../resources/resources.component.spec.ts | 4 ++-- .../components/resources/resources.component.ts | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 478fd9ab..378f5596 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # MobileContentAdmin [![Build Status](https://travis-ci.org/CruGlobal/mobile-content-admin.svg?branch=master)](https://travis-ci.org/CruGlobal/mobile-content-admin) [![codecov](https://codecov.io/gh/CruGlobal/mobile-content-admin/branch/master/graph/badge.svg)](https://codecov.io/gh/CruGlobal/mobile-content-admin) - This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.2.7. ## Development server -Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. +Run `yarn start` for a dev server. Navigate to `http://localhost:8080/`. The app will automatically reload if you change any of the source files. ## Code scaffolding @@ -13,15 +12,16 @@ Run `ng generate component component-name` to generate a new component. You can ## Build -Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. +Run `yarn build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. ## Running unit tests -Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). +Run `yarn test` to execute the unit tests via [Karma](https://karma-runner.github.io). +If you want to only run a single test on a file, change the file's root `describe` to `fdescribe` and Jamsine will only test that file. ## Running end-to-end tests -Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). +Run `yarn e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). Before running the tests make sure you are serving the app via `ng serve`. ## Further help diff --git a/src/app/components/resources/resources.component.html b/src/app/components/resources/resources.component.html index 1de55148..8f91d45f 100644 --- a/src/app/components/resources/resources.component.html +++ b/src/app/components/resources/resources.component.html @@ -1,9 +1,9 @@
- Tools + Content
diff --git a/src/app/components/resources/resources.component.spec.ts b/src/app/components/resources/resources.component.spec.ts index 1cc65f27..dd82d7ea 100644 --- a/src/app/components/resources/resources.component.spec.ts +++ b/src/app/components/resources/resources.component.spec.ts @@ -225,9 +225,9 @@ describe('ResourcesComponent', () => { done(); }); }); - it('should sort by tool order', (done) => { + it('should sort by content order', (done) => { comp.unfilteredResources = unfilteredResources; - comp.updateSort('tool'); + comp.updateSort('content'); setTimeout(() => { expect(comp.resources[0].name).toEqual( diff --git a/src/app/components/resources/resources.component.ts b/src/app/components/resources/resources.component.ts index 481af48a..87666dde 100644 --- a/src/app/components/resources/resources.component.ts +++ b/src/app/components/resources/resources.component.ts @@ -100,10 +100,10 @@ export class ResourcesComponent implements OnInit { }) : this.unfilteredResources; - if (this.sortOrder === 'tool') { + if (this.sortOrder === 'content') { this.resources.sort( (a, b) => - (a['attr-default-order'] || 100) - (b['attr-default-order'] || 100), + (a['attr-default-order'] || 1000) - (b['attr-default-order'] || 1000), ); } } From 2a809557fc55696abe433ce242500e2eeda972ec Mon Sep 17 00:00:00 2001 From: Bizz <56281168+dr-bizz@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:33:33 -0400 Subject: [PATCH 5/9] Adding new async publish feature for single and bulk. (#700) * Adding new async publish feature for single and bulk. * Fixing type errors. Disabling buttons during active requests * Adding get single resource * Adding ability to publish without Draft. Added draft/live support. Now calling resource instead of allResources when fetching publish status * Adding more test coverage and ensuring the code is simplified. Fixed publishing error issue on the initial Post request to publish the language. * Adding an error label to show if the language had a publishing error. --- .../multiple-draft-generator.component.css | 17 ++ .../multiple-draft-generator.component.html | 101 +++++-- ...multiple-draft-generator.component.spec.ts | 144 +++++++++- .../multiple-draft-generator.component.ts | 264 ++++++++++++++++-- .../resource/resource.component.html | 2 +- .../translation-version-badge.component.html | 9 +- .../translation/translation.component.html | 26 +- .../translation/translation.component.spec.ts | 194 ++++++++++++- .../translation/translation.component.ts | 91 ++++-- src/app/models/message.ts | 5 + src/app/models/translation.ts | 2 +- src/app/service/draft.service.ts | 24 +- .../service/resource/resource.service.spec.ts | 39 +++ src/app/service/resource/resource.service.ts | 14 + 14 files changed, 818 insertions(+), 114 deletions(-) create mode 100644 src/app/components/multiple-draft-generator/multiple-draft-generator.component.css create mode 100644 src/app/models/message.ts 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( From c4309dc064eeb6653009d7bdcf28f1f35c5c96ea Mon Sep 17 00:00:00 2001 From: Bizz <56281168+dr-bizz@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:36:52 -0400 Subject: [PATCH 6/9] Revert "Adding new async publish feature for single and bulk. (#700)" (#706) This reverts commit 2a809557fc55696abe433ce242500e2eeda972ec. --- .../multiple-draft-generator.component.css | 17 -- .../multiple-draft-generator.component.html | 101 ++----- ...multiple-draft-generator.component.spec.ts | 144 +--------- .../multiple-draft-generator.component.ts | 264 ++---------------- .../resource/resource.component.html | 2 +- .../translation-version-badge.component.html | 9 +- .../translation/translation.component.html | 26 +- .../translation/translation.component.spec.ts | 194 +------------ .../translation/translation.component.ts | 91 ++---- src/app/models/message.ts | 5 - src/app/models/translation.ts | 2 +- src/app/service/draft.service.ts | 24 +- .../service/resource/resource.service.spec.ts | 39 --- src/app/service/resource/resource.service.ts | 14 - 14 files changed, 114 insertions(+), 818 deletions(-) delete mode 100644 src/app/components/multiple-draft-generator/multiple-draft-generator.component.css delete mode 100644 src/app/models/message.ts 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 deleted file mode 100644 index 00a27f06..00000000 --- a/src/app/components/multiple-draft-generator/multiple-draft-generator.component.css +++ /dev/null @@ -1,17 +0,0 @@ -.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 af2051fc..c4fc4aa5 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,92 +1,35 @@ 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 b0f3b4b5..b835168d 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,10 +1,4 @@ -import { - ComponentFixture, - TestBed, - discardPeriodicTasks, - fakeAsync, - tick, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NgbActiveModal, NgbAlert, @@ -13,26 +7,20 @@ 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, - selectedForAction: boolean, + generateDraft: boolean, language: string, ) => { const l = new Language(); @@ -41,57 +29,15 @@ describe('MultipleDraftGeneratorComponent', () => { const t = new Translation(); t.language = l; t.is_published = isPublished; - t['is-published'] = isPublished; - t.selectedForAction = selectedForAction; + t.generateDraft = generateDraft; 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, - TranslationVersionBadgeComponent, - ], + declarations: [MultipleDraftGeneratorComponent], imports: [NgbModule.forRoot(), FormsModule], - providers: [ - { provide: DraftService, useValue: customDraftServiceStub }, - { provide: NgbActiveModal }, - { provide: ResourceService, useValue: customResourceServiceStub }, - { provide: LanguageService }, - ], + providers: [{ provide: DraftService }, { provide: NgbActiveModal }], }).compileComponents(); fixture = TestBed.createComponent(MultipleDraftGeneratorComponent); @@ -107,18 +53,17 @@ describe('MultipleDraftGeneratorComponent', () => { const r = new Resource(); r['latest-drafts-translations'] = translations; comp.resource = r; - comp.actionType = 'publish'; fixture.detectChanges(); }); - it('shows languages with and without drafts', () => { + it('only shows languages without drafts', () => { expect( fixture.debugElement.queryAll(By.directive(NgbButtonLabel)).length, - ).toBe(4); + ).toBe(3); }); - it('shows confirm message to publish selected languages', () => { + it('confirm message lists all languages', () => { comp.showConfirmAlert(); fixture.detectChanges(); @@ -126,78 +71,7 @@ describe('MultipleDraftGeneratorComponent', () => { By.directive(NgbAlert), ); expect(alert.nativeElement.textContent).toContain( - `Are you sure you want to publish these languages: Chinese, French?`, + `${comp.baseConfirmMessage} 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 aea296aa..1947d11e 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,275 +1,59 @@ -import { Component, OnDestroy } from '@angular/core'; +import { Component } 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 implements OnDestroy { +export class MultipleDraftGeneratorComponent { resource: Resource; translations: Translation[]; - actionType: ActionType = 'publish'; + confirmMessage: string; - errorMessage: string[]; - sucessfulMessages: string[]; - alertMessage: string; - sucessfulMessage: string; - checkToEnsureTranslationIsPublished: number; - disableButtons: boolean; + saving: boolean; + errorMessage: string; + + readonly baseConfirmMessage = + 'Are you sure you want to generate a draft for these languages:'; 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.selectedForAction, + (translation) => translation.generateDraft, ); if (this.translations.length === 0) { return; } - const selectedTranslations = this.translations + const message = this.translations .map((translation) => translation.language.name) .join(', '); - 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}?`; - } + this.confirmMessage = `${this.baseConfirmMessage} ${message}?`; } - async publishOrCreateDrafts(): Promise { - this.confirmMessage = null; - this.errorMessage = []; - const promises: APICall[] = []; - this.disableButtons = true; + generateDrafts(): void { + this.saving = true; + this.errorMessage = null; - // 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), - ); + this.translations.forEach((translation, index) => { + this.draftService + .createDraft(translation) + .then(() => { + if (index === this.translations.length - 1) { + this.ngbActiveModal.close(); } - }), - ); - - // 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']; + }) + .catch((message) => { + this.saving = false; + this.errorMessage = message; }); }); } diff --git a/src/app/components/resource/resource.component.html b/src/app/components/resource/resource.component.html index 0674456d..f2a020de 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()" > - Bulk Actions + Generate multiple drafts
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 7cd84fa2..b315a268 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,16 +1,9 @@ {{ 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 0d00c9be..34bcdb90 100644 --- a/src/app/components/translation/translation.component.html +++ b/src/app/components/translation/translation.component.html @@ -38,7 +38,7 @@

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

> Download -

-
- +
- - {{ sucessfulMessage }} + + Publishing... - - {{ alertMessage }} + + Saving... {{ errorMessage }} diff --git a/src/app/components/translation/translation.component.spec.ts b/src/app/components/translation/translation.component.spec.ts index c2377003..24c9f8f5 100644 --- a/src/app/components/translation/translation.component.spec.ts +++ b/src/app/components/translation/translation.component.spec.ts @@ -1,11 +1,4 @@ -import { - async, - ComponentFixture, - discardPeriodicTasks, - fakeAsync, - TestBed, - tick, -} from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslationComponent } from './translation.component'; import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DraftService } from '../../service/draft.service'; @@ -19,12 +12,10 @@ 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 { CustomTipService } from '../../service/custom-tip.service'; import { CustomManifest } from '../../models/custom-manifest'; -import { MessageType } from '../../models/message'; +import { CustomTipService } from '../../service/custom-tip.service'; import { TranslationVersionBadgeComponent } from './translation-version-badge/translation-version-badge.component'; describe('TranslationComponent', () => { @@ -35,8 +26,6 @@ describe('TranslationComponent', () => { let customTipsServiceStub; let modalServiceStub; let customManifestServiceStub; - let customDraftServiceStub; - let customResourceServiceStub; let resourceComponent: ResourceComponent; let language: Language; @@ -84,12 +73,6 @@ describe('TranslationComponent', () => { modalServiceStub = { open() {}, }; - customDraftServiceStub = { - publishDraft() {}, - }; - customResourceServiceStub = { - getResource() {}, - }; const modalRef = { componentInstance: {}, result: Promise.resolve(), @@ -104,41 +87,20 @@ 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, useValue: customDraftServiceStub }, + { provide: DraftService }, { provide: CustomPageService, useValue: customPageServiceStub }, { provide: CustomTipService, useValue: customTipsServiceStub }, { provide: CustomManifestService, useValue: customManifestServiceStub }, { provide: NgbModal, useValue: modalServiceStub }, - { provide: ResourceService, useValue: customResourceServiceStub }, ], }).compileComponents(); })); @@ -149,9 +111,6 @@ describe('TranslationComponent', () => { resourceComponent = new ResourceComponent(null, null); comp.translationLoaded = resourceComponent.translationLoaded$; - comp.errorMessage = ''; - comp.alertMessage = ''; - comp.sucessfulMessage = ''; const pageWithCustomPage = buildPage(2); @@ -165,7 +124,6 @@ describe('TranslationComponent', () => { comp.language = language; const resource = new Resource(); - resource.id = 15; resource.pages = [buildPage(1), pageWithCustomPage]; resource.tips = []; resource['custom-manifests'] = [ @@ -181,11 +139,11 @@ describe('TranslationComponent', () => { fixture.detectChanges(); }); - it(`should show action button with 'Publish'`, () => { + it(`should show action button with 'New Draft'`, () => { const element: DebugElement = fixture.debugElement - .queryAll(By.css('.btn.btn-success')) + .queryAll(By.css('.btn.btn-secondary')) .pop(); - expect(element.nativeElement.textContent.trim()).toBe('Publish'); + expect(element.nativeElement.textContent.trim()).toBe('New Draft'); }); it(`should show status badge with 'None'`, () => { @@ -194,140 +152,6 @@ 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)', () => { @@ -462,15 +286,15 @@ describe('TranslationComponent', () => { }); describe('action button', () => { - it(`should say 'Publish' for published translations`, () => { + it(`should say 'New Draft' for published translations`, () => { translation.is_published = true; fixture.detectChanges(); const element: DebugElement = fixture.debugElement - .queryAll(By.css('.btn.btn-success')) + .queryAll(By.css('.btn.btn-secondary')) .pop(); - expect(element.nativeElement.textContent.trim()).toBe('Publish'); + expect(element.nativeElement.textContent.trim()).toBe('New Draft'); }); 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 72b3f261..46b3cb6c 100644 --- a/src/app/components/translation/translation.component.ts +++ b/src/app/components/translation/translation.component.ts @@ -6,7 +6,6 @@ import { OnChanges, Output, EventEmitter, - OnDestroy, } from '@angular/core'; import { Translation } from '../../models/translation'; import { DraftService } from '../../service/draft.service'; @@ -30,14 +29,12 @@ 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, OnDestroy { +export class TranslationComponent implements OnInit, OnChanges { @Input() language: Language; @Input() resource: Resource; @Input() translationLoaded: Observable; @@ -46,11 +43,10 @@ export class TranslationComponent implements OnInit, OnChanges, OnDestroy { 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, @@ -58,7 +54,6 @@ export class TranslationComponent implements OnInit, OnChanges, OnDestroy { private draftService: DraftService, private customManifestService: CustomManifestService, private modalService: NgbModal, - private resourceService: ResourceService, ) {} ngOnInit(): void { @@ -81,10 +76,6 @@ export class TranslationComponent implements OnInit, OnChanges, OnDestroy { } } - ngOnDestroy() { - clearInterval(this.checkToEnsureDraftIsPublished); - } - getPages(): AbstractPage[] { const _tPages = this.translation.resource.pages.map((page) => { const customPage: CustomPage = this.translation.language[ @@ -147,65 +138,29 @@ export class TranslationComponent implements OnInit, OnChanges, OnDestroy { return tip as Tip; } - renderMessage(type: MessageType, text: string, time?: number) { - this[`${type}Message`] = text; - if (time) { - setTimeout(() => { - this[`${type}Message`] = ''; - }, time); - } - } + publishDraft(): void { + this.publishing = true; + this.errorMessage = null; + + const t = Translation.copy(this.translation); + t.is_published = true; - async publish(): Promise { - this.renderMessage(MessageType.error, ''); - this.renderMessage(MessageType.success, 'Publishing...'); this.draftService - .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)); + .updateDraft(t) + .then(() => this.loadAllResources()) + .catch(this.handleError.bind(this)) + .then(() => (this.publishing = 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); - } + 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)); } createCustomPage(page: Page): void { diff --git a/src/app/models/message.ts b/src/app/models/message.ts deleted file mode 100644 index 1726c44e..00000000 --- a/src/app/models/message.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum MessageType { - success = 'sucessful', - alert = 'alert', - error = 'error', -} diff --git a/src/app/models/translation.ts b/src/app/models/translation.ts index 736ae170..5d12769a 100644 --- a/src/app/models/translation.ts +++ b/src/app/models/translation.ts @@ -9,7 +9,7 @@ export class Translation { resource: Resource; version: number; - selectedForAction: boolean; + generateDraft: 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 f97f4e2b..807a0a3c 100644 --- a/src/app/service/draft.service.ts +++ b/src/app/service/draft.service.ts @@ -7,12 +7,10 @@ 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(); @@ -61,29 +59,19 @@ export class DraftService extends AbstractService { .catch(this.handleError); } - publishDraft( - resource: Resource, - translation: Translation, - ): Promise { + updateDraft(translation: Translation): Promise { const payload = { data: { - type: 'publish-translations', - relationships: { - languages: { - data: [ - { - id: translation.language.id, - type: 'language', - }, - ], - }, + type: 'translation', + attributes: { + is_published: translation.is_published, }, }, }; return this.http - .post( - `${this.resourcesUrl}/${resource.id}/translations/publish`, + .put( + `${this.draftsUrl}/${translation.id}`, 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 bc2d26a0..f1cb5c06 100644 --- a/src/app/service/resource/resource.service.spec.ts +++ b/src/app/service/resource/resource.service.spec.ts @@ -4,8 +4,6 @@ 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 = {}; @@ -24,7 +22,6 @@ describe('ResourceService', () => { const service = new ResourceService(mockHttp, mockAuthService); const resource = new Resource(); - resource.id = 13; beforeEach(() => { spyOn(mockHttp, 'post').and.returnValue( @@ -41,10 +38,6 @@ 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', () => { @@ -58,36 +51,4 @@ 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 22efc690..9f4ec616 100644 --- a/src/app/service/resource/resource.service.ts +++ b/src/app/service/resource/resource.service.ts @@ -27,20 +27,6 @@ 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( From de55548cb603b4357422eaeaf4c1b54ab469e7f0 Mon Sep 17 00:00:00 2001 From: Bizz <56281168+dr-bizz@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:48:34 -0400 Subject: [PATCH 7/9] GT-1968 - Adding new async publish feature for single and bulk. (#707) * Adding new async publish feature for single and bulk. * Fixing type errors. Disabling buttons during active requests * Adding get single resource * Adding ability to publish without Draft. Added draft/live support. Now calling resource instead of allResources when fetching publish status * Adding more tests coverage and ensuring the code is simplified. Fixed publishing error issue on the initial Post request to publish the language. * Adding an error label to show if the language had a publishing error. --- .../multiple-draft-generator.component.css | 17 ++ .../multiple-draft-generator.component.html | 101 +++++-- ...multiple-draft-generator.component.spec.ts | 144 +++++++++- .../multiple-draft-generator.component.ts | 264 ++++++++++++++++-- .../resource/resource.component.html | 2 +- .../translation-version-badge.component.html | 9 +- .../translation/translation.component.html | 26 +- .../translation/translation.component.spec.ts | 194 ++++++++++++- .../translation/translation.component.ts | 91 ++++-- src/app/models/message.ts | 5 + src/app/models/translation.ts | 2 +- src/app/service/draft.service.ts | 24 +- .../service/resource/resource.service.spec.ts | 39 +++ src/app/service/resource/resource.service.ts | 14 + 14 files changed, 818 insertions(+), 114 deletions(-) create mode 100644 src/app/components/multiple-draft-generator/multiple-draft-generator.component.css create mode 100644 src/app/models/message.ts 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( From e045d2f6db6e017fdc713109f4c364602ba84ec4 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Mon, 22 Apr 2024 10:10:44 -0400 Subject: [PATCH 8/9] Fixing Praxis values sent to server (1-5) --- .../service/tool-group/tool-group.service.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/service/tool-group/tool-group.service.ts b/src/app/service/tool-group/tool-group.service.ts index bfa9f4c5..4bb21e1e 100644 --- a/src/app/service/tool-group/tool-group.service.ts +++ b/src/app/service/tool-group/tool-group.service.ts @@ -18,37 +18,37 @@ export class ToolGroupService extends AbstractService { private readonly toolGroupsUrl = environment.base_url + 'tool-groups'; praxisConfidentData = { - 0: { + 1: { name: 'Very confident', }, - 1: { + 2: { name: 'Somewhat confident', }, - 2: { + 3: { name: 'Neutral', }, - 3: { + 4: { name: 'Not very confident', }, - 4: { + 5: { name: 'Not confident at all', }, }; praxisOpennessData = { - 0: { + 1: { name: 'Very Open', }, - 1: { + 2: { name: 'Somewhat open', }, - 2: { + 3: { name: 'Neutral', }, - 3: { + 4: { name: 'Not very open or interested', }, - 4: { + 5: { name: 'Not open at all', }, }; From 3255402c8162cdd573a178ee27c3dc39bfda08db Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Mon, 22 Apr 2024 10:11:07 -0400 Subject: [PATCH 9/9] Allow Praxis rules to be edited one at a time. --- src/app/components/tool-group/tool-group.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/tool-group/tool-group.component.html b/src/app/components/tool-group/tool-group.component.html index 6fedff77..7fcb3900 100644 --- a/src/app/components/tool-group/tool-group.component.html +++ b/src/app/components/tool-group/tool-group.component.html @@ -195,7 +195,7 @@ *ngFor="let rule of toolGroup['rules-praxis']" >
  • Rule