Define server error messages to display in FE from the server (#12973)
Currently, when a server query or mutation from the front-end fails, the error message defined server-side is displayed in a snackbar in the front-end. These error messages usually contain technical details that don't belong to the user interface, such as "ObjectMetadataCollection not found" or "invalid ENUM value for ...". **BE** In addition to the original error message that is still needed (for the request response, debugging, sentry monitoring etc.), we add a `displayedErrorMessage` that will be used in the snackbars. It's only relevant to add it for the messages that will reach the FE (ie. not in jobs or in rest api for instance) and if it can help the user sort out / fix things (ie. we do add displayedErrorMessage for "Cannot create multiple draft versions for the same workflow" or "Cannot delete [field], please update the label identifier field first", but not "Object metadata does not exist"), even if in practice in the FE users should not be able to perform an action that will not work (ie should not be able to save creation of multiple draft versions of the same workflows). **FE** To ease the usage we replaced enqueueSnackBar with enqueueErrorSnackBar and enqueueSuccessSnackBar with an api that only requires to pass on the error. If no displayedErrorMessage is specified then the default error message is `An error occured.`
This commit is contained in:
@ -17,7 +17,11 @@ export class ApprovedAccessDomainExceptionFilter implements ExceptionFilter {
|
||||
case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_VALIDATION_TOKEN_INVALID:
|
||||
case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED:
|
||||
case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_MUST_BE_A_COMPANY_DOMAIN:
|
||||
throw new ForbiddenError(exception.message);
|
||||
throw new ForbiddenError(exception.message, {
|
||||
extensions: {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
},
|
||||
});
|
||||
default: {
|
||||
const _exhaustiveCheck: never = exception.code;
|
||||
|
||||
|
||||
@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class ApprovedAccessDomainException extends CustomException {
|
||||
declare code: ApprovedAccessDomainExceptionCode;
|
||||
constructor(message: string, code: ApprovedAccessDomainExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: ApprovedAccessDomainExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { SendApprovedAccessDomainValidation } from 'twenty-emails';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
@ -18,8 +19,8 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { isWorkDomain } from 'src/utils/is-work-email';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { isWorkDomain } from 'src/utils/is-work-email';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
@ -42,6 +43,9 @@ export class ApprovedAccessDomainService {
|
||||
throw new ApprovedAccessDomainException(
|
||||
'Approved access domain has already been validated',
|
||||
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VERIFIED,
|
||||
{
|
||||
userFriendlyMessage: t`Approved access domain has already been validated`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -49,6 +53,9 @@ export class ApprovedAccessDomainService {
|
||||
throw new ApprovedAccessDomainException(
|
||||
'Approved access domain does not match email domain',
|
||||
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_DOES_NOT_MATCH_DOMAIN_EMAIL,
|
||||
{
|
||||
userFriendlyMessage: t`Approved access domain does not match email domain`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -118,6 +125,9 @@ export class ApprovedAccessDomainService {
|
||||
throw new ApprovedAccessDomainException(
|
||||
'Approved access domain has already been validated',
|
||||
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED,
|
||||
{
|
||||
userFriendlyMessage: t`Approved access domain has already been validated`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class AuthException extends CustomException {
|
||||
declare code: AuthExceptionCode;
|
||||
constructor(message: string, code: AuthExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: AuthExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Catch, ExceptionFilter } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
@ -16,26 +18,39 @@ export class AuthGraphqlApiExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: AuthException) {
|
||||
switch (exception.code) {
|
||||
case AuthExceptionCode.CLIENT_NOT_FOUND:
|
||||
throw new NotFoundError(exception.message);
|
||||
throw new NotFoundError(exception.message, {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
});
|
||||
case AuthExceptionCode.INVALID_INPUT:
|
||||
throw new UserInputError(exception.message);
|
||||
throw new UserInputError(exception.message, {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
});
|
||||
case AuthExceptionCode.FORBIDDEN_EXCEPTION:
|
||||
case AuthExceptionCode.INSUFFICIENT_SCOPES:
|
||||
case AuthExceptionCode.OAUTH_ACCESS_DENIED:
|
||||
case AuthExceptionCode.SSO_AUTH_FAILED:
|
||||
case AuthExceptionCode.USE_SSO_AUTH:
|
||||
case AuthExceptionCode.SIGNUP_DISABLED:
|
||||
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE:
|
||||
case AuthExceptionCode.INVALID_JWT_TOKEN_TYPE:
|
||||
throw new ForbiddenError(exception.message);
|
||||
throw new ForbiddenError(exception.message, {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
});
|
||||
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
userFriendlyMessage: t`Authentication is not enabled with this provider.`,
|
||||
});
|
||||
case AuthExceptionCode.EMAIL_NOT_VERIFIED:
|
||||
case AuthExceptionCode.INVALID_DATA:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: AuthExceptionCode.EMAIL_NOT_VERIFIED,
|
||||
userFriendlyMessage: t`Email is not verified.`,
|
||||
});
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
throw new AuthenticationError(exception.message, {
|
||||
userFriendlyMessage: t`You must be authenticated to perform this action.`,
|
||||
});
|
||||
case AuthExceptionCode.USER_NOT_FOUND:
|
||||
case AuthExceptionCode.WORKSPACE_NOT_FOUND:
|
||||
throw new AuthenticationError(exception.message);
|
||||
|
||||
@ -169,6 +169,9 @@ export class AuthService {
|
||||
throw new AuthException(
|
||||
'Wrong password',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`Wrong password`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared/constants';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
@ -67,6 +68,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Email is required`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -111,6 +115,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Password too weak',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Password too weak`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -130,6 +137,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Wrong password',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`Wrong password`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -153,6 +163,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Email is required`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -194,6 +207,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Workspace is not ready to welcome new members',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`Workspace is not ready to welcome new members`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -207,6 +223,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'User is not part of the workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`User is not part of the workspace`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -340,6 +359,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Email is required`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Catch, ExceptionFilter } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
EmailVerificationException,
|
||||
EmailVerificationExceptionCode,
|
||||
@ -13,17 +15,32 @@ import {
|
||||
export class EmailVerificationExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: EmailVerificationException) {
|
||||
switch (exception.code) {
|
||||
case EmailVerificationExceptionCode.TOKEN_EXPIRED:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: t`Request has expired, please try again.`,
|
||||
});
|
||||
case EmailVerificationExceptionCode.INVALID_TOKEN:
|
||||
case EmailVerificationExceptionCode.INVALID_APP_TOKEN_TYPE:
|
||||
case EmailVerificationExceptionCode.TOKEN_EXPIRED:
|
||||
case EmailVerificationExceptionCode.RATE_LIMIT_EXCEEDED:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: exception.code,
|
||||
});
|
||||
case EmailVerificationExceptionCode.EMAIL_MISSING:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
});
|
||||
case EmailVerificationExceptionCode.EMAIL_ALREADY_VERIFIED:
|
||||
case EmailVerificationExceptionCode.INVALID_EMAIL:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: t`Email already verified.`,
|
||||
});
|
||||
case EmailVerificationExceptionCode.EMAIL_VERIFICATION_NOT_REQUIRED:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: t`Email verification not required.`,
|
||||
});
|
||||
case EmailVerificationExceptionCode.INVALID_EMAIL:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
});
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
OnExecuteDoneHookResultOnNextHook,
|
||||
Plugin,
|
||||
} from '@envelop/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
|
||||
|
||||
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface';
|
||||
@ -24,8 +25,7 @@ import {
|
||||
|
||||
const DEFAULT_EVENT_ID_KEY = 'exceptionEventId';
|
||||
const SCHEMA_VERSION_HEADER = 'x-schema-version';
|
||||
const SCHEMA_MISMATCH_ERROR =
|
||||
'Your workspace has been updated with a new data model. Please refresh the page.';
|
||||
const SCHEMA_MISMATCH_ERROR = 'Schema version mismatch.';
|
||||
|
||||
type GraphQLErrorHandlerHookOptions = {
|
||||
metricsService: MetricsService;
|
||||
@ -191,11 +191,22 @@ export const useGraphQLErrorHandlerHook = <
|
||||
const transformedErrors = processedErrors.map((error) => {
|
||||
const graphqlError =
|
||||
error instanceof BaseGraphQLError
|
||||
? error
|
||||
? {
|
||||
...error,
|
||||
extensions: {
|
||||
...error.extensions,
|
||||
userFriendlyMessage:
|
||||
error.extensions.userFriendlyMessage ??
|
||||
t`An error occurred.`,
|
||||
},
|
||||
}
|
||||
: generateGraphQLErrorFromError(error);
|
||||
|
||||
if (error.eventId && eventIdKey) {
|
||||
graphqlError.extensions[eventIdKey] = error.eventId;
|
||||
graphqlError.extensions = {
|
||||
...graphqlError.extensions,
|
||||
[eventIdKey]: error.eventId,
|
||||
};
|
||||
}
|
||||
|
||||
return graphqlError;
|
||||
@ -224,7 +235,11 @@ export const useGraphQLErrorHandlerHook = <
|
||||
requestMetadataVersion &&
|
||||
requestMetadataVersion !== `${currentMetadataVersion}`
|
||||
) {
|
||||
throw new GraphQLError(SCHEMA_MISMATCH_ERROR);
|
||||
throw new GraphQLError(SCHEMA_MISMATCH_ERROR, {
|
||||
extensions: {
|
||||
userFriendlyMessage: t`Your workspace has been updated with a new data model. Please refresh the page.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,13 +1,25 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
BaseGraphQLError,
|
||||
ErrorCode,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export const generateGraphQLErrorFromError = (error: Error) => {
|
||||
export const generateGraphQLErrorFromError = (
|
||||
error: Error | CustomException,
|
||||
) => {
|
||||
const graphqlError = new BaseGraphQLError(
|
||||
error.message,
|
||||
ErrorCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
|
||||
if (error instanceof CustomException) {
|
||||
graphqlError.extensions.userFriendlyMessage =
|
||||
error.userFriendlyMessage ?? t`An error occurred.`;
|
||||
} else {
|
||||
graphqlError.extensions.userFriendlyMessage = t`An error occurred.`;
|
||||
}
|
||||
|
||||
return graphqlError;
|
||||
};
|
||||
|
||||
@ -159,8 +159,9 @@ export class UserInputError extends BaseGraphQLError {
|
||||
}
|
||||
|
||||
export class NotFoundError extends BaseGraphQLError {
|
||||
constructor(message: string) {
|
||||
super(message, ErrorCode.NOT_FOUND);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(message: string, extensions?: Record<string, any>) {
|
||||
super(message, ErrorCode.NOT_FOUND, extensions);
|
||||
|
||||
Object.defineProperty(this, 'name', { value: 'NotFoundError' });
|
||||
}
|
||||
@ -175,8 +176,9 @@ export class MethodNotAllowedError extends BaseGraphQLError {
|
||||
}
|
||||
|
||||
export class ConflictError extends BaseGraphQLError {
|
||||
constructor(message: string) {
|
||||
super(message, ErrorCode.CONFLICT);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(message: string, extensions?: Record<string, any>) {
|
||||
super(message, ErrorCode.CONFLICT, extensions);
|
||||
|
||||
Object.defineProperty(this, 'name', { value: 'ConflictError' });
|
||||
}
|
||||
|
||||
@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class RecordTransformerException extends CustomException {
|
||||
declare code: RecordTransformerExceptionCode;
|
||||
constructor(message: string, code: RecordTransformerExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: RecordTransformerExceptionCode,
|
||||
userFriendlyMessage?: string,
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,9 @@ export const recordTransformerGraphqlApiExceptionHandler = (
|
||||
case RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE:
|
||||
case RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE:
|
||||
case RecordTransformerExceptionCode.INVALID_URL:
|
||||
throw new UserInputError(error.message);
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
default: {
|
||||
assertUnreachable(error.code);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
CountryCallingCode,
|
||||
@ -61,6 +62,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
||||
throw new RecordTransformerException(
|
||||
`Invalid country code ${countryCode}`,
|
||||
RecordTransformerExceptionCode.INVALID_PHONE_COUNTRY_CODE,
|
||||
t`Invalid country code ${countryCode}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -74,6 +76,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
||||
throw new RecordTransformerException(
|
||||
`Invalid calling code ${callingCode}`,
|
||||
RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE,
|
||||
t`Invalid calling code ${callingCode}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -86,6 +89,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
||||
throw new RecordTransformerException(
|
||||
`Provided country code and calling code are conflicting`,
|
||||
RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE,
|
||||
t`Provided country code and calling code are conflicting`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -106,6 +110,7 @@ const parsePhoneNumberExceptionWrapper = ({
|
||||
throw new RecordTransformerException(
|
||||
`Provided phone number is invalid ${number}`,
|
||||
RecordTransformerExceptionCode.INVALID_PHONE_NUMBER,
|
||||
t`Provided phone number is invalid ${number}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -129,6 +134,7 @@ const validateAndInferMetadataFromPrimaryPhoneNumber = ({
|
||||
throw new RecordTransformerException(
|
||||
'Provided and inferred country code are conflicting',
|
||||
RecordTransformerExceptionCode.CONFLICTING_PHONE_COUNTRY_CODE,
|
||||
t`Provided and inferred country code are conflicting`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -140,6 +146,7 @@ const validateAndInferMetadataFromPrimaryPhoneNumber = ({
|
||||
throw new RecordTransformerException(
|
||||
'Provided and inferred calling code are conflicting',
|
||||
RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE,
|
||||
t`Provided and inferred calling code are conflicting`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -19,9 +19,17 @@ export const handleWorkflowTriggerException = (
|
||||
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER:
|
||||
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS:
|
||||
case WorkflowTriggerExceptionCode.FORBIDDEN:
|
||||
throw new UserInputError(exception.message);
|
||||
throw new UserInputError(exception.message, {
|
||||
extensions: {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
},
|
||||
});
|
||||
case WorkflowTriggerExceptionCode.NOT_FOUND:
|
||||
throw new NotFoundError(exception.message);
|
||||
throw new NotFoundError(exception.message, {
|
||||
extensions: {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
},
|
||||
});
|
||||
case WorkflowTriggerExceptionCode.INTERNAL_ERROR:
|
||||
throw exception;
|
||||
default: {
|
||||
|
||||
Reference in New Issue
Block a user