Skip to content

Commit

Permalink
Feature/cxint 2989 download proposal (#18837)
Browse files Browse the repository at this point in the history
Co-authored-by: Bose <[email protected]>
Co-authored-by: anjana-bl <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Larisa Staroverova <[email protected]>
  • Loading branch information
5 people authored May 21, 2024
1 parent 11540c3 commit 71ead56
Show file tree
Hide file tree
Showing 22 changed files with 541 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .env-cmdrc
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@
"CX_BASE_URL": "https://api.cg79x9wuu9-eccommerc1-s5-public.model-t.myhybris.cloud",
"CX_OPPS": "true"
}
}
}
4 changes: 3 additions & 1 deletion feature-libs/quote/assets/translations/en/quote.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,11 @@
"links": {
"newCart": "New Cart",
"quotes": "Quotes",
"download": "Download Proposal",
"a11y": {
"newCart": "Create new empty cart and navigate to it.",
"quotes": "Navigate to quote search result list."
"quotes": "Navigate to quote search result list.",
"download": "Start downloading a quote proposal"
}
},
"list": {
Expand Down
20 changes: 20 additions & 0 deletions feature-libs/quote/components/links/quote-links.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@
{{ 'quote.links.quotes' | cxTranslate }}
</a>
</li>
<li
*ngIf="
hasAttachment(quoteDetails.sapAttachments!) &&
isShowDownloadProposalButtonFeatureEnabled()
"
>
<button
[attr.aria-label]="'quote.links.a11y.download' | cxTranslate"
type="button"
class="link cx-action-link"
(click)="
onDownloadAttachment(
quoteDetails.code,
quoteDetails.sapAttachments!
)
"
>
{{ 'quote.links.download' | cxTranslate }}
</button>
</li>
</ul>
</section>
</ng-container>
113 changes: 111 additions & 2 deletions feature-libs/quote/components/links/quote-links.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
} from '@angular/core/testing';
import { Router, Routes } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { EventService, I18nTestingModule, Price } from '@spartacus/core';
import {
EventService,
I18nTestingModule,
Price,
FeatureConfigService,
} from '@spartacus/core';
import {
CartUtilsService,
QuoteDetailsReloadQueryEvent,
Expand All @@ -17,8 +22,9 @@ import {
QuoteFacade,
QuoteState,
} from '@spartacus/quote/root';
import { FileDownloadService } from '@spartacus/storefront';
import { UrlTestingModule } from 'projects/core/src/routing/configurable-routes/url-translation/testing/url-testing.module';
import { BehaviorSubject, NEVER, Observable } from 'rxjs';
import { BehaviorSubject, NEVER, Observable, of } from 'rxjs';
import { createEmptyQuote } from '../../core/testing/quote-test-utils';
import { CommonQuoteTestUtilsService } from '../testing/common-quote-test-utils.service';
import { QuoteLinksComponent } from './quote-links.component';
Expand Down Expand Up @@ -48,12 +54,34 @@ const mockQuote: Quote = {
totalPrice: totalPrice,
};

const mockQuoteAttachment = (): File => {
const blob = new Blob([''], { type: 'application/pdf' });
return blob as File;
};

const mockQuoteDetails$ = new BehaviorSubject<Quote>(mockQuote);

class MockCommerceQuotesFacade implements Partial<QuoteFacade> {
getQuoteDetails(): Observable<Quote> {
return mockQuoteDetails$.asObservable();
}

downloadAttachment(
_quoteCode: string,
_attachmentId: string
): Observable<Blob> {
return of(mockQuoteAttachment());
}
}

class MockFileDownloadService {
download(_url: string, _fileName?: string): void {}
}

class MockFeatureConfigService {
isEnabled(_feature: string): boolean {
return true;
}
}

describe('QuoteLinksComponent', () => {
Expand All @@ -63,6 +91,8 @@ describe('QuoteLinksComponent', () => {
let cartUtilsService: CartUtilsService;
let router: Router;
let eventService: EventService;
let quoteFacade: QuoteFacade;
let fileDownloadService: FileDownloadService;

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -81,6 +111,14 @@ describe('QuoteLinksComponent', () => {
provide: CartUtilsService,
useClass: MockCartUtilsService,
},
{
provide: FileDownloadService,
useClass: MockFileDownloadService,
},
{
provide: FeatureConfigService,
useClass: MockFeatureConfigService,
},
],
}).compileComponents();
});
Expand All @@ -91,6 +129,8 @@ describe('QuoteLinksComponent', () => {
cartUtilsService = TestBed.inject(CartUtilsService);
eventService = TestBed.inject(EventService);
router = TestBed.inject(Router);
quoteFacade = TestBed.inject(QuoteFacade);
fileDownloadService = TestBed.inject(FileDownloadService);
component = fixture.componentInstance;
mockQuoteDetails$.next(mockQuote);
fixture.detectChanges();
Expand Down Expand Up @@ -168,4 +208,73 @@ describe('QuoteLinksComponent', () => {

expect(router.url).toBe('/cxRoute:quotes');
}));

describe('Download proposal document', () => {
const vendorQuote: Quote = {
...mockQuote,
sapAttachments: [
{
id: mockQuote.code,
},
],
};

it('should display download button if there is a proposal document attached to the quote', () => {
mockQuoteDetails$.next(vendorQuote);
fixture.detectChanges();
const buttonContainerSection = CommonQuoteTestUtilsService.getHTMLElement(
htmlElem,
'section'
);
CommonQuoteTestUtilsService.expectElementPresent(
expect,
buttonContainerSection,
'button'
);
CommonQuoteTestUtilsService.expectElementToContainText(
expect,
htmlElem,
'button',
'download'
);
});

it('should not display download button if there is no proposal document attached to the quote', () => {
mockQuoteDetails$.next(mockQuote);
fixture.detectChanges();
const buttonContainerSection = CommonQuoteTestUtilsService.getHTMLElement(
htmlElem,
'section'
);
CommonQuoteTestUtilsService.expectElementNotPresent(
expect,
buttonContainerSection,
'button'
);
});

it('should download the proposal document attached when Download button is clicked', () => {
const spyDownloadAttachment = spyOn(
quoteFacade,
'downloadAttachment'
).and.returnValue(of(mockQuoteAttachment()));
const spyDownload = spyOn(fileDownloadService, 'download');
mockQuoteDetails$.next(vendorQuote);
fixture.detectChanges();
const downloadBtn = CommonQuoteTestUtilsService.getHTMLElement(
htmlElem,
'button'
);
downloadBtn.click();
fixture.detectChanges();
expect(spyDownloadAttachment).toHaveBeenCalledWith(
vendorQuote.code,
vendorQuote.code
);
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(spyDownload).toHaveBeenCalled();
});
});
});
});
43 changes: 41 additions & 2 deletions feature-libs/quote/components/links/quote-links.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
*/

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { EventService } from '@spartacus/core';
import { EventService, FeatureConfigService } from '@spartacus/core';
import {
CartUtilsService,
QuoteDetailsReloadQueryEvent,
} from '@spartacus/quote/core';
import { Quote, QuoteFacade } from '@spartacus/quote/root';
import { Quote, QuoteAttachment, QuoteFacade } from '@spartacus/quote/root';
import { FileDownloadService } from '@spartacus/storefront';
import { Observable } from 'rxjs';

