console: Add alert/confirm/prompt component

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8701
GitOrigin-RevId: 558b6d3e688e4d9d945ba33c459565f4b0f6f502
This commit is contained in:
Matthew Goodwin 2023-04-12 06:31:54 -05:00 committed by hasura-bot
parent a8b94120d1
commit aa273cdc4c
13 changed files with 1505 additions and 75 deletions

View File

@ -12,6 +12,7 @@ import '../src/lib/theme/tailwind.css';
import { store } from '../src/lib/store';
import '../src/lib/components/Common/Common.module.scss';
import { ToastsHub } from '../src/lib/new-components/Toasts';
import { AlertProvider } from '../src/lib/new-components/Alert/AlertProvider';
const channel = addons.getChannel();
initialize();
@ -74,10 +75,14 @@ export const decorators = [
Story => {
document.body.classList.add('hasura-tailwind-on');
return (
<div>
<ToastsHub />
<div className={'bg-legacybg'}>{Story()}</div>
</div>
<AlertProvider>
<div>
<ToastsHub />
<div className={'bg-legacybg'}>
<Story />
</div>
</div>
</AlertProvider>
);
},
];

View File

@ -9,6 +9,7 @@ import ErrorBoundary from '../Error/ErrorBoundary';
import globals from '../../Globals';
import styles from './App.module.scss';
import { ToastsHub } from '../../new-components/Toasts';
import { AlertProvider } from '../../new-components/Alert/AlertProvider';
import { theme } from '../UIKit/theme';
import { trackCustomEvent } from '../../features/Analytics';
@ -60,19 +61,21 @@ const App = ({
<GlobalContext.Provider value={globals}>
<ThemeProvider theme={theme}>
<ErrorBoundary metadata={metadata} dispatch={dispatch}>
<div>
{connectionFailMsg}
{ongoingRequest && (
<ProgressBar
percent={percent}
autoIncrement={true} // eslint-disable-line react/jsx-boolean-value
intervalTime={intervalTime}
spinner={false}
/>
)}
<div>{children}</div>
<ToastsHub />
</div>
<AlertProvider>
<div>
{connectionFailMsg}
{ongoingRequest && (
<ProgressBar
percent={percent}
autoIncrement={true} // eslint-disable-line react/jsx-boolean-value
intervalTime={intervalTime}
spinner={false}
/>
)}
<div>{children}</div>
<ToastsHub />
</div>
</AlertProvider>
</ErrorBoundary>
</ThemeProvider>
</GlobalContext.Provider>

View File

@ -0,0 +1,622 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
import React from 'react';
import { expect } from '@storybook/jest';
import { screen, userEvent, within } from '@storybook/testing-library';
import { useHasuraAlert } from '.';
import { Button } from '../Button';
import { useDestructiveAlert } from './AlertProvider';
export default {
title: 'components/Alert Dialog 🧬',
decorators: [
Story => (
<div className="p-4 flex gap-5 items-center max-w-screen">{Story()}</div>
),
],
} as ComponentMeta<any>;
/**
*
* Basic Alert
*
*/
export const Alert: ComponentStory<any> = () => {
const { hasuraAlert } = useHasuraAlert();
return (
<div className="w-full">
<Button
onClick={() => {
hasuraAlert({
message: 'This is an alert!',
title: 'Some Title',
});
}}
>
Open an alert!
</Button>
</div>
);
};
Alert.storyName = '🧰 Alert';
Alert.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
await expect(await screen.findByText('Some Title')).toBeInTheDocument();
};
/**
*
* Basic Confirm
*
*/
export const Confirm: ComponentStory<any> = () => {
const { hasuraConfirm } = useHasuraAlert();
return (
<div className="w-full">
<Button
onClick={() => {
hasuraConfirm({
message: 'This is a confirm!',
title: 'Some Title',
onClose: ({ confirmed }) => {},
});
}}
>
Open a confirm!
</Button>
</div>
);
};
Confirm.storyName = '🧰 Confirm';
Confirm.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
await expect(await screen.findByText('Some Title')).toBeInTheDocument();
};
/**
*
* Confirm Interaction Test
*
* - tests if user selection of confirm/cancel is set correctly
*
*/
export const ConfirmTest: ComponentStory<any> = () => {
const { hasuraConfirm } = useHasuraAlert();
const [choice, setChoice] = React.useState<'cancelled' | 'confirmed'>();
return (
<div className="w-full">
<Button
onClick={() => {
hasuraConfirm({
message: 'This is a confirm!',
title: 'Some Title',
onClose: ({ confirmed }) => {
setChoice(confirmed ? 'confirmed' : 'cancelled');
},
});
}}
>
Open a confirm!
</Button>
<div>
Your selection: <span data-testid="choice">{choice ?? ''}</span>
</div>
</div>
);
};
ConfirmTest.storyName = '🧪 Confirm';
ConfirmTest.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
await expect(await screen.findByText('Some Title')).toBeInTheDocument();
await userEvent.click(await screen.findByText('Ok'));
await expect(await screen.findByTestId('choice')).toHaveTextContent(
'confirmed'
);
await userEvent.click(canvas.getByRole('button'));
await expect(await screen.findByText('Some Title')).toBeInTheDocument();
await userEvent.click(await screen.findByText('Cancel'));
await expect(await screen.findByTestId('choice')).toHaveTextContent(
'cancelled'
);
};
/**
*
* Basic Prompt
*
*/
export const Prompt: ComponentStory<any> = () => {
const { hasuraPrompt } = useHasuraAlert();
const [choice, setChoice] = React.useState('');
return (
<div className="w-full">
<Button
onClick={() => {
hasuraPrompt({
message: 'This is a prompt',
title: 'Some Title',
onClose: result => {
if (result.confirmed) {
// discriminated union only makes result.promptValue available when user confirms
setChoice(result.promptValue);
} else {
//no prompt value here.
}
},
});
}}
>
Open a prompt!
</Button>
<div className="my-4">Your value: {choice}</div>
</div>
);
};
Prompt.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
await expect(await screen.findByText('Some Title')).toBeInTheDocument();
};
Prompt.storyName = '🧰 Prompt';
/**
*
* Prompt Interaction Test
*
* - Tests if value that user enters, and is passed to callback matches when set to component state
*
*/
export const PromptTest: ComponentStory<any> = () => {
const { hasuraPrompt } = useHasuraAlert();
const [value, setValue] = React.useState('');
return (
<div className="w-full">
<Button
onClick={() => {
hasuraPrompt({
message: 'This is a prompt',
title: 'Some Title',
promptLabel: 'Input Label',
onClose: result => {
if (result.confirmed) {
// discriminated union only makes result.promptValue available when user confirms
setValue(result.promptValue);
} else {
//no prompt value here.
}
},
});
}}
>
Open a prompt!
</Button>
<div className="my-4">
Your value: <span data-testid="prompt-value">{value}</span>
</div>
</div>
);
};
PromptTest.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
await expect(await screen.findByText('Some Title')).toBeInTheDocument();
await userEvent.type(await screen.findByLabelText('Input Label'), 'blah');
await userEvent.click(await screen.findByText('Ok'));
await expect(await screen.findByTestId('prompt-value')).toHaveTextContent(
'blah'
);
};
PromptTest.storyName = '🧪 Prompt';
/**
*
* Confirm with custom text
*
*/
export const CustomText: ComponentStory<any> = () => {
const { hasuraConfirm } = useHasuraAlert();
const [choice, setChoice] = React.useState('');
return (
<div className="w-full">
<Button
onClick={() => {
hasuraConfirm({
message: 'Which path will you choose?',
title: 'Choose Wisely!',
cancelText: 'Good',
confirmText: 'Evil',
onClose: ({ confirmed }) => {
setChoice(!confirmed ? 'Good 😇' : 'Evil 😈');
},
});
}}
>
Open a confirm!
</Button>
<div className="my-4">You chose: {choice}</div>
</div>
);
};
CustomText.storyName = '🎭 Variant - Custom Button Text';
CustomText.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
await expect(await screen.findByText('Choose Wisely!')).toBeInTheDocument();
await expect(await screen.findByText('Good')).toBeInTheDocument();
await expect(await screen.findByText('Evil')).toBeInTheDocument();
};
/**
*
* Confirm - with destructive flag
*
*/
export const Destructive: ComponentStory<any> = () => {
const { hasuraConfirm } = useHasuraAlert();
return (
<div className="w-full">
<Button
onClick={() => {
hasuraConfirm({
message: 'Do the risky thing?',
title: 'Are you sure?',
destructive: true,
onClose: ({ confirmed }) => {
//do something
},
});
}}
>
Open a destructive confirm!
</Button>
</div>
);
};
Destructive.storyName = '🎭 Variant - Destructive';
Destructive.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
await expect(await screen.findByText('Are you sure?')).toBeInTheDocument();
await expect(
screen.getByRole('button', {
name: /Ok/i,
})
).toHaveClass('text-red-600');
};
const doAsyncAction = () => {
return new Promise<void>(res => {
setTimeout(() => {
res();
}, 3000);
});
};
/**
*
* Confirm - demonstrates Async Mode
*
*/
export const AsyncMode: ComponentStory<any> = () => {
const { hasuraConfirm } = useHasuraAlert();
return (
<div className="w-full">
<Button
onClick={() => {
hasuraConfirm({
message: 'Async mode with a loading spinner',
title: 'Async Operation',
confirmText: 'Save Data',
onCloseAsync: async ({ confirmed }) => {
if (confirmed) {
await doAsyncAction();
}
},
});
}}
>
Open a confirm!
</Button>
</div>
);
};
AsyncMode.storyName = '🪄 Async Confirm';
AsyncMode.parameters = {
docs: {
description: {
story: `#### 🚦 Usage
- Use \`onCloseAsync\` instead of \`onClose\` and return a \`Promise\`. Loading spinner will show until Promise is resolved.`,
},
},
};
/**
*
* Confirm - demonstrates Async Mode w/ optional success state
*
*/
export const AsyncModeWithSuccess: ComponentStory<any> = () => {
const { hasuraConfirm } = useHasuraAlert();
return (
<div className="w-full">
<Button
onClick={() => {
hasuraConfirm({
message:
'Async action with a loading spinner followed by a success indication',
title: 'Async Operation',
confirmText: 'Save Data',
onCloseAsync: async ({ confirmed }) => {
if (confirmed) {
await doAsyncAction();
return { withSuccess: true, successText: 'Saved!' };
} else {
return { withSuccess: false };
}
},
});
}}
>
Open a confirm!
</Button>
</div>
);
};
AsyncModeWithSuccess.storyName = '🪄 Async Confirm - with success indicator';
AsyncModeWithSuccess.parameters = {
docs: {
description: {
story: `#### 🚦 Usage
- Use \`onCloseAsync\` instead of \`onClose\` and return a \`Promise\`. Loading spinner will show until Promise is resolved.
- To enable a success indication, return an object from your Promise like this: \`{ withSuccess: true, successText: 'Saved!' }\``,
},
},
};
/**
*
* Prompt - demonstrates Async Mode w/ success state
*
*/
export const AsyncPrompt: ComponentStory<any> = () => {
const { hasuraPrompt } = useHasuraAlert();
const [value, setValue] = React.useState('');
return (
<div className="w-full">
<Button
onClick={() => {
hasuraPrompt({
message:
'Async action with a loading spinner followed by a success indication',
title: 'Async Operation',
confirmText: 'Save Data',
onCloseAsync: async result => {
if (result.confirmed) {
await doAsyncAction();
setValue(result.promptValue);
return { withSuccess: true, successText: 'Saved!' };
} else {
return { withSuccess: false };
}
},
});
}}
>
Open a prompt!
</Button>
<div className="my-4">
Your value: <span data-testid="prompt-value">{value}</span>
</div>
</div>
);
};
AsyncPrompt.storyName = '🪄 Async Prompt - with success indicator';
/**
*
* Alert - demonstrates Async Mode w/ success state
*
*/
export const AsyncAlert: ComponentStory<any> = () => {
const { hasuraAlert } = useHasuraAlert();
return (
<div className="w-full">
<Button
onClick={() => {
hasuraAlert({
message:
'Async action with a loading spinner followed by a success indication',
title: 'Async Operation',
confirmText: 'Save Data',
onCloseAsync: async () => {
await doAsyncAction();
return { withSuccess: true, successText: 'Saved!' };
},
});
}}
>
Open an alert!
</Button>
</div>
);
};
AsyncAlert.storyName = '🪄 Async Alert - with success indicator';
/**
*
* Async Error Handling - demonstrates built-in error handling
*
*/
export const ErrorHandling: ComponentStory<any> = () => {
const { hasuraAlert } = useHasuraAlert();
return (
<div className="w-full">
<Button
onClick={() => {
hasuraAlert({
message:
'This alert will throw an error during the onClose callback',
title: 'Some Operation',
confirmText: 'Save Data',
onClose: () => {
throw new Error('Whoops this was not handled!');
},
});
}}
>
Open an alert!
</Button>
</div>
);
};
ErrorHandling.storyName = '🪄 Error Handling';
/**
*
* Async Error Handling - demonstrates built-in error handling
*
*/
export const AsyncErrorHandling: ComponentStory<any> = () => {
const { hasuraAlert } = useHasuraAlert();
return (
<div className="w-full">
<Button
onClick={() => {
hasuraAlert({
message: 'This alert will throw an error after a timeout',
title: 'Async Operation',
confirmText: 'Save Data',
onCloseAsync: async () => {
await doAsyncAction();
throw new Error('Whoops this was not handled!');
},
});
}}
>
Open an alert!
</Button>
</div>
);
};
AsyncErrorHandling.storyName = '🪄 Error Handling - Async Mode';
/**
*
* useDestructiveConfirm - demonstrates wrapper function
*
*/
export const DestructiveConfirm: ComponentStory<any> = () => {
const { destructiveConfirm } = useDestructiveAlert();
return (
<div className="w-full">
<Button
onClick={() => {
destructiveConfirm({
resourceName: 'My Database',
resourceType: 'Data Source',
destroyTerm: 'remove',
onConfirm: async () => {
await doAsyncAction();
//return a boolean to indicate success:
return true;
},
});
}}
>
Open a destructive confirm!
</Button>
</div>
);
};
DestructiveConfirm.storyName = '💥 Destructive Confirm';
DestructiveConfirm.parameters = {
docs: {
description: {
story: `#### 🚦 Usage
- When needing a confirm to delete a resource, this hook standardizes the UI/UX and language.`,
},
},
};
/**
*
* useDestructivePrompt - demonstrates wrapper function
*
*/
export const DestructivePrompt: ComponentStory<any> = () => {
const { destructivePrompt } = useDestructiveAlert();
return (
<div className="w-full">
<Button
onClick={() => {
destructivePrompt({
resourceName: 'My Database',
resourceType: 'Data Source',
destroyTerm: 'remove',
onConfirm: async () => {
await doAsyncAction();
//return a boolean to indicate success:
return true;
},
});
}}
>
Open an destructive prompt!
</Button>
</div>
);
};
DestructivePrompt.storyName = '💥 Destructive Prompt';
DestructivePrompt.parameters = {
docs: {
description: {
story: `#### 🚦 Usage
- When needing a prompt to delete a resource, this hook standardizes the UI/UX and language.`,
},
},
};

