mirror of
https://github.com/valitydev/dashboard.git
synced 2024-11-06 02:25:23 +00:00
IMP-165, IMP-167: Add wallet manager and refactor nested table (#170)
This commit is contained in:
parent
5397e45f40
commit
1ef4111a6c
4573
package-lock.json
generated
4573
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@ -9,7 +9,7 @@
|
||||
"build": "ng build && transloco-optimize dist/assets/i18n",
|
||||
"test": "ng test",
|
||||
"i18n:extract": "transloco-keys-manager extract && prettier src/assets/i18n/** --write",
|
||||
"i18n:clean": "transloco-keys-manager extract --remove-extra-keys && prettier src/assets/i18n/* --write",
|
||||
"i18n:clean": "transloco-keys-manager extract --remove-extra-keys && prettier src/assets/i18n/* --write && prettier src/assets/i18n/** --write",
|
||||
"i18n:check": "transloco-keys-manager find --emit-error-on-extra-keys",
|
||||
"coverage": "npx http-server -c-1 -o -p 9875 ./coverage",
|
||||
"lint": "ng lint --max-warnings=0",
|
||||
@ -25,27 +25,27 @@
|
||||
"spell:fix": "cspell --no-progress --show-suggestions --show-context **"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.8",
|
||||
"@angular/cdk": "~17.0.4",
|
||||
"@angular/common": "^17.0.8",
|
||||
"@angular/compiler": "^17.0.8",
|
||||
"@angular/core": "^17.0.8",
|
||||
"@angular/forms": "^17.0.8",
|
||||
"@angular/material": "~17.0.4",
|
||||
"@angular/material-moment-adapter": "^17.0.4",
|
||||
"@angular/platform-browser": "^17.0.8",
|
||||
"@angular/platform-browser-dynamic": "^17.0.8",
|
||||
"@angular/router": "^17.0.8",
|
||||
"@angular/animations": "^17.1.3",
|
||||
"@angular/cdk": "~17.1.2",
|
||||
"@angular/common": "^17.1.3",
|
||||
"@angular/compiler": "^17.1.3",
|
||||
"@angular/core": "^17.1.3",
|
||||
"@angular/forms": "^17.1.3",
|
||||
"@angular/material": "~17.1.2",
|
||||
"@angular/material-moment-adapter": "^17.1.2",
|
||||
"@angular/platform-browser": "^17.1.3",
|
||||
"@angular/platform-browser-dynamic": "^17.1.3",
|
||||
"@angular/router": "^17.1.3",
|
||||
"@ngneat/transloco": "^6.0.4",
|
||||
"@ngneat/until-destroy": "^9.0.0",
|
||||
"@sentry/angular-ivy": "^7.92.0",
|
||||
"@sentry/integrations": "^7.92.0",
|
||||
"@sentry/tracing": "^7.92.0",
|
||||
"@vality/ng-core": "^17.0.0",
|
||||
"@sentry/angular-ivy": "^7.100.1",
|
||||
"@sentry/integrations": "^7.100.1",
|
||||
"@sentry/tracing": "^7.100.1",
|
||||
"@vality/ng-core": "^17.1.1-pr-57-4da820a.0",
|
||||
"@vality/swag-anapi-v2": "2.0.1-32ed85f.0",
|
||||
"@vality/swag-api-keys-v2": "0.1.2-f0ece04.0",
|
||||
"@vality/swag-claim-management": "0.1.1-6b6711b.0",
|
||||
"@vality/swag-organizations": "1.0.0",
|
||||
"@vality/swag-organizations": "1.0.1-de0cd06.0",
|
||||
"@vality/swag-payments": "0.1.3-77c86a5.0",
|
||||
"@vality/swag-url-shortener": "0.1.0",
|
||||
"@vality/swag-wallet": "0.1.3-e6d0c88.0",
|
||||
@ -54,12 +54,12 @@
|
||||
"css-element-queries": "1.2.3",
|
||||
"humanize-duration": "^3.19.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"keycloak-angular": "^15.0.0",
|
||||
"keycloak-angular": "^15.1.0",
|
||||
"keycloak-js": "^18.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"ng-apexcharts": "1.7.1",
|
||||
"ng-flex-layout": "^17.0.1-beta.2",
|
||||
"ng-flex-layout": "^17.1.3-beta.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"short-uuid": "4.2.0",
|
||||
"tslib": "^2.4.0",
|
||||
@ -68,12 +68,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "^17.0.0",
|
||||
"@angular-devkit/build-angular": "^17.0.9",
|
||||
"@angular-devkit/build-angular": "^17.1.3",
|
||||
"@angular-eslint/builder": "^17.2.0",
|
||||
"@angular-eslint/schematics": "^17.2.0",
|
||||
"@angular/cli": "^17.0.9",
|
||||
"@angular/compiler-cli": "^17.0.8",
|
||||
"@angular/language-service": "^17.0.8",
|
||||
"@angular/cli": "^17.1.3",
|
||||
"@angular/compiler-cli": "^17.1.3",
|
||||
"@angular/language-service": "^17.1.3",
|
||||
"@ngneat/transloco-keys-manager": "^3.8.0",
|
||||
"@ngneat/transloco-optimize": "^5.0.3",
|
||||
"@types/d3": "^5.7.0",
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { ResourceScopeId, RoleId } from '@vality/swag-organizations';
|
||||
import { ResourceScopeId } from '@vality/swag-organizations';
|
||||
|
||||
import { RoleId } from '../../auth/types/role-id';
|
||||
import { DictionaryService } from '../utils';
|
||||
|
||||
export type ResourceScopeIdInternal = ResourceScopeId | 'Wallet';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@ -14,6 +17,7 @@ export class OrganizationsDictionaryService {
|
||||
Accountant: this.t.translate('organizations.roleId.Accountant', null, 'dictionary'),
|
||||
Integrator: this.t.translate('organizations.roleId.Integrator', null, 'dictionary'),
|
||||
Manager: this.t.translate('organizations.roleId.Manager', null, 'dictionary'),
|
||||
WalletManager: this.t.translate('organizations.roleId.WalletManager', null, 'dictionary'),
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
}));
|
||||
resourceScopeId$ = this.dictionaryService.create<ResourceScopeId>(() => ({
|
||||
@ -21,6 +25,12 @@ export class OrganizationsDictionaryService {
|
||||
Shop: this.t.translate('organizations.resourceScopeId.Shop', null, 'dictionary'),
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
}));
|
||||
resourceScopeIdPlural$ = this.dictionaryService.create<ResourceScopeId>(() => ({
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
Shop: this.t.translate('organizations.resourceScopeIdPlural.Shop', null, 'dictionary'),
|
||||
Wallet: this.t.translate('organizations.resourceScopeIdPlural.Wallet', null, 'dictionary'),
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
}));
|
||||
|
||||
constructor(
|
||||
private t: TranslocoService,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
|
||||
import { RoleAccessGroup } from './types/role-access';
|
||||
import { RoleAccessName } from './types/role-access-name';
|
||||
import { RoleId } from './types/role-id';
|
||||
|
||||
export const ROLE_ACCESS_GROUPS: RoleAccessGroup[] = [
|
||||
{
|
||||
@ -70,7 +69,7 @@ export const ROLE_ACCESS_GROUPS: RoleAccessGroup[] = [
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.Wallets,
|
||||
availableRoles: [RoleId.Administrator, RoleId.Accountant, RoleId.Integrator],
|
||||
availableRoles: [RoleId.WalletManager],
|
||||
},
|
||||
{
|
||||
name: RoleAccessName.Claims,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
import { Overwrite } from 'utility-types';
|
||||
|
||||
import { RoleAccessName } from './role-access-name';
|
||||
import { RoleId } from './role-id';
|
||||
|
||||
export interface RoleAccess {
|
||||
name: RoleAccessName;
|
||||
|
10
src/app/auth/types/role-id.ts
Normal file
10
src/app/auth/types/role-id.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* https://github.com/valitydev/bouncer-policies/blob/4bbeaef91653a40bf4583acc581bff1e5aba0ba6/policies/service/authz/roles/data.yaml#L15
|
||||
*/
|
||||
export enum RoleId {
|
||||
Administrator = 'Administrator',
|
||||
Manager = 'Manager',
|
||||
Accountant = 'Accountant',
|
||||
Integrator = 'Integrator',
|
||||
WalletManager = 'WalletManager',
|
||||
}
|
@ -1,70 +1,56 @@
|
||||
<dsh-nested-table
|
||||
<div
|
||||
*transloco="let t; scope: 'organization-section'; read: 'organizationSection.changeRolesTable'"
|
||||
class="wrapper"
|
||||
>
|
||||
<dsh-nested-table-row>
|
||||
<dsh-nested-table-col class="dsh-body-2"></dsh-nested-table-col>
|
||||
<dsh-nested-table-col
|
||||
*ngFor="let roleId of roleIds"
|
||||
class="dsh-body-2"
|
||||
fxLayoutAlign="center center"
|
||||
><button dsh-button (click)="show(roleId)">
|
||||
{{ (roleIdDict$ | async)?.[roleId] }}
|
||||
</button></dsh-nested-table-col
|
||||
>
|
||||
<dsh-nested-table-col *ngIf="isAllowAdd">
|
||||
<span
|
||||
class="dsh-text-color-secondary dsh-body-1"
|
||||
style="text-decoration: underline; cursor: pointer"
|
||||
(click)="show()"
|
||||
>{{ t('info') }}</span
|
||||
>
|
||||
<dsh-nested-table
|
||||
[cellsTemplates]="cellsTemplates"
|
||||
[columns]="columns$ | async"
|
||||
[data]="data$ | async"
|
||||
[footerTemplates]="footerTemplates"
|
||||
[headersTemplates]="{ add: headerTpl }"
|
||||
>
|
||||
<ng-template #headerTpl let-column="column">
|
||||
<button color="accent" dsh-button (click)="add()">{{ t('add') }}</button>
|
||||
</dsh-nested-table-col>
|
||||
</dsh-nested-table-row>
|
||||
|
||||
<ng-container *ngIf="shops$ | async" [dshNestedTableCollapse]="true">
|
||||
<dsh-nested-table-row>
|
||||
<dsh-nested-table-col>
|
||||
<dsh-nested-table-collapse-button
|
||||
>{{ t('shops') }} ({{
|
||||
(shops$ | async).length
|
||||
}})</dsh-nested-table-collapse-button
|
||||
>
|
||||
</dsh-nested-table-col>
|
||||
<dsh-nested-table-col *ngFor="let roleId of roleIds">
|
||||
<mat-checkbox
|
||||
[checked]="checkedAll(roleId) | async"
|
||||
[disabled]="disabledAll(roleId)"
|
||||
[indeterminate]="isIntermediate(roleId) | async"
|
||||
[ngClass]="{ disabled: disabledAll(roleId) }"
|
||||
(change)="toggleAll(roleId)"
|
||||
></mat-checkbox>
|
||||
</dsh-nested-table-col>
|
||||
</dsh-nested-table-row>
|
||||
<dsh-nested-table-group *dshNestedTableCollapseBody [displayedCount]="10">
|
||||
<dsh-nested-table-row *ngFor="let shop of shops$ | async">
|
||||
<dsh-nested-table-col>{{ shop.details.name }}</dsh-nested-table-col>
|
||||
<dsh-nested-table-col *ngFor="let roleId of roleIds">
|
||||
<mat-checkbox
|
||||
[checked]="checked(roleId, shop.id) | async"
|
||||
[disabled]="disabled(roleId, shop.id) | async"
|
||||
[ngClass]="{ disabled: (disabled(roleId, shop.id) | async) }"
|
||||
(change)="toggle(roleId, shop.id, $event.checked)"
|
||||
></mat-checkbox>
|
||||
</dsh-nested-table-col>
|
||||
</dsh-nested-table-row>
|
||||
</dsh-nested-table-group>
|
||||
</ng-container>
|
||||
|
||||
<dsh-nested-table-row>
|
||||
<dsh-nested-table-col class="dsh-body-2"></dsh-nested-table-col>
|
||||
<dsh-nested-table-col
|
||||
*ngFor="let roleId of roleIds"
|
||||
class="dsh-body-2"
|
||||
fxLayoutAlign="center center"
|
||||
>
|
||||
</ng-template>
|
||||
<ng-template #roleCellTpl let-column="column" let-value="value">
|
||||
<mat-checkbox
|
||||
*ngIf="
|
||||
value?.scope !== 'Wallet' && column.field !== 'WalletManager';
|
||||
else selectedTpl
|
||||
"
|
||||
[checked]="checked(column.field, value?.shop?.id, value?.scope) | async"
|
||||
[disabled]="
|
||||
(disabled(column.field, value?.shop?.id, value?.scope) | async) || inProgress
|
||||
"
|
||||
[indeterminate]="
|
||||
value?.shop || value?.scope === 'Wallet'
|
||||
? false
|
||||
: (isIntermediate(column.field) | async)
|
||||
"
|
||||
(change)="toggle(column.field, value?.shop?.id, $event.checked)"
|
||||
></mat-checkbox>
|
||||
<ng-template #selectedTpl>
|
||||
<dsh-selection
|
||||
[selected]="checked(column.field, value?.shop?.id, value?.scope) | async"
|
||||
class="selection"
|
||||
></dsh-selection>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template #footerCellTpl let-column="column" let-value="value">
|
||||
<button
|
||||
[disabled]="!(isAllowRemoves$ | async)"
|
||||
[disabled]="!(isAllowRemove(column?.field) | async) || inProgress"
|
||||
color="warn"
|
||||
dsh-button
|
||||
(click)="remove(roleId)"
|
||||
(click)="remove(column.field)"
|
||||
>
|
||||
{{ t('remove') }}
|
||||
</button>
|
||||
</dsh-nested-table-col>
|
||||
</dsh-nested-table-row>
|
||||
</dsh-nested-table>
|
||||
</ng-template>
|
||||
</dsh-nested-table>
|
||||
</div>
|
||||
|
@ -2,6 +2,11 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
.wrapper {
|
||||
overflow: auto;
|
||||
|
||||
.selection {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { ApiShopsService } from '@dsh/app/api';
|
||||
import { DialogConfig, DIALOG_CONFIG } from '@dsh/app/sections/tokens';
|
||||
import { provideMockService, provideMockToken } from '@dsh/app/shared/tests';
|
||||
|
||||
import { ChangeRolesTableComponent } from './change-roles-table.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-host',
|
||||
template: `<dsh-change-roles-table></dsh-change-roles-table>`,
|
||||
})
|
||||
class HostComponent {}
|
||||
|
||||
describe('ChangeRolesTableComponent', () => {
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
let debugElement: DebugElement;
|
||||
let component: ChangeRolesTableComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
declarations: [HostComponent, ChangeRolesTableComponent],
|
||||
providers: [
|
||||
provideMockService(ApiShopsService),
|
||||
provideMockService(MatDialog),
|
||||
provideMockToken(DIALOG_CONFIG, {} as DialogConfig),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
debugElement = fixture.debugElement.query(By.directive(ChangeRolesTableComponent));
|
||||
component = debugElement.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,35 +1,50 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
OnChanges,
|
||||
booleanAttribute,
|
||||
ViewChild,
|
||||
TemplateRef,
|
||||
signal,
|
||||
computed,
|
||||
Injector,
|
||||
DestroyRef,
|
||||
} from '@angular/core';
|
||||
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { ComponentChanges } from '@vality/ng-core';
|
||||
import { ResourceScopeId, RoleId, Organization } from '@vality/swag-organizations';
|
||||
import {
|
||||
ComponentChanges,
|
||||
getEnumValues,
|
||||
DialogService,
|
||||
DialogResponseStatus,
|
||||
} from '@vality/ng-core';
|
||||
import { ResourceScopeId, Organization } from '@vality/swag-organizations';
|
||||
import { Shop } from '@vality/swag-payments';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
import { BehaviorSubject, combineLatest, EMPTY, Observable, of, ReplaySubject } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, Observable, of, ReplaySubject, filter, defer } from 'rxjs';
|
||||
import { first, map, switchMap, tap, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { OrganizationsDictionaryService, MemberRoleOptionalId } from '@dsh/app/api/organizations';
|
||||
import { ShopsService } from '@dsh/app/api/payments';
|
||||
import { DialogConfig, DIALOG_CONFIG } from '@dsh/app/sections/tokens';
|
||||
import {
|
||||
OrganizationsDictionaryService,
|
||||
MemberRoleOptionalId,
|
||||
ResourceScopeIdInternal,
|
||||
} from '@dsh/app/api/organizations';
|
||||
import { ShopsService, toLiveShops } from '@dsh/app/api/payments';
|
||||
import { RoleId } from '@dsh/app/auth/types/role-id';
|
||||
import { sortRoleIds } from '@dsh/app/shared/components/organization-roles/utils/sort-role-ids';
|
||||
import { NestedTableColumn, NestedTableNode } from '@dsh/components/nested-table';
|
||||
import { addDialogsClass } from '@dsh/utils/add-dialogs-class';
|
||||
|
||||
import { equalRoles } from '../members/components/edit-roles-dialog/utils/equal-roles';
|
||||
|
||||
import { SelectRoleDialogComponent } from './components/select-role-dialog/select-role-dialog.component';
|
||||
import { SelectRoleDialogResult } from './components/select-role-dialog/types/select-role-dialog-result';
|
||||
import { SelectRoleDialogData } from './components/select-role-dialog/types/selected-role-dialog-data';
|
||||
|
||||
@UntilDestroy()
|
||||
type DataItem = { shop?: Pick<Shop, 'id' | 'details'>; scope?: ResourceScopeIdInternal };
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-change-roles-table',
|
||||
templateUrl: 'change-roles-table.component.html',
|
||||
@ -38,64 +53,105 @@ import { SelectRoleDialogData } from './components/select-role-dialog/types/sele
|
||||
export class ChangeRolesTableComponent implements OnInit, OnChanges {
|
||||
@Input() set roles(roles: MemberRoleOptionalId[]) {
|
||||
if (!isNil(roles)) {
|
||||
this.addRoleIds(roles.map(({ roleId }) => roleId as RoleId));
|
||||
this.roles$.next(roles);
|
||||
this.addRoleIds(roles.map(({ roleId }) => roleId));
|
||||
}
|
||||
}
|
||||
get roles(): MemberRoleOptionalId[] {
|
||||
return this.roles$.value;
|
||||
}
|
||||
@Input() organization: Organization;
|
||||
|
||||
/**
|
||||
* Edit mode:
|
||||
* - no batch changes
|
||||
* - there must be at least one role
|
||||
*/
|
||||
@Input({ transform: booleanAttribute }) editMode: boolean;
|
||||
@Input({ transform: booleanAttribute }) hasAtLeastOneRole: boolean;
|
||||
@Input({ transform: booleanAttribute }) controlled: boolean;
|
||||
@Input() inProgress = false;
|
||||
|
||||
@Output() selectedRoles = new EventEmitter<MemberRoleOptionalId[]>();
|
||||
@Output() addedRoles = new EventEmitter<MemberRoleOptionalId[]>();
|
||||
@Output() removedRoles = new EventEmitter<MemberRoleOptionalId[]>();
|
||||
|
||||
@ViewChild('roleCellTpl') cellTemplate: TemplateRef<unknown>;
|
||||
@ViewChild('footerCellTpl') footerTemplate: TemplateRef<unknown>;
|
||||
|
||||
organization$ = new ReplaySubject<Organization>(1);
|
||||
roleIds: RoleId[] = [];
|
||||
roleIds = signal<Set<RoleId>>(new Set());
|
||||
shops$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.shopsService.getShopsForParty({ partyID: organization.party }),
|
||||
),
|
||||
map((shops) => toLiveShops(shops)),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
roleIdDict$ = this.organizationsDictionaryService.roleId$;
|
||||
|
||||
get availableRoles(): RoleId[] {
|
||||
return Object.values(RoleId).filter((r) => !this.roleIds.includes(r));
|
||||
}
|
||||
|
||||
get isAllowAdd(): boolean {
|
||||
return !!this.availableRoles.length && !this.hasAdminRole;
|
||||
}
|
||||
|
||||
availableRoles = computed(() => Object.values(RoleId).filter((r) => !this.roleIds().has(r)));
|
||||
roles$ = new BehaviorSubject<MemberRoleOptionalId[]>([]);
|
||||
|
||||
isAllowRemoves$ = this.roles$.pipe(
|
||||
map(
|
||||
(roles) =>
|
||||
!this.editMode || roles.some((r) => roles.some((b) => b.roleId !== r.roleId)),
|
||||
),
|
||||
columns$ = combineLatest([
|
||||
toObservable(this.roleIds),
|
||||
this.organizationsDictionaryService.roleId$,
|
||||
defer(() => toObservable(this.isAllowAdd, { injector: this.injector })),
|
||||
this.organizationsDictionaryService.resourceScopeIdPlural$,
|
||||
]).pipe(
|
||||
map(([roleIds, rolesDict, isAllowAdd, scopesDict]): NestedTableColumn<DataItem>[] => [
|
||||
{
|
||||
field: 'name',
|
||||
header: '',
|
||||
formatter: (d) => (d.scope ? scopesDict[d.scope] : d.shop.details.name),
|
||||
style: { 'min-width': '130px' },
|
||||
},
|
||||
...Array.from(roleIds).map((r) => ({
|
||||
field: r,
|
||||
header: rolesDict?.[r] || r,
|
||||
style: { 'text-align': 'center' },
|
||||
})),
|
||||
...(isAllowAdd
|
||||
? [
|
||||
{
|
||||
field: 'add',
|
||||
header: '',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
);
|
||||
data$: Observable<NestedTableNode<DataItem>[]> = combineLatest([this.shops$, this.roles$]).pipe(
|
||||
map(([shops, roles]) => [
|
||||
{
|
||||
value: { scope: ResourceScopeId.Shop },
|
||||
children: [
|
||||
...shops,
|
||||
...roles
|
||||
.filter((r) => r.scope?.id === ResourceScopeId.Shop)
|
||||
.map((r) => r.scope.resourceId)
|
||||
.filter((id) => !!id && shops.every((s) => s.id !== id))
|
||||
.map((id) => ({ id, details: { name: `${id}` } })),
|
||||
].map((shop) => ({ value: { shop } })),
|
||||
expanded: true,
|
||||
},
|
||||
{
|
||||
value: { scope: 'Wallet' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
private get hasAdminRole() {
|
||||
return !!this.roles.find((r) => r.id === RoleId.Administrator);
|
||||
get cellsTemplates() {
|
||||
return Object.fromEntries(getEnumValues(RoleId).map((r) => [r, this.cellTemplate]));
|
||||
}
|
||||
|
||||
get footerTemplates() {
|
||||
return Object.fromEntries(getEnumValues(RoleId).map((r) => [r, this.footerTemplate]));
|
||||
}
|
||||
|
||||
private isAllowAdd = computed(
|
||||
() =>
|
||||
!!this.availableRoles().length &&
|
||||
!this.roles.some((r) => r.id === RoleId.Administrator),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private shopsService: ShopsService,
|
||||
private dialog: MatDialog,
|
||||
@Inject(DIALOG_CONFIG) private dialogConfig: DialogConfig,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private dialogService: DialogService,
|
||||
private organizationsDictionaryService: OrganizationsDictionaryService,
|
||||
private injector: Injector,
|
||||
private destroyRef: DestroyRef,
|
||||
) {}
|
||||
|
||||
ngOnChanges({ organization }: ComponentChanges<ChangeRolesTableComponent>) {
|
||||
@ -105,48 +161,41 @@ export class ChangeRolesTableComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.roles$.pipe(untilDestroyed(this)).subscribe((roles) => this.selectedRoles.emit(roles));
|
||||
this.roles$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((roles) => this.selectedRoles.emit(roles));
|
||||
}
|
||||
|
||||
add(): void {
|
||||
const removeDialogsClass = addDialogsClass(this.dialog.openDialogs, 'dsh-hidden');
|
||||
this.dialog
|
||||
.open<SelectRoleDialogComponent, SelectRoleDialogData, SelectRoleDialogResult>(
|
||||
SelectRoleDialogComponent,
|
||||
{
|
||||
...this.dialogConfig.large,
|
||||
data: { availableRoles: this.availableRoles },
|
||||
},
|
||||
)
|
||||
this.dialogService
|
||||
.open(SelectRoleDialogComponent, { availableRoles: this.availableRoles() })
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
tap(() => removeDialogsClass()),
|
||||
switchMap((result) =>
|
||||
typeof result === 'object' ? of(result.selectedRoleId) : EMPTY,
|
||||
),
|
||||
untilDestroyed(this),
|
||||
filter((result) => result.status === DialogResponseStatus.Success),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((roleId) => {
|
||||
this.addRoleIds([roleId]);
|
||||
if (roleId === RoleId.Administrator) {
|
||||
this.addRoles([{ roleId: RoleId.Administrator }]);
|
||||
.subscribe(({ data: { selectedRoleId } }) => {
|
||||
this.addRoleIds([selectedRoleId]);
|
||||
if (
|
||||
selectedRoleId === RoleId.Administrator ||
|
||||
selectedRoleId === RoleId.WalletManager
|
||||
) {
|
||||
this.addRoles([{ roleId: selectedRoleId }]);
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
show(roleId: RoleId) {
|
||||
show(roleId?: RoleId) {
|
||||
const removeDialogsClass = addDialogsClass(this.dialog.openDialogs, 'dsh-hidden');
|
||||
this.dialog
|
||||
.open<SelectRoleDialogComponent, SelectRoleDialogData, SelectRoleDialogResult>(
|
||||
SelectRoleDialogComponent,
|
||||
{
|
||||
...this.dialogConfig.large,
|
||||
data: { availableRoles: [roleId], isShow: true },
|
||||
},
|
||||
)
|
||||
this.dialogService
|
||||
.open(SelectRoleDialogComponent, {
|
||||
availableRoles: roleId ? [roleId] : getEnumValues(RoleId),
|
||||
isShow: true,
|
||||
})
|
||||
.afterClosed()
|
||||
.pipe(untilDestroyed(this))
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
removeDialogsClass();
|
||||
@ -159,7 +208,11 @@ export class ChangeRolesTableComponent implements OnInit, OnChanges {
|
||||
this.removeRoles(this.roles.filter((r) => r.roleId === roleId));
|
||||
}
|
||||
|
||||
toggle(roleId: RoleId, resourceId: string): void {
|
||||
toggle(roleId: RoleId, resourceId?: string): void {
|
||||
if (!resourceId) {
|
||||
this.toggleAll(roleId);
|
||||
return;
|
||||
}
|
||||
const role: MemberRoleOptionalId = {
|
||||
roleId,
|
||||
scope: { id: ResourceScopeId.Shop, resourceId },
|
||||
@ -172,65 +225,51 @@ export class ChangeRolesTableComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
toggleAll(roleId: RoleId): void {
|
||||
const roles = this.roles.filter((r) => r.roleId === roleId);
|
||||
combineLatest([this.shops$, this.checkedAll(roleId)])
|
||||
.pipe(first(), untilDestroyed(this))
|
||||
.subscribe(([shops, isCheckedAll]) => {
|
||||
if (isCheckedAll) {
|
||||
this.removeRoles(roles);
|
||||
} else {
|
||||
const newRoles = shops
|
||||
.filter((s) => !roles.find((r) => r.scope?.resourceId === s.id))
|
||||
.map(({ id: resourceId }) => ({
|
||||
roleId,
|
||||
scope: { id: ResourceScopeId.Shop, resourceId },
|
||||
}));
|
||||
this.addRoles(newRoles);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disabled(roleId: RoleId, resourceId: string): Observable<boolean> {
|
||||
if (roleId === RoleId.Administrator) {
|
||||
disabled(
|
||||
roleId: RoleId,
|
||||
resourceId?: string,
|
||||
scopeId?: ResourceScopeIdInternal,
|
||||
): Observable<boolean> {
|
||||
if ([RoleId.Administrator, RoleId.WalletManager].includes(roleId) || scopeId === 'Wallet') {
|
||||
return of(true);
|
||||
}
|
||||
if (!this.editMode) {
|
||||
if (!this.hasAtLeastOneRole) {
|
||||
return of(false);
|
||||
}
|
||||
return combineLatest([this.roles$, this.checked(roleId, resourceId)]).pipe(
|
||||
map(([roles, isChecked]) => roles.length <= 1 && isChecked),
|
||||
);
|
||||
}
|
||||
|
||||
disabledAll(roleId: RoleId): boolean {
|
||||
return roleId === RoleId.Administrator || this.editMode;
|
||||
}
|
||||
|
||||
checked(roleId: RoleId, resourceId: string): Observable<boolean> {
|
||||
return this.roles$.pipe(
|
||||
map(
|
||||
(roles) =>
|
||||
roleId === RoleId.Administrator ||
|
||||
!!roles.find((r) =>
|
||||
equalRoles(r, { roleId, scope: { id: ResourceScopeId.Shop, resourceId } }),
|
||||
),
|
||||
([roles, isChecked]) =>
|
||||
isChecked &&
|
||||
(roles.length <= 1 ||
|
||||
(!resourceId && uniqBy(roles, (r) => r.roleId).length <= 1)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
checkedAll(roleId: RoleId): Observable<boolean> {
|
||||
return combineLatest([this.shops$, this.roles$]).pipe(
|
||||
map(([shops, roles]) => {
|
||||
const shopIds = shops.map(({ id }) => id);
|
||||
return (
|
||||
roleId === RoleId.Administrator ||
|
||||
shops.length <=
|
||||
roles.filter(
|
||||
(r) => r.roleId === roleId && shopIds.includes(r.scope?.resourceId),
|
||||
).length
|
||||
);
|
||||
}),
|
||||
checked(
|
||||
roleId: RoleId,
|
||||
resourceId?: string,
|
||||
scopeId?: ResourceScopeIdInternal,
|
||||
): Observable<boolean> {
|
||||
if (scopeId === 'Wallet') {
|
||||
return of(roleId === RoleId.WalletManager);
|
||||
}
|
||||
if (roleId === RoleId.Administrator) {
|
||||
return of(true);
|
||||
}
|
||||
return combineLatest([
|
||||
resourceId
|
||||
? of([resourceId])
|
||||
: this.shops$.pipe(map((shops) => shops.map(({ id }) => id))),
|
||||
this.roles$,
|
||||
]).pipe(
|
||||
map(([shopIds, roles]) =>
|
||||
shopIds.every((resourceId) =>
|
||||
roles.find((r) =>
|
||||
equalRoles(r, { roleId, scope: { id: ResourceScopeId.Shop, resourceId } }),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -249,12 +288,41 @@ export class ChangeRolesTableComponent implements OnInit, OnChanges {
|
||||
);
|
||||
}
|
||||
|
||||
isAllowRemove(roleId: RoleId) {
|
||||
return this.roles$.pipe(
|
||||
map((roles) => !this.hasAtLeastOneRole || roles.some((r) => r.roleId !== roleId)),
|
||||
);
|
||||
}
|
||||
|
||||
private toggleAll(roleId: RoleId): void {
|
||||
const roles = this.roles.filter((r) => r.roleId === roleId);
|
||||
combineLatest([this.shops$, this.checked(roleId)])
|
||||
.pipe(first(), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(([shops, isCheckedAll]) => {
|
||||
if (isCheckedAll) {
|
||||
this.removeRoles(roles);
|
||||
} else {
|
||||
const newRoles = shops
|
||||
.filter((s) => !roles.find((r) => r.scope?.resourceId === s.id))
|
||||
.map(({ id: resourceId }) => ({
|
||||
roleId,
|
||||
scope: { id: ResourceScopeId.Shop, resourceId },
|
||||
}));
|
||||
this.addRoles(newRoles);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addRoleIds(roleIds: RoleId[]) {
|
||||
this.roleIds = Array.from(new Set([...this.roleIds, ...roleIds])).sort(sortRoleIds);
|
||||
const newRoleIds = new Set(roleIds.sort(sortRoleIds));
|
||||
if (Array.from(newRoleIds).every((r) => this.roleIds().has(r))) {
|
||||
return;
|
||||
}
|
||||
this.roleIds.update((v) => (newRoleIds.forEach((r) => v.add(r)), new Set(v)));
|
||||
}
|
||||
|
||||
private removeRoleIds(roleIds: RoleId[]) {
|
||||
this.roleIds = this.roleIds.filter((r) => !roleIds.includes(r));
|
||||
this.roleIds.update((v) => (roleIds.forEach((r) => v.delete(r)), new Set(v)));
|
||||
}
|
||||
|
||||
private addRoles(roles: MemberRoleOptionalId[]) {
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
import { FlexModule } from 'ng-flex-layout';
|
||||
|
||||
import { BaseDialogModule } from '@dsh/app/shared/components/dialog/base-dialog';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { BootstrapIconModule } from '@dsh/components/indicators';
|
||||
import { SelectionModule } from '@dsh/components/indicators/selection';
|
||||
import { NestedTableModule } from '@dsh/components/nested-table';
|
||||
|
||||
@ -28,6 +31,10 @@ import { SelectRoleDialogComponent } from './components/select-role-dialog/selec
|
||||
ReactiveFormsModule,
|
||||
SelectionModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
BootstrapIconModule,
|
||||
FormsModule,
|
||||
MatTooltip,
|
||||
],
|
||||
declarations: [ChangeRolesTableComponent, SelectRoleDialogComponent],
|
||||
exports: [ChangeRolesTableComponent],
|
||||
|
@ -1,40 +1,35 @@
|
||||
<dsh-base-dialog
|
||||
*transloco="let t; scope: 'organization-section'; read: 'organizationSection.selectRoleDialog'"
|
||||
[noActions]="dialogData.isShow"
|
||||
[title]="t('title')"
|
||||
(cancel)="cancel()"
|
||||
>
|
||||
<mat-radio-group [formControl]="roleControl">
|
||||
<dsh-nested-table [rowsGridTemplateColumns]="rowsGridTemplateColumns">
|
||||
<dsh-nested-table-row *ngIf="!data.isShow">
|
||||
<dsh-nested-table-col></dsh-nested-table-col>
|
||||
<dsh-nested-table-col *ngFor="let role of roles" fxLayoutAlign="center center">
|
||||
<span class="dsh-body-2 header">{{ (roleIdDict$ | async)?.[role] }}</span>
|
||||
</dsh-nested-table-col>
|
||||
</dsh-nested-table-row>
|
||||
<dsh-nested-table-row *ngIf="!data.isShow">
|
||||
<dsh-nested-table-col></dsh-nested-table-col>
|
||||
<dsh-nested-table-col *ngFor="let role of roles">
|
||||
<mat-radio-button [value]="role"></mat-radio-button>
|
||||
</dsh-nested-table-col>
|
||||
</dsh-nested-table-row>
|
||||
|
||||
<dsh-nested-table-row *ngFor="let access of accesses">
|
||||
<dsh-nested-table-col>
|
||||
<span [class.dsh-body-2]="access.isHeader">{{
|
||||
(roleAccessDict$ | async)?.[access.name]
|
||||
}}</span>
|
||||
</dsh-nested-table-col>
|
||||
<dsh-nested-table-col *ngFor="let role of roles" fxLayoutAlign="center center">
|
||||
<dsh-selection
|
||||
*ngIf="access.availableRoles !== undefined"
|
||||
[selected]="access.availableRoles.includes(role)"
|
||||
></dsh-selection>
|
||||
</dsh-nested-table-col>
|
||||
</dsh-nested-table-row>
|
||||
<div class="content">
|
||||
<dsh-nested-table
|
||||
[cellsTemplates]="cellsTemplates"
|
||||
[columns]="columns$ | async"
|
||||
[data]="data"
|
||||
>
|
||||
<ng-template #accessCellTpl let-column="column" let-index="index" let-value="value">
|
||||
<dsh-selection
|
||||
*ngIf="value && value?.availableRoles !== undefined"
|
||||
[selected]="value?.availableRoles?.includes?.(column.field)"
|
||||
class="selection"
|
||||
></dsh-selection>
|
||||
<div *ngIf="!value && index === 0" class="selection">
|
||||
<mat-radio-group
|
||||
[disabled]="!dialogData.availableRoles.includes(column?.field)"
|
||||
[ngModel]="selectedRole$ | async"
|
||||
(ngModelChange)="selectedRole$.next(column.field)"
|
||||
>
|
||||
<mat-radio-button [value]="column.field"></mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</ng-template>
|
||||
</dsh-nested-table>
|
||||
</mat-radio-group>
|
||||
<ng-container *ngIf="!data.isShow" dshBaseDialogActions>
|
||||
<button [disabled]="roleControl.invalid" color="accent" dsh-button (click)="select()">
|
||||
</div>
|
||||
<ng-container *ngIf="!dialogData.isShow" dshBaseDialogActions>
|
||||
<button [disabled]="!(selectedRole$ | async)" color="accent" dsh-button (click)="select()">
|
||||
{{ t('select') }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
@ -2,3 +2,18 @@
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::ng-deep mat-radio-button * {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
|
||||
.selection {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
@ -1,43 +0,0 @@
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
|
||||
import { provideMockService, provideMockToken } from '@dsh/app/shared/tests';
|
||||
|
||||
import { SelectRoleDialogComponent } from './select-role-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-host',
|
||||
template: `<dsh-select-role-dialog></dsh-select-role-dialog>`,
|
||||
})
|
||||
class HostComponent {}
|
||||
|
||||
describe('SelectRoleDialogComponent', () => {
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
let debugElement: DebugElement;
|
||||
let component: SelectRoleDialogComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReactiveFormsModule],
|
||||
declarations: [HostComponent, SelectRoleDialogComponent],
|
||||
providers: [
|
||||
provideMockToken(MAT_DIALOG_DATA, { availableRoles: Object.values(RoleId) }),
|
||||
provideMockService(MatDialogRef),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
debugElement = fixture.debugElement.query(By.directive(SelectRoleDialogComponent));
|
||||
component = debugElement.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,58 +1,86 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { Validators, FormBuilder } from '@angular/forms';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
import { Component, ViewChild, TemplateRef, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { DialogSuperclass, DEFAULT_DIALOG_CONFIG, getEnumValues } from '@vality/ng-core';
|
||||
import { combineLatest, Observable, ReplaySubject } from 'rxjs';
|
||||
import { map, first } from 'rxjs/operators';
|
||||
|
||||
import { OrganizationsDictionaryService } from '@dsh/app/api/organizations';
|
||||
import { RoleAccess, ROLE_ACCESS_GROUPS } from '@dsh/app/auth';
|
||||
import { BaseDialogResponseStatus } from '@dsh/app/shared/components/dialog/base-dialog';
|
||||
import { ROLE_PRIORITY_DESC } from '@dsh/app/shared/components/organization-roles/utils/sort-role-ids';
|
||||
import { ROLE_ACCESS_GROUPS, RoleAccessGroup } from '@dsh/app/auth';
|
||||
import { RoleId } from '@dsh/app/auth/types/role-id';
|
||||
import {
|
||||
ROLE_PRIORITY_DESC,
|
||||
sortRoleIds,
|
||||
} from '@dsh/app/shared/components/organization-roles/utils/sort-role-ids';
|
||||
import { NestedTableColumn, NestedTableNode } from '@dsh/components/nested-table';
|
||||
|
||||
import { RoleAccessesDictionaryService } from './services/role-accesses-dictionary.service';
|
||||
import { SelectRoleDialogResult } from './types/select-role-dialog-result';
|
||||
import { SelectRoleDialogData } from './types/selected-role-dialog-data';
|
||||
|
||||
interface FlatRoleAccess extends RoleAccess {
|
||||
isHeader: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-select-role-dialog',
|
||||
templateUrl: 'select-role-dialog.component.html',
|
||||
styleUrls: ['select-role-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SelectRoleDialogComponent {
|
||||
roleControl = this.fb.control<RoleId>(null, Validators.required);
|
||||
accesses: FlatRoleAccess[] = ROLE_ACCESS_GROUPS.map((r) => ({ ...r, isHeader: true })).flatMap(
|
||||
(r) => [r, ...(r.children || [])] as FlatRoleAccess[],
|
||||
);
|
||||
export class SelectRoleDialogComponent extends DialogSuperclass<
|
||||
SelectRoleDialogComponent,
|
||||
{ availableRoles: RoleId[]; isShow?: boolean },
|
||||
{ selectedRoleId: RoleId }
|
||||
> {
|
||||
static defaultDialogConfig = DEFAULT_DIALOG_CONFIG.large;
|
||||
|
||||
selectedRole$ = new ReplaySubject<RoleId>(1);
|
||||
roleIdDict$ = this.organizationsDictionaryService.roleId$;
|
||||
roleAccessDict$ = this.roleAccessesDictionaryService.roleAccessDict$;
|
||||
get rowsGridTemplateColumns() {
|
||||
return `2fr ${'1fr '.repeat(this.data.availableRoles.length)}`;
|
||||
columns$: Observable<NestedTableColumn<RoleAccessGroup>[]> = combineLatest([
|
||||
this.roleIdDict$,
|
||||
this.roleAccessDict$,
|
||||
]).pipe(
|
||||
map(([roleIdDict, roleAccessDict]) => [
|
||||
{
|
||||
field: 'name',
|
||||
header: '',
|
||||
formatter: (d) => (d ? roleAccessDict[d.name] : ''),
|
||||
},
|
||||
...this.roles.sort(sortRoleIds).map((r) => ({ field: r, header: roleIdDict[r] })),
|
||||
]),
|
||||
);
|
||||
data: NestedTableNode<RoleAccessGroup>[] = [
|
||||
...(this.dialogData.isShow ? [] : [{ value: null }]),
|
||||
...ROLE_ACCESS_GROUPS.map((g) => ({
|
||||
value: g,
|
||||
children: g.children?.map?.((a) => ({ value: a })),
|
||||
expanded: true,
|
||||
})),
|
||||
];
|
||||
|
||||
@ViewChild('accessCellTpl') accessCellTpl: TemplateRef<unknown>;
|
||||
|
||||
get cellsTemplates() {
|
||||
return Object.fromEntries(getEnumValues(RoleId).map((r) => [r, this.accessCellTpl]));
|
||||
}
|
||||
|
||||
get roles() {
|
||||
return this.data.availableRoles.sort(
|
||||
return (this.dialogData?.availableRoles || []).sort(
|
||||
(a, b) => ROLE_PRIORITY_DESC[a] - ROLE_PRIORITY_DESC[b],
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) private data: SelectRoleDialogData,
|
||||
private dialogRef: MatDialogRef<SelectRoleDialogComponent, SelectRoleDialogResult>,
|
||||
private fb: FormBuilder,
|
||||
private destroyRef: DestroyRef,
|
||||
private organizationsDictionaryService: OrganizationsDictionaryService,
|
||||
private roleAccessesDictionaryService: RoleAccessesDictionaryService,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dialogRef.close(BaseDialogResponseStatus.Error);
|
||||
this.closeWithError();
|
||||
}
|
||||
|
||||
select() {
|
||||
this.dialogRef.close({
|
||||
selectedRoleId: this.roleControl.value,
|
||||
});
|
||||
this.selectedRole$
|
||||
.pipe(first(), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((selectedRoleId) => {
|
||||
this.closeWithSuccess({ selectedRoleId });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
|
||||
import { BaseDialogResponseStatus } from '@dsh/app/shared/components/dialog/base-dialog';
|
||||
|
||||
export type SelectRoleDialogResult = BaseDialogResponseStatus | { selectedRoleId: RoleId };
|
@ -1,6 +0,0 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
|
||||
export interface SelectRoleDialogData {
|
||||
availableRoles: RoleId[];
|
||||
isShow?: boolean;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
import { RoleId } from '@dsh/app/auth/types/role-id';
|
||||
|
||||
export interface ShopsRole {
|
||||
id: RoleId;
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { DialogResponseStatus, DialogResponse } from '@vality/ng-core';
|
||||
import { Invitation, Organization, RevokeInvitationRequest } from '@vality/swag-organizations';
|
||||
import { filter, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { InvitationsService } from '@dsh/app/api/organizations';
|
||||
import { ErrorService, NotificationService } from '@dsh/app/shared';
|
||||
import { ConfirmActionDialogComponent, ConfirmActionDialogResult } from '@dsh/components/popups';
|
||||
import { ConfirmActionDialogComponent } from '@dsh/components/popups';
|
||||
import { ignoreBeforeCompletion } from '@dsh/utils';
|
||||
|
||||
@UntilDestroy()
|
||||
@ -31,12 +32,10 @@ export class InvitationComponent {
|
||||
@ignoreBeforeCompletion
|
||||
cancel() {
|
||||
return this.dialog
|
||||
.open<ConfirmActionDialogComponent, void, ConfirmActionDialogResult>(
|
||||
ConfirmActionDialogComponent,
|
||||
)
|
||||
.open<ConfirmActionDialogComponent, void, DialogResponse>(ConfirmActionDialogComponent)
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter((r) => r === 'confirm'),
|
||||
filter((r) => r.status === DialogResponseStatus.Success),
|
||||
switchMap(() =>
|
||||
this.invitationsService.revokeInvitation({
|
||||
orgId: this.orgId,
|
||||
|
@ -5,10 +5,11 @@
|
||||
(cancel)="cancel()"
|
||||
>
|
||||
<dsh-change-roles-table
|
||||
[organization]="data.organization"
|
||||
[inProgress]="!!(progress$ | async)"
|
||||
[organization]="dialogData.organization"
|
||||
[roles]="roles$ | async"
|
||||
controlled
|
||||
editMode
|
||||
hasAtLeastOneRole
|
||||
(addedRoles)="addRoles($event)"
|
||||
(removedRoles)="removeRoles($event)"
|
||||
></dsh-change-roles-table>
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { DialogSuperclass, DEFAULT_DIALOG_CONFIG, progressTo } from '@vality/ng-core';
|
||||
import { MemberRole } from '@vality/swag-organizations';
|
||||
import { BehaviorSubject, defer, forkJoin, of, Subscription } from 'rxjs';
|
||||
import { catchError, shareReplay, switchMap, map } from 'rxjs/operators';
|
||||
|
||||
import { MembersService } from '@dsh/app/api/organizations';
|
||||
import { ErrorService } from '@dsh/app/shared';
|
||||
import { BaseDialogResponseStatus } from '@dsh/app/shared/components/dialog/base-dialog';
|
||||
|
||||
import { EditRolesDialogData } from './types/edit-roles-dialog-data';
|
||||
|
||||
@ -17,41 +16,51 @@ import { EditRolesDialogData } from './types/edit-roles-dialog-data';
|
||||
templateUrl: 'edit-roles-dialog.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class EditRolesDialogComponent {
|
||||
export class EditRolesDialogComponent extends DialogSuperclass<
|
||||
EditRolesDialogData,
|
||||
EditRolesDialogData
|
||||
> {
|
||||
static defaultDialogConfig = DEFAULT_DIALOG_CONFIG.large;
|
||||
|
||||
roles$ = defer(() => this.updateRoles$).pipe(
|
||||
switchMap(() =>
|
||||
this.membersService
|
||||
.getOrgMember({ orgId: this.data.organization.id, userId: this.data.userId })
|
||||
.getOrgMember({
|
||||
orgId: this.dialogData.organization.id,
|
||||
userId: this.dialogData.userId,
|
||||
})
|
||||
.pipe(map((r) => r.roles)),
|
||||
),
|
||||
untilDestroyed(this),
|
||||
shareReplay(1),
|
||||
);
|
||||
progress$ = new BehaviorSubject(0);
|
||||
|
||||
private updateRoles$ = new BehaviorSubject<void>(null);
|
||||
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<EditRolesDialogComponent, BaseDialogResponseStatus>,
|
||||
@Inject(MAT_DIALOG_DATA) private data: EditRolesDialogData,
|
||||
private membersService: MembersService,
|
||||
private errorService: ErrorService,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.dialogRef.close(BaseDialogResponseStatus.Cancelled);
|
||||
this.closeWithCancellation();
|
||||
}
|
||||
|
||||
addRoles(roles: MemberRole[]): Subscription {
|
||||
return forkJoin(
|
||||
roles.map((memberRole) =>
|
||||
this.membersService.assignMemberRole({
|
||||
orgId: this.data.organization.id,
|
||||
userId: this.data.userId,
|
||||
orgId: this.dialogData.organization.id,
|
||||
userId: this.dialogData.userId,
|
||||
memberRole,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.pipe(
|
||||
progressTo(this.progress$),
|
||||
catchError((err) => {
|
||||
this.errorService.error(err);
|
||||
return of(undefined);
|
||||
@ -65,11 +74,12 @@ export class EditRolesDialogComponent {
|
||||
roles.map((role) =>
|
||||
this.membersService
|
||||
.removeMemberRole({
|
||||
orgId: this.data.organization.id,
|
||||
userId: this.data.userId,
|
||||
orgId: this.dialogData.organization.id,
|
||||
userId: this.dialogData.userId,
|
||||
memberRoleId: role.id,
|
||||
})
|
||||
.pipe(
|
||||
progressTo(this.progress$),
|
||||
catchError((err) => {
|
||||
this.errorService.error(err);
|
||||
return of(undefined);
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { MemberRoleOptionalId } from '@dsh/app/api/organizations';
|
||||
|
||||
export function equalRoles(a: MemberRoleOptionalId, b: MemberRoleOptionalId) {
|
||||
export function equalRoles(a: MemberRoleOptionalId, b: MemberRoleOptionalId): boolean {
|
||||
if (typeof a !== 'object' || typeof b !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (a.id && b.id) {
|
||||
return a.id === b.id;
|
||||
}
|
||||
return (
|
||||
(a.id && b.id && a.id === b.id) ||
|
||||
(a.roleId === b.roleId &&
|
||||
((!a.scope && !b.scope) ||
|
||||
(a.scope.id === b.scope.id && a.scope.resourceId === b.scope.resourceId)))
|
||||
a.roleId === b.roleId &&
|
||||
a.scope?.id === b.scope?.id &&
|
||||
a.scope?.resourceId === b.scope?.resourceId
|
||||
);
|
||||
}
|
||||
|
@ -2,26 +2,22 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { ComponentChanges } from '@vality/ng-core';
|
||||
import { ComponentChanges, DialogService, DialogResponseStatus } from '@vality/ng-core';
|
||||
import { Member, Organization } from '@vality/swag-organizations';
|
||||
import { filter, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { MembersService } from '@dsh/app/api/organizations';
|
||||
import { DialogConfig, DIALOG_CONFIG } from '@dsh/app/sections/tokens';
|
||||
import { ErrorService, NotificationService } from '@dsh/app/shared';
|
||||
import { OrganizationManagementService } from '@dsh/app/shared/services/organization-management/organization-management.service';
|
||||
import { ConfirmActionDialogComponent, ConfirmActionDialogResult } from '@dsh/components/popups';
|
||||
import { ConfirmActionDialogComponent } from '@dsh/components/popups';
|
||||
import { ignoreBeforeCompletion } from '@dsh/utils';
|
||||
|
||||
import { EditRolesDialogComponent } from '../edit-roles-dialog/edit-roles-dialog.component';
|
||||
import { EditRolesDialogData } from '../edit-roles-dialog/types/edit-roles-dialog-data';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
@ -36,8 +32,7 @@ export class MemberComponent implements OnChanges {
|
||||
@Output() changed = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
@Inject(DIALOG_CONFIG) private dialogConfig: DialogConfig,
|
||||
private dialogService: DialogService,
|
||||
private organizationManagementService: OrganizationManagementService,
|
||||
private membersService: MembersService,
|
||||
private notificationService: NotificationService,
|
||||
@ -52,13 +47,11 @@ export class MemberComponent implements OnChanges {
|
||||
|
||||
@ignoreBeforeCompletion
|
||||
removeFromOrganization() {
|
||||
return this.dialog
|
||||
.open<ConfirmActionDialogComponent, void, ConfirmActionDialogResult>(
|
||||
ConfirmActionDialogComponent,
|
||||
)
|
||||
return this.dialogService
|
||||
.open(ConfirmActionDialogComponent)
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter((r) => r === 'confirm'),
|
||||
filter((r) => r.status === DialogResponseStatus.Success),
|
||||
switchMap(() =>
|
||||
this.membersService.expelOrgMember({
|
||||
orgId: this.organization.id,
|
||||
@ -78,13 +71,10 @@ export class MemberComponent implements OnChanges {
|
||||
|
||||
@ignoreBeforeCompletion
|
||||
editRoles() {
|
||||
return this.dialog
|
||||
.open<EditRolesDialogComponent, EditRolesDialogData>(EditRolesDialogComponent, {
|
||||
...this.dialogConfig.large,
|
||||
data: {
|
||||
organization: this.organization,
|
||||
userId: this.member.id,
|
||||
},
|
||||
return this.dialogService
|
||||
.open(EditRolesDialogComponent, {
|
||||
organization: this.organization,
|
||||
userId: this.member.id,
|
||||
})
|
||||
.afterClosed()
|
||||
.pipe(untilDestroyed(this))
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { ComponentChanges } from '@vality/ng-core';
|
||||
import { ComponentChanges, DialogResponseStatus } from '@vality/ng-core';
|
||||
import { Organization } from '@vality/swag-organizations';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
import { filter, pluck, switchMap } from 'rxjs/operators';
|
||||
@ -22,7 +22,7 @@ import {
|
||||
} from '@dsh/app/shared/services';
|
||||
import { FetchOrganizationsService } from '@dsh/app/shared/services/fetch-organizations';
|
||||
import { OrganizationManagementService } from '@dsh/app/shared/services/organization-management/organization-management.service';
|
||||
import { ConfirmActionDialogComponent, ConfirmActionDialogResult } from '@dsh/components/popups';
|
||||
import { ConfirmActionDialogComponent } from '@dsh/components/popups';
|
||||
import { ignoreBeforeCompletion } from '@dsh/utils';
|
||||
|
||||
import { RenameOrganizationDialogComponent } from '../rename-organization-dialog/rename-organization-dialog.component';
|
||||
@ -64,12 +64,10 @@ export class OrganizationComponent implements OnChanges {
|
||||
@ignoreBeforeCompletion
|
||||
leave() {
|
||||
return this.dialog
|
||||
.open<ConfirmActionDialogComponent, void, ConfirmActionDialogResult>(
|
||||
ConfirmActionDialogComponent,
|
||||
)
|
||||
.open(ConfirmActionDialogComponent)
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter((r) => r === 'confirm'),
|
||||
filter((r) => r.status === DialogResponseStatus.Success),
|
||||
switchMap(() =>
|
||||
this.organizationsService.cancelOrgMembership({ orgId: this.organization.id }),
|
||||
),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UntypedFormArray, UntypedFormBuilder } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { toMinor } from '@vality/ng-core';
|
||||
import { toMinor, DialogResponseStatus } from '@vality/ng-core';
|
||||
import {
|
||||
InvoiceLineTaxMode,
|
||||
InvoiceLineTaxVAT,
|
||||
@ -129,7 +129,7 @@ export class CreateInvoiceTemplateService {
|
||||
this.dialog
|
||||
.open(ConfirmActionDialogComponent)
|
||||
.afterClosed()
|
||||
.pipe(filter((r) => r === 'confirm'))
|
||||
.pipe(filter((r) => r.status === DialogResponseStatus.Success))
|
||||
.subscribe(() => {
|
||||
this.cartForm.clear();
|
||||
this.addProduct();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { NotifyLogService } from '@vality/ng-core';
|
||||
import { NotifyLogService, DialogResponseStatus } from '@vality/ng-core';
|
||||
import { combineLatest, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, filter, switchMap, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -38,7 +38,7 @@ export class DeleteWebhookService {
|
||||
this.dialog
|
||||
.open(ConfirmActionDialogComponent)
|
||||
.afterClosed()
|
||||
.pipe(filter((r) => r === 'confirm')),
|
||||
.pipe(filter((r) => r.status === DialogResponseStatus.Success)),
|
||||
]),
|
||||
),
|
||||
switchMap(([webhookID]) =>
|
||||
|
@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { DialogResponseStatus } from '@vality/ng-core';
|
||||
import { combineLatest, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, filter, switchMap, takeUntil, tap, map, first } from 'rxjs/operators';
|
||||
|
||||
@ -38,7 +39,7 @@ export class CancelReportService {
|
||||
this.dialog
|
||||
.open(ConfirmActionDialogComponent)
|
||||
.afterClosed()
|
||||
.pipe(filter((r) => r === 'confirm')),
|
||||
.pipe(filter((r) => r.status === DialogResponseStatus.Success)),
|
||||
]),
|
||||
),
|
||||
switchMap(([reportID]) =>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { NotifyLogService } from '@vality/ng-core';
|
||||
import { NotifyLogService, DialogResponseStatus } from '@vality/ng-core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, filter, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
@ -24,7 +24,7 @@ export class ShopActionsService {
|
||||
.open(ConfirmActionDialogComponent)
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter((r) => r === 'confirm'),
|
||||
filter((r) => r.status === DialogResponseStatus.Success),
|
||||
switchMap(() => this.shopsService.suspendShopForParty({ shopID })),
|
||||
map(() => {
|
||||
this.log.success(
|
||||
@ -55,7 +55,7 @@ export class ShopActionsService {
|
||||
.open(ConfirmActionDialogComponent)
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter((r) => r === 'confirm'),
|
||||
filter((r) => r.status === DialogResponseStatus.Success),
|
||||
switchMap(() => this.shopsService.activateShopForParty({ shopID })),
|
||||
map(() => {
|
||||
this.log.success(
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TranslocoService } from '@ngneat/transloco';
|
||||
import { NotifyLogService } from '@vality/ng-core';
|
||||
import { NotifyLogService, DialogResponseStatus } from '@vality/ng-core';
|
||||
import { combineLatest, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, filter, switchMap, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -40,7 +40,7 @@ export class DeleteWebhookService {
|
||||
this.dialog
|
||||
.open(ConfirmActionDialogComponent)
|
||||
.afterClosed()
|
||||
.pipe(filter((r) => r === 'confirm')),
|
||||
.pipe(filter((r) => r.status === DialogResponseStatus.Success)),
|
||||
]),
|
||||
),
|
||||
switchMap(([{ webhookID, identityID }]) =>
|
||||
|
@ -1,5 +1,3 @@
|
||||
export enum BaseDialogResponseStatus {
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
Cancelled = 'canceled',
|
||||
}
|
||||
import { DialogResponseStatus as BaseDialogResponseStatus } from '@vality/ng-core';
|
||||
|
||||
export { BaseDialogResponseStatus };
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { ResourceScopeId, RoleId } from '@vality/swag-organizations';
|
||||
import { ResourceScopeId } from '@vality/swag-organizations';
|
||||
|
||||
import { RoleId } from '@dsh/app/auth/types/role-id';
|
||||
|
||||
export type ResourceId = string;
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { MemberRole } from '@vality/swag-organizations';
|
||||
|
||||
import { RoleId } from '@dsh/app/auth/types/role-id';
|
||||
|
||||
import { RoleGroup, RoleGroupScope } from '../types/role-group';
|
||||
|
||||
import { sortRoleIds } from './sort-role-ids';
|
||||
@ -9,7 +11,7 @@ export function groupRoles(roles: MemberRole[]): RoleGroup[] {
|
||||
.reduce<RoleGroup[]>((groups, role) => {
|
||||
let group: RoleGroup = groups.find((g) => g.id === role.roleId);
|
||||
if (!group) {
|
||||
group = { id: role.roleId, scopes: [] };
|
||||
group = { id: role.roleId as RoleId, scopes: [] };
|
||||
groups.push(group);
|
||||
}
|
||||
let scope: RoleGroupScope = group.scopes.find((s) => s.id === role.scope.id);
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { RoleId } from '@vality/swag-organizations';
|
||||
import { RoleId } from '@dsh/app/auth/types/role-id';
|
||||
|
||||
export const ROLE_PRIORITY_DESC: Record<RoleId, number> = {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
Administrator: 0,
|
||||
Manager: 1,
|
||||
Accountant: 2,
|
||||
Integrator: 3,
|
||||
[RoleId.Administrator]: 0,
|
||||
[RoleId.Manager]: 1,
|
||||
[RoleId.Accountant]: 2,
|
||||
[RoleId.Integrator]: 3,
|
||||
[RoleId.WalletManager]: 3,
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { Organization, Member, RoleId } from '@vality/swag-organizations';
|
||||
import { Organization, Member } from '@vality/swag-organizations';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
import {
|
||||
Observable,
|
||||
@ -16,6 +16,7 @@ import {
|
||||
import { switchMap, shareReplay, catchError, map, tap, filter } from 'rxjs/operators';
|
||||
|
||||
import { OrgsService, MembersService, DEFAULT_ORGANIZATION_NAME } from '@dsh/app/api/organizations';
|
||||
import { RoleId } from '@dsh/app/auth/types/role-id';
|
||||
import { KeycloakTokenInfoService } from '@dsh/app/shared/services/keycloak-token-info';
|
||||
|
||||
import { ErrorService } from '../error';
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Member, Organization, RoleId } from '@vality/swag-organizations';
|
||||
import { Member, Organization } from '@vality/swag-organizations';
|
||||
import { combineLatest, defer, Observable, ReplaySubject } from 'rxjs';
|
||||
import { map, pluck, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { MembersService } from '@dsh/app/api/organizations';
|
||||
import { RoleId } from '@dsh/app/auth/types/role-id';
|
||||
import { SHARE_REPLAY_CONF } from '@dsh/app/custom-operators';
|
||||
import { KeycloakTokenInfoService } from '@dsh/app/shared';
|
||||
|
||||
|
@ -103,11 +103,16 @@
|
||||
"resourceScopeId": {
|
||||
"Shop": "Shops"
|
||||
},
|
||||
"resourceScopeIdPlural": {
|
||||
"Shop": "Shops",
|
||||
"Wallet": "Wallets"
|
||||
},
|
||||
"roleId": {
|
||||
"Accountant": "Accountant",
|
||||
"Administrator": "Administrator",
|
||||
"Integrator": "Integrator",
|
||||
"Manager": "Manager"
|
||||
"Manager": "Manager",
|
||||
"WalletManager": "Wallets Manager"
|
||||
}
|
||||
},
|
||||
"payments": {
|
||||
|
@ -103,11 +103,16 @@
|
||||
"resourceScopeId": {
|
||||
"Shop": "Магазины"
|
||||
},
|
||||
"resourceScopeIdPlural": {
|
||||
"Shop": "Магазины",
|
||||
"Wallet": "Кошельки"
|
||||
},
|
||||
"roleId": {
|
||||
"Accountant": "Бухгалтер",
|
||||
"Administrator": "Администратор",
|
||||
"Integrator": "Интегратор",
|
||||
"Manager": "Менеджер"
|
||||
"Manager": "Менеджер",
|
||||
"WalletManager": "Менеджер по кошелькам"
|
||||
}
|
||||
},
|
||||
"payments": {
|
||||
|
@ -15,8 +15,8 @@
|
||||
},
|
||||
"changeRolesTable": {
|
||||
"add": "Add",
|
||||
"remove": "Delete",
|
||||
"shops": "Shops"
|
||||
"info": "Roles accesses",
|
||||
"remove": "Delete"
|
||||
},
|
||||
"createInvitationDialog": {
|
||||
"form": {
|
||||
@ -123,7 +123,7 @@
|
||||
"wallets": "Wallets"
|
||||
},
|
||||
"selectRoleDialog": {
|
||||
"select": "Select",
|
||||
"title": "Select role"
|
||||
"select": "Add",
|
||||
"title": "Roles"
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,8 @@
|
||||
},
|
||||
"changeRolesTable": {
|
||||
"add": "Добавить",
|
||||
"remove": "Удалить",
|
||||
"shops": "Магазины"
|
||||
"info": "Права доступа ролей",
|
||||
"remove": "Удалить"
|
||||
},
|
||||
"createInvitationDialog": {
|
||||
"form": {
|
||||
@ -123,7 +123,7 @@
|
||||
"wallets": "Кошельки"
|
||||
},
|
||||
"selectRoleDialog": {
|
||||
"select": "Выбрать",
|
||||
"title": "Выберите роль"
|
||||
"select": "Добавить",
|
||||
"title": "Роли"
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,4 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
@mixin dsh-nested-table-theme($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
$primary: map-get($theme, primary);
|
||||
|
||||
.dsh-nested-table {
|
||||
&-group {
|
||||
&-show-more {
|
||||
color: mat.get-color-from-palette($primary, 400);
|
||||
}
|
||||
}
|
||||
|
||||
&-item,
|
||||
&-row-item {
|
||||
border-color: map-get($foreground, dividers) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
export const ROW_ITEM_CLASS = 'class.dsh-nested-table-row-item';
|
@ -1 +0,0 @@
|
||||
export const TABLE_ITEM_CLASS = 'class.dsh-nested-table-item';
|
@ -1 +0,0 @@
|
||||
<ng-content></ng-content>
|
@ -1,23 +0,0 @@
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
padding: 8px;
|
||||
|
||||
& > ::ng-deep button,
|
||||
& > ::ng-deep mat-checkbox,
|
||||
& > ::ng-deep mat-radio-button {
|
||||
margin: auto;
|
||||
|
||||
.mdc-radio {
|
||||
padding: 0;
|
||||
}
|
||||
.mdc-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > ::ng-deep button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { NestedTableColComponent } from './nested-table-col.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-host',
|
||||
template: `<dsh-nested-table-col></dsh-nested-table-col>`,
|
||||
})
|
||||
class HostComponent {}
|
||||
|
||||
describe('NestedTableColComponent', () => {
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
let debugElement: DebugElement;
|
||||
let component: NestedTableColComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
declarations: [HostComponent, NestedTableColComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
debugElement = fixture.debugElement.query(By.directive(NestedTableColComponent));
|
||||
component = debugElement.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,14 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core';
|
||||
|
||||
import { ROW_ITEM_CLASS } from '@dsh/components/nested-table/classes/row-item-class';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-nested-table-col',
|
||||
templateUrl: 'nested-table-col.component.html',
|
||||
styleUrls: ['nested-table-col.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NestedTableColComponent {
|
||||
@HostBinding(ROW_ITEM_CLASS) readonly rowItemClass = true;
|
||||
@HostBinding('class.dsh-body-1') readonly body1Class = true;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
export const ANIMATION_TIMING = '225ms cubic-bezier(0.4,0.0,0.2,1)';
|
||||
|
||||
export const EXPANSION = trigger('expansion', [
|
||||
state('void', style({ height: '0px', padding: '0', opacity: 0.5 })),
|
||||
state('*', style({ height: '*', padding: '*', opacity: 1 })),
|
||||
transition('* <=> *', animate(ANIMATION_TIMING)),
|
||||
]);
|
@ -1,12 +0,0 @@
|
||||
<ng-content></ng-content>
|
||||
<dsh-nested-table-row *ngIf="showMoreDisplayed$ | async">
|
||||
<dsh-nested-table-col>
|
||||
<span
|
||||
*transloco="let t; scope: 'components'; read: 'components.shared'"
|
||||
class="dsh-nested-table-group-show-more dsh-body-1"
|
||||
(click)="showAll()"
|
||||
>
|
||||
{{ t('showMore') }}
|
||||
</span>
|
||||
</dsh-nested-table-col>
|
||||
</dsh-nested-table-row>
|
@ -1,10 +0,0 @@
|
||||
:host {
|
||||
overflow: hidden;
|
||||
|
||||
.dsh-nested-table-group {
|
||||
&-show-more {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
|
||||
import { NestedTableColComponent } from '@dsh/components/nested-table/components/nested-table-col/nested-table-col.component';
|
||||
import { NestedTableRowComponent } from '@dsh/components/nested-table/components/nested-table-row/nested-table-row.component';
|
||||
import { LayoutManagementService } from '@dsh/components/nested-table/services/layout-management/layout-management.service';
|
||||
|
||||
import { NestedTableGroupComponent } from './nested-table-group.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-host',
|
||||
template: `
|
||||
<dsh-nested-table-group [displayedCount]="displayedCount">
|
||||
<dsh-nested-table-row *ngFor="let i of rows"></dsh-nested-table-row>
|
||||
</dsh-nested-table-group>
|
||||
`,
|
||||
})
|
||||
class HostComponent {
|
||||
displayedCount: number;
|
||||
rows = new Array(10).fill(null);
|
||||
}
|
||||
|
||||
describe('NestedTableLimitedRowsComponent', () => {
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
let debugElement: DebugElement;
|
||||
let component: NestedTableGroupComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, CommonModule],
|
||||
declarations: [
|
||||
HostComponent,
|
||||
NestedTableGroupComponent,
|
||||
NestedTableRowComponent,
|
||||
NestedTableColComponent,
|
||||
],
|
||||
providers: [LayoutManagementService],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
debugElement = fixture.debugElement.query(By.directive(NestedTableGroupComponent));
|
||||
component = debugElement.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be init', () => {
|
||||
expect(component.showMoreDisplayed$).toBeObservable(cold('(a)', { a: false }));
|
||||
fixture.componentInstance.displayedCount = 2;
|
||||
fixture.detectChanges();
|
||||
expect(component.showMoreDisplayed$).toBeObservable(cold('(a)', { a: true }));
|
||||
});
|
||||
|
||||
describe('showAll', () => {
|
||||
it('should showMoreDisplayed', () => {
|
||||
fixture.componentInstance.displayedCount = 2;
|
||||
component.showAll();
|
||||
fixture.detectChanges();
|
||||
expect(component.showMoreDisplayed$).toBeObservable(cold('(a)', { a: false }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('template rows', () => {
|
||||
it('should display all', () => {
|
||||
const rows = fixture.debugElement.queryAll(By.directive(NestedTableRowComponent));
|
||||
expect(rows.length).toBe(10);
|
||||
for (const row of rows) {
|
||||
expect((row.componentInstance as NestedTableRowComponent).display).toBe('grid');
|
||||
}
|
||||
});
|
||||
|
||||
it('should display by limit', () => {
|
||||
fixture.componentInstance.displayedCount = 2;
|
||||
fixture.detectChanges();
|
||||
const rows = fixture.debugElement.queryAll(By.directive(NestedTableRowComponent));
|
||||
expect(rows.length).toBe(11);
|
||||
for (const row of rows.slice(0, 1)) {
|
||||
expect((row.componentInstance as NestedTableRowComponent).display).toBe('grid');
|
||||
}
|
||||
for (const row of rows.slice(2, 9)) {
|
||||
expect((row.componentInstance as NestedTableRowComponent).display).toBe('none');
|
||||
}
|
||||
expect((rows[10].componentInstance as NestedTableRowComponent).display).toBe('grid');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,81 +0,0 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ContentChildren,
|
||||
HostBinding,
|
||||
Input,
|
||||
OnChanges,
|
||||
QueryList,
|
||||
} from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { ComponentChanges } from '@vality/ng-core';
|
||||
import { BehaviorSubject, combineLatest, defer } from 'rxjs';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { TABLE_ITEM_CLASS } from '@dsh/components/nested-table/classes/table-item-class';
|
||||
import { NestedTableRowComponent } from '@dsh/components/nested-table/components/nested-table-row/nested-table-row.component';
|
||||
import { queryListStartedArrayChanges } from '@dsh/utils';
|
||||
|
||||
import { EXPANSION } from './expansion';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'dsh-nested-table-group',
|
||||
templateUrl: 'nested-table-group.component.html',
|
||||
styleUrls: ['nested-table-group.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [EXPANSION],
|
||||
})
|
||||
export class NestedTableGroupComponent implements AfterContentInit, OnChanges {
|
||||
@Input() displayedCount: number;
|
||||
showMoreDisplayed$ = defer(() =>
|
||||
combineLatest([
|
||||
queryListStartedArrayChanges(this.rowChildren),
|
||||
this.displayedAll$,
|
||||
this.displayedCount$,
|
||||
]),
|
||||
).pipe(
|
||||
// displayedCount + 1 - to show the last element instead of the "show all" link when the number of elements is only 1 more
|
||||
map(
|
||||
([rows, displayedAll, displayedCount]) =>
|
||||
!displayedAll && rows.length > displayedCount + 1,
|
||||
),
|
||||
untilDestroyed(this),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
@HostBinding(TABLE_ITEM_CLASS) readonly tableItemClass = true;
|
||||
@HostBinding('@expansion') readonly expansion;
|
||||
|
||||
@ContentChildren(NestedTableRowComponent) private rowChildren =
|
||||
new QueryList<NestedTableRowComponent>();
|
||||
private displayedAll$ = new BehaviorSubject<boolean>(false);
|
||||
private displayedCount$ = new BehaviorSubject(Infinity);
|
||||
|
||||
ngOnChanges({ displayedCount }: ComponentChanges<NestedTableGroupComponent>) {
|
||||
if (displayedCount) {
|
||||
this.displayedCount$.next(displayedCount.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.listenShowAll();
|
||||
}
|
||||
|
||||
showAll() {
|
||||
this.displayedAll$.next(true);
|
||||
}
|
||||
|
||||
private listenShowAll() {
|
||||
combineLatest([queryListStartedArrayChanges(this.rowChildren), this.showMoreDisplayed$])
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe(([rows, showMoreDisplayed]) => {
|
||||
if (showMoreDisplayed) {
|
||||
rows.forEach((row, idx) => row.setHidden(idx >= this.displayedCount));
|
||||
} else {
|
||||
rows.forEach((row) => row.setHidden(false));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
<ng-content></ng-content>
|
||||
<dsh-nested-table-col *ngFor="let col of fillCols">{{ col }}</dsh-nested-table-col>
|
@ -1,9 +0,0 @@
|
||||
:host {
|
||||
& > ::ng-deep .dsh-nested-table-row-item {
|
||||
border-right: 1px solid;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
|
||||
import { NestedTableColComponent } from '@dsh/components/nested-table/components/nested-table-col/nested-table-col.component';
|
||||
import { LayoutManagementService } from '@dsh/components/nested-table/services/layout-management/layout-management.service';
|
||||
|
||||
import { NestedTableRowComponent } from './nested-table-row.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-host',
|
||||
template: `
|
||||
<dsh-nested-table-row>
|
||||
<dsh-nested-table-col></dsh-nested-table-col>
|
||||
<dsh-nested-table-col></dsh-nested-table-col>
|
||||
<dsh-nested-table-col></dsh-nested-table-col>
|
||||
</dsh-nested-table-row>
|
||||
`,
|
||||
})
|
||||
class HostComponent {}
|
||||
|
||||
describe('NestedTableRowComponent', () => {
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
let debugElement: DebugElement;
|
||||
let component: NestedTableRowComponent;
|
||||
let layoutManagementService: LayoutManagementService;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [HostComponent, NestedTableRowComponent, NestedTableColComponent],
|
||||
providers: [LayoutManagementService],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
debugElement = fixture.debugElement.query(By.directive(NestedTableRowComponent));
|
||||
component = debugElement.componentInstance;
|
||||
layoutManagementService = TestBed.inject(LayoutManagementService);
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be init', () => {
|
||||
expect(component.colsCount$).toBeObservable(cold('(a)', { a: 3 }));
|
||||
layoutManagementService.setLayoutColsCount(4);
|
||||
expect(component.fillCols).toEqual(['']);
|
||||
});
|
||||
});
|
@ -1,79 +0,0 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChildren,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
OnInit,
|
||||
QueryList,
|
||||
Renderer2,
|
||||
} from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { pluck } from 'rxjs/operators';
|
||||
|
||||
import { NestedTableColComponent } from '@dsh/components/nested-table/components/nested-table-col/nested-table-col.component';
|
||||
import { queryListStartedArrayChanges } from '@dsh/utils';
|
||||
|
||||
import { TABLE_ITEM_CLASS } from '../../classes/table-item-class';
|
||||
import { LayoutManagementService } from '../../services/layout-management/layout-management.service';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'dsh-nested-table-row',
|
||||
templateUrl: 'nested-table-row.component.html',
|
||||
styleUrls: ['nested-table-row.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NestedTableRowComponent implements AfterContentInit, OnInit {
|
||||
@HostBinding(TABLE_ITEM_CLASS) readonly tableItemClass = true;
|
||||
@HostBinding('style.grid-template-columns') gridTemplateColumns: string;
|
||||
@HostBinding('style.display') display = 'grid';
|
||||
|
||||
colsCount$ = new ReplaySubject<number>(1);
|
||||
fillCols: string[];
|
||||
|
||||
@ContentChildren(NestedTableColComponent)
|
||||
private nestedTableColComponentChildren: QueryList<NestedTableColComponent>;
|
||||
|
||||
constructor(
|
||||
private layoutManagementService: LayoutManagementService,
|
||||
private el: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
private cdr: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.layoutManagementService.gridTemplateColumns$
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe((gridTemplateColumns) => {
|
||||
this.gridTemplateColumns = gridTemplateColumns;
|
||||
this.renderer.setStyle(
|
||||
this.el.nativeElement,
|
||||
'grid-template-columns',
|
||||
gridTemplateColumns,
|
||||
);
|
||||
});
|
||||
this.layoutManagementService.getFillCols(this.colsCount$).subscribe((fillCols) => {
|
||||
this.fillCols = fillCols;
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.listenColsCount();
|
||||
}
|
||||
|
||||
setHidden(hidden: boolean) {
|
||||
this.display = hidden ? 'none' : 'grid';
|
||||
this.renderer.setStyle(this.el.nativeElement, 'display', this.display);
|
||||
}
|
||||
|
||||
private listenColsCount() {
|
||||
queryListStartedArrayChanges(this.nestedTableColComponentChildren)
|
||||
.pipe(pluck('length'), untilDestroyed(this))
|
||||
.subscribe((colsCount) => this.colsCount$.next(colsCount));
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export * from './nested-table.module';
|
||||
export * from './nested-table.component';
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
import { IndicatorRotateState } from './types/indicator-rotate';
|
||||
|
||||
export const ANIMATION_TIMING = '225ms cubic-bezier(0.4,0.0,0.2,1)';
|
||||
|
||||
export const INDICATOR_ROTATE = trigger('indicatorRotate', [
|
||||
state(
|
||||
[IndicatorRotateState.Collapsed, 'void'].join(','),
|
||||
style({ transform: 'rotate(90deg)' }),
|
||||
),
|
||||
state(IndicatorRotateState.Expanded, style({ transform: 'rotate(180deg)' })),
|
||||
transition('expanded <=> collapsed, void => collapsed', animate(ANIMATION_TIMING)),
|
||||
]);
|
@ -1,2 +0,0 @@
|
||||
<div class="dsh-body-2"><ng-content></ng-content></div>
|
||||
<dsh-bi [@indicatorRotate]="animationState$ | async" icon="chevron-up"></dsh-bi>
|
@ -1,6 +0,0 @@
|
||||
:host {
|
||||
display: flex;
|
||||
place-content: stretch space-between;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { ExpansionService } from '@dsh/components/nested-table/nested-table-collapse/services/expansion/expansion.service';
|
||||
|
||||
import { NestedTableCollapseButtonComponent } from './nested-table-collapse-button.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-host',
|
||||
template: `<dsh-nested-table-collapse-button></dsh-nested-table-collapse-button>`,
|
||||
})
|
||||
class HostComponent {}
|
||||
|
||||
describe('NestedTableCollapseButtonComponent', () => {
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
let debugElement: DebugElement;
|
||||
let component: NestedTableCollapseButtonComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
declarations: [HostComponent, NestedTableCollapseButtonComponent],
|
||||
providers: [ExpansionService],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
debugElement = fixture.debugElement.query(By.directive(NestedTableCollapseButtonComponent));
|
||||
component = debugElement.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { ExpansionService } from '../../services/expansion/expansion.service';
|
||||
|
||||
import { INDICATOR_ROTATE } from './indicator-rotate';
|
||||
import { IndicatorRotateState } from './types/indicator-rotate';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'dsh-nested-table-collapse-button',
|
||||
templateUrl: 'nested-table-collapse-button.component.html',
|
||||
styleUrls: ['nested-table-collapse-button.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [INDICATOR_ROTATE],
|
||||
})
|
||||
export class NestedTableCollapseButtonComponent {
|
||||
animationState$ = this.expansionService.expanded$.pipe(
|
||||
map((expanded) =>
|
||||
expanded ? IndicatorRotateState.Expanded : IndicatorRotateState.Collapsed,
|
||||
),
|
||||
untilDestroyed(this),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
constructor(private expansionService: ExpansionService) {}
|
||||
|
||||
@HostListener('click') onClick() {
|
||||
this.expansionService.toggle();
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export enum IndicatorRotateState {
|
||||
Collapsed = 'Collapsed',
|
||||
Expanded = 'Expanded',
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { Directive, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
|
||||
import { ExpansionService } from '../../services/expansion/expansion.service';
|
||||
|
||||
@UntilDestroy()
|
||||
@Directive({
|
||||
selector: '[dshNestedTableCollapseBody]',
|
||||
})
|
||||
export class NestedTableCollapseBodyDirective implements OnInit {
|
||||
constructor(
|
||||
private templateRef: TemplateRef<unknown>,
|
||||
private viewContainer: ViewContainerRef,
|
||||
private expansionService: ExpansionService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.expansionService.expanded$
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe((expanded) => this.expand(expanded));
|
||||
}
|
||||
|
||||
private expand(expanded: boolean) {
|
||||
if (expanded) {
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
} else {
|
||||
this.viewContainer.clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './nested-table-collapse.module';
|
@ -1,15 +0,0 @@
|
||||
import { Directive, Input } from '@angular/core';
|
||||
|
||||
import { ExpansionService } from './services/expansion/expansion.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[dshNestedTableCollapse]',
|
||||
providers: [ExpansionService],
|
||||
})
|
||||
export class NestedTableCollapseDirective {
|
||||
@Input() set dshNestedTableCollapse(expanded: boolean) {
|
||||
this.expansionService.setExpanded(expanded);
|
||||
}
|
||||
|
||||
constructor(private expansionService: ExpansionService) {}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from 'ng-flex-layout';
|
||||
|
||||
import { BootstrapIconModule } from '@dsh/components/indicators';
|
||||
|
||||
import { NestedTableCollapseButtonComponent } from './components/nested-table-collapse-button/nested-table-collapse-button.component';
|
||||
import { NestedTableCollapseBodyDirective } from './directives/nested-table-collapse-body/nested-table-collapse-body.directive';
|
||||
import { NestedTableCollapseDirective } from './nested-table-collapse.directive';
|
||||
|
||||
const SHARED_COMPONENTS = [
|
||||
NestedTableCollapseDirective,
|
||||
NestedTableCollapseButtonComponent,
|
||||
NestedTableCollapseBodyDirective,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FlexLayoutModule, BootstrapIconModule],
|
||||
declarations: SHARED_COMPONENTS,
|
||||
exports: SHARED_COMPONENTS,
|
||||
})
|
||||
export class NestedTableCollapseModule {}
|
@ -1,45 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
|
||||
import { ExpansionService } from './expansion.service';
|
||||
|
||||
describe('ExpansionService', () => {
|
||||
let service: ExpansionService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [ExpansionService],
|
||||
});
|
||||
|
||||
service = TestBed.inject(ExpansionService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be init', () => {
|
||||
expect(service.expanded$).toBeObservable(cold('(a)', { a: false }));
|
||||
});
|
||||
|
||||
describe('setExpanded', () => {
|
||||
it('should be emit', () => {
|
||||
service.setExpanded(true);
|
||||
expect(service.expanded$).toBeObservable(cold('(a)', { a: true }));
|
||||
service.setExpanded(false);
|
||||
expect(service.expanded$).toBeObservable(cold('(a)', { a: false }));
|
||||
service.setExpanded(false);
|
||||
expect(service.expanded$).toBeObservable(cold('(a)', { a: false }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle', () => {
|
||||
it('should be toggle', () => {
|
||||
service.toggle();
|
||||
expect(service.expanded$).toBeObservable(cold('(a)', { a: true }));
|
||||
service.toggle();
|
||||
expect(service.expanded$).toBeObservable(cold('(a)', { a: false }));
|
||||
});
|
||||
});
|
||||
});
|
@ -1,17 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, defer } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class ExpansionService {
|
||||
expanded$ = defer(() => this._expanded$.asObservable());
|
||||
|
||||
private _expanded$ = new BehaviorSubject(false);
|
||||
|
||||
toggle() {
|
||||
this.setExpanded(!this._expanded$.value);
|
||||
}
|
||||
|
||||
setExpanded(expanded: boolean) {
|
||||
this._expanded$.next(expanded);
|
||||
}
|
||||
}
|
@ -1 +1,72 @@
|
||||
<ng-content></ng-content>
|
||||
<table [dataSource]="data" mat-table>
|
||||
<ng-container
|
||||
*ngFor="let column of columns; let columnIndex = index"
|
||||
[matColumnDef]="column.field"
|
||||
[sticky]="columnIndex === 0"
|
||||
>
|
||||
<th *matHeaderCellDef mat-header-cell style="text-align: center">
|
||||
<ng-container *ngIf="headersTemplates[column.field]; else defaultHeaderTpl">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="headersTemplates[column.field]; context: { column: column }"
|
||||
></ng-container>
|
||||
</ng-container>
|
||||
<ng-template #defaultHeaderTpl>
|
||||
{{ column.header }}
|
||||
</ng-template>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let item; let itemIndex = index"
|
||||
[style.width.px]="0"
|
||||
[style]="column.style"
|
||||
class="dsh-body-1"
|
||||
mat-cell
|
||||
>
|
||||
<div
|
||||
*ngIf="item.expandable && columnIndex === 0; else cellTpl"
|
||||
[style.margin-left.px]="item.level * 32"
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
gap: 8px;
|
||||
"
|
||||
(click)="toggle(item)"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="cellTpl"></ng-container>
|
||||
<dsh-bi
|
||||
[icon]="isExpanded(item) ? 'chevron-up' : 'chevron-right'"
|
||||
size="sm"
|
||||
></dsh-bi>
|
||||
</div>
|
||||
<ng-template #cellTpl>
|
||||
<div [class.dsh-body-2]="item.level === 0">
|
||||
<ng-container *ngIf="cellsTemplates[column.field]; else defaultCellTpl">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
cellsTemplates[column.field];
|
||||
context: { value: item.value, index: itemIndex, column: column }
|
||||
"
|
||||
></ng-container>
|
||||
</ng-container>
|
||||
<ng-template #defaultCellTpl>
|
||||
{{ column.formatter ? column.formatter(item.value) : '' }}
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-template>
|
||||
</td>
|
||||
<ng-container *ngIf="footerTemplates">
|
||||
<td *matFooterCellDef mat-footer-cell style="text-align: center">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="footerTemplates[column.field]; context: { column: column }"
|
||||
></ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||
<ng-container *ngIf="footerTemplates">
|
||||
<tr *matFooterRowDef="displayedColumns; sticky: true" mat-footer-row></tr>
|
||||
</ng-container>
|
||||
</table>
|
||||
|
@ -1,13 +0,0 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host > ::ng-deep .dsh-nested-table-item,
|
||||
::ng-deep .dsh-nested-table-item > ::ng-deep .dsh-nested-table-item {
|
||||
border-bottom: 1px solid;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
@ -1,52 +1,90 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ContentChildren,
|
||||
Input,
|
||||
OnChanges,
|
||||
QueryList,
|
||||
} from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { ComponentChanges } from '@vality/ng-core';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||
import { Component, Input, TemplateRef } from '@angular/core';
|
||||
import { MatTreeFlattener, MatTreeFlatDataSource } from '@angular/material/tree';
|
||||
import { of } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
import { NestedTableRowComponent } from '@dsh/components/nested-table/components/nested-table-row/nested-table-row.component';
|
||||
import { LayoutManagementService } from '@dsh/components/nested-table/services/layout-management/layout-management.service';
|
||||
import { queryListStartedArrayChanges } from '@dsh/utils';
|
||||
export type NestedTableNode<T = unknown> = {
|
||||
value: T;
|
||||
children?: NestedTableNode<T>[];
|
||||
expanded?: boolean;
|
||||
};
|
||||
|
||||
export type NestedTableFlatNode<T = unknown> = {
|
||||
value: T;
|
||||
expandable: boolean;
|
||||
level: number;
|
||||
initExpanded: boolean;
|
||||
};
|
||||
|
||||
export interface NestedTableColumn<T = unknown> {
|
||||
field: string;
|
||||
header: string;
|
||||
formatter?: (d: T) => string;
|
||||
style?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function flatten<T>(node: NestedTableNode<T>, level: number): NestedTableFlatNode<T> {
|
||||
return {
|
||||
value: node.value,
|
||||
expandable: node.children?.length > 0,
|
||||
level: level,
|
||||
initExpanded: node.expanded,
|
||||
};
|
||||
}
|
||||
|
||||
const TREE_CONTROL = new FlatTreeControl<NestedTableFlatNode>(
|
||||
(node) => node.level,
|
||||
(node) => node.expandable,
|
||||
);
|
||||
|
||||
const TREE_FLATTENER = new MatTreeFlattener<NestedTableNode, NestedTableFlatNode>(
|
||||
flatten,
|
||||
(node) => node.level,
|
||||
(node) => node.expandable,
|
||||
(node) => node.children,
|
||||
);
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'dsh-nested-table',
|
||||
templateUrl: 'nested-table.component.html',
|
||||
styleUrls: ['nested-table.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [LayoutManagementService],
|
||||
})
|
||||
export class NestedTableComponent implements AfterContentInit, OnChanges {
|
||||
@Input() rowsGridTemplateColumns: string;
|
||||
@ContentChildren(NestedTableRowComponent)
|
||||
nestedTableRowComponentChildren: QueryList<NestedTableRowComponent>;
|
||||
export class NestedTableComponent {
|
||||
@Input({
|
||||
transform: (data: NestedTableNode[]) => {
|
||||
const dataSource = new MatTreeFlatDataSource(TREE_CONTROL, TREE_FLATTENER);
|
||||
dataSource.data = data || [];
|
||||
dataSource
|
||||
.connect({ viewChange: of() })
|
||||
.pipe(first())
|
||||
.subscribe((flatten) => {
|
||||
for (const d of flatten) {
|
||||
if (d.initExpanded) {
|
||||
TREE_CONTROL.expand(d);
|
||||
}
|
||||
}
|
||||
});
|
||||
return dataSource;
|
||||
},
|
||||
})
|
||||
data!: MatTreeFlatDataSource<NestedTableNode, NestedTableFlatNode>;
|
||||
@Input() columns: NestedTableColumn[] = [];
|
||||
@Input() cellsTemplates: Record<string, TemplateRef<unknown>> = {};
|
||||
@Input() headersTemplates: Record<string, TemplateRef<unknown>> = {};
|
||||
@Input() footerTemplates: Record<string, TemplateRef<unknown>> = {};
|
||||
|
||||
constructor(private layoutManagementService: LayoutManagementService) {}
|
||||
get displayedColumns() {
|
||||
return (this.columns || []).map((c) => c.field);
|
||||
}
|
||||
|
||||
ngOnChanges({ rowsGridTemplateColumns }: ComponentChanges<NestedTableComponent>) {
|
||||
if (rowsGridTemplateColumns) {
|
||||
this.layoutManagementService.setRowsGridTemplateColumns(
|
||||
rowsGridTemplateColumns.currentValue,
|
||||
);
|
||||
toggle(data: NestedTableFlatNode) {
|
||||
if (data.expandable) {
|
||||
TREE_CONTROL.toggle(data);
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
queryListStartedArrayChanges(this.nestedTableRowComponentChildren)
|
||||
.pipe(
|
||||
switchMap((rows) => combineLatest(rows.map(({ colsCount$ }) => colsCount$))),
|
||||
map((rowsColsCounts) => Math.max(...rowsColsCounts)),
|
||||
distinctUntilChanged(),
|
||||
untilDestroyed(this),
|
||||
)
|
||||
.subscribe((colsCount) => this.layoutManagementService.setLayoutColsCount(colsCount));
|
||||
isExpanded(data: NestedTableFlatNode) {
|
||||
return TREE_CONTROL.isExpanded(data);
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,24 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
import { FlexLayoutModule } from 'ng-flex-layout';
|
||||
|
||||
import { NestedTableColComponent } from './components/nested-table-col/nested-table-col.component';
|
||||
import { NestedTableGroupComponent } from './components/nested-table-group/nested-table-group.component';
|
||||
import { NestedTableRowComponent } from './components/nested-table-row/nested-table-row.component';
|
||||
import { NestedTableCollapseModule } from './nested-table-collapse';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { BootstrapIconModule } from '@dsh/components/indicators';
|
||||
|
||||
import { NestedTableComponent } from './nested-table.component';
|
||||
|
||||
const SHARED_COMPONENTS = [
|
||||
NestedTableComponent,
|
||||
NestedTableColComponent,
|
||||
NestedTableRowComponent,
|
||||
NestedTableGroupComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FlexLayoutModule, TranslocoModule],
|
||||
declarations: SHARED_COMPONENTS,
|
||||
exports: [...SHARED_COMPONENTS, NestedTableCollapseModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslocoModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
ButtonModule,
|
||||
BootstrapIconModule,
|
||||
],
|
||||
declarations: [NestedTableComponent],
|
||||
exports: [NestedTableComponent],
|
||||
})
|
||||
export class NestedTableModule {}
|
||||
|
@ -1,52 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { LayoutManagementService } from './layout-management.service';
|
||||
|
||||
describe('LayoutManagementService', () => {
|
||||
let service: LayoutManagementService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [LayoutManagementService],
|
||||
});
|
||||
|
||||
service = TestBed.inject(LayoutManagementService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getFillCols', () => {
|
||||
it('should be return fill cols', () => {
|
||||
service.setLayoutColsCount(10);
|
||||
expect(service.getFillCols(of(6))).toBeObservable(
|
||||
cold('(a)', { a: new Array(4).fill('') }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('layoutColsCount$', () => {
|
||||
it('should be return layoutColsCount$', () => {
|
||||
service.setLayoutColsCount(10);
|
||||
expect(service.layoutColsCount$).toBeObservable(cold('(a)', { a: 10 }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('gridTemplateColumns$', () => {
|
||||
it('should be return default', () => {
|
||||
service.setLayoutColsCount(5);
|
||||
expect(service.gridTemplateColumns$).toBeObservable(
|
||||
cold('(a)', { a: '1fr 1fr 1fr 1fr 1fr' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be return by set', () => {
|
||||
service.setRowsGridTemplateColumns('1fr 100%');
|
||||
service.setLayoutColsCount(5);
|
||||
expect(service.gridTemplateColumns$).toBeObservable(cold('(a)', { a: '1fr 100%' }));
|
||||
});
|
||||
});
|
||||
});
|
@ -1,45 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, defer, Observable, of, ReplaySubject } from 'rxjs';
|
||||
import { distinctUntilChanged, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { SHARE_REPLAY_CONF } from '@dsh/app/custom-operators';
|
||||
|
||||
@Injectable()
|
||||
export class LayoutManagementService {
|
||||
layoutColsCount$ = defer(() => this._layoutColsCount$.asObservable());
|
||||
gridTemplateColumns$ = defer(() => this.rowsGridTemplateColumns$).pipe(
|
||||
switchMap((gridTemplateColumns) =>
|
||||
gridTemplateColumns
|
||||
? of(gridTemplateColumns)
|
||||
: this.layoutColsCount$.pipe(
|
||||
map((colsCount) =>
|
||||
LayoutManagementService.getDefaultGridTemplateColumns(colsCount),
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay(SHARE_REPLAY_CONF),
|
||||
);
|
||||
|
||||
private _layoutColsCount$ = new ReplaySubject<number>(1);
|
||||
private rowsGridTemplateColumns$ = new BehaviorSubject<string>(null);
|
||||
|
||||
getFillCols(colsCount$: Observable<number>): Observable<string[]> {
|
||||
return combineLatest([this.layoutColsCount$, colsCount$]).pipe(
|
||||
map(([baseCount, count]) => Math.max(baseCount - count, 0)),
|
||||
distinctUntilChanged(),
|
||||
map((count) => new Array(count).fill('')),
|
||||
);
|
||||
}
|
||||
|
||||
setRowsGridTemplateColumns(rowsGridTemplateColumns: string) {
|
||||
this.rowsGridTemplateColumns$.next(rowsGridTemplateColumns);
|
||||
}
|
||||
|
||||
setLayoutColsCount(colsCount: number) {
|
||||
this._layoutColsCount$.next(colsCount);
|
||||
}
|
||||
|
||||
private static getDefaultGridTemplateColumns(colsCount: number) {
|
||||
return new Array(colsCount).fill('1fr').join(' ');
|
||||
}
|
||||
}
|
@ -1,23 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
// TODO: replace with BaseDialogResponseStatus
|
||||
export type ConfirmActionDialogResult = 'cancel' | 'confirm';
|
||||
import { DialogSuperclass } from '@vality/ng-core';
|
||||
|
||||
@Component({
|
||||
templateUrl: 'confirm-action-dialog.component.html',
|
||||
styleUrls: ['confirm-action-dialog.component.scss'],
|
||||
})
|
||||
export class ConfirmActionDialogComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ConfirmActionDialogComponent, ConfirmActionDialogResult>,
|
||||
) {}
|
||||
|
||||
export class ConfirmActionDialogComponent extends DialogSuperclass<ConfirmActionDialogComponent> {
|
||||
cancel() {
|
||||
this.dialogRef.close('cancel');
|
||||
this.closeWithCancellation();
|
||||
}
|
||||
|
||||
confirm() {
|
||||
this.dialogRef.close('confirm');
|
||||
this.closeWithSuccess();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user