mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
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:
parent
74f282ad16
commit
f83028d2b2
@ -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();
|
||||
};
|
||||
|
@ -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 = () => {
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user