Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

43662 - Added error handling hook to StopAreaDetailsEdit #931

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions ui/src/components/forms/stop-area/useUpsertStopArea.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { gql } from '@apollo/client';
import { ApolloError, gql } from '@apollo/client';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
Expand All @@ -13,6 +13,7 @@ import {
showDangerToast,
} from '../../../utils';
import { StopAreaFormState } from './stopAreaFormSchema';
import { useStopAreaDetailsApolloErrorHandler } from './util/stopAreaDetailsErrorHandler';

const GQL_UPSERT_STOP_AREA = gql`
mutation UpsertStopArea($object: stop_registry_GroupOfStopPlacesInput) {
Expand Down Expand Up @@ -68,6 +69,8 @@ const mapFormStateToInput = ({

export const useUpsertStopArea = () => {
const { t } = useTranslation();
const { tryHandle: tryHandleApolloError } =
useStopAreaDetailsApolloErrorHandler();
const [upsertStopAreaMutation] = useUpsertStopAreaMutation();

/**
Expand All @@ -94,7 +97,13 @@ export const useUpsertStopArea = () => {
);

const defaultErrorHandler = useCallback(
(error: unknown) => {
(error: unknown, details?: StopAreaFormState) => {
if (error instanceof ApolloError) {
const isKnowError = tryHandleApolloError(error, details);
if (isKnowError) {
return;
}
}
if (error instanceof Error) {
showDangerToast(
`${t('errors.saveFailed')}, ${error}, ${error.message}`,
Expand All @@ -103,7 +112,7 @@ export const useUpsertStopArea = () => {
showDangerToast(`${t('errors.saveFailed')}, ${error}`);
}
},
[t],
[t, tryHandleApolloError],
);

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { ApolloError } from '@apollo/client';
import { useTranslation } from 'react-i18next';
import { showDangerToast } from '../../../../utils';
import { renderHook } from '../../../../utils/test-utils';
import { StopAreaFormState } from '../stopAreaFormSchema';
import { useStopAreaDetailsApolloErrorHandler } from './stopAreaDetailsErrorHandler';

jest.mock('react-i18next', () => ({
useTranslation: jest.fn(),
}));

jest.mock('i18next', () => ({
use: jest.fn(() => ({
init: jest.fn(),
})),
}));

jest.mock('../../../../utils', () => ({
showDangerToast: jest.fn(),
}));

type TestError = Partial<ApolloError> & {
cause: {
message: string;
extensions: { errorCode: string } | undefined;
};
};
const mockStateDefaults = {
indefinite: false,
latitude: 0,
longitude: 0,
memberStops: [],
validityStart: '',
};

describe('useStopAreaDetailsApolloErrorHandler', () => {
const tMock = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
(useTranslation as jest.Mock).mockReturnValue({ t: tMock });
});

test('should handle known error with details', () => {
const knownErrorCode = 'GROUP_OF_STOP_PLACES_UNIQUE_NAME';
const extensions = { errorCode: knownErrorCode };
const errorWithKnownCode: TestError = {
graphQLErrors: [],
networkError: null,
message: 'Known Error',
cause: { extensions, message: knownErrorCode },
};

tMock.mockImplementation(
(key, details) => `${key} ${JSON.stringify(details)}`,
);

const { result } = renderHook(() => useStopAreaDetailsApolloErrorHandler());

const details: StopAreaFormState = {
...mockStateDefaults,
label: 'Testlabel1',
name: 'Testname1',
}; // Mock details data
const handled = result.current.tryHandle(
errorWithKnownCode as ApolloError,
details,
);

expect(handled).toBe(true);
expect(tMock).toHaveBeenCalledWith(
'stopAreaDetails.errors.groupOfStopPlacesUniqueName',
details,
);
expect(showDangerToast).toHaveBeenCalledWith(
'stopAreaDetails.errors.groupOfStopPlacesUniqueName {"indefinite":false,"latitude":0,"longitude":0,"memberStops":[],"validityStart":"","label":"Testlabel1","name":"Testname1"}',
);
});

test('should handle known error without details', () => {
const knownErrorCode = 'GROUP_OF_STOP_PLACES_UNIQUE_DESCRIPTION';
const extensions = { errorCode: knownErrorCode };
const errorWithKnownCode: TestError = {
graphQLErrors: [],
networkError: null,
message: 'Known Error',
cause: { extensions, message: knownErrorCode },
};

tMock.mockImplementation((key) => key);

const { result } = renderHook(() => useStopAreaDetailsApolloErrorHandler());

const handled = result.current.tryHandle(errorWithKnownCode as ApolloError);

expect(handled).toBe(true);
expect(tMock).toHaveBeenCalledWith(
'stopAreaDetails.errors.groupOfStopPlacesUniqueDescription',
);
expect(showDangerToast).toHaveBeenCalledWith(
'stopAreaDetails.errors.groupOfStopPlacesUniqueDescription',
);
});

test('should not handle unknown error', () => {
const unknownErrorCode = 'UNKNOWN_ERROR_CODE';
const extensions = { errorCode: unknownErrorCode };
const unknownError: TestError = {
graphQLErrors: [],
networkError: null,
message: 'Unknown Error',
cause: { extensions, message: unknownErrorCode },
};

tMock.mockImplementation((key) => key);

const { result } = renderHook(() => useStopAreaDetailsApolloErrorHandler());

const handled = result.current.tryHandle(unknownError as ApolloError);

expect(handled).toBe(false);
expect(tMock).not.toHaveBeenCalled();
});

test('should not handle errors with no extensions', () => {
const errorWithNoExtensions: TestError = {
...mockStateDefaults,
graphQLErrors: [],
networkError: null,
message: 'Error with no extensions',
cause: { message: '', extensions: undefined }, // No extensions in this case
};

tMock.mockImplementation((key) => key);

const { result } = renderHook(() => useStopAreaDetailsApolloErrorHandler());

const handled = result.current.tryHandle(
errorWithNoExtensions as ApolloError,
);

expect(handled).toBe(false);
expect(tMock).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ApolloError } from '@apollo/client';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { showDangerToast } from '../../../../utils';
import { StopAreaFormState } from '../stopAreaFormSchema';

const ERRORS: Readonly<Map<string, string>> = new Map([
[
'GROUP_OF_STOP_PLACES_UNIQUE_NAME',
'stopAreaDetails.errors.groupOfStopPlacesUniqueName',
],
[
'GROUP_OF_STOP_PLACES_UNIQUE_DESCRIPTION',
'stopAreaDetails.errors.groupOfStopPlacesUniqueDescription',
],
]);
type ExtensionsType = { errorCode: string };

export function useStopAreaDetailsApolloErrorHandler() {
const { t } = useTranslation();

const tryHandle = useCallback(
(error: ApolloError, details?: StopAreaFormState): boolean => {
const errorNames = Array.from(ERRORS.keys());
const knownError: string | undefined = errorNames.find((key) => {
const extensions: ExtensionsType | undefined = error.cause
?.extensions as ExtensionsType;
return extensions?.errorCode === key;
});
if (knownError) {
const knownErrorKey = ERRORS.get(knownError);
if (knownErrorKey) {
if (details) {
showDangerToast(`${t(knownErrorKey, details)}`);
return true;
}
showDangerToast(`${t(knownErrorKey)}`);
return true;
}
}
return false;
},
[t],
);
return { tryHandle };
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const StopAreaDetailsEditImpl: ForwardRefRenderFunction<
showSuccessToast(t('stopArea.editSuccess'));
onFinishEditing();
} catch (err) {
defaultErrorHandler(err as Error);
defaultErrorHandler(err as Error, state);
}
setIsLoading(false);
};
Expand Down
4 changes: 4 additions & 0 deletions ui/src/locales/en-US/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@
"title": "Removing stop from the stop area",
"body": "The Stop will not be deleted, but it will no longer be associated with the stop area."
}
},
"errors": {
"groupOfStopPlacesUniqueName": "Stop area should have unique label but {{label}} is already in use!",
"groupOfStopPlacesUniqueDescription": "Stop area should have unique name but {{name}} is already in use!"
}
},
"stopPlaceTypes": {
Expand Down
4 changes: 4 additions & 0 deletions ui/src/locales/fi-FI/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@
"title": "Poistetaan pysäkki pysäkkialueen käytöstä",
"body": "Pysäkki jää edelleen jäljelle, se ei ole vain yhdistetty pysäkkialueeseen."
}
},
"errors": {
"groupOfStopPlacesUniqueName": "Pysäkkialueella tulee olla uniikki tunnus, mutta tunnus {{label}} on jo jonkin toisen alueen käytössä!",
"groupOfStopPlacesUniqueDescription": "Pysäkkialueella tulee olla uniikki nimi, mutta nimi {{name}} on jo jonkin toisen alueen käytössä!"
}
},
"stopPlaceTypes": {
Expand Down
12 changes: 12 additions & 0 deletions ui/src/utils/i18n/snakeToCamel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function snakeToCamel(snakeCaseStr: string): string {
return snakeCaseStr
.toLowerCase()
.split('_')
.map((word, index) => {
if (index === 0) {
return word;
}
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join('');
}
Loading