Skip to content

Commit

Permalink
feat(atomic): add error handling for atomic-commerce queries (#3917)
Browse files Browse the repository at this point in the history
* Refactor `QueryErrorCommon` into multiple functional components
* Create new `atomic-commerce-query-error` component

For now it does not handle any commerce API specific errors. But it
should at least have the "catch all" which is "Something went wrong"
with a "Show more" button
https://coveord.atlassian.net/browse/KIT-3034

---------

Co-authored-by: Frederic Beaudoin <[email protected]>
  • Loading branch information
olamothe and fbeaudoincoveo authored May 13, 2024
1 parent adff180 commit 39270e5
Show file tree
Hide file tree
Showing 22 changed files with 542 additions and 202 deletions.
10 changes: 6 additions & 4 deletions packages/atomic/cypress/e2e/query-error.cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ describe('Query Error Test Suites', () => {
new TestFixture().with(addQueryError()).withError().init();
});

CommonAssertions.assertAriaLiveMessage(
QueryErrorSelectors.ariaLive,
'wrong'
);
it('updates aria live message', () => {
CommonAssertions.assertAriaLiveMessageWithoutIt(
QueryErrorSelectors.ariaLive,
'wrong'
);
});

it('should display an error title', () => {
QueryErrorSelectors.errorTitle()
Expand Down
25 changes: 25 additions & 0 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,11 @@ export namespace Components {
*/
"setRenderFunction": (productRenderingFunction: ItemRenderingFunction) => Promise<void>;
}
/**
* The `atomic-commerce-query-error` component handles fatal errors when performing a query on the Commerce API. When the error is known, it displays a link to relevant documentation for debugging purposes. When the error is unknown, it displays a small text area with the JSON content of the error.
*/
interface AtomicCommerceQueryError {
}
/**
* The `atomic-commerce-query-summary` component displays information about the current range of results and the request duration (e.g., "Results 1-10 of 123 in 0.47 seconds").
*/
Expand Down Expand Up @@ -3166,6 +3171,15 @@ declare global {
prototype: HTMLAtomicCommerceProductListElement;
new (): HTMLAtomicCommerceProductListElement;
};
/**
* The `atomic-commerce-query-error` component handles fatal errors when performing a query on the Commerce API. When the error is known, it displays a link to relevant documentation for debugging purposes. When the error is unknown, it displays a small text area with the JSON content of the error.
*/
interface HTMLAtomicCommerceQueryErrorElement extends Components.AtomicCommerceQueryError, HTMLStencilElement {
}
var HTMLAtomicCommerceQueryErrorElement: {
prototype: HTMLAtomicCommerceQueryErrorElement;
new (): HTMLAtomicCommerceQueryErrorElement;
};
/**
* The `atomic-commerce-query-summary` component displays information about the current range of results and the request duration (e.g., "Results 1-10 of 123 in 0.47 seconds").
*/
Expand Down Expand Up @@ -4758,6 +4772,7 @@ declare global {
"atomic-commerce-load-more-products": HTMLAtomicCommerceLoadMoreProductsElement;
"atomic-commerce-pager": HTMLAtomicCommercePagerElement;
"atomic-commerce-product-list": HTMLAtomicCommerceProductListElement;
"atomic-commerce-query-error": HTMLAtomicCommerceQueryErrorElement;
"atomic-commerce-query-summary": HTMLAtomicCommerceQuerySummaryElement;
"atomic-commerce-recommendation-list": HTMLAtomicCommerceRecommendationListElement;
"atomic-commerce-search-box": HTMLAtomicCommerceSearchBoxElement;
Expand Down Expand Up @@ -5198,6 +5213,11 @@ declare namespace LocalJSX {
*/
"imageSize"?: ItemDisplayImageSize;
}
/**
* The `atomic-commerce-query-error` component handles fatal errors when performing a query on the Commerce API. When the error is known, it displays a link to relevant documentation for debugging purposes. When the error is unknown, it displays a small text area with the JSON content of the error.
*/
interface AtomicCommerceQueryError {
}
/**
* The `atomic-commerce-query-summary` component displays information about the current range of results and the request duration (e.g., "Results 1-10 of 123 in 0.47 seconds").
*/
Expand Down Expand Up @@ -7701,6 +7721,7 @@ declare namespace LocalJSX {
"atomic-commerce-load-more-products": AtomicCommerceLoadMoreProducts;
"atomic-commerce-pager": AtomicCommercePager;
"atomic-commerce-product-list": AtomicCommerceProductList;
"atomic-commerce-query-error": AtomicCommerceQueryError;
"atomic-commerce-query-summary": AtomicCommerceQuerySummary;
"atomic-commerce-recommendation-list": AtomicCommerceRecommendationList;
"atomic-commerce-search-box": AtomicCommerceSearchBox;
Expand Down Expand Up @@ -7904,6 +7925,10 @@ declare module "@stencil/core" {
*/
"atomic-commerce-pager": LocalJSX.AtomicCommercePager & JSXBase.HTMLAttributes<HTMLAtomicCommercePagerElement>;
"atomic-commerce-product-list": LocalJSX.AtomicCommerceProductList & JSXBase.HTMLAttributes<HTMLAtomicCommerceProductListElement>;
/**
* The `atomic-commerce-query-error` component handles fatal errors when performing a query on the Commerce API. When the error is known, it displays a link to relevant documentation for debugging purposes. When the error is unknown, it displays a small text area with the JSON content of the error.
*/
"atomic-commerce-query-error": LocalJSX.AtomicCommerceQueryError & JSXBase.HTMLAttributes<HTMLAtomicCommerceQueryErrorElement>;
/**
* The `atomic-commerce-query-summary` component displays information about the current range of results and the request duration (e.g., "Results 1-10 of 123 in 0.47 seconds").
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import '../../../global/global.pcss';
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {isNullOrUndefined} from '@coveo/bueno';
import {
Search,
ProductListing,
SearchState,
ProductListingState,
buildProductListing,
buildSearch,
} from '@coveo/headless/commerce';
import {Component, h, State} from '@stencil/core';
import {AriaLiveRegion} from '../../../utils/accessibility-utils';
import {
BindStateToController,
InitializableComponent,
InitializeBindings,
} from '../../../utils/initialization-utils';
import {QueryErrorContainer} from '../../common/query-error/container';
import {QueryErrorDescription} from '../../common/query-error/description';
import {QueryErrorDetails} from '../../common/query-error/details';
import {QueryErrorGuard} from '../../common/query-error/guard';
import {QueryErrorIcon} from '../../common/query-error/icon';
import {QueryErrorLink} from '../../common/query-error/link';
import {QueryErrorShowMore} from '../../common/query-error/show-more';
import {QueryErrorTitle} from '../../common/query-error/title';
import {getAriaMessageFromErrorType} from '../../common/query-error/utils';
import {CommerceBindings} from '../atomic-commerce-interface/atomic-commerce-interface';

/**
* The `atomic-commerce-query-error` component handles fatal errors when performing a query on the Commerce API. When the error is known, it displays a link to relevant documentation for debugging purposes. When the error is unknown, it displays a small text area with the JSON content of the error.
*
* @part icon - The SVG related to the error.
* @part title - The title of the error.
* @part description - A description of the error.
* @part doc-link - A link to the relevant documentation.
* @part more-info-btn - A button to request additional error information.
* @part error-info - Additional error information.
*
* @internal
*/
@Component({
tag: 'atomic-commerce-query-error',
styleUrl: 'atomic-commerce-query-error.pcss',
shadow: true,
})
export class AtomicQueryError
implements InitializableComponent<CommerceBindings>
{
@InitializeBindings() public bindings!: CommerceBindings;
public searchOrListing!: Search | ProductListing;

@BindStateToController('searchOrListing')
@State()
private searchOrListingState!: SearchState | ProductListingState;
@State() public error!: Error;
@State() showMoreInfo = false;

@AriaLiveRegion('commerce-query-error')
protected ariaMessage!: string;

public initialize() {
if (this.bindings.interfaceElement.type === 'product-listing') {
this.searchOrListing = buildProductListing(this.bindings.engine);
} else {
this.searchOrListing = buildSearch(this.bindings.engine);
}
}

public render() {
const {error} = this.searchOrListingState;

const {
bindings: {
i18n,
engine: {
configuration: {organizationId, organizationEndpoints, platformUrl},
},
},
} = this;
const hasError = !isNullOrUndefined(error);
const actualPlatformUrl =
organizationEndpoints?.platform || platformUrl || '';
if (hasError) {
this.ariaMessage = getAriaMessageFromErrorType(
i18n,
organizationId,
actualPlatformUrl,
error?.type
);
}
return (
<QueryErrorGuard hasError={hasError}>
<QueryErrorContainer>
<QueryErrorIcon errorType={error?.type} />
<QueryErrorTitle
errorType={error?.type}
i18n={i18n}
organizationId={organizationId}
/>
<QueryErrorDescription
i18n={i18n}
organizationId={organizationId}
url={organizationEndpoints?.platform || platformUrl || ''}
errorType={error?.type}
/>
<QueryErrorShowMore
link={<QueryErrorLink i18n={i18n} errorType={error?.type} />}
onShowMore={() => (this.showMoreInfo = !this.showMoreInfo)}
i18n={i18n}
/>
<QueryErrorDetails error={error} show={this.showMoreInfo} />
</QueryErrorContainer>
</QueryErrorGuard>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {FunctionalComponent, h} from '@stencil/core';

export const QueryErrorContainer: FunctionalComponent = (_, children) => {
return <div class="text-center p-8">{children}</div>;
};
20 changes: 20 additions & 0 deletions packages/atomic/src/components/common/query-error/description.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {FunctionalComponent, h} from '@stencil/core';
import {i18n} from 'i18next';
import {getErrorDescriptionFromErrorType} from './utils';

interface QueryErrorDescriptionProps {
errorType?: string;
i18n: i18n;
url: string;
organizationId: string;
}

export const QueryErrorDescription: FunctionalComponent<
QueryErrorDescriptionProps
> = ({errorType, i18n, url, organizationId}) => {
return (
<p part="description" class="text-lg text-neutral-dark mt-2.5">
{getErrorDescriptionFromErrorType(i18n, organizationId, url, errorType)}
</p>
);
};
22 changes: 22 additions & 0 deletions packages/atomic/src/components/common/query-error/details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {FunctionalComponent, h} from '@stencil/core';

interface QueryErrorDetailsProps {
error: unknown;
show: boolean;
}
export const QueryErrorDetails: FunctionalComponent<QueryErrorDetailsProps> = ({
error,
show,
}) => {
if (!show) {
return;
}
return (
<pre
part="error-info"
class="text-left border border-neutral bg-neutral-light p-3 rounded mt-8 whitespace-pre-wrap"
>
<code>{JSON.stringify(error, null, 2)}</code>
</pre>
);
};
16 changes: 16 additions & 0 deletions packages/atomic/src/components/common/query-error/guard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Fragment, FunctionalComponent, h} from '@stencil/core';

interface QueryErrorGuardProps {
hasError: boolean;
}

export const QueryErrorGuard: FunctionalComponent<QueryErrorGuardProps> = (
{hasError},
children
) => {
if (!hasError) {
return;
}

return <Fragment>{children}</Fragment>;
};
40 changes: 40 additions & 0 deletions packages/atomic/src/components/common/query-error/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {FunctionalComponent, h} from '@stencil/core';
import CannotAccess from '../../../images/cannot-access.svg';
import Indexing from '../../../images/indexing.svg';
import NoConnection from '../../../images/no-connection.svg';
import SearchInactive from '../../../images/search-inactive.svg';
import SomethingWrong from '../../../images/something-wrong.svg';
import {KnownErrorType} from './known-error-types';

interface QueryErrorIconProps {
errorType?: string;
}

export const QueryErrorIcon: FunctionalComponent<QueryErrorIconProps> = ({
errorType,
}) => {
const getIconFromErrorType = () => {
switch (errorType as KnownErrorType) {
case 'Disconnected':
return NoConnection;

case 'NoEndpointsException':
return Indexing;

case 'InvalidTokenException':
return CannotAccess;
case 'OrganizationIsPausedException':
return SearchInactive;
default:
return SomethingWrong;
}
};

return (
<atomic-icon
part="icon"
icon={getIconFromErrorType()}
class="w-1/2 max-w-lg"
></atomic-icon>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type KnownErrorType =
| 'Disconnected'
| 'NoEndpointsException'
| 'InvalidTokenException'
| 'OrganizationIsPausedException';
34 changes: 34 additions & 0 deletions packages/atomic/src/components/common/query-error/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {FunctionalComponent, h} from '@stencil/core';
import {i18n} from 'i18next';
import {KnownErrorType} from './known-error-types';

interface QueryErrorLinkProps {
errorType?: string;
i18n: i18n;
}

export const QueryErrorLink: FunctionalComponent<QueryErrorLinkProps> = ({
errorType,
i18n,
}) => {
const getErrorLink = () => {
switch (errorType as KnownErrorType) {
case 'NoEndpointsException':
return 'https://docs.coveo.com/en/mcc80216';
case 'InvalidTokenException':
return 'https://docs.coveo.com/en/102';
case 'OrganizationIsPausedException':
return 'https://docs.coveo.com/l6af0467';
default:
return null;
}
};

const link = getErrorLink();

return link ? (
<a href={link} part="doc-link" class="btn-primary p-3 mt-10 inline-block">
{i18n.t('coveo-online-help')}
</a>
) : null;
};
Loading

0 comments on commit 39270e5

Please sign in to comment.