Epic FE-1031: webhooks (#190)

* FE-1032: integrations module added. (#183)

* FE-1032: integrations module added.

* FE-1033: webhooks search (#185)

* webhooks receiver added

* FE-1034: webhooks panels (#188)

* FE-1035: webhook creation (#192)

* FE-1037: empty search n loading fix (#195)

* FE-1037: empty-result n loading. also webhooks sort added
This commit is contained in:
Aleksandra Usacheva 2020-04-13 12:10:27 +03:00 committed by GitHub
parent 885fb09097
commit 1f11f9f9e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 947 additions and 11 deletions

14
package-lock.json generated
View File

@ -3994,6 +3994,15 @@
"@types/lodash": "*"
}
},
"@types/lodash.sortby": {
"version": "4.7.6",
"resolved": "https://registry.npmjs.org/@types/lodash.sortby/-/lodash.sortby-4.7.6.tgz",
"integrity": "sha512-EnvAOmKvEg7gdYpYrS6+fVFPw5dL9rBnJi3vcKI7wqWQcLJVF/KRXK9dH29HjGNVvFUj0s9prRP3J8jEGnGKDw==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/lodash.template": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/@types/lodash.template/-/lodash.template-4.4.6.tgz",
@ -10397,6 +10406,11 @@
"resolved": "https://registry.npmjs.org/lodash.round/-/lodash.round-4.0.4.tgz",
"integrity": "sha1-EBtrpcbOzJmPKrvoC2Apr/0l0mI="
},
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
},
"lodash.template": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz",

View File

@ -55,6 +55,7 @@
"lodash.negate": "^3.0.2",
"lodash.random": "^3.2.0",
"lodash.round": "^4.0.4",
"lodash.sortby": "^4.7.0",
"lodash.template": "^4.4.0",
"moment": "^2.24.0",
"ng-yandex-metrika": "^3.0.2",
@ -84,6 +85,7 @@
"@types/lodash.negate": "^3.0.6",
"@types/lodash.random": "^3.2.6",
"@types/lodash.round": "^4.0.6",
"@types/lodash.sortby": "^4.7.6",
"@types/lodash.template": "^4.4.6",
"@types/moment": "^2.13.0",
"@types/node": "^12.12.32",

View File

@ -9,3 +9,4 @@ export * from './files';
export * from './kontur-focus';
export * from './messages';
export * from './capi';
export * from './webhooks';

View File

@ -0,0 +1,2 @@
export * from './webhooks.module';
export * from './webhooks.service';

View File

@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';
import { CAPIModule } from '../capi';
import { WebhooksService } from './webhooks.service';
@NgModule({
imports: [CAPIModule],
providers: [WebhooksService]
})
export class WebhooksModule {}

View File

@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Webhook, WebhooksService as ApiWebhooksService } from '../../api-codegen/capi';
import { genXRequestID } from '../utils';
@Injectable()
export class WebhooksService {
constructor(private apiWebhooksService: ApiWebhooksService) {}
createWebhook(params: Webhook): Observable<Webhook> {
return this.apiWebhooksService.createWebhook(genXRequestID(), params);
}
getWebhooks(): Observable<Webhook[]> {
return this.apiWebhooksService.getWebhooks(genXRequestID());
}
getWebhookByID(weebhookID: string): Observable<Webhook> {
return this.apiWebhooksService.getWebhookByID(genXRequestID(), weebhookID);
}
deleteWebhookByID(weebhookID: string): Observable<any> {
return this.apiWebhooksService.deleteWebhookByID(genXRequestID(), weebhookID);
}
}

View File

@ -0,0 +1 @@
export * from './integrations.module';

View File

@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { IntegrationsComponent } from './integrations.component';
const routes: Routes = [
{
path: '',
component: IntegrationsComponent,
children: [
{
path: 'webhooks',
loadChildren: () => import('./webhooks/webhooks.module').then(m => m.WebhooksModule)
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class IntegrationsRoutingModule {}

View File

@ -0,0 +1,24 @@
<dsh-scroll-up></dsh-scroll-up>
<div
class="dsh-integrations"
*transloco="let t; scope: 'payment-section'; read: 'paymentSection.integrations'"
fxLayout="column"
fxLayoutGap="20px"
>
<h1 class="mat-display-1">{{ t.headline }}</h1>
<nav dsh-tab-nav-bar>
<a
dsh-tab-link
*ngFor="let link of links"
[routerLink]="link.path"
routerLinkActive
#rla="routerLinkActive"
[active]="rla.isActive"
>
{{ t.tabs[link.path] }}
</a>
</nav>
<div>
<router-outlet></router-outlet>
</div>
</div>

View File

@ -0,0 +1,4 @@
.dsh-integrations {
max-width: 720px;
margin: auto;
}

View File

@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { Router, RouterEvent } from '@angular/router';
import { filter } from 'rxjs/operators';
@Component({
templateUrl: 'integrations.component.html',
styleUrls: ['integrations.component.scss']
})
export class IntegrationsComponent {
links = [
{
path: 'webhooks'
}
];
constructor(private router: Router) {
this.router.events
.pipe(filter((e: RouterEvent) => e.url && e.url.endsWith('integrations')))
.subscribe(e => this.router.navigate([e.url, 'webhooks']));
}
}

View File

@ -0,0 +1,16 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { TranslocoModule } from '@ngneat/transloco';
import { LayoutModule } from '@dsh/components/layout';
import { ScrollUpModule } from '@dsh/components/navigation';
import { IntegrationsRoutingModule } from './integrations-routing.module';
import { IntegrationsComponent } from './integrations.component';
@NgModule({
imports: [IntegrationsRoutingModule, CommonModule, FlexModule, LayoutModule, TranslocoModule, ScrollUpModule],
declarations: [IntegrationsComponent]
})
export class IntegrationsModule {}

View File

@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Subject } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
import { CreateWebhookComponent } from './create-webhook/create-webhook.component';
import { ReceiveWebhooksService } from './receive-webhooks.service';
@Injectable()
export class CreateWebhookService {
private createWebhook$ = new Subject();
constructor(private dialog: MatDialog, private receiveWebhooksService: ReceiveWebhooksService) {
this.createWebhook$.pipe(switchMap(() => this.openCreateClaimDialog())).subscribe(() => {
this.receiveWebhooksService.receiveWebhooks();
});
}
createWebhook() {
this.createWebhook$.next();
}
private openCreateClaimDialog() {
return this.dialog
.open(CreateWebhookComponent, { width: '560px', disableClose: true })
.afterClosed()
.pipe(filter(r => r === 'created'));
}
}

View File

@ -0,0 +1,33 @@
<div fxLayout="column" [fxLayoutGap]="layoutGap" *transloco="let t; scope: 'webhook'; read: 'webhook'">
<div class="mat-title">{{ t.create.title }}</div>
<form [formGroup]="form" fxLayout="column">
<mat-form-field fxFlex>
<mat-label>{{ t.details.shop }}</mat-label>
<mat-select formControlName="shop" required>
<mat-option *ngFor="let shop of shops$ | async" [value]="shop.id">
{{ shop.details.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ t.details.url }}</mat-label>
<input formControlName="url" matInput type="text" autocomplete="off" required />
</mat-form-field>
<section class="checkbox-section" fxLayout="column" fxLayoutGap="10px">
<label class="mat-body-2">{{ t.create.types }}:</label>
<div formArrayName="eventTypes" fxLayout="column" fxLayoutGap="10px">
<div *ngFor="let eventType of eventTypes.controls; let i = index" [formGroupName]="i">
<mat-checkbox formControlName="selected" class="mat-body-1">{{
t.events.types[eventType.controls.eventName.value]
}}</mat-checkbox>
</div>
</div>
</section>
</form>
<div fxLayout="row" fxLayoutAlign="space-between center" *transloco="let m">
<button dsh-button (click)="cancel()">{{ m.cancel }}</button>
<button dsh-button color="accent" [disabled]="(isLoading$ | async) || !form.valid" (click)="save()">
{{ t.create.attach }}
</button>
</div>
</div>

View File

@ -0,0 +1,42 @@
import { Component, Inject } from '@angular/core';
import { FormArray } from '@angular/forms';
import { MatDialogRef } from '@angular/material/dialog';
import { filter } from 'rxjs/operators';
import { ShopService } from '../../../../../api/shop';
import { LAYOUT_GAP } from '../../../../constants';
import { CreateWebhookService } from './create-webhook.service';
@Component({
templateUrl: 'create-webhook.component.html',
providers: [CreateWebhookService]
})
export class CreateWebhookComponent {
form = this.createWebhookService.form;
types = this.createWebhookService.invoiceTypes;
shops$ = this.shopService.shops$;
isLoading$ = this.createWebhookService.isLoading$;
constructor(
private dialogRef: MatDialogRef<CreateWebhookComponent>,
private createWebhookService: CreateWebhookService,
@Inject(LAYOUT_GAP) public layoutGap: string,
private shopService: ShopService
) {
this.createWebhookService.webhookCreated$.pipe(filter(r => !!r)).subscribe(r => {
this.dialogRef.close(r);
});
}
get eventTypes() {
return this.form.get('eventTypes') as FormArray;
}
save() {
this.createWebhookService.save();
}
cancel() {
this.dialogRef.close(false);
}
}

View File

@ -0,0 +1,72 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { Subject } from 'rxjs';
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
import { InvoicesTopic } from '../../../../../api-codegen/capi/swagger-codegen';
import { WebhooksService } from '../../../../../api/webhooks';
import { booleanDebounceTime, progress, SHARE_REPLAY_CONF } from '../../../../../custom-operators';
import { FormParams } from './form-params';
import { formValuesToWebhook } from './form-values-to-webhook';
const oneMustBeSelected: ValidatorFn = (control: FormGroup): ValidationErrors | null =>
control.value.map(c => c.selected).includes(true) ? null : { Error: 'At least one of checkboxes select needed' };
@Injectable()
export class CreateWebhookService {
invoiceTypes = Object.values(InvoicesTopic.EventTypesEnum);
form = this.initForm();
private createWebhook$: Subject<FormParams> = new Subject();
webhookCreated$: Subject<'created' | null> = new Subject();
isLoading$ = progress(this.createWebhook$, this.webhookCreated$).pipe(
booleanDebounceTime(),
shareReplay(SHARE_REPLAY_CONF)
);
constructor(
private fb: FormBuilder,
private webhooksService: WebhooksService,
private snackBar: MatSnackBar,
private transloco: TranslocoService
) {
this.createWebhook$
.pipe(
map(formValuesToWebhook),
switchMap(v =>
this.webhooksService.createWebhook(v).pipe(
catchError(err => {
console.error(err);
this.snackBar.open(this.transloco.translate('httpError'), 'OK');
this.webhookCreated$.next(null);
return [];
})
)
)
)
.subscribe(() => this.webhookCreated$.next('created'));
}
save() {
this.createWebhook$.next(this.form.value);
}
private initForm(): FormGroup {
return this.fb.group({
shop: ['', Validators.required],
url: ['', Validators.required],
eventTypes: this.fb.array(
this.invoiceTypes.map(t =>
this.fb.group({
eventName: t,
selected: false
})
),
[oneMustBeSelected]
)
});
}
}

View File

@ -0,0 +1,12 @@
import { InvoicesTopic } from '../../../../../api-codegen/capi/swagger-codegen';
interface EventType {
eventName: InvoicesTopic.EventTypesEnum;
selected: boolean;
}
export interface FormParams {
shop: string;
url: string;
eventTypes: EventType[];
}

View File

@ -0,0 +1,12 @@
import { Webhook } from '../../../../../api-codegen/capi/swagger-codegen';
import { FormParams } from './form-params';
export const formValuesToWebhook = (v: FormParams): Webhook =>
({
url: v.url,
scope: {
shopID: v.shop,
eventTypes: v.eventTypes.filter(e => e.selected).map(e => e.eventName),
topic: 'InvoicesTopic'
}
} as Webhook);

View File

@ -0,0 +1 @@
export * from './webhooks.module';

View File

@ -0,0 +1,56 @@
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import sortBy from 'lodash.sortby';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { catchError, filter, map, shareReplay, switchMap } from 'rxjs/operators';
import { Webhook } from '../../../../api-codegen/capi/swagger-codegen';
import { WebhooksService } from '../../../../api/webhooks';
import { booleanDebounceTime, progress, SHARE_REPLAY_CONF } from '../../../../custom-operators';
@Injectable()
export class ReceiveWebhooksService {
private webhooksState$ = new BehaviorSubject(null);
private receiveWebhooks$ = new Subject();
webhooks$: Observable<Webhook[]> = this.webhooksState$.pipe(
filter(s => !!s),
map(w => sortBy(w, i => !i.active)),
shareReplay(SHARE_REPLAY_CONF)
);
webhooksReceived$ = this.webhooks$.pipe(
map(s => !!s),
shareReplay(SHARE_REPLAY_CONF)
);
isLoading$ = progress(this.receiveWebhooks$, this.webhooksState$).pipe(
booleanDebounceTime(),
shareReplay(SHARE_REPLAY_CONF)
);
constructor(
private webhooksService: WebhooksService,
private snackBar: MatSnackBar,
private transloco: TranslocoService
) {
this.receiveWebhooks$
.pipe(
switchMap(_ =>
this.webhooksService.getWebhooks().pipe(
catchError(err => {
console.error(err);
this.snackBar.open(this.transloco.translate('httpError'), 'OK');
return of([]);
})
)
)
)
.subscribe(webhooks => this.webhooksState$.next(webhooks));
}
receiveWebhooks() {
this.receiveWebhooks$.next();
}
}

View File

@ -0,0 +1,9 @@
@import '../../../../../../../node_modules/@angular/material/theming';
@mixin dsh-webhook-panel-theme($theme) {
$gray: map-get($theme, gray);
.dsh-webhook-header {
color: map-get($gray, 500);
}
}

View File

@ -0,0 +1,4 @@
<ng-container *transloco="let t; scope: 'webhook'; read: 'webhook.actions'">
<div class="mat-title">{{ t.title }}</div>
<button dsh-stroked-button color="warn" (click)="delete()">{{ t.buttons.delete }}</button>
</ng-container>

View File

@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
import { RemoveWebhookService } from './remove-webhook.service';
@Component({
selector: 'dsh-actions',
templateUrl: 'actions.component.html',
providers: [RemoveWebhookService]
})
export class ActionsComponent {
@Input()
webhookID: string;
constructor(private removeWebhookService: RemoveWebhookService) {}
delete() {
this.removeWebhookService.removeWebhook(this.webhookID);
}
}

View File

@ -0,0 +1,56 @@
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { forkJoin, of, Subject } from 'rxjs';
import { catchError, filter, switchMap } from 'rxjs/operators';
import { ConfirmActionDialogComponent } from '@dsh/components/popups';
import { WebhooksService } from '../../../../../../api/webhooks';
import { ReceiveWebhooksService } from '../../receive-webhooks.service';
@Injectable()
export class RemoveWebhookService {
private removeWebhook$: Subject<string> = new Subject();
constructor(
private dialog: MatDialog,
private receiveWebhooksService: ReceiveWebhooksService,
private webhooksService: WebhooksService,
private snackBar: MatSnackBar,
private transloco: TranslocoService
) {
this.removeWebhook$
.pipe(
switchMap(webhookID => forkJoin([of(webhookID), this.openConfirmDialog()])),
switchMap(([webhookID]) =>
this.webhooksService.deleteWebhookByID(webhookID).pipe(
catchError(err => {
console.error(err);
this.snackBar.open(this.transloco.translate('httpError'), 'OK');
return 'error';
})
)
)
)
.subscribe(() => {
this.receiveWebhooks();
});
}
removeWebhook(webhookID: string) {
this.removeWebhook$.next(webhookID);
}
private receiveWebhooks() {
this.receiveWebhooksService.receiveWebhooks();
}
private openConfirmDialog() {
return this.dialog
.open(ConfirmActionDialogComponent)
.afterClosed()
.pipe(filter(r => r === 'confirm'));
}
}

View File

@ -0,0 +1,17 @@
<div
class="dsh-webhook-panel-details-section"
fxLayout="column"
[fxLayoutGap]="layoutGap"
*transloco="let t; scope: 'webhook'; read: 'webhook.details'"
>
<dsh-details-item [title]="t.url">
<div class="mat-body-1">
{{ webhook.url }}
</div>
</dsh-details-item>
<dsh-details-item [title]="t.shop">
<div class="mat-body-1">
{{ shopName }}
</div>
</dsh-details-item>
</div>

View File

@ -0,0 +1,18 @@
import { Component, Inject, Input } from '@angular/core';
import { Webhook } from '../../../../../../api-codegen/capi/swagger-codegen';
import { LAYOUT_GAP } from '../../../../../constants';
@Component({
selector: 'dsh-details',
templateUrl: 'details.component.html'
})
export class DetailsComponent {
@Input()
webhook: Webhook;
@Input()
shopName: string;
constructor(@Inject(LAYOUT_GAP) public layoutGap: string) {}
}

View File

@ -0,0 +1,6 @@
<div class="dsh-webhook-panel-events-section" *transloco="let t; scope: 'webhook'; read: 'webhook.events'">
<div class="mat-title">{{ t.title }}</div>
<ul fxLayout="column" [fxLayoutGap]="layoutGap" class="dsh-webhooks-event-list">
<li class="mat-body-1" *ngFor="let event of events">{{ t.types[event] }}</li>
</ul>
</div>

View File

@ -0,0 +1,5 @@
.dsh-webhooks-event-list {
list-style: none;
padding-left: 0;
margin: 0;
}

View File

@ -0,0 +1,17 @@
import { Component, Inject, Input } from '@angular/core';
import { InvoicesTopic } from '../../../../../../api-codegen/capi/swagger-codegen';
import { LAYOUT_GAP } from '../../../../../constants';
type InvoicesEventTypesEnum = InvoicesTopic.EventTypesEnum;
@Component({
selector: 'dsh-events',
templateUrl: 'events.component.html',
styleUrls: ['events.component.scss']
})
export class EventsComponent {
@Input()
events: InvoicesEventTypesEnum[];
constructor(@Inject(LAYOUT_GAP) public layoutGap: string) {}
}

View File

@ -0,0 +1,16 @@
<div class="dsh-webhook-panel-key-section" *transloco="let t; scope: 'webhook'; read: 'webhook.key'">
<div class="mat-title">{{ t.title }}</div>
<div fxLayout="column" [fxLayoutGap]="layoutGap">
<div class="mat-caption">
{{ t.publicKey }}
</div>
<div class="mat-body-1 pre-wrap">
{{ key }}
</div>
<div>
<button dsh-button [cdkCopyToClipboard]="key" (cdkCopyToClipboardCopied)="copied($event)">
{{ t.buttons.copy }}
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,3 @@
.pre-wrap {
white-space: pre-wrap;
}

View File

@ -0,0 +1,25 @@
import { Component, Inject, Input } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { LAYOUT_GAP } from '../../../../../constants';
@Component({
selector: 'dsh-key',
templateUrl: 'key.component.html',
styleUrls: ['key.component.scss']
})
export class KeyComponent {
@Input()
key: string;
constructor(
@Inject(LAYOUT_GAP) public layoutGap: string,
private snackBar: MatSnackBar,
private transloco: TranslocoService
) {}
copied(isCopied: boolean) {
this.snackBar.open(this.transloco.translate(isCopied ? 'copied' : 'copyFailed'), 'OK', { duration: 1000 });
}
}

View File

@ -0,0 +1,35 @@
<dsh-expand-panel #expandPanel *transloco="let t; scope: 'webhook'; read: 'webhook.details'">
<div fxLayout="column" fxLayoutGap="10px">
<div class="mat-body-1">{{ webhook.url }}</div>
<div fxLayout="row" fxLayoutAlign="space-between center">
<span class="mat-caption">{{ shopName$ | async }}</span>
<span class="mat-caption">
{{ webhook.active ? t.status.active : t.status.inactive }}
</span>
</div>
</div>
<dsh-expand-panel-more>
<div fxLayout="column" [fxLayoutGap]="layoutGap" fxFlex="100">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="mat-caption dsh-webhook-header" *transloco="let d; scope: 'webhook'; read: 'webhook'">
{{ d.webhook }} #{{ webhook.id }}
</div>
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="10px">
<div class="mat-caption dsh-webhook-header">
{{ webhook.active ? t.status.active : t.status.inactive }}
</div>
<button dsh-icon-button>
<mat-icon svgIcon="keyboard_arrow_up" (click)="expandPanel.collapse($event)"></mat-icon>
</button>
</div>
</div>
<dsh-details [webhook]="webhook" [shopName]="shopName$ | async"></dsh-details>
<mat-divider></mat-divider>
<dsh-events [events]="events"></dsh-events>
<mat-divider></mat-divider>
<dsh-key [key]="webhook.publicKey"></dsh-key>
<mat-divider *ngIf="webhook.active"></mat-divider>
<dsh-actions [webhookID]="webhook.id" *ngIf="webhook.active"></dsh-actions>
</div>
</dsh-expand-panel-more>
</dsh-expand-panel>

View File

@ -0,0 +1,35 @@
import { Component, Inject, Input, OnChanges, SimpleChanges } from '@angular/core';
import isEqual from 'lodash.isequal';
import { pluck } from 'rxjs/operators';
import { InvoicesTopic, Webhook } from '../../../../../api-codegen/capi/swagger-codegen';
import { LAYOUT_GAP } from '../../../../constants';
import { WebhookPanelService } from './webhook-panel.service';
type InvoicesEventTypesEnum = InvoicesTopic.EventTypesEnum;
@Component({
selector: 'dsh-webhook-panel',
templateUrl: 'webhook-panel.component.html',
providers: [WebhookPanelService]
})
export class WebhookPanelComponent implements OnChanges {
@Input()
webhook: Webhook;
events: InvoicesEventTypesEnum[] = [];
shopName$ = this.webhookPanelService.shopInfo$.pipe(pluck('name'));
constructor(@Inject(LAYOUT_GAP) public layoutGap: string, private webhookPanelService: WebhookPanelService) {}
ngOnChanges({ webhook }: SimpleChanges) {
if (!isEqual(webhook.previousValue, webhook.currentValue)) {
const {
scope: { eventTypes, shopID }
} = webhook.currentValue;
this.events = eventTypes;
this.webhookPanelService.getShopInfo(shopID);
}
}
}

View File

@ -0,0 +1,36 @@
import { ClipboardModule } from '@angular/cdk/clipboard';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { TranslocoModule } from '@ngneat/transloco';
import { ButtonModule } from '@dsh/components/buttons';
import { LayoutModule } from '@dsh/components/layout';
import { ConfirmActionDialogModule } from '@dsh/components/popups';
import { ActionsComponent } from './actions/actions.component';
import { DetailsComponent } from './details/details.component';
import { EventsComponent } from './events/events.component';
import { KeyComponent } from './key/key.component';
import { WebhookPanelComponent } from './webhook-panel.component';
@NgModule({
imports: [
ClipboardModule,
LayoutModule,
CommonModule,
ButtonModule,
FlexModule,
MatIconModule,
TranslocoModule,
MatDialogModule,
ConfirmActionDialogModule,
MatDividerModule
],
declarations: [WebhookPanelComponent, ActionsComponent, DetailsComponent, EventsComponent, KeyComponent],
exports: [WebhookPanelComponent]
})
export class WebhookPanelModule {}

View File

@ -0,0 +1,32 @@
import { Injectable, OnDestroy } from '@angular/core';
import { combineLatest, Observable, of, Subject, Subscription } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, switchMap } from 'rxjs/operators';
import { ShopService } from '../../../../../api/shop';
import { SHARE_REPLAY_CONF } from '../../../../../custom-operators';
import { mapToShopInfo, ShopInfo } from '../../../operations/operators';
@Injectable()
export class WebhookPanelService implements OnDestroy {
private getShopInfo$ = new Subject<string>();
private subscription: Subscription = Subscription.EMPTY;
shopInfo$: Observable<ShopInfo> = this.getShopInfo$.pipe(
distinctUntilChanged(),
switchMap(shopID => combineLatest([of(shopID), this.shopService.shops$.pipe(mapToShopInfo)])),
map(([shopID, shops]) => shops.find(shop => shop.shopID === shopID)),
shareReplay(SHARE_REPLAY_CONF)
);
constructor(private shopService: ShopService) {
this.subscription = this.shopInfo$.subscribe();
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
getShopInfo(shopID: string) {
this.getShopInfo$.next(shopID);
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { WebhooksComponent } from './webhooks.component';
const routes: Routes = [
{
path: '',
component: WebhooksComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)]
})
export class WebhooksRoutingModule {}

View File

@ -0,0 +1,27 @@
<div fxLayout="column" [fxLayoutGap]="layoutGap" *transloco="let t; scope: 'webhook'; read: 'webhook'">
<div
fxLayout.lt-md="column"
fxLayout="row"
fxLayoutAlign.lt-md="center stretch"
fxLayoutAlign="space-between center"
[fxLayoutGap]="layoutGap"
>
<div class="mat-caption">
{{ t.description }}
</div>
<button dsh-stroked-button color="accent" (click)="createWebhook()" fxFlex.gt-sm="25">{{ t.attach }}</button>
</div>
<div *ngIf="isLoading$ | async" fxLayout fxLayoutAlign="center center">
<dsh-spinner></dsh-spinner>
</div>
<dsh-empty-search-result
*ngIf="!(webhooks$ | async)?.length && (webhooksReceived$ | async)"
[text]="t.emptyResult"
></dsh-empty-search-result>
<div fxLayout="column" [fxLayoutGap]="layoutGap" *ngIf="!(isLoading$ | async)">
<dsh-webhook-panel *ngFor="let webhook of webhooks$ | async" [webhook]="webhook"></dsh-webhook-panel>
</div>
</div>

View File

@ -0,0 +1,28 @@
import { Component, Inject, OnInit } from '@angular/core';
import { LAYOUT_GAP } from '../../../constants';
import { CreateWebhookService } from './create-webhook.service';
import { ReceiveWebhooksService } from './receive-webhooks.service';
@Component({
templateUrl: 'webhooks.component.html'
})
export class WebhooksComponent implements OnInit {
webhooks$ = this.receiveWebhooksService.webhooks$;
isLoading$ = this.receiveWebhooksService.isLoading$;
webhooksReceived$ = this.receiveWebhooksService.webhooksReceived$;
constructor(
private receiveWebhooksService: ReceiveWebhooksService,
@Inject(LAYOUT_GAP) public layoutGap: string,
private createWebhookService: CreateWebhookService
) {}
ngOnInit(): void {
this.receiveWebhooksService.receiveWebhooks();
}
createWebhook() {
this.createWebhookService.createWebhook();
}
}

View File

@ -0,0 +1,47 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { TranslocoModule } from '@ngneat/transloco';
import { ButtonModule } from '@dsh/components/buttons';
import { EmptySearchResultModule } from '@dsh/components/empty-search-result';
import { SpinnerModule } from '@dsh/components/indicators';
import { ShopService } from '../../../../api/shop';
import { WebhooksModule as ApiWebhooksModule } from '../../../../api/webhooks';
import { CreateWebhookService } from './create-webhook.service';
import { CreateWebhookComponent } from './create-webhook/create-webhook.component';
import { ReceiveWebhooksService } from './receive-webhooks.service';
import { WebhookPanelModule } from './webhook-panel/webhook-panel.module';
import { WebhooksRoutingModule } from './webhooks-routing.module';
import { WebhooksComponent } from './webhooks.component';
@NgModule({
imports: [
WebhooksRoutingModule,
ApiWebhooksModule,
ButtonModule,
MatDialogModule,
CommonModule,
FlexModule,
ReactiveFormsModule,
MatFormFieldModule,
MatSelectModule,
MatInputModule,
MatCheckboxModule,
TranslocoModule,
WebhookPanelModule,
SpinnerModule,
EmptySearchResultModule
],
declarations: [WebhooksComponent, CreateWebhookComponent],
entryComponents: [CreateWebhookComponent],
providers: [ReceiveWebhooksService, CreateWebhookService, ShopService]
})
export class WebhooksModule {}

View File

@ -29,4 +29,14 @@
>
{{ t.reports }}
</dsh-state-nav-item>
<dsh-state-nav-item
routerLink="./integrations"
routerLinkActive
#integrations="routerLinkActive"
[selected]="integrations.isActive"
withIcon
icon="build"
>
{{ t.integrations }}
</dsh-state-nav-item>
</dsh-state-nav>

View File

@ -14,6 +14,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
import { TranslocoModule, TRANSLOCO_SCOPE } from '@ngneat/transloco';
import { ButtonModule } from '@dsh/components/buttons';
import { EmptySearchResultModule } from '@dsh/components/empty-search-result';
import { FormControlsModule, RangeDatepickerModule } from '@dsh/components/form-controls';
import { IndicatorsModule } from '@dsh/components/indicators';
import { LayoutModule } from '@dsh/components/layout';
@ -23,7 +24,6 @@ import { TableModule } from '@dsh/components/table';
import { InvoiceModule } from '../../../../api';
import { FromMinorModule } from '../../../../from-minor';
import { LanguageModule } from '../../../../language';
import { EmptySearchResultModule } from '../../empty-search-result';
import { LastUpdatedModule } from '../last-updated/last-updated.module';
import { CreateInvoiceDialogComponent } from './create-invoice-dialog';
import { InvoicesRoutingModule } from './invoices-routing.module';

View File

@ -11,6 +11,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
import { TranslocoModule, TRANSLOCO_SCOPE } from '@ngneat/transloco';
import { ButtonModule } from '@dsh/components/buttons';
import { EmptySearchResultModule } from '@dsh/components/empty-search-result';
import { FormControlsModule, RangeDatepickerModule } from '@dsh/components/form-controls';
import { IndicatorsModule } from '@dsh/components/indicators';
import { LayoutModule } from '@dsh/components/layout';
@ -19,7 +20,6 @@ import { TableModule } from '@dsh/components/table';
import { FromMinorModule } from '../../../../from-minor';
import { LanguageModule } from '../../../../language';
import { EmptySearchResultModule } from '../../empty-search-result';
import { LastUpdatedModule } from '../last-updated/last-updated.module';
import { PaymentsRoutingModule } from './payments-routing.module';
import { PaymentsComponent } from './payments.component';

View File

@ -11,6 +11,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
import { TranslocoModule, TRANSLOCO_SCOPE } from '@ngneat/transloco';
import { ButtonModule } from '@dsh/components/buttons';
import { EmptySearchResultModule } from '@dsh/components/empty-search-result';
import { FormControlsModule, RangeDatepickerModule } from '@dsh/components/form-controls';
import { IndicatorsModule } from '@dsh/components/indicators';
import { LayoutModule } from '@dsh/components/layout';
@ -18,7 +19,6 @@ import { StateNavModule } from '@dsh/components/navigation';
import { TableModule } from '@dsh/components/table';
import { FromMinorModule } from '../../../../from-minor';
import { EmptySearchResultModule } from '../../empty-search-result';
import { LastUpdatedModule } from '../last-updated/last-updated.module';
import { RefundsRoutingModule } from './refunds-routing.module';
import { RefundsComponent } from './refunds.component';

View File

@ -10,15 +10,19 @@ const paymentSectionRoutes: Routes = [
children: [
{
path: 'operations',
loadChildren: () => import('./operations/operations.module').then(mod => mod.OperationsModule)
loadChildren: () => import('./operations/operations.module').then(m => m.OperationsModule)
},
{
path: 'reports',
loadChildren: () => import('./reports/reports.module').then(mod => mod.ReportsModule)
loadChildren: () => import('./reports/reports.module').then(m => m.ReportsModule)
},
{
path: 'payouts',
loadChildren: () => import('./payouts/payouts.module').then(mod => mod.PayoutsModule)
loadChildren: () => import('./payouts/payouts.module').then(m => m.PayoutsModule)
},
{
path: 'integrations',
loadChildren: () => import('./integrations/integrations.module').then(m => m.IntegrationsModule)
}
]
}

View File

@ -13,6 +13,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
import { TranslocoModule } from '@ngneat/transloco';
import { ButtonModule } from '@dsh/components/buttons';
import { EmptySearchResultModule } from '@dsh/components/empty-search-result';
import { FormControlsModule } from '@dsh/components/form-controls';
import { IndicatorsModule } from '@dsh/components/indicators';
import { LayoutModule } from '@dsh/components/layout';
@ -20,7 +21,6 @@ import { StateNavModule } from '@dsh/components/navigation';
import { TableModule } from '@dsh/components/table';
import { ReportsModule as ReportsApiModule } from '../../../api';
import { EmptySearchResultModule } from '../empty-search-result';
import { LastUpdatedModule } from '../operations/last-updated/last-updated.module';
import { CreateReportDialogComponent } from './create-report-dialog';
import { ReportsRoutingModule } from './reports-routing.module';

View File

@ -2,7 +2,8 @@
"nav": {
"operations": "Операции",
"reports": "Отчеты",
"payouts": "Возмещения"
"payouts": "Возмещения",
"integrations": "Интеграция"
},
"operations": {
"headline": "Операции",
@ -11,5 +12,11 @@
"refunds": "Возвраты",
"invoices": "Инвойсы"
}
},
"integrations": {
"headline": "Интеграция",
"tabs": {
"webhooks": "Webhooks"
}
}
}

View File

@ -22,6 +22,8 @@
"loading": "Загрузка...",
"ok": "OK",
"noDocs": "Нет прикрепленных документов",
"copied": "Скопировано",
"copyFailed": "Ошибка копирования",
"period": {
"fromTime": "Начало периода",
"toTime": "Конец периода"

View File

@ -0,0 +1,56 @@
{
"webhook": "Webhook",
"description": "Webhook это инструмент для получения асинхронных оповещений при наступлении одного или группы интересующих вас событий. Например: платеж успешно завершен.",
"attach": "Установить webhook",
"emptyResult": "Webhookи отсутствуют",
"details": {
"status": {
"active": "Активен",
"inactive": "Неактивен"
},
"url": "URL, на который будут поступать оповещения о произошедших событиях",
"shop": "Магазин"
},
"events": {
"title": "Набор событий",
"types": {
"CustomerCreated": "CustomerCreated (плательщик создан)",
"CustomerDeleted": "CustomerDeleted (плательщик удален)",
"CustomerReady": "CustomerReady (плательщик готов)",
"CustomerBindingStarted": "CustomerBindingStarted (привязка к плательщику запущена)",
"CustomerBindingSucceeded": "CustomerBindingSucceeded (привязка к плательщику успешно завершена)",
"CustomerBindingFailed": "CustomerBindingFailed (привязка к плательщику завершена с неудачей)",
"InvoiceCreated": "InvoiceCreated (создан новый инвойс)",
"InvoicePaid": "InvoicePaid (инвойс перешел в состояние \"Оплачен\")",
"InvoiceCancelled": "InvoiceCancelled (инвойс отменен по истечению срока давности)",
"InvoiceFulfilled": "InvoiceFulfilled (инвойс успешно погашен)",
"PaymentStarted": "PaymentStarted (создан платеж)",
"PaymentProcessed": "PaymentProcessed (платеж в обработке)",
"PaymentCaptured": "PaymentCaptured (платеж успешно завершен)",
"PaymentCancelled": "PaymentCancelled (платеж успешно отменен)",
"PaymentRefunded": "PaymentRefunded (платеж успешно возвращен)",
"PaymentFailed": "PaymentFailed (при проведении платежа возникла ошибка)",
"PaymentRefundCreated": "PaymentRefundCreated (возврат платежа успешно создан)",
"PaymentRefundSucceeded": "PaymentRefundSucceeded (возврат платежа прошел успешно)",
"PaymentRefundFailed": "PaymentRefundFailed (возврат платежа завершился неудачей)"
}
},
"actions": {
"title": "Действия",
"buttons": {
"delete": "Удалить webhook"
}
},
"key": {
"title": "Публичный ключ",
"publicKey": "Содержимое публичного ключа, служащего для проверки авторитативности приходящих на url оповещений",
"buttons": {
"copy": "Скопировать ключ"
}
},
"create": {
"title": "Установить новый webhook",
"types": "Набор типов событий, о которых следует оповещать",
"attach": "Установить"
}
}

View File

@ -1,3 +1,3 @@
<div *transloco="let t" class="mat-headline dsh-empty-search-result">
{{ t.emptySearchResult }}
{{ text ? text : t.emptySearchResult }}
</div>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'dsh-empty-search-result',
@ -6,4 +6,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
styleUrls: ['empty-search-result.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptySearchResultComponent {}
export class EmptySearchResultComponent {
@Input()
text?: string;
}

View File

@ -35,6 +35,7 @@
@import '../../app/sections/invoice-details/cart/cart-theme';
@import '../../app/sections/invoice-details/payments/payments-theme';
@import '../../app/sections/payment-section/payouts/payout-panel/payout-panel-theme';
@import '../../app/sections/payment-section/integrations/webhooks/webhook-panel/webhook-panel-theme';
@mixin dsh-theme($theme) {
body.#{map-get($theme, name)} {
@ -72,5 +73,6 @@
@include dsh-panel-theme($theme);
@include dsh-range-datepicker-theme($theme);
@include dsh-payout-panel-theme($theme);
@include dsh-webhook-panel-theme($theme);
}
}