View File

@ -0,0 +1,148 @@
import * as AlertDialog from '@radix-ui/react-alert-dialog';
import clsx from 'clsx';
import React from 'react';
import { BsCheckCircleFill } from 'react-icons/bs';
import useUpdateEffect from '../../hooks/useUpdateEffect';
import { sanitizeGraphQLFieldNames } from '../../utils';
import { Button } from '../Button';
import { Input } from '../Form';
import { AlertComponentProps } from './component-types';
const buttonMode = (props: AlertComponentProps) => {
return props.success
? 'success'
: props.mode !== 'alert' && props.destructive
? 'destructive'
: 'primary';
};
function Buttons(props: AlertComponentProps & { promptValue: string }) {
const {
confirmText,
onClose,
onCloseAsync,
promptValue,
mode,
isLoading,
success,
} = props;
return (
<div className="flex justify-end gap-[12px]">
{(mode === 'confirm' || mode === 'prompt') && !success && (
<AlertDialog.Cancel asChild>
{/* CANCEL BUTTON: */}
<Button
autoFocus
disabled={isLoading}
onClick={() => {
if (mode === 'prompt') {
(onClose ?? onCloseAsync)?.({ confirmed: false });
} else {
(onClose ?? onCloseAsync)?.({ confirmed: false });
}
}}
>
{props?.cancelText ?? 'Cancel'}
</Button>
</AlertDialog.Cancel>
)}
<AlertDialog.Action asChild>
{/* CONFIRM BUTTON: */}
<Button
autoFocus
disabled={isLoading}
className={clsx(success && 'pointer-events-none select-none')}
onClick={() => {
// pointer-events-none should handle this, but just in case...
if (success) return;
if (mode === 'prompt') {
(onClose ?? onCloseAsync)?.({
confirmed: true,
promptValue,
});
} else {
(onClose ?? onCloseAsync)?.({ confirmed: true });
}
}}
iconPosition="end"
icon={success ? <BsCheckCircleFill /> : undefined}
mode={buttonMode(props)}
isLoading={isLoading}
>
{confirmText ?? 'Ok'}
</Button>
</AlertDialog.Action>
</div>
);
}
export const Alert = (props: AlertComponentProps) => {
const { title, message, mode, open, isLoading, success } = props;
// we only want to apply an open prop if it's supplied as true/false, but NOT undefined. which likely means the trigger element is in use.
const openProps = React.useMemo(
() => ({
...(typeof open !== 'undefined' && { open: open }),
}),
[open]
);
// makes sure the prompt values are cleared after close
useUpdateEffect(() => {
if (open === false) {
setPromptValue('');
}
}, [open]);
const [promptValue, setPromptValue] = React.useState('');
return (
<AlertDialog.Root {...openProps}>
<AlertDialog.Portal>
<AlertDialog.Overlay className="bg-gray-700/40 z-[100] data-[state=open]:animate-fadeIn fixed inset-0" />
<AlertDialog.Content className="z-[101] data-[state=open]:animate-alertContentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[500px] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none">
<AlertDialog.Title className="text-gray-900 m-0 text-[17px] font-medium">
{title}
</AlertDialog.Title>
<AlertDialog.Description
className={clsx(
'text-gray-700 mt-4 text-[15px] leading-normal',
mode === 'prompt' ? 'mb-3' : 'mb-5'
)}
>
{message}
</AlertDialog.Description>
{mode === 'prompt' && (
<div className="mb-5">
{!!props.promptLabel && (
<label
className={clsx('block pt-1 text-muted mb-1')}
htmlFor="prompt-input"
>
{props.promptLabel}
</label>
)}
<Input
disabled={isLoading || success}
placeholder={props?.promptPlaceholder ?? ''}
name="prompt-input"
//disabled={isLoading || success}
fieldProps={{ value: promptValue, autoFocus: true }}
onChange={e => {
let value = e.target.value;
if (props.sanitizeGraphQL) {
value = sanitizeGraphQLFieldNames(e.target.value);
}
setPromptValue(value);
}}
/>
</div>
)}
<Buttons {...props} promptValue={promptValue} />
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
);
};

