console: fix some minor issues with Alert functions

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8859
GitOrigin-RevId: 3ed709bf46c3036232d723b4506f2940a465c9e1
This commit is contained in:
Matthew Goodwin 2023-04-25 12:59:31 -05:00 committed by hasura-bot
parent 74f282ad16
commit f83028d2b2
3 changed files with 354 additions and 159 deletions

View File

@ -1,9 +1,10 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
import React from 'react';
import React, { useReducer } from 'react';
import { expect } from '@storybook/jest';
import { expect, jest } from '@storybook/jest';
import { screen, userEvent, within } from '@storybook/testing-library';
import { useHasuraAlert } from '.';
import useUpdateEffect from '../../hooks/useUpdateEffect';
import { Button } from '../Button';
import { useDestructiveAlert } from './AlertProvider';
@ -620,3 +621,171 @@ DestructivePrompt.parameters = {
},
},
};
const logger = {
log: (x: string) => {
console.log(x);
},
};
export const ReferenceStability: ComponentStory<any> = () => {
const { hasuraAlert, hasuraConfirm, hasuraPrompt } = useHasuraAlert();
const { destructiveConfirm, destructivePrompt } = useDestructiveAlert();
const [, forceUpdate] = useReducer(x => x + 1, 0);
useUpdateEffect(() => {
logger.log('hasuraAlert reference changed');
}, [hasuraAlert]);
useUpdateEffect(() => {
logger.log('hasuraConfirm reference changed');
}, [hasuraConfirm]);
useUpdateEffect(() => {
logger.log('hasuraPrompt reference changed');
}, [hasuraPrompt]);
useUpdateEffect(() => {
logger.log('destructiveConfirm reference changed');
}, [destructiveConfirm]);
useUpdateEffect(() => {
logger.log('destructivePrompt reference changed');
}, [destructivePrompt]);
React.useEffect(() => {
console.log('expected render 👍');
});
const twButtonStyles =
'border-gray-900 bg-slate-100 border-solid border rounded p-2 active:bg-slate-300';
return (
<div className="w-full">
<ul>
<li>This story will automatically test referential stability.</li>
<li>Any referential changes will be logged to the console.</li>
<li>
To test manually, try pressing the buttons and checking for logs in
the console.
</li>
</ul>
<div className="space-y-2 gap-2 flex flex-row">
<button
data-testid="force-update"
className={twButtonStyles}
onClick={() => {
forceUpdate();
}}
>
force render
</button>
<button
data-testid="alert"
className={twButtonStyles}
onClick={() => {
hasuraAlert({ title: 'Test', message: 'test' });
}}
>
open alert
</button>
<button
data-testid="confirm"
className={twButtonStyles}
onClick={() => {
hasuraConfirm({
title: 'Test',
message: 'test',
onClose: () => {},
});
}}
>
open confirm
</button>
<button
data-testid="prompt"
className={twButtonStyles}
onClick={() => {
hasuraPrompt({ title: 'Test', message: 'test', onClose: () => {} });
}}
>
open prompt
</button>
<button
data-testid="destructive-confirm"
className={twButtonStyles}
onClick={() => {
destructiveConfirm({
resourceName: 'test',
resourceType: 'test',
onConfirm: async () => true,
});
}}
>
open destructive confirm
</button>
<button
data-testid="destructive-prompt"
className={twButtonStyles}
onClick={() => {
destructivePrompt({
resourceName: 'test',
resourceType: 'test',
onConfirm: async () => true,
});
}}
>
open destructive prompt
</button>
</div>
</div>
);
};
ReferenceStability.play = async ({ canvasElement }) => {
const logSpy = jest.spyOn(logger, 'log');
const canvas = within(canvasElement);
//test a force render
await userEvent.click(canvas.getByTestId('force-update'));
await expect(logSpy).not.toHaveBeenCalled();
//test alert call
await userEvent.click(canvas.getByTestId('alert'));
await userEvent.click(await screen.findByText('Ok'));
await expect(logSpy).not.toHaveBeenCalled();
//test confirm call
await userEvent.click(canvas.getByTestId('confirm'));
await userEvent.click(await screen.findByText('Ok'));
await expect(logSpy).not.toHaveBeenCalled();
//test prompt call
await userEvent.click(canvas.getByTestId('prompt'));
await userEvent.click(await screen.findByText('Ok'));
await expect(logSpy).not.toHaveBeenCalled();
//test destructive confirm
await userEvent.click(canvas.getByTestId('destructive-confirm'));
await userEvent.click(await screen.findByText('Cancel'));
await expect(logSpy).not.toHaveBeenCalled();
//test destructive prompt
await userEvent.click(canvas.getByTestId('destructive-prompt'));
await userEvent.click(await screen.findByText('Cancel'));
await expect(logSpy).not.toHaveBeenCalled();
};

