diff --git a/src/app/parties/parties.component.html b/src/app/parties/parties.component.html index 935e1a4c5..d52bf5014 100644 --- a/src/app/parties/parties.component.html +++ b/src/app/parties/parties.component.html @@ -3,6 +3,7 @@ [columns]="columns" [data]="parties$ | async" [filter]="initSearchParams$ | async" + [hasMore]="hasMore$ | async" [progress]="inProgress$ | async" externalFilter standaloneFilter diff --git a/src/app/parties/parties.component.ts b/src/app/parties/parties.component.ts index 4429d47ed..f2265e689 100644 --- a/src/app/parties/parties.component.ts +++ b/src/app/parties/parties.component.ts @@ -38,6 +38,7 @@ export class PartiesComponent implements OnInit { parties$ = this.fetchFullDomainObjectsService.result$.pipe( map((objs) => objs.map((obj) => obj.object.party_config)), ); + hasMore$ = this.fetchFullDomainObjectsService.hasMore$; columns: Column[] = [ { field: 'id', diff --git a/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/consts/wallet-event-types.ts b/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/consts/wallet-event-types.ts new file mode 100644 index 000000000..c143923f0 --- /dev/null +++ b/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/consts/wallet-event-types.ts @@ -0,0 +1,12 @@ +import { EventType } from '@vality/fistful-proto/webhooker'; + +export const WALLET_EVENT_TYPES: EventType = { + withdrawal: { + started: {}, + succeeded: {}, + failed: {}, + }, + destination: { + created: {}, + }, +}; diff --git a/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/create-wallet-webhook-dialog.component.html b/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/create-wallet-webhook-dialog.component.html index 43350c694..a9988b4aa 100644 --- a/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/create-wallet-webhook-dialog.component.html +++ b/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/create-wallet-webhook-dialog.component.html @@ -1,12 +1,16 @@ - +
+ + +
+
+ Wallet Event Types +
+ +
+
- diff --git a/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/create-wallet-webhook-dialog.component.ts b/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/create-wallet-webhook-dialog.component.ts index fb6277018..1b625e105 100644 --- a/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/create-wallet-webhook-dialog.component.ts +++ b/src/app/parties/party/wallet-webhooks/create-wallet-webhook-dialog/create-wallet-webhook-dialog.component.ts @@ -1,32 +1,41 @@ -import { distinctUntilChanged, map, of } from 'rxjs'; - import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, form, required } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; -import { WebhookParams } from '@vality/fistful-proto/webhooker'; +import { EventType, WebhookParams } from '@vality/fistful-proto/webhooker'; import { DialogModule, DialogSuperclass, + InputFieldModule, NotifyLogService, - getValueChanges, progressTo, } from '@vality/matez'; -import { isTypeWithAliases } from '@vality/ng-thrift'; import { ThriftWalletWebhooksManagementService } from '~/api/services'; -import { FistfulThriftFormComponent } from '~/components/fistful-thrift-form'; -import { DomainMetadataFormExtensionsService } from '~/components/thrift-api-crud'; +import { EventTypesFieldComponent } from '~/components/event-types-field'; +import { PartyWallet, WalletMerchantFieldComponent } from '~/components/wallet-merchant-field'; + +import { WALLET_EVENT_TYPES } from './consts/wallet-event-types'; + +interface CreateWalletWebhookModel { + partyWallet: PartyWallet; + url: string; + eventTypes: EventType[]; +} @Component({ selector: 'cc-create-wallet-webhook-dialog', imports: [ CommonModule, DialogModule, - FistfulThriftFormComponent, ReactiveFormsModule, MatButtonModule, + WalletMerchantFieldComponent, + InputFieldModule, + FormField, + EventTypesFieldComponent, ], changeDetection: ChangeDetectionStrategy.Eager, templateUrl: './create-wallet-webhook-dialog.component.html', @@ -37,27 +46,37 @@ export class CreateWalletWebhookDialogComponent extends DialogSuperclass< > { private walletWebhooksManagementService = inject(ThriftWalletWebhooksManagementService); private log = inject(NotifyLogService); - private domainMetadataFormExtensionsService = inject(DomainMetadataFormExtensionsService); - control = new FormControl>( - { party_id: this.dialogData.partyId }, - { nonNullable: true }, - ); + walletEventTypes = WALLET_EVENT_TYPES as Record; + + controlModel = signal({ + partyWallet: { + party_id: this.dialogData.partyId, + wallet_id: null, + }, + url: '', + eventTypes: [], + }); + control = form(this.controlModel, (schemaPath) => { + required(schemaPath.partyWallet.party_id); + required(schemaPath.url); + required(schemaPath.eventTypes); + }); progress = signal(0); - extensions$ = this.domainMetadataFormExtensionsService.createFullDomainObjectsOptionsByType( - 'WalletConfigObject', - 'wallet_config', - getValueChanges(this.control).pipe( - map((value) => value?.party_id ?? this.dialogData.partyId), - distinctUntilChanged(), - map((partyId) => (obj) => obj.object.wallet_config.data.party_ref.id === partyId), - ), - (data) => of(isTypeWithAliases(data, 'WalletID', 'webhooker')), - ); create() { + const { partyWallet, url, eventTypes } = this.controlModel(); + const params: WebhookParams = { + party_id: partyWallet.party_id, + ...(partyWallet.wallet_id ? { wallet_id: partyWallet.wallet_id } : {}), + url, + event_filter: { + types: new Set(eventTypes), + }, + }; + this.walletWebhooksManagementService - .Create(this.control.value as WebhookParams) + .Create(params) .pipe(progressTo(this.progress)) .subscribe(() => { this.log.success('Webhook created'); diff --git a/src/app/parties/party/webhooks/create-webhook-dialog/consts/shop-invoice-event-types.ts b/src/app/parties/party/webhooks/create-webhook-dialog/consts/shop-invoice-event-types.ts new file mode 100644 index 000000000..d402ef1a9 --- /dev/null +++ b/src/app/parties/party/webhooks/create-webhook-dialog/consts/shop-invoice-event-types.ts @@ -0,0 +1,42 @@ +import { InvoiceEventType } from '@vality/domain-proto/webhooker'; + +export const SHOP_INVOICE_EVENT_TYPES: InvoiceEventType = { + created: {}, + status_changed: { + value: { + unpaid: {}, + paid: {}, + cancelled: {}, + fulfilled: {}, + }, + }, + payment: { + created: {}, + status_changed: { + value: { + pending: {}, + processed: {}, + captured: {}, + cancelled: {}, + failed: {}, + refunded: {}, + }, + }, + invoice_payment_refund_change: { + invoice_payment_refund_created: {}, + invoice_payment_refund_status_changed: { + value: { + pending: {}, + succeeded: {}, + failed: {}, + }, + }, + }, + user_interaction: { + status: { + requested: {}, + completed: {}, + }, + }, + }, +}; diff --git a/src/app/parties/party/webhooks/create-webhook-dialog/create-webhook-dialog.component.html b/src/app/parties/party/webhooks/create-webhook-dialog/create-webhook-dialog.component.html index 9b6c7f285..5eb30e879 100644 --- a/src/app/parties/party/webhooks/create-webhook-dialog/create-webhook-dialog.component.html +++ b/src/app/parties/party/webhooks/create-webhook-dialog/create-webhook-dialog.component.html @@ -1,12 +1,19 @@ - +
+ + +
+
+ Invoice Event Types +
+ +
+
- diff --git a/src/app/parties/party/webhooks/create-webhook-dialog/create-webhook-dialog.component.ts b/src/app/parties/party/webhooks/create-webhook-dialog/create-webhook-dialog.component.ts index 8ba62c9d9..73a88afcf 100644 --- a/src/app/parties/party/webhooks/create-webhook-dialog/create-webhook-dialog.component.ts +++ b/src/app/parties/party/webhooks/create-webhook-dialog/create-webhook-dialog.component.ts @@ -1,33 +1,41 @@ -import { distinctUntilChanged, map } from 'rxjs'; - import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, form, required } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; -import { WebhookParams } from '@vality/domain-proto/webhooker'; +import { InvoiceEventType, WebhookParams } from '@vality/domain-proto/webhooker'; import { DialogModule, DialogSuperclass, + InputFieldModule, NotifyLogService, - getValueChanges, progressTo, } from '@vality/matez'; import { ThriftShopWebhooksManagementService } from '~/api/services'; -import { - DomainMetadataFormExtensionsService, - DomainThriftFormComponent, -} from '~/components/thrift-api-crud'; +import { EventTypesFieldComponent } from '~/components/event-types-field'; +import { PartyShop, ShopMerchantFieldComponent } from '~/components/shop-merchant-field'; + +import { SHOP_INVOICE_EVENT_TYPES } from './consts/shop-invoice-event-types'; + +interface CreateWebhookModel { + partyShop: PartyShop; + url: string; + eventTypes: InvoiceEventType[]; +} @Component({ selector: 'cc-create-webhook-dialog', imports: [ CommonModule, DialogModule, - DomainThriftFormComponent, ReactiveFormsModule, MatButtonModule, + ShopMerchantFieldComponent, + InputFieldModule, + FormField, + EventTypesFieldComponent, ], changeDetection: ChangeDetectionStrategy.Eager, templateUrl: './create-webhook-dialog.component.html', @@ -38,26 +46,39 @@ export class CreateWebhookDialogComponent extends DialogSuperclass< > { private webhooksManagementService = inject(ThriftShopWebhooksManagementService); private log = inject(NotifyLogService); - private domainMetadataFormExtensionsService = inject(DomainMetadataFormExtensionsService); - control = new FormControl>( - { party_ref: { id: this.dialogData.partyId } }, - { nonNullable: true }, - ); + controlModel = signal({ + partyShop: { + party_id: this.dialogData.partyId, + shop_id: null, + }, + url: '', + eventTypes: [], + }); + control = form(this.controlModel, (schemaPath) => { + required(schemaPath.partyShop.party_id); + required(schemaPath.partyShop.shop_id); + required(schemaPath.url); + required(schemaPath.eventTypes); + }); progress = signal(0); - extensions$ = this.domainMetadataFormExtensionsService.createFullDomainObjectsOptionsByType( - 'ShopConfigObject', - 'shop_config', - getValueChanges(this.control).pipe( - map((value) => value?.party_ref?.id ?? this.dialogData.partyId), - distinctUntilChanged(), - map((partyId) => (obj) => obj.object.shop_config.data.party_ref.id === partyId), - ), - ); + eventTypesStructure = SHOP_INVOICE_EVENT_TYPES as Record; create() { + const { partyShop, url, eventTypes } = this.controlModel(); + const params: WebhookParams = { + party_ref: { id: partyShop.party_id }, + url, + event_filter: { + invoice: { + shop_ref: { id: partyShop.shop_id }, + types: new Set(eventTypes), + }, + }, + }; + this.webhooksManagementService - .Create(this.control.value as WebhookParams) + .Create(params) .pipe(progressTo(this.progress)) .subscribe(() => { this.log.success('Webhook created'); diff --git a/src/components/event-types-field/event-types-field.component.html b/src/components/event-types-field/event-types-field.component.html new file mode 100644 index 000000000..566f16408 --- /dev/null +++ b/src/components/event-types-field/event-types-field.component.html @@ -0,0 +1,56 @@ +
+
+ + Selected: {{ selectedLeafIds().size }} / {{ allLeaves().length }} + +
+ + +
+
+ +
+ @for (node of tree(); track node.id) { + + } +
+
+ + +
+ + {{ node.label }} + + + @if (node.children?.length) { +
+ @for (child of node.children; track child.id) { + + } +
+ } +
+
diff --git a/src/components/event-types-field/event-types-field.component.ts b/src/components/event-types-field/event-types-field.component.ts new file mode 100644 index 000000000..d99ca51af --- /dev/null +++ b/src/components/event-types-field/event-types-field.component.ts @@ -0,0 +1,117 @@ +import { isEqual } from 'lodash-es'; + +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + forwardRef, + input, + model, +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { FormValueControl } from '@angular/forms/signals'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; + +import { EventTypeTreeNode } from './types'; +import { createEventTypesTree } from './utils/create-event-types-tree'; + +@Component({ + selector: 'cc-event-types-field', + templateUrl: './event-types-field.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + imports: [CommonModule, MatCheckboxModule, MatButtonModule], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EventTypesFieldComponent), + multi: true, + }, + ], +}) +export class EventTypesFieldComponent + implements FormValueControl, ControlValueAccessor +{ + structure = input.required>(); + disabled = model(false); + value = model([]); + + tree = computed[]>(() => createEventTypesTree(this.structure())); + allLeaves = computed[]>(() => this.tree().flatMap((node) => node.leaves)); + + selectedLeafIds = computed>(() => { + const selected = new Set(); + const val = this.value(); + if (!val || val.length === 0) { + return selected; + } + for (const leaf of this.allLeaves()) { + if (val.some((item) => isEqual(item, leaf.eventType))) { + selected.add(leaf.id); + } + } + return selected; + }); + + private onChange: (val: T[]) => void = () => undefined; + private onTouched: () => void = () => undefined; + + isChecked(node: EventTypeTreeNode): boolean { + const selected = this.selectedLeafIds(); + return node.leaves.length > 0 && node.leaves.every((leaf) => selected.has(leaf.id)); + } + + isIndeterminate(node: EventTypeTreeNode): boolean { + const selected = this.selectedLeafIds(); + const count = node.leaves.filter((leaf) => selected.has(leaf.id)).length; + return count > 0 && count < node.leaves.length; + } + + toggleNode(node: EventTypeTreeNode, checked: boolean): void { + const currentSelected = new Set(this.selectedLeafIds()); + for (const leaf of node.leaves) { + if (checked) { + currentSelected.add(leaf.id); + } else { + currentSelected.delete(leaf.id); + } + } + const newTypes = this.allLeaves() + .filter((leaf) => currentSelected.has(leaf.id)) + .map((leaf) => leaf.eventType!); + this.updateValue(newTypes); + } + + selectAll(): void { + const allTypes = this.allLeaves().map((leaf) => leaf.eventType!); + this.updateValue(allTypes); + } + + clearAll(): void { + this.updateValue([]); + } + + writeValue(value: T[] | Set | null): void { + const array = value ? (Array.isArray(value) ? value : Array.from(value)) : []; + this.value.set(array); + } + + registerOnChange(fn: (val: T[]) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled.set(isDisabled); + } + + private updateValue(val: T[]): void { + this.value.set(val); + this.onChange(val); + this.onTouched(); + } +} diff --git a/src/components/event-types-field/index.ts b/src/components/event-types-field/index.ts new file mode 100644 index 000000000..9b72bc07b --- /dev/null +++ b/src/components/event-types-field/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * from './utils/create-event-types-tree'; +export * from './event-types-field.component'; diff --git a/src/components/event-types-field/types.ts b/src/components/event-types-field/types.ts new file mode 100644 index 000000000..a286eb0f2 --- /dev/null +++ b/src/components/event-types-field/types.ts @@ -0,0 +1,8 @@ +export interface EventTypeTreeNode { + id: string; + label: string; + path: string[]; + eventType?: T; + children?: EventTypeTreeNode[]; + leaves: EventTypeTreeNode[]; +} diff --git a/src/components/event-types-field/utils/create-event-types-tree.spec.ts b/src/components/event-types-field/utils/create-event-types-tree.spec.ts new file mode 100644 index 000000000..e6cdad21c --- /dev/null +++ b/src/components/event-types-field/utils/create-event-types-tree.spec.ts @@ -0,0 +1,184 @@ +import { InvoiceEventType } from '@vality/domain-proto/webhooker'; +import { EventType } from '@vality/fistful-proto/webhooker'; + +import { createEventTypesTree } from './create-event-types-tree'; + +const TEST_INVOICE_EVENT_TYPES: InvoiceEventType = { + created: {}, + status_changed: { + value: { + unpaid: {}, + paid: {}, + cancelled: {}, + fulfilled: {}, + }, + }, + payment: { + created: {}, + status_changed: { + value: { + pending: {}, + processed: {}, + captured: {}, + cancelled: {}, + failed: {}, + refunded: {}, + }, + }, + invoice_payment_refund_change: { + invoice_payment_refund_created: {}, + invoice_payment_refund_status_changed: { + value: { + pending: {}, + succeeded: {}, + failed: {}, + }, + }, + }, + user_interaction: { + status: { + requested: {}, + completed: {}, + }, + }, + }, +}; + +const TEST_WALLET_EVENT_TYPES: EventType = { + withdrawal: { + started: {}, + succeeded: {}, + failed: {}, + }, + destination: { + created: {}, + }, +}; + +describe('createEventTypesTree', () => { + it('returns empty array for empty object', () => { + expect(createEventTypesTree({})).toEqual([]); + }); + + it('creates single leaf node correctly', () => { + const tree = createEventTypesTree({ created: {} }); + + expect(tree).toHaveLength(1); + expect(tree[0].id).toBe('created'); + expect(tree[0].label).toBe('Created'); + expect(tree[0].path).toEqual(['created']); + expect(tree[0].eventType).toEqual({ created: {} }); + expect(tree[0].leaves).toHaveLength(1); + expect(tree[0].leaves[0]).toBe(tree[0]); + }); + + it('collapses "value" wrapper key and preserves eventType structure', () => { + const tree = createEventTypesTree({ + status_changed: { + value: { + unpaid: {}, + paid: {}, + }, + }, + }); + + expect(tree).toHaveLength(1); + expect(tree[0].id).toBe('status_changed'); + expect(tree[0].label).toBe('Status Changed'); + expect(tree[0].children).toHaveLength(2); + expect(tree[0].children?.[0].label).toBe('Unpaid'); + expect(tree[0].children?.[0].path).toEqual(['status_changed', 'value', 'unpaid']); + expect(tree[0].children?.[0].eventType).toEqual({ + status_changed: { value: { unpaid: {} } }, + }); + expect(tree[0].children?.[1].label).toBe('Paid'); + expect(tree[0].children?.[1].path).toEqual(['status_changed', 'value', 'paid']); + expect(tree[0].children?.[1].eventType).toEqual({ + status_changed: { value: { paid: {} } }, + }); + expect(tree[0].leaves).toHaveLength(2); + }); + + it('collapses "status" wrapper key and preserves eventType structure', () => { + const tree = createEventTypesTree({ + user_interaction: { + status: { + requested: {}, + completed: {}, + }, + }, + }); + + expect(tree).toHaveLength(1); + expect(tree[0].id).toBe('user_interaction'); + expect(tree[0].label).toBe('User Interaction'); + expect(tree[0].children).toHaveLength(2); + expect(tree[0].children?.[0].label).toBe('Requested'); + expect(tree[0].children?.[0].eventType).toEqual({ + user_interaction: { status: { requested: {} } }, + }); + expect(tree[0].children?.[1].label).toBe('Completed'); + expect(tree[0].children?.[1].eventType).toEqual({ + user_interaction: { status: { completed: {} } }, + }); + expect(tree[0].leaves).toHaveLength(2); + }); + + it('correctly creates full tree for TEST_INVOICE_EVENT_TYPES', () => { + const tree = createEventTypesTree( + TEST_INVOICE_EVENT_TYPES as Record, + ); + + expect(tree).toHaveLength(3); + expect(tree.map((node) => node.label)).toEqual(['Created', 'Status Changed', 'Payment']); + + const allLeaves = tree.flatMap((node) => node.leaves); + expect(allLeaves).toHaveLength(18); + + const createdNode = tree.find((n) => n.id === 'created'); + expect(createdNode?.leaves).toHaveLength(1); + + const statusChangedNode = tree.find((n) => n.id === 'status_changed'); + expect(statusChangedNode?.leaves).toHaveLength(4); + + const paymentNode = tree.find((n) => n.id === 'payment'); + expect(paymentNode?.leaves).toHaveLength(13); + + const refundCreatedLeaf = allLeaves.find( + (leaf) => + leaf.id === 'payment.invoice_payment_refund_change.invoice_payment_refund_created', + ); + expect(refundCreatedLeaf?.eventType).toEqual({ + payment: { + invoice_payment_refund_change: { + invoice_payment_refund_created: {}, + }, + }, + }); + }); + + it('correctly creates full tree for TEST_WALLET_EVENT_TYPES', () => { + const tree = createEventTypesTree( + TEST_WALLET_EVENT_TYPES as Record, + ); + + expect(tree).toHaveLength(2); + expect(tree.map((node) => node.label)).toEqual(['Withdrawal', 'Destination']); + + const allLeaves = tree.flatMap((node) => node.leaves); + expect(allLeaves).toHaveLength(4); + + const withdrawalNode = tree.find((n) => n.id === 'withdrawal'); + expect(withdrawalNode?.leaves).toHaveLength(3); + + const destinationNode = tree.find((n) => n.id === 'destination'); + expect(destinationNode?.leaves).toHaveLength(1); + + const startedLeaf = allLeaves.find((leaf) => leaf.id === 'withdrawal.started'); + expect(startedLeaf?.eventType).toEqual({ + withdrawal: { + started: {}, + }, + }); + }); +}); diff --git a/src/components/event-types-field/utils/create-event-types-tree.ts b/src/components/event-types-field/utils/create-event-types-tree.ts new file mode 100644 index 000000000..b2ab70ef4 --- /dev/null +++ b/src/components/event-types-field/utils/create-event-types-tree.ts @@ -0,0 +1,65 @@ +import { startCase } from 'lodash-es'; +import set from 'lodash-es/set'; + +import { EventTypeTreeNode } from '../types'; + +function parseNode(key: string, value: unknown, currentPath: string[]): EventTypeTreeNode { + const isObject = typeof value === 'object' && value !== null; + const keys = isObject ? Object.keys(value) : []; + + if (keys.length === 1 && (keys[0] === 'value' || keys[0] === 'status')) { + const wrapperKey = keys[0]; + const innerValue = (value as Record)[wrapperKey]; + const innerKeys = + typeof innerValue === 'object' && innerValue !== null ? Object.keys(innerValue) : []; + const children = innerKeys.map((k) => + parseNode(k, (innerValue as Record)[k], [ + ...currentPath, + wrapperKey, + k, + ]), + ); + const leaves = children.flatMap((c) => c.leaves); + return { + id: currentPath.join('.'), + label: startCase(key), + path: currentPath, + children, + leaves, + }; + } + + if (keys.length === 0) { + const eventTypeObj = {}; + set(eventTypeObj, currentPath, {}); + const leafNode: EventTypeTreeNode = { + id: currentPath.join('.'), + label: startCase(key), + path: currentPath, + eventType: eventTypeObj as T, + leaves: [], + }; + leafNode.leaves = [leafNode]; + return leafNode; + } + + const children = keys.map((k) => + parseNode(k, (value as Record)[k], [...currentPath, k]), + ); + const leaves = children.flatMap((c) => c.leaves); + return { + id: currentPath.join('.'), + label: startCase(key), + path: currentPath, + children, + leaves, + }; +} + +export function createEventTypesTree( + eventTypesStructure: Record, +): EventTypeTreeNode[] { + return Object.keys(eventTypesStructure).map((key) => + parseNode(key, eventTypesStructure[key], [key]), + ); +} diff --git a/src/components/wallet-field/wallet-field.component.html b/src/components/wallet-field/wallet-field.component.html index e491f6fb8..75dc44f29 100644 --- a/src/components/wallet-field/wallet-field.component.html +++ b/src/components/wallet-field/wallet-field.component.html @@ -1,13 +1,13 @@ diff --git a/src/components/wallet-field/wallet-field.component.ts b/src/components/wallet-field/wallet-field.component.ts index 0277579f9..384e59ac3 100644 --- a/src/components/wallet-field/wallet-field.component.ts +++ b/src/components/wallet-field/wallet-field.component.ts @@ -1,50 +1,72 @@ -import { Observable } from 'rxjs'; +import { of } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ChangeDetectionStrategy, Component, Input, booleanAttribute, inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + booleanAttribute, + inject, + input, +} from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; -import { DomainObjectType, WalletID } from '@vality/domain-proto/domain'; +import { DomainObjectType, PartyConfigRef, WalletID } from '@vality/domain-proto/domain'; import { FormControlSuperclass, Option, SelectFieldComponent, createControlProviders, + observableResource, } from '@vality/matez'; -import { FetchDomainObjectsService } from '~/api/domain-config'; +import { ThriftRepositoryService } from '~/api/services'; @Component({ selector: 'cc-wallet-field', templateUrl: 'wallet-field.component.html', - providers: [...createControlProviders(() => WalletFieldComponent), FetchDomainObjectsService], + providers: [...createControlProviders(() => WalletFieldComponent)], changeDetection: ChangeDetectionStrategy.Eager, standalone: false, }) export class WalletFieldComponent extends FormControlSuperclass { - private fetchDomainObjectsService = inject(FetchDomainObjectsService); + private repositoryService = inject(ThriftRepositoryService); @Input() label: string; @Input({ transform: booleanAttribute }) required: boolean; @Input() size?: SelectFieldComponent['size']; @Input() appearance?: SelectFieldComponent['appearance']; @Input() hint?: string; - @Input({ transform: booleanAttribute }) multiple = false; - - options$: Observable[]> = this.fetchDomainObjectsService.result$.pipe( - map((objs) => - objs.map((obj) => ({ - value: obj.ref.wallet_config.id, - label: obj.name || `#${obj.ref.wallet_config.id}`, - description: obj.description, - })), - ), - ); - progress$ = this.fetchDomainObjectsService.isLoading$; + multiple = input(false, { transform: booleanAttribute }); + partyId = input(); - search(search: string) { - this.fetchDomainObjectsService.load( - { type: DomainObjectType.wallet_config, query: search }, - { size: 1000 }, - ); - } + wallets = observableResource({ + params: toObservable(this.partyId).pipe(map((partyId) => ({ partyId, query: '' }))), + loader: ({ partyId, query }) => + !partyId && !query + ? of([]) + : (partyId + ? this.repositoryService + .GetRelatedGraph({ + ref: { party_config: { id: partyId } }, + type: DomainObjectType.wallet_config, + }) + .pipe(map(({ nodes }) => Array.from(nodes))) + : this.repositoryService + .SearchObjects({ + type: DomainObjectType.wallet_config, + query: query || '*', + limit: 1000, + }) + .pipe(map((res) => res.result || [])) + ).pipe( + map((objs): Option[] => + objs.map((obj) => ({ + value: obj.ref.wallet_config.id, + label: obj.name || `#${obj.ref.wallet_config.id}`, + description: obj.description, + })), + ), + ), + }); } diff --git a/src/components/wallet-merchant-field/index.ts b/src/components/wallet-merchant-field/index.ts new file mode 100644 index 000000000..7de84cf26 --- /dev/null +++ b/src/components/wallet-merchant-field/index.ts @@ -0,0 +1 @@ +export * from './wallet-merchant-field.component'; diff --git a/src/components/wallet-merchant-field/wallet-merchant-field.component.html b/src/components/wallet-merchant-field/wallet-merchant-field.component.html new file mode 100644 index 000000000..b97fc2705 --- /dev/null +++ b/src/components/wallet-merchant-field/wallet-merchant-field.component.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/src/components/wallet-merchant-field/wallet-merchant-field.component.ts b/src/components/wallet-merchant-field/wallet-merchant-field.component.ts new file mode 100644 index 000000000..06597fdb2 --- /dev/null +++ b/src/components/wallet-merchant-field/wallet-merchant-field.component.ts @@ -0,0 +1,55 @@ +import { distinctUntilChanged, filter, map, switchMap } from 'rxjs'; + +import { CommonModule } from '@angular/common'; +import { Component, DestroyRef, Injector, OnInit, inject, model } from '@angular/core'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormField, FormValueControl, form } from '@angular/forms/signals'; + +import { PartyConfigRef, WalletID } from '@vality/domain-proto/domain'; + +import { DomainService } from '~/api/domain-config'; + +import { MerchantFieldModule } from '../merchant-field'; +import { WalletFieldModule } from '../wallet-field'; + +export interface PartyWallet { + party_id: PartyConfigRef['id']; + wallet_id: WalletID; +} + +@Component({ + selector: 'cc-wallet-merchant-field', + templateUrl: './wallet-merchant-field.component.html', + imports: [CommonModule, ReactiveFormsModule, WalletFieldModule, MerchantFieldModule, FormField], +}) +export class WalletMerchantFieldComponent implements FormValueControl, OnInit { + private dr = inject(DestroyRef); + private domainService = inject(DomainService); + private injector = inject(Injector); + + value = model({ + party_id: null, + wallet_id: null, + }); + control = form(this.value); + + ngOnInit() { + toObservable(this.value, { injector: this.injector }) + .pipe( + map((v) => v.wallet_id), + filter(Boolean), + distinctUntilChanged(), + switchMap((walletId) => + this.domainService.get({ wallet_config: { id: walletId } }), + ), + map((wallet) => wallet.object.wallet_config.data.party_ref.id), + takeUntilDestroyed(this.dr), + ) + .subscribe((party_id) => { + if (party_id !== this.value().party_id) { + this.value.update((v) => ({ ...v, party_id })); + } + }); + } +}