From 1651e9d7be1c03909a64c6134b62eecf38b2af2d Mon Sep 17 00:00:00 2001 From: Arjun Date: Fri, 1 Dec 2023 11:39:47 +0530 Subject: [PATCH 1/2] feat: added the routing guard for beta pages (#2627) * added the routing guard for beta pages * added service method --- .../beta-page-feature-flag.guard.spec.ts | 77 +++++++++++++++++++ .../guards/beta-page-feature-flag.guard.ts | 32 ++++++++ src/app/core/services/org-settings.service.ts | 9 +++ 3 files changed, 118 insertions(+) create mode 100644 src/app/core/guards/beta-page-feature-flag.guard.spec.ts create mode 100644 src/app/core/guards/beta-page-feature-flag.guard.ts diff --git a/src/app/core/guards/beta-page-feature-flag.guard.spec.ts b/src/app/core/guards/beta-page-feature-flag.guard.spec.ts new file mode 100644 index 0000000000..cb6f3eef05 --- /dev/null +++ b/src/app/core/guards/beta-page-feature-flag.guard.spec.ts @@ -0,0 +1,77 @@ +import { ActivatedRoute, Router } from '@angular/router'; +import { OrgSettingsService } from '../services/org-settings.service'; +import { BetaPageFeatureFlagGuard } from './beta-page-feature-flag.guard'; +import { TestBed } from '@angular/core/testing'; +import { Observable, of } from 'rxjs'; +import { routerStateSnapshotData } from '../mock-data/router-state-snapshot.data'; + +describe('BetaPageFeatureFlagGuard', () => { + let guard: BetaPageFeatureFlagGuard; + let orgSettingsService: jasmine.SpyObj; + let router: jasmine.SpyObj; + let activatedRoute: jasmine.SpyObj; + + beforeEach(() => { + const orgSettingsServiceSpy = jasmine.createSpyObj('OrgSettingsService', ['isBetaPageEnabledForPath']); + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + + TestBed.configureTestingModule({ + providers: [ + { + provide: OrgSettingsService, + useValue: orgSettingsServiceSpy, + }, + { + provide: Router, + useValue: routerSpy, + }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + routeConfig: { + path: 'my_view_report', + }, + data: { + url: '/enterprise/my_view_report', + root: null, + }, + }, + }, + }, + ], + }); + guard = TestBed.inject(BetaPageFeatureFlagGuard); + orgSettingsService = TestBed.inject(OrgSettingsService) as jasmine.SpyObj; + activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; + router = TestBed.inject(Router) as jasmine.SpyObj; + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); + + describe('canActivate(): ', () => { + it('Should navigate to the beta page if the feature flag is enabled', (done) => { + orgSettingsService.isBetaPageEnabledForPath.and.returnValue(of(true)); + const canActivate = guard.canActivate(activatedRoute.snapshot, routerStateSnapshotData) as Observable; + canActivate.subscribe((res) => { + expect(orgSettingsService.isBetaPageEnabledForPath).toHaveBeenCalledTimes(1); + expect(res).toBeFalse(); + expect(router.navigate).toHaveBeenCalledWith(['/', 'enterprise', 'my_view_report_beta', {}]); + done(); + }); + }); + + it('Should navigate to the correct page if the feature flag is disabled', (done) => { + orgSettingsService.isBetaPageEnabledForPath.and.returnValue(of(false)); + const canActivate = guard.canActivate(activatedRoute.snapshot, routerStateSnapshotData) as Observable; + canActivate.subscribe((res) => { + expect(orgSettingsService.isBetaPageEnabledForPath).toHaveBeenCalledTimes(1); + expect(res).toBeTrue(); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + }); + }); +}); diff --git a/src/app/core/guards/beta-page-feature-flag.guard.ts b/src/app/core/guards/beta-page-feature-flag.guard.ts new file mode 100644 index 0000000000..6301ae3d73 --- /dev/null +++ b/src/app/core/guards/beta-page-feature-flag.guard.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { Observable, map, of } from 'rxjs'; +import { OrgSettingsService } from '../services/org-settings.service'; + +@Injectable({ + providedIn: 'root', +}) +export class BetaPageFeatureFlagGuard implements CanActivate { + constructor(private orgSettingsService: OrgSettingsService, private router: Router) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Observable | Promise | boolean | UrlTree { + const currentPath = route.routeConfig && route.routeConfig.path; + + if (currentPath) { + return this.orgSettingsService.isBetaPageEnabledForPath(currentPath).pipe( + map((isBetaPageEnabled) => { + if (isBetaPageEnabled) { + this.router.navigate(['/', 'enterprise', `${currentPath}_beta`, { ...route.params }]); + return false; + } + return true; + }) + ); + } else { + return of(true); + } + } +} diff --git a/src/app/core/services/org-settings.service.ts b/src/app/core/services/org-settings.service.ts index d6da4af8ea..d7a3bd9a80 100644 --- a/src/app/core/services/org-settings.service.ts +++ b/src/app/core/services/org-settings.service.ts @@ -36,6 +36,15 @@ export class OrgSettingsService { return this.apiService.post('/org/settings', data); } + isBetaPageEnabledForPath(currentPath: string): Observable { + const pathSettingsFlagMap = { + my_view_report: 'mobile_app_view_report_beta_enabled', + view_team_report: 'mobile_app_view_report_beta_enabled', + }; + const featureFlag = pathSettingsFlagMap[currentPath]; + return this.get().pipe(map((orgSettings: OrgSettings) => orgSettings[featureFlag])); + } + getIncomingAccountingObject(incomingAccountExport: AccountingExportSettings): IncomingAccountObject { const accounting: IncomingAccountObject = { enabled: false, From af24455e692333f63eb385a564bc46a02f30d97e Mon Sep 17 00:00:00 2001 From: Omkar Joshi <65808188+OmkarJ13@users.noreply.github.com> Date: Mon, 4 Dec 2023 10:58:16 +0530 Subject: [PATCH 2/2] fix: Billable field not auto-filling the default value (#2628) --- .../core/services/expense-fields.service.ts | 20 ++++++++++--------- .../add-edit-per-diem-1.page.spec.ts | 5 ++++- .../add-edit-per-diem.page.ts | 9 ++++++++- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/app/core/services/expense-fields.service.ts b/src/app/core/services/expense-fields.service.ts index 4f864e67b0..6c1b4e6d52 100644 --- a/src/app/core/services/expense-fields.service.ts +++ b/src/app/core/services/expense-fields.service.ts @@ -12,6 +12,7 @@ import { OrgCategory } from '../models/v1/org-category.model'; import { AuthService } from './auth.service'; import { DateService } from './date.service'; import { SpenderPlatformV1ApiService } from './spender-platform-v1-api.service'; +import { cloneDeep } from 'lodash'; @Injectable({ providedIn: 'root', @@ -20,7 +21,7 @@ export class ExpenseFieldsService { constructor( private spenderPlatformV1ApiService: SpenderPlatformV1ApiService, private authService: AuthService, - private dateService: DateService, + private dateService: DateService ) {} @Cacheable() @@ -33,10 +34,10 @@ export class ExpenseFieldsService { is_enabled: 'eq.true', is_custom: 'eq.false', }, - }), + }) ), map((res) => this.transformFrom(res.data)), - map((res) => this.dateService.fixDates(res)), + map((res) => this.dateService.fixDates(res)) ); } @@ -131,14 +132,14 @@ export class ExpenseFieldsService { expenseFieldMap[expenseField.column_name] = expenseFieldsList; }); return expenseFieldMap; - }), + }) ); } filterByOrgCategoryId( tfcMap: Partial, fields: string[], - orgCategory: OrgCategory, + orgCategory: OrgCategory ): Observable> { const orgCategoryId = orgCategory && orgCategory.id; return of(fields).pipe( @@ -171,7 +172,7 @@ export class ExpenseFieldsService { } return filteredField; }) - .filter((filteredField) => !!filteredField), + .filter((filteredField) => !!filteredField) ), switchMap((fields) => from(fields)), map((field) => ({ @@ -180,7 +181,7 @@ export class ExpenseFieldsService { reduce((acc, curr) => { acc[curr.field] = curr; return acc; - }, {}), + }, {}) ); } @@ -194,7 +195,7 @@ export class ExpenseFieldsService { To handle both case added this, it can take the type based on use case, but, ideally, we should have a single type of response */ getDefaultTxnFieldValues( - txnFields: Partial | Partial, + txnFields: Partial | Partial ): Partial { const defaultValues = {}; for (const configurationColumn in txnFields) { @@ -210,7 +211,8 @@ export class ExpenseFieldsService { } private formatBillableFields(expenseFields: ExpenseField[]): ExpenseField[] { - return expenseFields.map((field) => { + const expenseFieldsCopy = cloneDeep(expenseFields); + return expenseFieldsCopy.map((field) => { if (!field.is_custom && field.field_name.toLowerCase() === 'billable') { field.default_value = field.default_value === 'true'; } diff --git a/src/app/fyle/add-edit-per-diem/add-edit-per-diem-1.page.spec.ts b/src/app/fyle/add-edit-per-diem/add-edit-per-diem-1.page.spec.ts index 1ac6d280f8..f5b8b3f7f8 100644 --- a/src/app/fyle/add-edit-per-diem/add-edit-per-diem-1.page.spec.ts +++ b/src/app/fyle/add-edit-per-diem/add-edit-per-diem-1.page.spec.ts @@ -52,6 +52,7 @@ import { txnFieldsData2 } from 'src/app/core/mock-data/expense-field-obj.data'; import { defaultTxnFieldValuesData2 } from 'src/app/core/mock-data/default-txn-field-values.data'; import { orgSettingsCCCDisabled } from 'src/app/core/mock-data/org-settings.data'; import { ExpenseType } from 'src/app/core/enums/expense-type.enum'; +import { expectedProjectsResponse } from 'src/app/core/test-data/projects.spec.data'; export function TestCases1(getTestBed) { return describe('add-edit-per-diem test cases set 1', () => { @@ -553,12 +554,13 @@ export function TestCases1(getTestBed) { ); expenseFieldsService.filterByOrgCategoryId.and.returnValue(of(mockTxnFieldData)); expenseFieldsService.getDefaultTxnFieldValues.and.returnValue(defaultTxnFieldValuesData2); + component.fg.controls.project.setValue(expectedProjectsResponse[0]); component.fg.controls.purpose.setValue(''); component.fg.controls.costCenter.setValue(null); component.fg.controls.from_dt.setValue('2023-01-01'); component.fg.controls.num_days.setValue(32); component.fg.controls.to_dt.setValue('2023-02-02'); - component.fg.controls.billable.setValue(true); + component.fg.controls.billable.setValue(null); component.setupTfcDefaultValues(); @@ -572,6 +574,7 @@ export function TestCases1(getTestBed) { expect(expenseFieldsService.getDefaultTxnFieldValues).toHaveBeenCalledOnceWith(mockTxnFieldData); expect(component.fg.controls.costCenter.value).toEqual(15818); expect(component.fg.controls.purpose.value).toEqual('test_term'); + expect(component.fg.controls.billable.value).toEqual(true); }); it('getPaymentModes(): should get payment modes', (done) => { diff --git a/src/app/fyle/add-edit-per-diem/add-edit-per-diem.page.ts b/src/app/fyle/add-edit-per-diem/add-edit-per-diem.page.ts index e1a94ca5ad..73589e16ba 100644 --- a/src/app/fyle/add-edit-per-diem/add-edit-per-diem.page.ts +++ b/src/app/fyle/add-edit-per-diem/add-edit-per-diem.page.ts @@ -548,7 +548,14 @@ export class AddEditPerDiemPage implements OnInit { for (const defaultValueColumn in defaultValues) { if (defaultValues.hasOwnProperty(defaultValueColumn)) { const control = keyToControlMap[defaultValueColumn]; - if (!control.value && defaultValueColumn !== 'billable') { + if (!control.value && !control.touched && defaultValueColumn !== 'billable') { + control.patchValue(defaultValues[defaultValueColumn]); + } else if ( + defaultValueColumn === 'billable' && + this.fg.controls.project.value && + (control.value === undefined || control.value === null) && + !control.touched + ) { control.patchValue(defaultValues[defaultValueColumn]); } }