View File

@ -0,0 +1,294 @@
import capitalize from 'lodash/capitalize';
import React, { createContext } from 'react';
import { hasuraToast } from '../Toasts';
import { Alert } from './Alert';
import { AlertComponentProps } from './component-types';
import {
AlertMode,
AlertParams,
ConfirmParams,
DismissAlertParams,
Params,
PromptParams,
} from './types';
interface AlertContextType {
hasuraAlert: (props: Params<'alert'>) => void;
hasuraConfirm: (props: Params<'confirm'>) => void;
hasuraPrompt: (props: Params<'prompt'>) => void;
}
const AlertContext = createContext<AlertContextType>({
hasuraAlert: async () => {
// init
},
hasuraConfirm: async () => {
// init
},
hasuraPrompt: async () => {
// init
},
});
export const useHasuraAlert = () => {
const ctx = React.useContext(AlertContext);
return ctx;
};
export const AlertProvider: React.FC = ({ children }) => {
const [showAlert, setShowAlert] = React.useState(false);
const [componentProps, setComponentProps] =
React.useState<AlertComponentProps | null>(null);
const [loading, setLoading] = React.useState(false);
const [success, setSuccess] = React.useState(false);
const [successText, setSuccessText] = React.useState<string | null>(null);
const closeAndCleanup = React.useCallback(() => {
setShowAlert(false);
//reset success state
setSuccess(false);
setSuccessText(null);
setLoading(false);
setComponentProps(null);
}, []);
const dismissAlert = React.useCallback(
(props?: DismissAlertParams) => {
const { withSuccess, successText, successDelay = 1500 } = props ?? {};
if (withSuccess) {
if (successText) {
setSuccessText(successText);
}
//show a success state
setSuccess(true);
setTimeout(() => {
closeAndCleanup();
}, successDelay);
} else {
closeAndCleanup();
}
},
[closeAndCleanup]
);
// ideally errors should be handled elsewhere
// this is a failsafe to prevent an error state from freezing an alert on screen
const handleUnhandledError = React.useCallback(
(e: unknown, mode: AlertMode) => {
setLoading(false);
closeAndCleanup();
hasuraToast({
type: 'error',
title: `Unhandled error occurred while executing ${mode} dialog.`,
message: e?.toString() ?? JSON.stringify(e),
});
},
[closeAndCleanup]
);
// function that will fire an alert:
const fireAlert = React.useCallback(
(params: AlertParams | ConfirmParams | PromptParams) => {
// just in case so we don't display a broken alert
if (!params) throw Error('Invalid state passed to fireAlert()');
const extendedProps = { ...params };
if (
extendedProps?.onClose ||
//handle when alert has no onClose or onCloseAsync passed
(params.mode === 'alert' && !params?.onClose && !params?.onCloseAsync)
) {
//SYNC MODE:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extendedProps.onClose = async (args: any) => {
closeAndCleanup();
// call original callback
try {
params.onClose?.(args);
} catch (e) {
handleUnhandledError(e, params.mode);
}
};
} else {
//ASYNC MODE:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extendedProps.onCloseAsync = async (args: any) => {
setLoading(true);
try {
const result = await params.onCloseAsync?.(args);
setLoading(false);
dismissAlert({
withSuccess: result?.withSuccess,
successText: result?.successText,
});
} catch (e) {
handleUnhandledError(e, params.mode);
}
};
}
// this is to prevent an issue where if moving from other radix components that involve overlays, it will get confused and leave the overlay pointer-events:none property on the body.
// the timeout prevents overlapping css calls to modify the pointer events on the body
setTimeout(() => {
setComponentProps({ ...extendedProps });
setShowAlert(true);
}, 0);
},
[closeAndCleanup, dismissAlert]
);
return (
<AlertContext.Provider
value={{
hasuraAlert: params => fireAlert({ ...params, mode: 'alert' }),
hasuraConfirm: params => fireAlert({ ...params, mode: 'confirm' }),
hasuraPrompt: params => fireAlert({ ...params, mode: 'prompt' }),
}}
>
{children}
<Alert
{...(componentProps ?? {
message: 'error',
title: 'error',
mode: 'alert',
onClose: () => {
// init
},
})}
isLoading={loading}
open={showAlert}
success={success}
confirmText={
success && !!successText ? successText : componentProps?.confirmText
}
/>
</AlertContext.Provider>
);
};
/**
*
* These are wrapper hooks that simplify and standardize the API/UI/UX for remove/delete operations
*
*/
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.');
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) {
return {
withSuccess: success,
successText: `${capitalize(destroyTerm)}d ${resourceName}`,
};
} else {
return;
}
}
},
});
};
};
export const useDestructiveAlert = () => {
const destructiveConfirm = useDestructiveConfirm();
const destructivePrompt = useDestructivePrompt();
return {
destructiveConfirm,
destructivePrompt,
};
};

