From d15616669e90ebc31fe75e7946afd4da0689b262 Mon Sep 17 00:00:00 2001 From: Olga Shen Date: Thu, 14 Nov 2024 18:23:38 +0800 Subject: [PATCH] fix(features/home): network action default value Fix the default value for network action by removing the extra space, and ensure the default_values_list_text field is handled if it's missing in the response. Fix the issue where the button color doesn't change on the network action detail page when it is disabled. Remove the unused ActionsDialogComponent. --- .../action-details/action-details.page.html | 6 +- .../action-details/action-details.page.scss | 22 -- .../action-details/action-details.page.ts | 13 +- .../home/details/actions/actions.page.ts | 282 +----------------- .../actions-dialog.component.html | 19 -- .../actions-dialog.component.scss | 0 .../actions-dialog.component.spec.ts | 30 -- .../actions-dialog.component.ts | 123 -------- .../shared/actions/service/actions.service.ts | 2 +- ...export-private-key-modal.component.spec.ts | 4 +- .../order-detail-dialog.component.spec.ts | 4 +- src/app/shared/shared.module.ts | 2 - 12 files changed, 19 insertions(+), 488 deletions(-) delete mode 100644 src/app/shared/actions/actions-dialog/actions-dialog.component.html delete mode 100644 src/app/shared/actions/actions-dialog/actions-dialog.component.scss delete mode 100644 src/app/shared/actions/actions-dialog/actions-dialog.component.spec.ts delete mode 100644 src/app/shared/actions/actions-dialog/actions-dialog.component.ts diff --git a/src/app/features/home/details/actions/action-details/action-details.page.html b/src/app/features/home/details/actions/action-details/action-details.page.html index 1da436183..a1b9365a2 100644 --- a/src/app/features/home/details/actions/action-details/action-details.page.html +++ b/src/app/features/home/details/actions/action-details/action-details.page.html @@ -33,14 +33,14 @@
{{ price$ | async }} NUM
- + diff --git a/src/app/features/home/details/actions/action-details/action-details.page.scss b/src/app/features/home/details/actions/action-details/action-details.page.scss index 8cbba2bb5..41c373600 100644 --- a/src/app/features/home/details/actions/action-details/action-details.page.scss +++ b/src/app/features/home/details/actions/action-details/action-details.page.scss @@ -72,28 +72,6 @@ ion-footer { font-weight: 700; } } - - .custom-button { - padding: 16px 20px; - background-color: #3880ff; - color: white; - border: none; - border-radius: 37px; - cursor: pointer; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 13px; - font-style: normal; - font-weight: 600; - line-height: normal; - letter-spacing: 0.26px; - text-transform: uppercase; - - &:hover { - background-color: #0056b3; - } - } } ion-spinner { diff --git a/src/app/features/home/details/actions/action-details/action-details.page.ts b/src/app/features/home/details/actions/action-details/action-details.page.ts index 6664e6c78..ba496ae75 100644 --- a/src/app/features/home/details/actions/action-details/action-details.page.ts +++ b/src/app/features/home/details/actions/action-details/action-details.page.ts @@ -123,7 +123,9 @@ export class ActionDetailsPage { const formModel: any = {}; for (const param of params) - formModel[param.name_text] = param.default_values_list_text[0] || ''; + formModel[param.name_text] = param.default_values_list_text?.length + ? param.default_values_list_text[0].trim() + : ''; return formModel; } @@ -140,10 +142,11 @@ export class ActionDetailsPage { key: param.name_text, type: 'select', templateOptions: { - options: param.default_values_list_text.map(value => ({ - label: value, - value: value, - })), + options: + param.default_values_list_text?.map(value => ({ + label: value.trim(), + value: value.trim(), + })) ?? [], placeholder: param.placeholder_text, disabled: !param.user_input_boolean, required: !isOptional, diff --git a/src/app/features/home/details/actions/actions.page.ts b/src/app/features/home/details/actions/actions.page.ts index b60d7cb6a..94c2292f0 100644 --- a/src/app/features/home/details/actions/actions.page.ts +++ b/src/app/features/home/details/actions/actions.page.ts @@ -1,35 +1,12 @@ import { Component } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute, Router } from '@angular/router'; -import { Browser } from '@capacitor/browser'; -import { TranslocoService } from '@ngneat/transloco'; -import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { combineLatest, forkJoin, iif, of } from 'rxjs'; -import { catchError, concatMap, first, map, take, tap } from 'rxjs/operators'; -import { ActionsDialogComponent } from '../../../../shared/actions/actions-dialog/actions-dialog.component'; +import { UntilDestroy } from '@ngneat/until-destroy'; +import { catchError } from 'rxjs/operators'; import { Action, ActionsService, } from '../../../../shared/actions/service/actions.service'; -import { OrderHistoryService } from '../../../../shared/actions/service/order-history.service'; -import { BlockingActionService } from '../../../../shared/blocking-action/blocking-action.service'; -import { DiaBackendAuthService } from '../../../../shared/dia-backend/auth/dia-backend-auth.service'; -import { DiaBackendSeriesRepository } from '../../../../shared/dia-backend/series/dia-backend-series-repository.service'; -import { - DiaBackendStoreService, - NetworkAppOrder, -} from '../../../../shared/dia-backend/store/dia-backend-store.service'; -import { DiaBackendWalletService } from '../../../../shared/dia-backend/wallet/dia-backend-wallet.service'; import { ErrorService } from '../../../../shared/error/error.service'; -import { OrderDetailDialogComponent } from '../../../../shared/order-detail-dialog/order-detail-dialog.component'; -import { ProofRepository } from '../../../../shared/repositories/proof/proof-repository.service'; -import { browserToolbarColor } from '../../../../utils/constants'; -import { - VOID$, - isNonNullable, -} from '../../../../utils/rx-operators/rx-operators'; -import { InformationSessionService } from '../information/session/information-session.service'; @UntilDestroy() @Component({ @@ -42,270 +19,17 @@ export class ActionsPage { .getActions$() .pipe(catchError((err: unknown) => this.errorService.toastError$(err))); - private readonly id$ = this.route.paramMap.pipe( - map(params => params.get('id')), - isNonNullable() - ); - constructor( private readonly router: Router, private readonly actionsService: ActionsService, private readonly errorService: ErrorService, - private readonly translocoService: TranslocoService, - private readonly blockingActionService: BlockingActionService, - private readonly route: ActivatedRoute, - private readonly authService: DiaBackendAuthService, - private readonly snackBar: MatSnackBar, - private readonly dialog: MatDialog, - private readonly storeService: DiaBackendStoreService, - private readonly orderHistoryService: OrderHistoryService, - private readonly diaBackendStoreService: DiaBackendStoreService, - private readonly diaBackendSeriesRepository: DiaBackendSeriesRepository, - private readonly diaBackendWalletService: DiaBackendWalletService, - private readonly informationSessionService: InformationSessionService, - private readonly proofRepository: ProofRepository + private readonly route: ActivatedRoute ) {} - canPerformAction$(action: Action) { - if (action.title_text === 'List in CaptureClub') { - /* - Workaround: - Currently there isn't a simple way to check whether an asset is listed in - CaptureClub or not. So I first query List all Products API with - associated_id parameter set to the assets cid. And then use list series - API and check through all nested collections. See discussion here - https://app.asana.com/0/0/1201558520076805/1201995911008176/f - */ - return this.id$.pipe( - concatMap(cid => - forkJoin([ - this.diaBackendStoreService.listAllProducts$({ - associated_id: cid, - service_name: 'CaptureClub', - }), - of(cid), - ]) - ), - concatMap(([response, cid]) => { - if (response.count > 0) { - throw new Error( - this.translocoService.translate('message.hasListedInCaptureApp') - ); - } - return of(cid); - }), - concatMap(async cid => { - let currentOffset = 0; - const limit = 100; - while (true) { - const response = await this.diaBackendSeriesRepository - .fetchAll$({ offset: currentOffset, limit }) - .toPromise(); - const listedAsSeries = response.results.some(serie => - serie.collections.some(collection => - collection.assets.some(asset => asset.cid === cid) - ) - ); - if (listedAsSeries) { - throw new Error( - this.translocoService.translate('message.hasListedInCaptureApp') - ); - } - if (response.next == null) { - break; - } - currentOffset += response.results.length; - } - return VOID$; - }), - take(1) - ); - } - return VOID$; - } - - openActionDialog$(action: Action) { - return combineLatest([ - this.actionsService.getParams$(action.params_list_custom_param1 ?? []), - this.authService.token$, - this.id$, - ]).pipe( - first(), - concatMap(([params, token, id]) => { - const dialogRef = this.dialog.open( - ActionsDialogComponent, - { - disableClose: true, - data: { - action: action, - params: params, - }, - } - ); - return dialogRef.afterClosed().pipe( - isNonNullable(), - concatMap(data => - of({ - networkApp: action.network_app_id_text, - actionArgs: { ...data, token: token, cid: id }, - } as CreateOrderInput) - ) - ); - }) - ); - } - - openOrderDialog$(orderStatus: NetworkAppOrder) { - const dialogRef = this.dialog.open( - OrderDetailDialogComponent, - { - disableClose: true, - data: orderStatus, - width: '80%', - } - ); - return dialogRef.afterClosed().pipe( - isNonNullable(), - concatMap((orderId: string) => of(orderId)) - ); - } - - createOrder$(appName: string, actionArgs: any) { - return this.storeService.createNetworkAppOrder(appName, actionArgs).pipe( - catchError((err: unknown) => - this.errorService.toastDiaBackendError$(err) - ), - isNonNullable() - ); - } - - confirmOrder$(id: string) { - return this.storeService.confirmNetworkAppOrder(id).pipe( - catchError((err: unknown) => - this.errorService.toastDiaBackendError$(err) - ), - isNonNullable() - ); - } - - createOrderHistory$(networkAppOrder: NetworkAppOrder) { - return this.id$.pipe( - first(), - isNonNullable(), - concatMap(cid => - this.orderHistoryService.createOrderHistory$(networkAppOrder, cid) - ), - catchError((err: unknown) => { - return this.errorService.toastError$(err); - }) - ); - } - - redirectToExternalUrl(url: string, orderId: string) { - this.id$ - .pipe( - first(), - isNonNullable(), - tap(cid => - Browser.open({ - url: `${url}?cid=${cid}&order_id=${orderId}`, - toolbarColor: browserToolbarColor, - }) - ), - catchError((err: unknown) => { - return this.errorService.toastError$(err); - }), - untilDestroyed(this) - ) - .subscribe(); - } - - removeCapture() { - if (this.informationSessionService.activatedDetailedCapture) { - this.informationSessionService.activatedDetailedCapture.proof$.subscribe( - proof => { - if (proof) { - this.proofRepository.remove(proof); - this.router.navigate(['/home']); - } - } - ); - } - } - performAction(action: Action) { this.router.navigate(['action-details'], { relativeTo: this.route, state: action, }); } - - doAction(action: Action) { - this.blockingActionService - .run$(this.canPerformAction$(action)) - .pipe( - catchError((err: unknown) => { - return this.errorService.toastError$(err); - }), - concatMap(() => this.openActionDialog$(action)), - concatMap(createOrderInput => - this.blockingActionService.run$( - forkJoin([ - this.createOrder$( - createOrderInput.networkApp, - createOrderInput.actionArgs - ), - // To display "Insufficient NUM" in order confirmation dialog, - // we need to sync asset wallet balance if the action cost NUM. - iif( - () => action.action_cost_number > 0, - this.diaBackendWalletService.syncAssetWalletBalance$(), - VOID$ - ), - ]) - ) - ), - concatMap(([orderStatus, _]) => this.openOrderDialog$(orderStatus)), - concatMap(orderId => - this.blockingActionService.run$(this.confirmOrder$(orderId)) - ), - tap(networkAppOrder => { - /* - Workaround: - Create a order history record only if the total cost is > 0 to prevent race condition - between app creating the order history record v.s. bubble workflow checking whether a - record already exists and if not create a new one, especially for network actions that - don't require any cost (and hence backend calls the webhook immediately). See - https://dt42-numbers.slack.com/archives/C0323488MEJ/p1648006014291339 - */ - if (Number(networkAppOrder.total_cost) !== 0) { - this.createOrderHistory$(networkAppOrder).subscribe(); - } - }), - tap(() => { - this.snackBar.open( - this.translocoService.translate('message.sentSuccessfully') - ); - }), - tap(networkAppOrder => { - if (action.ext_action_destination_text) { - this.redirectToExternalUrl( - action.ext_action_destination_text, - networkAppOrder.id - ); - } - }), - tap(() => { - if (action.hide_capture_after_execution_boolean ?? false) - this.removeCapture(); - }), - untilDestroyed(this) - ) - .subscribe(); - } -} - -interface CreateOrderInput { - networkApp: string; - actionArgs: any; } diff --git a/src/app/shared/actions/actions-dialog/actions-dialog.component.html b/src/app/shared/actions/actions-dialog/actions-dialog.component.html deleted file mode 100644 index e84777c88..000000000 --- a/src/app/shared/actions/actions-dialog/actions-dialog.component.html +++ /dev/null @@ -1,19 +0,0 @@ -
-

{{ title }}

-

{{ description }}

- -
- -
- - - - -
diff --git a/src/app/shared/actions/actions-dialog/actions-dialog.component.scss b/src/app/shared/actions/actions-dialog/actions-dialog.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/shared/actions/actions-dialog/actions-dialog.component.spec.ts b/src/app/shared/actions/actions-dialog/actions-dialog.component.spec.ts deleted file mode 100644 index a9bb64695..000000000 --- a/src/app/shared/actions/actions-dialog/actions-dialog.component.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { SharedTestingModule } from '../../shared-testing.module'; -import { ActionsDialogComponent } from './actions-dialog.component'; - -describe('ActionsDialogComponent', () => { - let component: ActionsDialogComponent; - let fixture: ComponentFixture; - - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ActionsDialogComponent], - imports: [SharedTestingModule], - providers: [ - { provide: MatDialogRef, useValue: {} }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(ActionsDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }) - ); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/shared/actions/actions-dialog/actions-dialog.component.ts b/src/app/shared/actions/actions-dialog/actions-dialog.component.ts deleted file mode 100644 index 1882d95ee..000000000 --- a/src/app/shared/actions/actions-dialog/actions-dialog.component.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { UntypedFormGroup } from '@angular/forms'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { Platform } from '@ionic/angular'; -import { FormlyFieldConfig } from '@ngx-formly/core'; -import { Action, Param } from '../service/actions.service'; - -@Component({ - selector: 'app-actions-dialog', - templateUrl: './actions-dialog.component.html', - styleUrls: ['./actions-dialog.component.scss'], -}) -export class ActionsDialogComponent { - readonly form = new UntypedFormGroup({}); - readonly fields: FormlyFieldConfig[] = []; - readonly model: any = {}; - - readonly title: string = ''; - readonly description: string = ''; - readonly params: Param[] = []; - - constructor( - private readonly dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: MatDialogData, - private readonly platform: Platform - ) { - if (data.action !== undefined && data.params !== undefined) { - this.title = data.action.title_text; - this.description = data.action.description_text; - this.params = data.params; - this.createFormModel(); - this.createFormFields(); - this.autoFocusFirstFieldOnIOS(); - } - } - - private createFormModel() { - for (const param of this.params) - this.model[param.name_text] = param.default_values_list_text[0] || ''; - } - - private createFormFields() { - for (const param of this.params) { - const isOptional = param.optional_boolean ?? false; - - if (param.type_text === 'dropdown') - this.fields.push({ - key: param.name_text, - type: 'select', - templateOptions: { - options: param.default_values_list_text.map(value => ({ - label: value, - value: value, - })), - placeholder: param.placeholder_text, - disabled: !param.user_input_boolean, - required: !isOptional, - }, - }); - else if (param.type_text === 'number') - this.fields.push({ - key: param.name_text, - type: 'input', - templateOptions: { - type: 'number', - label: param.display_text_text, - placeholder: param.placeholder_text, - disabled: !param.user_input_boolean, - max: param.max_number, - min: param.min_number, - required: !isOptional, - }, - }); - else - this.fields.push({ - key: param.name_text, - type: 'input', - templateOptions: { - type: 'text', - label: param.display_text_text, - placeholder: param.placeholder_text, - disabled: !param.user_input_boolean, - required: !isOptional, - }, - }); - } - } - - /** - * WORKAROUND: https://github.com/numbersprotocol/capture-lite/issues/2284 - * This workaround is specific to iOS. - * Action dialog form fields are created using @ngx-formly. - * On Android it auto-focuses on 1st field even if 1st field is prepopulated. - * On iOS it focuses on field that is not prepopulated yet thus having - * different behavior based on Platform. To unify behavior this methods - * forces to focus on 1st field on iOS platform. - * @returns - */ - autoFocusFirstFieldOnIOS() { - if (!this.platform.is('ios')) return; - if (this.fields.length <= 0) return; - if ( - this.fields[0].templateOptions?.type !== 'number' && - this.fields[0].templateOptions?.type !== 'text' - ) - return; - const autoFocusAfterMilliseconds = 700; - setTimeout(() => (this.fields[0].focus = true), autoFocusAfterMilliseconds); - } - - send() { - this.dialogRef.close(this.model); - } - - cancel() { - this.dialogRef.close(); - } -} - -interface MatDialogData { - action: Action | undefined; - params: Param[] | undefined; -} diff --git a/src/app/shared/actions/service/actions.service.ts b/src/app/shared/actions/service/actions.service.ts index c5d932664..97f3548aa 100644 --- a/src/app/shared/actions/service/actions.service.ts +++ b/src/app/shared/actions/service/actions.service.ts @@ -53,7 +53,7 @@ export interface Action { } export interface Param { - readonly default_values_list_text: string[]; + readonly default_values_list_text?: string[]; readonly description_text: string; readonly display_text_text: string; readonly name_text: string; diff --git a/src/app/shared/export-private-key-modal/export-private-key-modal.component.spec.ts b/src/app/shared/export-private-key-modal/export-private-key-modal.component.spec.ts index ae8983b53..0b21aa2df 100644 --- a/src/app/shared/export-private-key-modal/export-private-key-modal.component.spec.ts +++ b/src/app/shared/export-private-key-modal/export-private-key-modal.component.spec.ts @@ -1,9 +1,9 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { SharedTestingModule } from '../../shared/shared-testing.module'; import { ExportPrivateKeyModalComponent } from './export-private-key-modal.component'; -describe('ActionsDialogComponent', () => { +describe('ExportPrivateKeyModalComponent', () => { let component: ExportPrivateKeyModalComponent; let fixture: ComponentFixture; diff --git a/src/app/shared/order-detail-dialog/order-detail-dialog.component.spec.ts b/src/app/shared/order-detail-dialog/order-detail-dialog.component.spec.ts index f02d835b8..db0dcdd9d 100644 --- a/src/app/shared/order-detail-dialog/order-detail-dialog.component.spec.ts +++ b/src/app/shared/order-detail-dialog/order-detail-dialog.component.spec.ts @@ -1,9 +1,9 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { SharedTestingModule } from '../../shared/shared-testing.module'; import { OrderDetailDialogComponent } from './order-detail-dialog.component'; -describe('ActionsDialogComponent', () => { +describe('OrderDetailDialogComponent', () => { let component: OrderDetailDialogComponent; let fixture: ComponentFixture; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 01b2b6fde..a43c44bea 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -11,7 +11,6 @@ import { FormlyMaterialModule } from '@ngx-formly/material'; import { GoProBluetoothService } from '../features/settings/go-pro/services/go-pro-bluetooth.service'; import { GoProMediaService } from '../features/settings/go-pro/services/go-pro-media.service'; import { GoProWifiService } from '../features/settings/go-pro/services/go-pro-wifi.service'; -import { ActionsDialogComponent } from './actions/actions-dialog/actions-dialog.component'; import { AvatarComponent } from './avatar/avatar.component'; import { CapacitorPluginsModule } from './capacitor-plugins/capacitor-plugins.module'; import { CaptureBackButtonComponent } from './capture-back-button/capture-back-button.component'; @@ -31,7 +30,6 @@ import { UserGuideService } from './user-guide/user-guide.service'; const declarations = [ MigratingDialogComponent, - ActionsDialogComponent, AvatarComponent, MediaComponent, MediaRebrandedComponent,