From f10522c34eb0c7d14ca79a1346dc2d364517ea60 Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Tue, 22 Sep 2026 00:03:46 +0800 Subject: [PATCH 1/6] Implement role scope management for WalletManager and update related tests --- src/api/org-management/utils/member-roles.ts | 5 +++ .../manage-roles-dialog.component.spec.ts | 36 +++++++++++++++++++ .../party/members/utils/member-roles.spec.ts | 13 +++++++ .../member-role-panel.component.html | 2 +- .../member-role-panel.component.ts | 14 +++++--- 5 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/api/org-management/utils/member-roles.ts b/src/api/org-management/utils/member-roles.ts index 282d5b57c..d2262c841 100644 --- a/src/api/org-management/utils/member-roles.ts +++ b/src/api/org-management/utils/member-roles.ts @@ -3,6 +3,7 @@ import uniq from 'lodash-es/uniq'; import { domain } from '@vality/org-management-proto/admin_management'; import { ROLE_PRIORITY, sortRoleIds } from '../constants'; +import { SCOPES, ScopeId } from '../types'; export { ROLE_PRIORITY, sortRoleIds }; @@ -11,6 +12,10 @@ export interface RoleLike { scope?: domain.RoleScope; } +export function getRoleScopes(roleId?: string): readonly ScopeId[] { + return roleId === 'WalletManager' ? ['Wallet'] : SCOPES; +} + export function isWalletRole(role: RoleLike): boolean { return role.scope?.scope_id === 'Wallet' || role.role_id === 'WalletManager'; } diff --git a/src/app/parties/party/members/components/manage-roles-dialog/manage-roles-dialog.component.spec.ts b/src/app/parties/party/members/components/manage-roles-dialog/manage-roles-dialog.component.spec.ts index 85cd01944..76f35223e 100644 --- a/src/app/parties/party/members/components/manage-roles-dialog/manage-roles-dialog.component.spec.ts +++ b/src/app/parties/party/members/components/manage-roles-dialog/manage-roles-dialog.component.spec.ts @@ -259,6 +259,42 @@ describe('ManageRolesDialogComponent', () => { expect(service.RemoveMemberRole).not.toHaveBeenCalled(); }); + it('restricts WalletManager scope to only Wallet and displays wallets in catalog', async () => { + await openRole('WalletManager'); + panel() + .querySelector( + '[data-assignment-form] v-select-field:nth-of-type(1) .ng-select-control', + ) + .click(); + await fixture.whenStable(); + const scopeOptions = Array.from( + document.querySelectorAll('.ng-select-option'), + ).map((item) => item.textContent.trim()); + expect(scopeOptions).toEqual(['Wallet']); + document.body.click(); + await fixture.whenStable(); + + panel() + .querySelector( + '[data-assignment-form] v-select-field:nth-of-type(2) .ng-select-control', + ) + .click(); + await fixture.whenStable(); + const resourceOptions = Array.from( + document.querySelectorAll('.ng-select-option'), + ).map((item) => item.textContent.trim()); + expect(resourceOptions).toEqual(['Wallet 1']); + document.body.click(); + await fixture.whenStable(); + + await selectOption('resource', 'Wallet 1'); + await assign(); + expect(service.AssignMemberRole).toHaveBeenCalledExactlyOnceWith('org-1', 'user-1', { + role_id: 'WalletManager', + scope: { scope_id: 'Wallet', resource_id: 'wallet-1' }, + }); + }); + it('omits scope for organization-wide access and uses the returned ID for removal', async () => { await openRole('Administrator'); await assign(); diff --git a/src/app/parties/party/members/utils/member-roles.spec.ts b/src/app/parties/party/members/utils/member-roles.spec.ts index 779634c8a..ba5238838 100644 --- a/src/app/parties/party/members/utils/member-roles.spec.ts +++ b/src/app/parties/party/members/utils/member-roles.spec.ts @@ -3,6 +3,7 @@ import { domain } from '@vality/org-management-proto/admin_management'; import { getGeneralRoleIds, getRoleIds, + getRoleScopes, getWalletRoleIds, isWalletRole, sortRoleIds, @@ -130,4 +131,16 @@ describe('member-roles utils', () => { ]); }); }); + + describe('getRoleScopes', () => { + it('should return only Wallet scope for WalletManager', () => { + expect(getRoleScopes('WalletManager')).toEqual(['Wallet']); + }); + + it('should return all scopes for other roles or undefined', () => { + expect(getRoleScopes('Manager')).toEqual(['Shop', 'Wallet']); + expect(getRoleScopes('Administrator')).toEqual(['Shop', 'Wallet']); + expect(getRoleScopes()).toEqual(['Shop', 'Wallet']); + }); + }); }); diff --git a/src/components/member-roles-manager/member-role-panel/member-role-panel.component.html b/src/components/member-roles-manager/member-role-panel/member-role-panel.component.html index 3d47a5162..27919abef 100644 --- a/src/components/member-roles-manager/member-role-panel/member-role-panel.component.html +++ b/src/components/member-roles-manager/member-role-panel/member-role-panel.component.html @@ -50,7 +50,7 @@