View File

@ -0,0 +1,19 @@
import {
AlertMode,
AlertParams,
CommonAlertBase,
ConfirmParams,
PromptParams,
} from './types';
export type AlertComponentProps = AlertProps | ConfirmProps | PromptProps;
type AlertProps = PropsBase<'alert'> & AlertParams;
type ConfirmProps = PropsBase<'confirm'> & ConfirmParams;
type PromptProps = PropsBase<'prompt'> & PromptParams;
export type PropsBase<Mode extends AlertMode> = CommonAlertBase<Mode> & {
open?: boolean;
isLoading?: boolean;
success?: boolean;
};

View File

@ -0,0 +1 @@
export { useHasuraAlert, useDestructiveAlert } from './AlertProvider';

View File

@ -0,0 +1,87 @@
// see: https://stackoverflow.com/questions/57103834/typescript-omit-a-property-from-all-interfaces-in-a-union-but-keep-the-union-s
// TS docs: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
type DistributiveOmit<T, K extends PropertyKey> = T extends any
? Omit<T, K>
: never;
export type AlertMode = 'alert' | 'confirm' | 'prompt';
export type onCloseAsyncPromiseReturn = DismissAlertParams | void;
export type CommonAlertBase<Mode extends AlertMode> = {
title: string;
message: React.ReactNode;
mode: Mode;
confirmText?: string;
};
export type ConfirmableBase = {
cancelText?: string;
destructive?: boolean;
};
/**
*
* For all alert modes types, onClose/onCloseAsync are added with an XOR (exclusive or) -- that is, only one or the other can be present on the type
*
*/
export type AlertParams = CommonAlertBase<'alert'> &
(
| { onClose?: () => void; onCloseAsync?: never }
| {
onClose?: never;
onCloseAsync?: () => Promise<onCloseAsyncPromiseReturn>;
}
);
export type ConfirmParams = CommonAlertBase<'confirm'> &
ConfirmableBase &
(
| { onClose: (args: onCloseProps) => void; onCloseAsync?: never }
| {
onClose?: never;
onCloseAsync: (
args: onCloseProps
) => Promise<onCloseAsyncPromiseReturn>;
}
);
export type PromptParams = CommonAlertBase<'prompt'> &
ConfirmableBase & {
promptLabel?: React.ReactNode;
promptPlaceholder?: string;
sanitizeGraphQL?: boolean;
} & (
| { onClose: (args: onClosePromptProps) => void; onCloseAsync?: never }
| {
onClose?: never;
onCloseAsync: (
args: onClosePromptProps
) => Promise<onCloseAsyncPromiseReturn>;
}
);
export type Params<Mode extends AlertMode> = DistributiveOmit<
Extract<AlertParams | ConfirmParams | PromptParams, { mode: Mode }>,
'mode'
>;
// alert/confirm
export type onCloseProps = { confirmed: boolean };
// prompt:
export type onClosePromptProps =
| {
confirmed: true;
promptValue: string;
}
| {
confirmed: false;
};
export type DismissAlertParams = {
withSuccess?: boolean;
successText?: string;
successDelay?: number;
};