@Component({
Expand All @@ -22,6 +23,8 @@ export class QuoteLinksComponent {
protected quoteFacade = inject(QuoteFacade);
protected cartUtilsService = inject(CartUtilsService);
protected eventService = inject(EventService);
protected fileDownloadService = inject(FileDownloadService);
private featureConfig = inject(FeatureConfigService);

quoteDetails$: Observable<Quote> = this.quoteFacade.getQuoteDetails();

Expand All @@ -34,4 +37,40 @@ export class QuoteLinksComponent {
this.eventService.dispatch({}, QuoteDetailsReloadQueryEvent);
this.cartUtilsService.goToNewCart();
}

/**
* Click handler for download button.
*
* @param quoteCode - The quote ID (aka code)
* @param attachments - Array of attachments belonging to the quote. It is expected to contain only 1 entry.
*/
onDownloadAttachment(quoteCode: string, attachments: QuoteAttachment[]) {
const attachmentId = attachments[0].id;
const filename = attachments[0].filename || attachmentId;
this.quoteFacade
.downloadAttachment(quoteCode, attachmentId)
.subscribe((res) => {
const url = URL.createObjectURL(new Blob([res], { type: res.type }));
this.fileDownloadService.download(url, `${filename}.pdf`);
});
}

/**
* Determines if there is any document attached with the quote.
*
* @param attachments - an array of attachments to the quote
* @returns - if the document is present, returns 'true', otherwise 'false'.
*/
hasAttachment(attachments: QuoteAttachment[]): boolean {
return attachments?.length > 0;
}

/**
* Determines if the feature for showing the download button is enabled.
*
* @returns - if the feature is enabled, returns 'true', otherwise 'false'.
*/
isShowDownloadProposalButtonFeatureEnabled(): boolean {
return this.featureConfig.isEnabled('showDownloadProposalButton');
}
}
14 changes: 14 additions & 0 deletions feature-libs/quote/core/connectors/quote.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,18 @@ export abstract class QuoteAdapter {
entryNumber: string,
comment: QuoteComment
): Observable<unknown>;

/**
* Downloads the proposal document associated with a quote.
*
* @param userId - Quote user
* @param quoteCode - Quote code
* @param attachmentId - Attachment ID
* @returns Observable emitting a Blob response
*/
abstract downloadAttachment(
userId: string,
quoteCode: string,
attachmentId: string
): Observable<Blob>;
}
22 changes: 22 additions & 0 deletions feature-libs/quote/core/connectors/quote.connector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import createSpy = jasmine.createSpy;
const userId = 'user1';
const cartId = 'cart1';
const quoteCode = 'quote1';
const attachmentId = 'attachment1';
const quoteEntryNumber = 'entryNumber1';
const pagination = {
currentPage: 1,
Expand Down Expand Up @@ -71,6 +72,11 @@ class MockCommerceQuotesAdapter implements Partial<QuoteAdapter> {
`addQuoteEntryComment-${userId}-${quoteCode}-${entryNumber}-${comment}`
)
);
downloadAttachment = createSpy(
'CommerceQuotesAdapter.downloadAttachment'
).and.callFake((userId: string, quoteCode: string, attachmentId: string) =>
of(`downloadAttachment-${userId}-${quoteCode}-${attachmentId}`)
);
}

describe('QuoteConnector', () => {
Expand Down Expand Up @@ -214,4 +220,20 @@ describe('QuoteConnector', () => {
comment
);
});

it('downloadAttachment should call adapter', () => {
let result;
classUnderTest
.downloadAttachment(userId, quoteCode, attachmentId)
.pipe(take(1))
.subscribe((res) => (result = res));
expect(result).toBe(
`downloadAttachment-${userId}-${quoteCode}-${attachmentId}`
);
expect(quoteAdapter.downloadAttachment).toHaveBeenCalledWith(
userId,
quoteCode,
attachmentId
);
});
});
20 changes: 20 additions & 0 deletions feature-libs/quote/core/connectors/quote.connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,24 @@ export class QuoteConnector {
comment
);
}

/**
* Downloads the proposal document associated with a quote.
*
* @param userId - Quote user
* @param quoteCode - Quote code
* @param attachmentId - Attachment ID
* @returns Observable emitting a Blob response
*/
downloadAttachment(
userId: string,
quoteCode: string,
attachmentId: string
): Observable<Blob> {
return this.quoteAdapter.downloadAttachment(
userId,
quoteCode,
attachmentId
);
}
}
Loading

0 comments on commit 71ead56

Please sign in to comment.