Assign role

diff --git a/src/components/member-roles-manager/member-role-panel/member-role-panel.component.ts b/src/components/member-roles-manager/member-role-panel/member-role-panel.component.ts index 8b06dc2b8..73150b30d 100644 --- a/src/components/member-roles-manager/member-role-panel/member-role-panel.component.ts +++ b/src/components/member-roles-manager/member-role-panel/member-role-panel.component.ts @@ -4,8 +4,8 @@ import { computed, effect, input, + linkedSignal, output, - signal, } from '@angular/core'; import { FormField, disabled, form } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; @@ -15,7 +15,7 @@ import { MatIconModule } from '@angular/material/icon'; import { Option, SelectFieldModule } from '@vality/matez'; import { domain } from '@vality/org-management-proto/admin_management'; -import { SCOPES, ScopeId } from '~/api/org-management'; +import { SCOPES, ScopeId, getRoleScopes } from '~/api/org-management'; @Component({ selector: 'cc-member-role-panel', @@ -51,9 +51,15 @@ export class MemberRolePanelComponent { }); } - newRole = signal({ scopeId: SCOPES[0] as ScopeId, resourceId: '' }); + scopes = computed(() => getRoleScopes(this.roleId())); + scopeOptions = computed(() => + this.scopes().map((scopeId) => ({ label: scopeId, value: scopeId })), + ); + newRole = linkedSignal(() => ({ + scopeId: this.scopes()[0] as ScopeId, + resourceId: '', + })); control = form(this.newRole, (path) => disabled(path, () => this.disabled())); - scopeOptions = SCOPES.map((scopeId) => ({ label: scopeId, value: scopeId })); request = computed(() => { const { scopeId, resourceId } = this.newRole(); return { From ccd3fd3ff85be33eab636b7aab6d0963218de06e Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:06:39 +0800 Subject: [PATCH 2/6] Update @vality/domain-proto version to 2.0.2-8d6174b --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e07dba4df..7b4fb8fd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@ngneat/input-mask": "^6.1.0", "@sentry/angular": "^10.68.0", "@tailwindcss/postcss": "^4.3.1", - "@vality/domain-proto": "^2.0.2-6e28041.0", + "@vality/domain-proto": "^2.0.2-8d6174b.0", "@vality/fistful-proto": "^2.0.1-360c737.0", "@vality/machinegun-proto": "^1.0.1-273f2f3.0", "@vality/magista-proto": "^2.0.2-bd58cea.0", @@ -5750,9 +5750,9 @@ ] }, "node_modules/@vality/domain-proto": { - "version": "2.0.2-6e28041.0", - "resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-2.0.2-6e28041.0.tgz", - "integrity": "sha512-rYC60Set3hpQcktjAL+rKRfRz1u0FHCUTSokHbJuIN6QsbS5p7Jo2GqyPOw5yKy7Rpaiv9JtKmGUi0TRlql5DQ==", + "version": "2.0.2-8d6174b.0", + "resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-2.0.2-8d6174b.0.tgz", + "integrity": "sha512-F2TdNS2VHVhTot+xA8UV2vJMESgopQnxRNYNIxleytyCg6ebzzb+/ihrHqxQc/1/eBtgG2qagTzr9376+FT2eA==", "license": "Apache-2.0" }, "node_modules/@vality/fistful-proto": { diff --git a/package.json b/package.json index c64f4fbea..674675521 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@ngneat/input-mask": "^6.1.0", "@sentry/angular": "^10.68.0", "@tailwindcss/postcss": "^4.3.1", - "@vality/domain-proto": "^2.0.2-6e28041.0", + "@vality/domain-proto": "^2.0.2-8d6174b.0", "@vality/fistful-proto": "^2.0.1-360c737.0", "@vality/machinegun-proto": "^1.0.1-273f2f3.0", "@vality/magista-proto": "^2.0.2-bd58cea.0", From c47381004291ea9b53ebf497847e4da32f5302a6 Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:05:29 +0800 Subject: [PATCH 3/6] Refactor organization dialog and list components to use cc-user-field for Owner ID --- .../create-organization-dialog.component.html | 2 +- .../create-organization-dialog.component.ts | 2 ++ .../organizations-list/organizations-list.component.html | 6 +----- .../organizations-list/organizations-list.component.ts | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.html b/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.html index cae1621c6..f67b0c2b6 100644 --- a/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.html +++ b/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.html @@ -4,7 +4,7 @@ @if (!partyId) { } - + diff --git a/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.ts b/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.ts index ad110bd5b..dbb032acb 100644 --- a/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.ts +++ b/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.ts @@ -15,6 +15,7 @@ import { domain } from '@vality/org-management-proto/admin_management'; import { ThriftOrganizationManagementService } from '~/api/services'; import { MerchantFieldModule } from '~/components/merchant-field'; +import { UserFieldComponent } from '~/components/user-field'; export interface CreateOrganizationDialogData { partyId?: domain.PartyID; @@ -36,6 +37,7 @@ interface CreateOrganizationModel { MerchantFieldModule, InputFieldModule, FormField, + UserFieldComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './create-organization-dialog.component.html', diff --git a/src/app/organizations/components/organizations-list/organizations-list.component.html b/src/app/organizations/components/organizations-list/organizations-list.component.html index e2f682dfd..51d70adfa 100644 --- a/src/app/organizations/components/organizations-list/organizations-list.component.html +++ b/src/app/organizations/components/organizations-list/organizations-list.component.html @@ -5,11 +5,7 @@ - + Date: Wed, 23 Sep 2026 22:11:10 +0800 Subject: [PATCH 4/6] Remove required attribute from Owner ID field in create organization dialog --- .../create-organization-dialog.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.html b/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.html index f67b0c2b6..345446f19 100644 --- a/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.html +++ b/src/app/organizations/components/create-organization-dialog/create-organization-dialog.component.html @@ -4,7 +4,7 @@ @if (!partyId) { } - + From 1c202f395301e5d201a0aa019220d3bd2defdd8e Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:24:24 +0800 Subject: [PATCH 5/6] Enhance value component to support lazy loading and improve error handling in organizations list --- .../lib/components/value/value.component.html | 40 +++++++++++++------ .../lib/components/value/value.component.ts | 13 +++++- .../organizations-list.component.ts | 13 +++++- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/projects/matez/src/lib/components/value/value.component.html b/projects/matez/src/lib/components/value/value.component.html index d49f5f1b5..4d0625579 100644 --- a/projects/matez/src/lib/components/value/value.component.html +++ b/projects/matez/src/lib/components/value/value.component.html @@ -1,10 +1,25 @@ -@if (!value() && lazyValue() && !lazyVisible()) { - +@if (!value() && lazyValue()) { + } @else { @if (value$ | async; as v) {
+ @if (lazyValue() && !lazyVisible()) { + + } + @if (v.error) { + error + } @if (v.prefix) { + } + diff --git a/projects/matez/src/lib/components/value/value.component.ts b/projects/matez/src/lib/components/value/value.component.ts index da162ec48..ce3b000a6 100644 --- a/projects/matez/src/lib/components/value/value.component.ts +++ b/projects/matez/src/lib/components/value/value.component.ts @@ -65,8 +65,17 @@ export class ValueComponent { @Input() highlight?: string | null; - value$ = combineLatest([toObservable(this.value), toObservable(this.lazyValue)]).pipe( - switchMap(([value, lazyValue]) => (isObservable(lazyValue) ? lazyValue : of(value))), + value$ = combineLatest([ + toObservable(this.value), + toObservable(this.lazyValue), + toObservable(this.lazyVisible), + ]).pipe( + switchMap(([value, lazyValue, lazyVisible]) => { + if (lazyVisible && isObservable(lazyValue)) { + return lazyValue; + } + return of(value); + }), shareReplay({ refCount: true, bufferSize: 1 }), ); valueText$ = this.value$.pipe( diff --git a/src/app/organizations/components/organizations-list/organizations-list.component.ts b/src/app/organizations/components/organizations-list/organizations-list.component.ts index 8ad9dbb23..c09d6d8c9 100644 --- a/src/app/organizations/components/organizations-list/organizations-list.component.ts +++ b/src/app/organizations/components/organizations-list/organizations-list.component.ts @@ -119,7 +119,18 @@ export class OrganizationsListComponent { { field: 'owner_id', header: 'Owner', - cell: (org) => ({ value: org.owner_id }), + cell: (org) => ({ value: '', description: org.owner_id }), + lazyCell: (org) => + this.organizationsService.GetUser(org.owner_id).pipe( + map((res) => ({ + value: res.email, + description: org.owner_id, + })), + catchError((err) => { + this.log.error(err); + return of({ value: '', description: org.owner_id, error: err }); + }), + ), }, { field: 'status', From 2126db684fec008aced097e428e2ea9a64037ffb Mon Sep 17 00:00:00 2001 From: Ray <11846445+A77AY@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:34:30 +0800 Subject: [PATCH 6/6] Refactor value component to streamline template structure and improve readability --- .../lib/components/value/value.component.html | 214 +++++++++--------- 1 file changed, 102 insertions(+), 112 deletions(-) diff --git a/projects/matez/src/lib/components/value/value.component.html b/projects/matez/src/lib/components/value/value.component.html index 4d0625579..9dc177eef 100644 --- a/projects/matez/src/lib/components/value/value.component.html +++ b/projects/matez/src/lib/components/value/value.component.html @@ -1,124 +1,114 @@ -@if (!value() && lazyValue()) { - -} @else { - @if (value$ | async; as v) { -
- @if (lazyValue() && !lazyVisible()) { - - } - @if (v.error) { - error - } - @if (v.prefix) { - - } -
+ @if (lazyValue() && !lazyVisible()) { + + } + @if (v.error) { + error + } + @if (v.prefix) { + + } +
+ @if (v.template) { + + } @else { + @if (v.color) { + + + } @else { - @if (v.color) { - - - + + } + + + @if (v.tooltip) { + + + } @else { - + + + } + - - @if (v.tooltip) { - - - - } @else { - - - + + @switch (v.type) { + @case ('menu') { + } - - - - @switch (v.type) { - @case ('menu') { - - } - @case ('toggle') { - - } - @default { - @let valueText = valueText$ | async; - @if (inProgress$ | async) { - - } @else if (valueText) { - - } @else if (emptySymbol) { - {{ - emptySymbol - }} - } + @case ('toggle') { + + } + @default { + @let valueText = valueText$ | async; + @if (inProgress$ | async) { + + } @else if (valueText) { + + } @else if (emptySymbol) { + {{ emptySymbol }} } } - - } + } + + } - @if (v.description) { -
- } -
- @if (v.postfix) { - + @if (v.description) { +
}
- } @else { - - } + @if (v.postfix) { + + } +
+} @else if (lazyValue() && !lazyVisible()) { + +} @else { + } - - - @if (lazyValue() && !lazyVisible()) { - - } -