View File

@ -55,6 +55,9 @@ export const VariantMode: ComponentStory<typeof Button> = () => (
<Button mode="destructive" onClick={action('onClick')}>
<span>Destructive</span>
</Button>
<Button mode="success" onClick={action('onClick')}>
<span>Success</span>
</Button>
</>
);
VariantMode.storyName = '🎭 Variant - Mode';
@ -144,6 +147,14 @@ export const StateLoading: ComponentStory<typeof Button> = () => (
>
<span>Loading</span>
</Button>
<Button
isLoading
loadingText="Loading..."
mode="success"
onClick={action('onClick')}
>
<span>Loading</span>
</Button>
</div>
<div className="flex gap-2">
<Button
@ -218,6 +229,9 @@ export const StateDisabled: ComponentStory<typeof Button> = () => (
<Button disabled mode="destructive" onClick={action('onClick')}>
<span>Disabled</span>
</Button>
<Button disabled mode="success" onClick={action('onClick')}>
<span>Disabled</span>
</Button>
</div>
<div className="flex gap-2">
<Button disabled size="sm" onClick={action('onClick')}>

View File

@ -2,7 +2,7 @@ import React, { ReactElement } from 'react';
import { CgSpinner } from 'react-icons/cg';
import clsx from 'clsx';
type ButtonModes = 'default' | 'destructive' | 'primary';
type ButtonModes = 'default' | 'destructive' | 'primary' | 'success';
type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps extends React.ComponentProps<'button'> {
@ -42,6 +42,10 @@ export interface ButtonProps extends React.ComponentProps<'button'> {
* The button will take the maximum with possible
*/
full?: boolean;
// /**
// * Will use newer flat style buttons
// */
// appearance?: 'flat' | 'default';
}
export const buttonSizing: Record<ButtonSize, string> = {
@ -50,17 +54,15 @@ export const buttonSizing: Record<ButtonSize, string> = {
sm: 'h-btnsm px-sm ',
};
export const buttonModesStyles: Record<ButtonModes, string> = {
default:
'text-gray-600 bg-gray-50 from-transparent to-white border-gray-300 hover:border-gray-400 disabled:border-gray-300 focus-visible:from-bg-gray-50 focus-visible:to-bg-gray-50',
destructive:
'text-red-600 bg-gray-50 from-transparent to-white border-gray-300 hover:border-gray-400 disabled:border-gray-300 focus-visible:from-bg-gray-50 focus-visible:to-bg-gray-50',
primary:
'text-gray-600 from-primary to-primary-light border-primary-dark hover:border-primary-darker focus-visible:from-primary focus-visible:to-primary disabled:border-primary-dark',
};
const twWhiteBg = `bg-gray-50 from-transparent to-white border-gray-300 hover:border-gray-400 disabled:border-gray-300 focus-visible:from-bg-gray-50 focus-visible:to-bg-gray-50`;
export const sharedButtonStyle =
'items-center max-w-full justify-center inline-flex items-center text-sm font-sans font-semibold bg-gradient-to-t border rounded shadow-sm focus-visible:outline-none focus-visible:bg-gradient-to-t focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-yellow-400 disabled:opacity-60';
export const twButtonStyles = {
all: `items-center max-w-full justify-center inline-flex text-sm font-sans font-semibold bg-gradient-to-t border rounded shadow-sm focus-visible:outline-none focus-visible:bg-gradient-to-t focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-yellow-400 disabled:opacity-60`,
default: `text-gray-600 ${twWhiteBg} focus-visible:ring-gray-400`,
destructive: `text-red-600 ${twWhiteBg} focus-visible:ring-red-400`,
success: `text-green-600 ${twWhiteBg} focus-visible:ring-green-400`,
primary: `text-gray-600 from-primary to-primary-light border-primary-dark hover:border-primary-darker focus-visible:from-primary focus-visible:to-primary disabled:border-primary-dark`,
};
const fullWidth = 'w-full';
@ -78,15 +80,18 @@ export const Button = (props: ButtonProps) => {
full,
...otherHtmlAttributes
} = props;
const isDisabled = disabled || isLoading;
const styles = twButtonStyles;
const buttonAttributes = {
type,
...otherHtmlAttributes,
disabled: isDisabled,
className: clsx(
sharedButtonStyle,
buttonModesStyles[mode],
styles.all,
styles[mode],
buttonSizing[size],
isDisabled ? 'cursor-not-allowed' : '',
full && fullWidth,

View File

@ -18,12 +18,13 @@
"@hookform/resolvers": "2.8.10",
"@netsells/storybook-mockdate": "^0.3.2",
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-alert-dialog": "^1.0.3",
"@radix-ui/react-checkbox": "1.0.1",
"@radix-ui/react-collapsible": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^1.0.0",
"@radix-ui/react-radio-group": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.0.2",
"@radix-ui/react-scroll-area": "^1.0.3",
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-tabs": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
@ -104,7 +105,7 @@
"react-helmet": "5.2.1",
"react-hook-form": "7.15.4",
"react-hot-toast": "2.4.0",
"react-icons": "^4.7.1",
"react-icons": "^4.8.0",
"react-json-view": "^1.21.3",
"react-loading-skeleton": "^3.1.0",
"react-lottie": "^1.2.3",
@ -155,6 +156,7 @@
"@babel/preset-typescript": "7.12.13",
"@babel/register": "7.9.0",
"@babel/runtime": "7.14.8",
"@faker-js/faker": "^7.6.0",
"@graphql-codegen/cli": "2.13.8",
"@graphql-codegen/typescript-operations": "^2.5.5",
"@hookform/devtools": "4.0.1",
@ -216,6 +218,7 @@
"@types/mini-css-extract-plugin": "0.9.1",
"@types/node": "18.11.9",
"@types/optimize-css-assets-webpack-plugin": "5.0.1",
"@types/random-words": "^1.1.2",
"@types/react": "17.0.39",
"@types/react-addons-test-utils": "0.14.25",
"@types/react-autosuggest": "^10.1.5",
@ -303,6 +306,7 @@
"path-browserify": "^1.0.1",
"postcss": "8.4.19",
"prettier": "^2.6.2",
"random-words": "^1.3.0",
"react-a11y": "0.2.8",
"react-hot-loader": "4.13.0",
"react-refresh": "^0.10.0",
@ -3554,6 +3558,16 @@
"@f/map-obj": "^1.2.2"
}
},
"node_modules/@faker-js/faker": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz",
"integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==",
"dev": true,
"engines": {
"node": ">=14.0.0",
"npm": ">=6.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz",
@ -11147,6 +11161,37 @@
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.3.tgz",
"integrity": "sha512-QXFy7+bhGi0u+paF2QbJeSCHZs4gLMJIPm6sajUamyW0fro6g1CaSGc5zmc4QmK2NlSGUrq8m+UsUqJYtzvXow==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-dialog": "1.0.3",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-slot": "1.0.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.0.tgz",
@ -11289,21 +11334,21 @@
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.2.tgz",
"integrity": "sha512-EKxxp2WNSmUPkx4trtWNmZ4/vAYEg7JkAfa1HKBUnaubw9eHzf1Orr9B472lJYaYz327RHDrd4R95fsw7VR8DA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.3.tgz",
"integrity": "sha512-owNhq36kNPqC2/a+zJRioPg6HHnTn5B/sh/NjTY8r4W9g1L5VJlrzZIVcBr7R9Mg8iLjVmh6MGgMlfoVf/WO/A==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-dismissable-layer": "1.0.2",
"@radix-ui/react-dismissable-layer": "1.0.3",
"@radix-ui/react-focus-guards": "1.0.0",
"@radix-ui/react-focus-scope": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.2",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-portal": "1.0.1",
"@radix-ui/react-portal": "1.0.2",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-slot": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0",
"aria-hidden": "^1.1.1",
@ -11314,6 +11359,49 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.3.tgz",
"integrity": "sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-use-callback-ref": "1.0.0",
"@radix-ui/react-use-escape-keydown": "1.0.2"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.2.tgz",
"integrity": "sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.2"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz",
@ -11398,13 +11486,13 @@
}
},
"node_modules/@radix-ui/react-focus-scope": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.1.tgz",
"integrity": "sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.2.tgz",
"integrity": "sha512-spwXlNTfeIprt+kaEWE/qYuYT3ZAqJiAGjN/JgdvgVDTu8yc+HuX+WOWXrKliKnLnwck0F6JDkqIERncnih+4A==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-use-callback-ref": "1.0.0"
},
"peerDependencies": {
@ -11412,6 +11500,19 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz",
@ -24406,6 +24507,12 @@
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"node_modules/@types/random-words": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@types/random-words/-/random-words-1.1.2.tgz",
"integrity": "sha512-gULpJ68bNovfBWPWNNhwJgd/GcKdfkPpXXQGgACQWffgy6LRiJB4+4s/IslhFJKQvb5wBlnlOwFJ6RawHU5z3A==",
"dev": true
},
"node_modules/@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
@ -55019,6 +55126,15 @@
"url": "https://opencollective.com/ramda"
}
},
"node_modules/random-words": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/random-words/-/random-words-1.3.0.tgz",
"integrity": "sha512-brwCGe+DN9DqZrAQVNj1Tct1Lody6GrYL/7uei5wfjeQdacFyFd2h/51LNlOoBMzIKMS9xohuL4+wlF/z1g/xg==",
"dev": true,
"dependencies": {
"seedrandom": "^3.0.5"
}
},
"node_modules/randomatic": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz",
@ -56243,9 +56359,9 @@
}
},
"node_modules/react-icons": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz",
"integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==",
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz",
"integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==",
"peerDependencies": {
"react": "*"
}
@ -58864,6 +58980,12 @@
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
"dev": true
},
"node_modules/seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
"dev": true
},
"node_modules/seek-bzip": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz",
@ -62926,9 +63048,9 @@
}
},
"node_modules/type-fest": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.6.1.tgz",
"integrity": "sha512-htXWckxlT6U4+ilVgweNliPqlsVSSucbxVexRYllyMVJDtf5rTjv6kF/s+qAd4QSL1BZcnJPEJavYBPQiWuZDA==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.8.0.tgz",
"integrity": "sha512-FVNSzGQz9Th+/9R6Lvv7WIAkstylfHN2/JYxkyhhmKFYh9At2DST8t6L6Lref9eYO8PXFTfG9Sg1Agg0K3vq3Q==",
"dev": true,
"engines": {
"node": ">=14.16"
@ -65497,9 +65619,9 @@
}
},
"node_modules/zustand": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.6.tgz",
"integrity": "sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.7.tgz",
"integrity": "sha512-dY8ERwB9Nd21ellgkBZFhudER8KVlelZm8388B5nDAXhO/+FZDhYMuRnqDgu5SYyRgz/iaf8RKnbUs/cHfOGlQ==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
@ -67939,6 +68061,12 @@
"@f/map-obj": "^1.2.2"
}
},
"@faker-js/faker": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz",
"integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==",
"dev": true
},
"@floating-ui/core": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz",
@ -73843,6 +73971,31 @@
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-alert-dialog": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.3.tgz",
"integrity": "sha512-QXFy7+bhGi0u+paF2QbJeSCHZs4gLMJIPm6sajUamyW0fro6g1CaSGc5zmc4QmK2NlSGUrq8m+UsUqJYtzvXow==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-dialog": "1.0.3",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-slot": "1.0.1"
},
"dependencies": {
"@radix-ui/react-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.1"
}
}
}
},
"@radix-ui/react-arrow": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.0.tgz",
@ -73953,25 +74106,58 @@
}
},
"@radix-ui/react-dialog": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.2.tgz",
"integrity": "sha512-EKxxp2WNSmUPkx4trtWNmZ4/vAYEg7JkAfa1HKBUnaubw9eHzf1Orr9B472lJYaYz327RHDrd4R95fsw7VR8DA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.3.tgz",
"integrity": "sha512-owNhq36kNPqC2/a+zJRioPg6HHnTn5B/sh/NjTY8r4W9g1L5VJlrzZIVcBr7R9Mg8iLjVmh6MGgMlfoVf/WO/A==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-dismissable-layer": "1.0.2",
"@radix-ui/react-dismissable-layer": "1.0.3",
"@radix-ui/react-focus-guards": "1.0.0",
"@radix-ui/react-focus-scope": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.2",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-portal": "1.0.1",
"@radix-ui/react-portal": "1.0.2",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-slot": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"dependencies": {
"@radix-ui/react-dismissable-layer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.3.tgz",
"integrity": "sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-use-callback-ref": "1.0.0",
"@radix-ui/react-use-escape-keydown": "1.0.2"
}
},
"@radix-ui/react-portal": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.2.tgz",
"integrity": "sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.2"
}
},
"@radix-ui/react-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.1"
}
}
}
},
"@radix-ui/react-direction": {
@ -74039,14 +74225,25 @@
}
},
"@radix-ui/react-focus-scope": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.1.tgz",
"integrity": "sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.2.tgz",
"integrity": "sha512-spwXlNTfeIprt+kaEWE/qYuYT3ZAqJiAGjN/JgdvgVDTu8yc+HuX+WOWXrKliKnLnwck0F6JDkqIERncnih+4A==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-use-callback-ref": "1.0.0"
},
"dependencies": {
"@radix-ui/react-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.1"
}
}
}
},
"@radix-ui/react-id": {
@ -83945,6 +84142,12 @@
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"@types/random-words": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@types/random-words/-/random-words-1.1.2.tgz",
"integrity": "sha512-gULpJ68bNovfBWPWNNhwJgd/GcKdfkPpXXQGgACQWffgy6LRiJB4+4s/IslhFJKQvb5wBlnlOwFJ6RawHU5z3A==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
@ -107703,6 +107906,15 @@
"integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==",
"dev": true
},
"random-words": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/random-words/-/random-words-1.3.0.tgz",
"integrity": "sha512-brwCGe+DN9DqZrAQVNj1Tct1Lody6GrYL/7uei5wfjeQdacFyFd2h/51LNlOoBMzIKMS9xohuL4+wlF/z1g/xg==",
"dev": true,
"requires": {
"seedrandom": "^3.0.5"
}
},
"randomatic": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz",
@ -108611,9 +108823,9 @@
}
},
"react-icons": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz",
"integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw=="
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz",
"integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg=="
},
"react-input-autosize": {
"version": "2.2.2",
@ -110625,6 +110837,12 @@
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
"dev": true
},
"seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
"dev": true
},
"seek-bzip": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz",
@ -113814,9 +114032,9 @@
"dev": true
},
"type-fest": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.6.1.tgz",
"integrity": "sha512-htXWckxlT6U4+ilVgweNliPqlsVSSucbxVexRYllyMVJDtf5rTjv6kF/s+qAd4QSL1BZcnJPEJavYBPQiWuZDA==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.8.0.tgz",
"integrity": "sha512-FVNSzGQz9Th+/9R6Lvv7WIAkstylfHN2/JYxkyhhmKFYh9At2DST8t6L6Lref9eYO8PXFTfG9Sg1Agg0K3vq3Q==",
"dev": true
},
"type-is": {
@ -115776,9 +115994,9 @@
"integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ=="
},
"zustand": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.6.tgz",
"integrity": "sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.7.tgz",
"integrity": "sha512-dY8ERwB9Nd21ellgkBZFhudER8KVlelZm8388B5nDAXhO/+FZDhYMuRnqDgu5SYyRgz/iaf8RKnbUs/cHfOGlQ==",
"requires": {
"use-sync-external-store": "1.2.0"
}