View File

@ -146,15 +146,32 @@ export const AlertProvider: React.FC = ({ children }) => {
setShowAlert(true);
}, 0);
},
[closeAndCleanup, dismissAlert]
[closeAndCleanup, dismissAlert, handleUnhandledError]
);
const hasuraAlert = React.useCallback(
params => fireAlert({ ...params, mode: 'alert' }),
[fireAlert]
);
const hasuraConfirm = React.useCallback(
params => fireAlert({ ...params, mode: 'confirm' }),
[fireAlert]
);
const hasuraPrompt = React.useCallback(
params => fireAlert({ ...params, mode: 'prompt' }),
[fireAlert]
);
return (
<AlertContext.Provider
value={{
hasuraAlert: params => fireAlert({ ...params, mode: 'alert' }),
hasuraConfirm: params => fireAlert({ ...params, mode: 'confirm' }),
hasuraPrompt: params => fireAlert({ ...params, mode: 'prompt' }),
hasuraAlert,
hasuraConfirm,
hasuraPrompt,
//use these to test instable references:
// hasuraAlert: params => fireAlert({ ...params, mode: 'alert' }),
// hasuraConfirm: params => fireAlert({ ...params, mode: 'confirm' }),
// hasuraPrompt: params => fireAlert({ ...params, mode: 'prompt' }),
}}
>
{children}
@ -186,88 +203,33 @@ export const AlertProvider: React.FC = ({ children }) => {
const useDestructiveConfirm = () => {
const { hasuraConfirm } = useHasuraAlert();
return ({
resourceName,
resourceType,
destroyTerm = 'remove',
onConfirm,
}: {
resourceName: string;
resourceType: string;
destroyTerm?: 'delete' | 'remove';
onConfirm: () => Promise<boolean>;
}) => {
if (!onConfirm) throw new Error('onCloseAsync() is required.');
return React.useCallback(
({
resourceName,
resourceType,
destroyTerm = 'remove',
onConfirm,
}: {
resourceName: string;
resourceType: string;
destroyTerm?: 'delete' | 'remove';
onConfirm: () => Promise<boolean>;
}) => {
if (!onConfirm) throw new Error('onCloseAsync() is required.');
hasuraConfirm({
title: `${capitalize(destroyTerm)} ${resourceType}`,
message: (
<div>
Are you sure you want to {destroyTerm} {resourceType}:{' '}
<strong>{resourceName}</strong>?
</div>
),
confirmText: 'Remove',
destructive: true,
onCloseAsync: async ({ confirmed }) => {
if (!confirmed) return;
hasuraConfirm({
title: `${capitalize(destroyTerm)} ${resourceType}`,
message: (
<div>
Are you sure you want to {destroyTerm} {resourceType}:{' '}
<strong>{resourceName}</strong>?
</div>
),
confirmText: 'Remove',
destructive: true,
onCloseAsync: async ({ confirmed }) => {
if (!confirmed) return;
const success = await onConfirm();
if (success) {
return {
withSuccess: success,
successText: `${capitalize(destroyTerm)}d ${resourceName}`,
};
} else {
return;
}
},
});
};
};
const useDestructivePrompt = () => {
const { hasuraPrompt } = useHasuraAlert();
return ({
resourceName,
resourceType,
destroyTerm = 'remove',
onConfirm,
}: {
resourceName: string;
resourceType: string;
destroyTerm?: 'delete' | 'remove';
onConfirm: () => Promise<boolean>;
}) => {
if (!onConfirm) throw new Error('onCloseAsync() is required.');
hasuraPrompt({
title: `${capitalize(destroyTerm)} ${resourceType}`,
message: (
<div>
Are you sure you want to {destroyTerm} {resourceType}:{' '}
<strong>{resourceName}</strong>?
</div>
),
confirmText: 'Remove',
destructive: true,
promptLabel: (
<div>
Type <strong>{resourceName}</strong> to confirm this action.
</div>
),
onCloseAsync: async result => {
if (!result.confirmed) return;
if (result.promptValue !== resourceName) {
hasuraToast({
type: 'error',
title: `Entry Not Matching`,
message: `Your entry "${result.promptValue}" does not match "${resourceName}".`,
});
return;
} else {
const success = await onConfirm();
if (success) {
@ -278,10 +240,71 @@ const useDestructivePrompt = () => {
} else {
return;
}
}
},
});
};
},
});
},
[hasuraConfirm]
);
};
const useDestructivePrompt = () => {
const { hasuraPrompt } = useHasuraAlert();
return React.useCallback(
({
resourceName,
resourceType,
destroyTerm = 'remove',
onConfirm,
}: {
resourceName: string;
resourceType: string;
destroyTerm?: 'delete' | 'remove';
onConfirm: () => Promise<boolean>;
}) => {
if (!onConfirm) throw new Error('onCloseAsync() is required.');
hasuraPrompt({
title: `${capitalize(destroyTerm)} ${resourceType}`,
message: (
<div>
Are you sure you want to {destroyTerm} {resourceType}:{' '}
<strong>{resourceName}</strong>?
</div>
),
confirmText: 'Remove',
destructive: true,
promptLabel: (
<div>
Type <strong>{resourceName}</strong> to confirm this action.
</div>
),
onCloseAsync: async result => {
if (!result.confirmed) return;
if (result.promptValue !== resourceName) {
hasuraToast({
type: 'error',
title: `Entry Not Matching`,
message: `Your entry "${result.promptValue}" does not match "${resourceName}".`,
});
return;
} else {
const success = await onConfirm();
if (success) {
return {
withSuccess: success,
successText: `${capitalize(destroyTerm)}d ${resourceName}`,
};
} else {
return;
}
}
},
});
},
[hasuraPrompt]
);
};
export const useDestructiveAlert = () => {

View File

@ -1,11 +1,12 @@
import clsx from 'clsx';
import React, { ReactElement } from 'react';
import { CgSpinner } from 'react-icons/cg';
import clsx from 'clsx';
type ButtonModes = 'default' | 'destructive' | 'primary' | 'success';
type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps extends React.ComponentProps<'button'> {
export interface ButtonProps
extends Omit<React.ComponentProps<'button'>, 'ref'> {
/**
* Flag indicating whether the button is disabled
*/
@ -66,84 +67,86 @@ export const twButtonStyles = {
const fullWidth = 'w-full';
export const Button = (props: ButtonProps) => {
const {
mode = 'default',
type = 'button',
size = 'md',
children,
icon,
iconPosition = 'start',
isLoading,
loadingText,
disabled,
full,
...otherHtmlAttributes
} = props;
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props, forwardedRef) => {
const {
mode = 'default',
type = 'button',
size = 'md',
children,
icon,
iconPosition = 'start',
isLoading,
loadingText,
disabled,
full,
...otherHtmlAttributes
} = props;
const isDisabled = disabled || isLoading;
const isDisabled = disabled || isLoading;
const styles = twButtonStyles;
const styles = twButtonStyles;
const buttonAttributes = {
type,
...otherHtmlAttributes,
disabled: isDisabled,
className: clsx(
styles.all,
styles[mode],
buttonSizing[size],
isDisabled ? 'cursor-not-allowed' : '',
full && fullWidth,
otherHtmlAttributes?.className
),
};
const buttonAttributes = {
type,
...otherHtmlAttributes,
disabled: isDisabled,
className: clsx(
styles.all,
styles[mode],
buttonSizing[size],
isDisabled ? 'cursor-not-allowed' : '',
full && fullWidth,
otherHtmlAttributes?.className
),
};
if (isLoading) {
return (
<button {...buttonAttributes} ref={forwardedRef}>
{!!loadingText && (
<span className="whitespace-nowrap mr-2">{loadingText}</span>
)}
<CgSpinner
className={`animate-spin ${size === 'sm' ? 'w-4 h-4' : 'w-5 h-5'}`}
/>
</button>
);
}
if (!icon) {
return (
<button {...buttonAttributes} ref={forwardedRef}>
<span className="whitespace-nowrap max-w-full">{children}</span>
</button>
);
}
if (isLoading) {
return (
<button {...buttonAttributes}>
{!!loadingText && (
<span className="whitespace-nowrap mr-2">{loadingText}</span>
<button {...buttonAttributes} ref={forwardedRef}>
{iconPosition === 'start' && (
<ButtonIcon
icon={icon}
size={size}
iconPosition={iconPosition}
buttonHasChildren={!!children}
/>
)}
<CgSpinner
className={`animate-spin ${size === 'sm' ? 'w-4 h-4' : 'w-5 h-5'}`}
/>
</button>
);
}
if (!icon) {
return (
<button {...buttonAttributes}>
<span className="whitespace-nowrap max-w-full">{children}</span>
{iconPosition === 'end' && (
<ButtonIcon
icon={icon}
size={size}
iconPosition={iconPosition}
buttonHasChildren={!!children}
/>
)}
</button>
);
}
return (
<button {...buttonAttributes}>
{iconPosition === 'start' && (
<ButtonIcon
icon={icon}
size={size}
iconPosition={iconPosition}
buttonHasChildren={!!children}
/>
)}
<span className="whitespace-nowrap max-w-full">{children}</span>
{iconPosition === 'end' && (
<ButtonIcon
icon={icon}
size={size}
iconPosition={iconPosition}
buttonHasChildren={!!children}
/>
)}
</button>
);
};
);
function ButtonIcon(props: {
size?: ButtonSize;