feat: настройки уведомлений в панели свойств веб-части
Tests / frontend-test (pull_request) Successful in 1m14s
Tests / frontend-test (pull_request) Successful in 1m14s
Переключатель «Уведомления согласующим» (по умолчанию включён, undefined у старых экземпляров — тоже вкл.) и необязательное поле «Слать только на адрес» вместо зашитой заглушки NOTIFY_ONLY_TO — теперь письма идут всем. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,6 @@
|
|||||||
"title": { "default": "request-portal" },
|
"title": { "default": "request-portal" },
|
||||||
"description": { "default": "request-portal description" },
|
"description": { "default": "request-portal description" },
|
||||||
"officeFabricIconFontName": "Page",
|
"officeFabricIconFontName": "Page",
|
||||||
"properties": {}
|
"properties": { "notificationsEnabled": true }
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as ReactDom from 'react-dom';
|
import * as ReactDom from 'react-dom';
|
||||||
import { Version } from '@microsoft/sp-core-library';
|
import { Version } from '@microsoft/sp-core-library';
|
||||||
import { type IPropertyPaneConfiguration, PropertyPaneDropdown } from '@microsoft/sp-property-pane';
|
import {
|
||||||
|
type IPropertyPaneConfiguration,
|
||||||
|
PropertyPaneDropdown,
|
||||||
|
PropertyPaneTextField,
|
||||||
|
PropertyPaneToggle
|
||||||
|
} from '@microsoft/sp-property-pane';
|
||||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||||
|
|
||||||
@@ -11,7 +16,12 @@ import { IRequestPortalProps } from './components/IRequestPortalProps';
|
|||||||
import { initSP } from './shared/pnp/pnpjs';
|
import { initSP } from './shared/pnp/pnpjs';
|
||||||
import { installErrorLogging } from './shared/diagnostics';
|
import { installErrorLogging } from './shared/diagnostics';
|
||||||
import { setImpersonatedIdentity } from './shared/impersonation';
|
import { setImpersonatedIdentity } from './shared/impersonation';
|
||||||
import { APPROVAL_API_BASE_URL_LOCAL, APPROVAL_API_BASE_URL_PROD, setApprovalApiBaseUrl } from './shared/config';
|
import {
|
||||||
|
APPROVAL_API_BASE_URL_LOCAL,
|
||||||
|
APPROVAL_API_BASE_URL_PROD,
|
||||||
|
setApprovalApiBaseUrl,
|
||||||
|
setNotificationSettings
|
||||||
|
} from './shared/config';
|
||||||
import { initListPartitions } from './shared/approval';
|
import { initListPartitions } from './shared/approval';
|
||||||
import { ErrorOverlay } from './shared/ui';
|
import { ErrorOverlay } from './shared/ui';
|
||||||
import { PropertyPaneDebugUser } from './propertyPane/PropertyPaneDebugUser';
|
import { PropertyPaneDebugUser } from './propertyPane/PropertyPaneDebugUser';
|
||||||
@@ -28,6 +38,13 @@ export interface IRequestPortalWebPartProps {
|
|||||||
debugUserDisplayName: string;
|
debugUserDisplayName: string;
|
||||||
/** Куда шлём запросы approval-route (маршруты согласования) — прод или локальный backend. */
|
/** Куда шлём запросы approval-route (маршруты согласования) — прод или локальный backend. */
|
||||||
approvalApiBaseUrl: string;
|
approvalApiBaseUrl: string;
|
||||||
|
/**
|
||||||
|
* Уведомления согласующим (поток Power Automate). По умолчанию включены:
|
||||||
|
* у веб-частей, добавленных до появления свойства, оно undefined — это тоже «вкл».
|
||||||
|
*/
|
||||||
|
notificationsEnabled?: boolean;
|
||||||
|
/** Непустой адрес — письма только если активный шаг назначен на него (обкатка). Пусто — всем. */
|
||||||
|
notifyOnlyTo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequestPortalWebPartProps> {
|
export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequestPortalWebPartProps> {
|
||||||
@@ -41,6 +58,7 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
|
|||||||
setImpersonatedIdentity(this.properties.debugUserQuery, this.properties.debugUserDisplayName);
|
setImpersonatedIdentity(this.properties.debugUserQuery, this.properties.debugUserDisplayName);
|
||||||
this.properties.approvalApiBaseUrl = this.properties.approvalApiBaseUrl || APPROVAL_API_BASE_URL_PROD;
|
this.properties.approvalApiBaseUrl = this.properties.approvalApiBaseUrl || APPROVAL_API_BASE_URL_PROD;
|
||||||
setApprovalApiBaseUrl(this.properties.approvalApiBaseUrl);
|
setApprovalApiBaseUrl(this.properties.approvalApiBaseUrl);
|
||||||
|
this._applyNotificationSettings();
|
||||||
|
|
||||||
const element: React.ReactElement = React.createElement(
|
const element: React.ReactElement = React.createElement(
|
||||||
ErrorOverlay,
|
ErrorOverlay,
|
||||||
@@ -71,6 +89,7 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
|
|||||||
// панели без перезапуска), но render случается позже onInit.
|
// панели без перезапуска), но render случается позже onInit.
|
||||||
this.properties.approvalApiBaseUrl = this.properties.approvalApiBaseUrl || APPROVAL_API_BASE_URL_PROD;
|
this.properties.approvalApiBaseUrl = this.properties.approvalApiBaseUrl || APPROVAL_API_BASE_URL_PROD;
|
||||||
setApprovalApiBaseUrl(this.properties.approvalApiBaseUrl);
|
setApprovalApiBaseUrl(this.properties.approvalApiBaseUrl);
|
||||||
|
this._applyNotificationSettings();
|
||||||
|
|
||||||
// Партиции грузим здесь, а не по требованию из форм: имена списков нужны
|
// Партиции грузим здесь, а не по требованию из форм: имена списков нужны
|
||||||
// формам синхронно (см. shared/listTitles.ts), а формы — лениво подгружаемые
|
// формам синхронно (см. shared/listTitles.ts), а формы — лениво подгружаемые
|
||||||
@@ -92,7 +111,9 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _applyNotificationSettings(): void {
|
||||||
|
setNotificationSettings(this.properties.notificationsEnabled !== false, this.properties.notifyOnlyTo || '');
|
||||||
|
}
|
||||||
|
|
||||||
private _getEnvironmentMessage(): Promise<string> {
|
private _getEnvironmentMessage(): Promise<string> {
|
||||||
if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
|
if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
|
||||||
@@ -196,6 +217,23 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupName: 'Уведомления',
|
||||||
|
groupFields: [
|
||||||
|
PropertyPaneToggle('notificationsEnabled', {
|
||||||
|
label: 'Уведомления согласующим',
|
||||||
|
onText: 'Включены',
|
||||||
|
offText: 'Выключены',
|
||||||
|
checked: this.properties.notificationsEnabled !== false
|
||||||
|
}),
|
||||||
|
PropertyPaneTextField('notifyOnlyTo', {
|
||||||
|
label: 'Слать только на адрес',
|
||||||
|
description: 'Для обкатки: письмо уходит, только если шаг назначен на этот e-mail. Пусто — всем.',
|
||||||
|
placeholder: 'user@tnsu.kz',
|
||||||
|
disabled: this.properties.notificationsEnabled === false
|
||||||
|
})
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,9 @@
|
|||||||
import { AadHttpClient } from '@microsoft/sp-http';
|
import { AadHttpClient } from '@microsoft/sp-http';
|
||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { FLOW_RESOURCE, FLOW_NOTIFY_URL } from '../config';
|
import { FLOW_RESOURCE, FLOW_NOTIFY_URL, getNotificationsEnabled, getNotifyOnlyTo } from '../config';
|
||||||
import { ApprovalListService } from './approvalList.service';
|
import { ApprovalListService } from './approvalList.service';
|
||||||
import { REQUEST_STATUS } from './models/statuses';
|
import { REQUEST_STATUS } from './models/statuses';
|
||||||
|
|
||||||
/**
|
|
||||||
* ВРЕМЕННАЯ ЗАГЛУШКА на период обкатки рассылки: уведомление отправляется,
|
|
||||||
* только если активный шаг назначен на этот адрес. Для всех остальных
|
|
||||||
* согласующих поток не вызывается вообще — письма не будет.
|
|
||||||
*
|
|
||||||
* Ограничение стоит ЗДЕСЬ, на стороне портала, а не в потоке Power Automate,
|
|
||||||
* потому что адресата поток определяет САМ по RowID — тело запроса на выбор
|
|
||||||
* получателя не влияет. Значит, единственный способ не отправить письмо, не
|
|
||||||
* трогая поток, — не дёргать его. Переадресовать письмо на тестовый ящик
|
|
||||||
* отсюда нельзя по той же причине: поток такого параметра не принимает.
|
|
||||||
*
|
|
||||||
* Пустая строка — заглушка снята, уведомления идут всем как обычно.
|
|
||||||
*/
|
|
||||||
const NOTIFY_ONLY_TO = 'gorbenko.pavel@tnsu.kz';
|
|
||||||
|
|
||||||
// Данные формы для тела запроса к потоку (постоянны для формы).
|
// Данные формы для тела запроса к потоку (постоянны для формы).
|
||||||
export interface INotifyTarget {
|
export interface INotifyTarget {
|
||||||
requestTypeId: number; // RequestTypeId (id типа заявки во внешнем API маршрутов)
|
requestTypeId: number; // RequestTypeId (id типа заявки во внешнем API маршрутов)
|
||||||
@@ -42,20 +27,29 @@ async function postNotify(context: WebPartContext, body: Record<string, unknown>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Разрешена ли отправка на согласующего активной строки (см. NOTIFY_ONLY_TO).
|
// Разрешена ли отправка на согласующего активной строки — по свойствам веб-части
|
||||||
|
// notificationsEnabled / notifyOnlyTo (см. setNotificationSettings в config.ts).
|
||||||
//
|
//
|
||||||
// Неизвестный адрес (пользователь не найден, поле пустое, запрос упал) —
|
// Ограничение стоит ЗДЕСЬ, на стороне портала, а не в потоке Power Automate,
|
||||||
// НЕ отправляем: пока заглушка включена, сомнение трактуется в пользу
|
// потому что адресата поток определяет САМ по RowID — тело запроса на выбор
|
||||||
// «письмо живому человеку лучше не слать», а не наоборот.
|
// получателя не влияет. Значит, единственный способ не отправить письмо, не
|
||||||
|
// трогая поток, — не дёргать его.
|
||||||
|
//
|
||||||
|
// При заданном notifyOnlyTo неизвестный адрес (пользователь не найден, поле
|
||||||
|
// пустое, запрос упал) — НЕ отправляем: сомнение трактуется в пользу «письмо
|
||||||
|
// живому человеку лучше не слать».
|
||||||
async function isNotifyAllowed(approval: ApprovalListService, approverId: number): Promise<boolean> {
|
async function isNotifyAllowed(approval: ApprovalListService, approverId: number): Promise<boolean> {
|
||||||
if (!NOTIFY_ONLY_TO) return true;
|
if (!getNotificationsEnabled()) return false;
|
||||||
|
|
||||||
|
const onlyTo = getNotifyOnlyTo();
|
||||||
|
if (!onlyTo) return true;
|
||||||
|
|
||||||
const email = await approval.resolveUserEmail(approverId);
|
const email = await approval.resolveUserEmail(approverId);
|
||||||
if (email.toLowerCase() === NOTIFY_ONLY_TO.toLowerCase()) return true;
|
if (email.toLowerCase() === onlyTo.toLowerCase()) return true;
|
||||||
|
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Оповещение] Включена заглушка рассылки: письмо для «${email || 'адрес неизвестен'}» не отправлено ` +
|
`[Оповещение] Рассылка ограничена одним адресом: письмо для «${email || 'адрес неизвестен'}» не отправлено ` +
|
||||||
`(разрешён только ${NOTIFY_ONLY_TO})`
|
`(разрешён только ${onlyTo})`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,28 @@ export function getApprovalApiBaseUrl(): string {
|
|||||||
return _approvalApiBaseUrl;
|
return _approvalApiBaseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Уведомления согласующим (поток Power Automate, см. approval/notify.service.ts).
|
||||||
|
// Настраивается свойствами веб-части notificationsEnabled / notifyOnlyTo — тот
|
||||||
|
// же модульный синглтон, что и у approvalApiBaseUrl выше.
|
||||||
|
// enabled — false выключает рассылку целиком;
|
||||||
|
// onlyTo — непустой адрес: письма уходят ТОЛЬКО если активный шаг назначен
|
||||||
|
// на него (обкатка рассылки); пусто — всем.
|
||||||
|
let _notificationsEnabled = true;
|
||||||
|
let _notifyOnlyTo = '';
|
||||||
|
|
||||||
|
export function setNotificationSettings(enabled: boolean, onlyTo: string): void {
|
||||||
|
_notificationsEnabled = enabled;
|
||||||
|
_notifyOnlyTo = (onlyTo || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNotificationsEnabled(): boolean {
|
||||||
|
return _notificationsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNotifyOnlyTo(): string {
|
||||||
|
return _notifyOnlyTo;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Справочники (Dictionary API) — тот же gateway api.tnsu.kz и тот же AAD-app
|
* Справочники (Dictionary API) — тот же gateway api.tnsu.kz и тот же AAD-app
|
||||||
* (APPROVAL_API_CLIENT_ID), но другой путь/сервис. Используется, например, для
|
* (APPROVAL_API_CLIENT_ID), но другой путь/сервис. Используется, например, для
|
||||||
|
|||||||
Reference in New Issue
Block a user