View File

@ -56,12 +56,13 @@
"@hookform/resolvers": "2.8.10",
"@netsells/storybook-mockdate": "^0.3.2",
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-alert-dialog": "^1.0.3",
"@radix-ui/react-checkbox": "1.0.1",
"@radix-ui/react-collapsible": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^1.0.0",
"@radix-ui/react-radio-group": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.0.2",
"@radix-ui/react-scroll-area": "^1.0.3",
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-tabs": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
@ -142,7 +143,7 @@
"react-helmet": "5.2.1",
"react-hook-form": "7.15.4",
"react-hot-toast": "2.4.0",
"react-icons": "^4.7.1",
"react-icons": "^4.8.0",
"react-json-view": "^1.21.3",
"react-loading-skeleton": "^3.1.0",
"react-lottie": "^1.2.3",
@ -193,6 +194,7 @@
"@babel/preset-typescript": "7.12.13",
"@babel/register": "7.9.0",
"@babel/runtime": "7.14.8",
"@faker-js/faker": "^7.6.0",
"@graphql-codegen/cli": "2.13.8",
"@graphql-codegen/typescript-operations": "^2.5.5",
"@hookform/devtools": "4.0.1",
@ -254,6 +256,7 @@
"@types/mini-css-extract-plugin": "0.9.1",
"@types/node": "18.11.9",
"@types/optimize-css-assets-webpack-plugin": "5.0.1",
"@types/random-words": "^1.1.2",
"@types/react": "17.0.39",
"@types/react-addons-test-utils": "0.14.25",
"@types/react-autosuggest": "^10.1.5",
@ -341,6 +344,7 @@
"path-browserify": "^1.0.1",
"postcss": "8.4.19",
"prettier": "^2.6.2",
"random-words": "^1.3.0",
"react-a11y": "0.2.8",
"react-hot-loader": "4.13.0",
"react-refresh": "^0.10.0",
@ -357,10 +361,10 @@
"stylus": "^0.55.0",
"tailwindcss": "3.2.4",
"tailwindcss-radix": "^2.5.0",
"type-fest": "^3.6.1",
"ts-jest": "29.0.5",
"ts-node": "10.9.1",
"tslib": "^2.3.0",
"type-fest": "^3.6.1",
"typescript": "4.9.5",
"unplugin-dynamic-asset-loader": "1.0.0",
"url-loader": "^4.1.1",

View File

@ -199,6 +199,14 @@ module.exports = {
opacity: '0',
},
},
fadeIn: {
from: { opacity: 0 },
to: { opacity: 1 },
},
alertContentShow: {
from: { opacity: 0, transform: 'translate(-50%, -48%) scale(0.96)' },
to: { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
},
},
animation: {
collapsibleContentOpen: 'collapsibleContentOpen 300ms ease-out',
@ -216,6 +224,8 @@ module.exports = {
notificationClose: 'notificationClose 300ms ease-in-out',
dropdownMenuContentOpen: 'dropdownMenuContentOpen 100ms ease-in',
dropdownMenuContentClose: 'dropdownMenuContentClose 100ms ease-out',
overlayShow: 'overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1)',
contentShow: 'contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)',
},
},
},