AsyncButtonSubscription Improvements
This commit is contained in:
parent
b5f22212d9
commit
3c14ecb6ec
@ -29,7 +29,7 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
|
||||
const [ tokens, tokensError ] = GuildSubscriptions.useTokensSubscription(guild);
|
||||
|
||||
const [ guildMeta, guildMetaError ] = GuildSubscriptions.useGuildMetadataSubscription(guild);
|
||||
const [ iconSrc ] = ReactHelper.useAsyncActionSubscription(
|
||||
const [ iconSrc ] = ReactHelper.useOneTimeAsyncAction(
|
||||
async () => await ElementsUtil.getImageSrcFromResourceFailSoftly(guild, guildMeta?.iconResourceId ?? null),
|
||||
'./img/loading.svg',
|
||||
[ guild, guildMeta?.iconResourceId ]
|
||||
|
@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import React, { createRef, FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||
import GuildsManager from '../../guilds-manager';
|
||||
import moment from 'moment';
|
||||
import TextInput from '../components/input-text';
|
||||
@ -15,7 +15,7 @@ import UI from '../../ui';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import ElementsUtil from '../require/elements-util';
|
||||
import InvitePreview from '../components/invite-preview';
|
||||
import ReactHelper, { ExpectedError } from '../require/react-helper';
|
||||
import ReactHelper from '../require/react-helper';
|
||||
import * as fs from 'fs/promises';
|
||||
import Button from '../components/button';
|
||||
|
||||
@ -60,8 +60,6 @@ export interface AddGuildOverlayProps {
|
||||
const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps) => {
|
||||
const { document, ui, guildsManager, addGuildData } = props;
|
||||
|
||||
const isMounted = ReactHelper.useIsMountedRef();
|
||||
|
||||
const expired = addGuildData.expires < new Date().getTime();
|
||||
const exampleDisplayName = useMemo(() => getExampleDisplayName(), []);
|
||||
const exampleAvatarPath = useMemo(() => getExampleAvatarPath(), []);
|
||||
@ -69,15 +67,13 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
||||
const [ displayName, setDisplayName ] = useState<string>(exampleDisplayName);
|
||||
const [ avatarBuff, setAvatarBuff ] = useState<Buffer | null>(null);
|
||||
|
||||
const [ addGuildFailedMessage, setAddGuildFailedMessage ] = useState<string | null>(null);
|
||||
|
||||
const [ displayNameInputMessage, setDisplayNameInputMessage ] = useState<string | null>(null);
|
||||
const [ displayNameInputValid, setDisplayNameInputValid ] = useState<boolean>(false);
|
||||
|
||||
const [ avatarInputMessage, setAvatarInputMessage ] = useState<string | null>(null);
|
||||
const [ avatarInputValid, setAvatarInputValid ] = useState<boolean>(false);
|
||||
|
||||
const [ exampleAvatarBuff, exampleAvatarBuffError ] = ReactHelper.useAsyncActionSubscription(
|
||||
const [ exampleAvatarBuff, exampleAvatarBuffError ] = ReactHelper.useOneTimeAsyncAction(
|
||||
async () => await fs.readFile(exampleAvatarPath),
|
||||
null,
|
||||
[ exampleAvatarPath ]
|
||||
@ -94,48 +90,41 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
||||
if ( !avatarInputValid && avatarInputMessage) return avatarInputMessage;
|
||||
if (!displayNameInputValid && displayNameInputMessage) return displayNameInputMessage;
|
||||
return null;
|
||||
}, [ exampleAvatarBuffError, avatarBuff, displayNameInputValid, displayNameInputMessage, avatarInputValid, avatarInputMessage, addGuildFailedMessage ]);
|
||||
}, [ exampleAvatarBuffError, avatarBuff, displayNameInputValid, displayNameInputMessage, avatarInputValid, avatarInputMessage ]);
|
||||
|
||||
const [ _, submitError, submitButtonText, submitButtonShaking, submitButtonCallback ] = ReactHelper.useAsyncButtonSubscription(async (): Promise<void> => {
|
||||
if (validationErrorMessage || !avatarBuff) throw new ExpectedError('invalid input');
|
||||
|
||||
if (expired) {
|
||||
setAddGuildFailedMessage('token expired');
|
||||
throw new ExpectedError('token expired');
|
||||
}
|
||||
|
||||
let newGuild: CombinedGuild;
|
||||
try {
|
||||
newGuild = await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff);
|
||||
if (!isMounted.current) return;
|
||||
setAddGuildFailedMessage(null);
|
||||
} catch (e: unknown) {
|
||||
if (!isMounted.current) return;
|
||||
LOG.error('error adding new guild', e);
|
||||
if (e instanceof Error) {
|
||||
setAddGuildFailedMessage(e.message);
|
||||
} else {
|
||||
setAddGuildFailedMessage('error adding new guild');
|
||||
const [ submitFunc, submitButtonText, submitButtonShaking, submitFailMessage ] = ReactHelper.useSubmitButton(
|
||||
async () => {
|
||||
if (validationErrorMessage || !avatarBuff) return { result: null, errorMessage: 'Validation failed' };
|
||||
if (expired) return { result: null, errorMessage: 'Token expired' };
|
||||
|
||||
let newGuild: CombinedGuild;
|
||||
try {
|
||||
newGuild = await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff);
|
||||
} catch (e: unknown) {
|
||||
LOG.error('error adding new guild', e);
|
||||
return { result: null, errorMessage: 'Error adding new guild' };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
const guildElement = await ui.addGuild(guildsManager, newGuild);
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
guildElement.click();
|
||||
}, { start: 'Submit', pending: 'Submitting...', error: 'Try Again', done: 'Done' }, [ displayName, avatarBuff, displayNameInputValid, avatarInputValid ]);
|
||||
const guildElement = await ui.addGuild(guildsManager, newGuild);
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
guildElement.click();
|
||||
|
||||
return { result: newGuild, errorMessage: null };
|
||||
},
|
||||
[ displayName, avatarBuff, displayNameInputValid, avatarInputValid ]
|
||||
);
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
if (validationErrorMessage) return validationErrorMessage;
|
||||
if (addGuildFailedMessage) return addGuildFailedMessage;
|
||||
if (submitFailMessage) return submitFailMessage;
|
||||
return null;
|
||||
}, [ validationErrorMessage, addGuildFailedMessage ])
|
||||
}, [ validationErrorMessage, submitFailMessage ]);
|
||||
|
||||
return (
|
||||
<div className="content add-guild">
|
||||
<InvitePreview
|
||||
name={addGuildData.name} iconSrc={addGuildData.iconSrc}
|
||||
url={addGuildData.url} expiresFromNow={moment.duration(addGuildData.expires - Date.now(), 'milliseconds')}
|
||||
url={addGuildData.url} expiresFromNow={moment.duration(addGuildData.expires - Date.now(), 'ms')}
|
||||
/>
|
||||
<div className="divider"></div>
|
||||
<div className="personalization">
|
||||
@ -152,12 +141,12 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
||||
maxLength={Globals.MAX_DISPLAY_NAME_LENGTH}
|
||||
value={displayName} setValue={setDisplayName}
|
||||
setValid={setDisplayNameInputValid} setMessage={setDisplayNameInputMessage}
|
||||
onEnterKeyDown={submitButtonCallback}
|
||||
onEnterKeyDown={submitFunc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SubmitOverlayLower errorMessage={errorMessage}>
|
||||
<Button shaking={submitButtonShaking} onClick={submitButtonCallback}>{submitButtonText}</Button>
|
||||
<Button shaking={submitButtonShaking} onClick={submitFunc}>{submitButtonText}</Button>
|
||||
</SubmitOverlayLower>
|
||||
</div>
|
||||
);
|
||||
|
@ -11,7 +11,7 @@ import SubmitOverlayLower from '../components/submit-overlay-lower';
|
||||
import Globals from '../../globals';
|
||||
import ElementsUtil from '../require/elements-util';
|
||||
import { Channel } from '../../data-types';
|
||||
import ReactHelper, { ExpectedError } from '../require/react-helper';
|
||||
import ReactHelper from '../require/react-helper';
|
||||
import Button from '../components/button';
|
||||
|
||||
export interface ChannelOverlayProps {
|
||||
@ -22,11 +22,7 @@ export interface ChannelOverlayProps {
|
||||
const ChannelOverlay: FC<ChannelOverlayProps> = (props: ChannelOverlayProps) => {
|
||||
const { document, guild, channel } = props;
|
||||
|
||||
const isMounted = ReactHelper.useIsMountedRef();
|
||||
|
||||
const nameInputRef = createRef<HTMLInputElement>();
|
||||
const flavorTextInputRef = createRef<HTMLInputElement>();
|
||||
const submitButtonRef = createRef<HTMLDivElement>();
|
||||
|
||||
const [ edited, setEdited ] = useState<boolean>(false);
|
||||
|
||||
@ -39,8 +35,6 @@ const ChannelOverlay: FC<ChannelOverlayProps> = (props: ChannelOverlayProps) =>
|
||||
const [ flavorTextInputValid, setFlavorTextInputValid ] = useState<boolean>(false);
|
||||
const [ flavorTextInputMessage, setFlavorTextInputMessage ] = useState<string | null>(null);
|
||||
|
||||
const [ submitFailMessage, setSubmitFailMessage ] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
nameInputRef.current?.focus();
|
||||
}, []);
|
||||
@ -66,37 +60,39 @@ const ChannelOverlay: FC<ChannelOverlayProps> = (props: ChannelOverlayProps) =>
|
||||
return null;
|
||||
}, [ nameInputValid, nameInputMessage, flavorTextInputValid, flavorTextInputMessage ]);
|
||||
|
||||
const [ _, submitError, submitButtonText, submitButtonShaking, submit ] = ReactHelper.useAsyncButtonSubscription(async (): Promise<void> => {
|
||||
if (validationErrorMessage) throw new ExpectedError('invalid input');
|
||||
setSubmitFailMessage(null);
|
||||
const [ submitFunc, submitButtonText, submitButtonShaking, submitFailMessage ] = ReactHelper.useSubmitButton(
|
||||
async () => {
|
||||
if (validationErrorMessage) return { result: null, errorMessage: 'Invalid input' };
|
||||
|
||||
if (!edited) {
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Make sure to null out flavor text if empty
|
||||
if (channel) {
|
||||
await guild.requestDoUpdateChannel(channel.id, name, flavorText === '' ? null : flavorText);
|
||||
} else {
|
||||
await guild.requestDoCreateChannel(name, flavorText === '' ? null : flavorText);
|
||||
if (!edited) {
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
return { result: null, errorMessage: null };
|
||||
}
|
||||
if (!isMounted.current) return;
|
||||
} catch (e: unknown) {
|
||||
if (!isMounted.current) return;
|
||||
setSubmitFailMessage('Error submitting');
|
||||
throw e;
|
||||
}
|
||||
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
}, { start: channel ? 'Modify' : 'Create', pending: 'Submitting...', error: 'Error', done: 'Done' }, [ edited, validationErrorMessage, name, flavorText ]);
|
||||
|
||||
try {
|
||||
// Make sure to null out flavor text if empty
|
||||
if (channel) {
|
||||
await guild.requestDoUpdateChannel(channel.id, name, flavorText === '' ? null : flavorText);
|
||||
} else {
|
||||
await guild.requestDoCreateChannel(name, flavorText === '' ? null : flavorText);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
LOG.error(`Error ${channel ? 'updating' : 'creating'} channel`, e);
|
||||
return { result: null, errorMessage: `Error ${channel ? 'updating' : 'creating'} channel`}
|
||||
}
|
||||
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
return { result: null, errorMessage: null };
|
||||
},
|
||||
[ edited, validationErrorMessage, name, flavorText ],
|
||||
{ start: channel ? 'Modify Channel' : 'Create Channel' }
|
||||
);
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
if (validationErrorMessage) return validationErrorMessage;
|
||||
if (submitError) return 'Unable to modify channel';
|
||||
if (submitFailMessage) return submitFailMessage;
|
||||
return null;
|
||||
}, [ validationErrorMessage, submitError ]);
|
||||
}, [ validationErrorMessage, submitFailMessage ]);
|
||||
|
||||
return (
|
||||
<div className="content submit-dialog modify-channel">
|
||||
@ -114,22 +110,21 @@ const ChannelOverlay: FC<ChannelOverlayProps> = (props: ChannelOverlayProps) =>
|
||||
value={name} setValue={setName}
|
||||
setValid={setNameInputValid} setMessage={setNameInputMessage}
|
||||
valueMap={value => value.toLowerCase().replace(' ', '-').replace(/[^a-z0-9-]/, '')}
|
||||
onEnterKeyDown={() => flavorTextInputRef.current?.focus()}
|
||||
onEnterKeyDown={submitFunc}
|
||||
/>
|
||||
</div>
|
||||
<div className="flavor-text">
|
||||
<TextInput
|
||||
ref={flavorTextInputRef}
|
||||
label="Flavor Text" placeholder={channel?.flavorText ?? '(optional)'}
|
||||
maxLength={Globals.MAX_CHANNEL_FLAVOR_TEXT_LENGTH}
|
||||
allowEmpty={true}
|
||||
value={flavorText} setValue={setFlavorText}
|
||||
setValid={setFlavorTextInputValid} setMessage={setFlavorTextInputMessage}
|
||||
onEnterKeyDown={submit}
|
||||
onEnterKeyDown={submitFunc}
|
||||
/>
|
||||
</div>
|
||||
<SubmitOverlayLower errorMessage={errorMessage} infoMessage={infoMessage}>
|
||||
<Button shaking={submitButtonShaking} onClick={submit}>{submitButtonText}</Button>
|
||||
<Button shaking={submitButtonShaking} onClick={submitFunc}>{submitButtonText}</Button>
|
||||
</SubmitOverlayLower>
|
||||
</div>
|
||||
);
|
||||
|
@ -23,13 +23,15 @@ export interface ImageOverlayProps {
|
||||
const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
|
||||
const { guild, resourceId, resourceName } = props;
|
||||
|
||||
// TODO: Handle errors
|
||||
|
||||
const [ resource, resourceError ] = GuildSubscriptions.useResourceSubscription(guild, resourceId);
|
||||
const [ resourceImgSrc, resourceImgSrcError ] = ReactHelper.useAsyncActionSubscription(
|
||||
const [ resourceImgSrc, resourceImgSrcError ] = ReactHelper.useOneTimeAsyncAction(
|
||||
async () => await ElementsUtil.getImageSrcFromBufferFailSoftly(resource?.data ?? null),
|
||||
'./img/loading.svg',
|
||||
[ guild, resource ]
|
||||
)
|
||||
const [ resourceFileTypeInfo, resourceFileTypeInfoError ] = ReactHelper.useAsyncActionSubscription(
|
||||
const [ resourceFileTypeInfo, resourceFileTypeInfoError ] = ReactHelper.useOneTimeAsyncAction(
|
||||
async () => {
|
||||
if (!resource) return null;
|
||||
const fileTypeInfo = (await FileType.fromBuffer(resource.data)) ?? null;
|
||||
|
@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import React, { createRef, FC, useEffect, useMemo, useState } from 'react';
|
||||
import React, { createRef, FC, MutableRefObject, useEffect, useMemo, useState } from 'react';
|
||||
import { ConnectionInfo } from '../../data-types';
|
||||
import Globals from '../../globals';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
@ -12,7 +12,7 @@ import TextInput from '../components/input-text';
|
||||
import SubmitOverlayLower from '../components/submit-overlay-lower';
|
||||
import ElementsUtil from '../require/elements-util';
|
||||
import GuildSubscriptions from '../require/guild-subscriptions';
|
||||
import ReactHelper, { ExpectedError } from '../require/react-helper';
|
||||
import ReactHelper from '../require/react-helper';
|
||||
import Button from '../components/button';
|
||||
|
||||
export interface PersonalizeOverlayProps {
|
||||
@ -27,8 +27,6 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
|
||||
throw new Error('bad avatar');
|
||||
}
|
||||
|
||||
const isMounted = ReactHelper.useIsMountedRef();
|
||||
|
||||
const [ avatarResource, avatarResourceError ] = GuildSubscriptions.useResourceSubscription(guild, connection.avatarResourceId)
|
||||
|
||||
const displayNameInputRef = createRef<HTMLInputElement>();
|
||||
@ -45,8 +43,6 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
|
||||
const [ avatarInputValid, setAvatarInputValid ] = useState<boolean>(false);
|
||||
const [ avatarInputMessage, setAvatarInputMessage ] = useState<string | null>(null);
|
||||
|
||||
const [ submitFailMessage, setSubmitFailMessage ] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (avatarResource) {
|
||||
if (avatarBuff === savedAvatarBuff) setAvatarBuff(avatarResource.data);
|
||||
@ -71,45 +67,46 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
|
||||
return null;
|
||||
}, [ displayNameInputValid, displayNameInputMessage, avatarInputValid, avatarInputMessage ]);
|
||||
|
||||
const [ _, submitError, submitButtonText, submitButtonShaking, submitButtonCallback ] = ReactHelper.useAsyncButtonSubscription(async (): Promise<void> => {
|
||||
if (validationErrorMessage) throw new ExpectedError('invalid input');
|
||||
setSubmitFailMessage(null);
|
||||
|
||||
if (displayName !== savedDisplayName) {
|
||||
// Save display name
|
||||
try {
|
||||
await guild.requestSetDisplayName(displayName);
|
||||
if (!isMounted.current) return;
|
||||
setSavedDisplayName(displayName);
|
||||
} catch (e: unknown) {
|
||||
if (!isMounted.current) return;
|
||||
setSubmitFailMessage('error setting guild name');
|
||||
throw e;
|
||||
const [ submitFunc, submitButtonText, submitButtonShaking, submitFailMessage ] = ReactHelper.useSubmitButton(
|
||||
async (isMounted: MutableRefObject<boolean>) => {
|
||||
if (validationErrorMessage) return { result: null, errorMessage: 'Invalid input' };
|
||||
|
||||
if (displayName !== savedDisplayName) {
|
||||
// Save display name
|
||||
try {
|
||||
await guild.requestSetDisplayName(displayName);
|
||||
if (!isMounted.current) return { result: null, errorMessage: null };
|
||||
setSavedDisplayName(displayName);
|
||||
} catch (e: unknown) {
|
||||
LOG.error('Error setting guild name', e);
|
||||
return { result: null, errorMessage: 'Error setting guild name' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (avatarBuff && avatarBuff?.toString('hex') !== savedAvatarBuff?.toString('hex')) {
|
||||
// Save avatar
|
||||
try {
|
||||
await guild.requestSetAvatar(avatarBuff);
|
||||
if (!isMounted.current) return;
|
||||
setSavedAvatarBuff(avatarBuff);
|
||||
} catch (e: unknown) {
|
||||
if (!isMounted.current) return;
|
||||
setSubmitFailMessage('error setting avatar');
|
||||
throw e;
|
||||
if (avatarBuff && avatarBuff?.toString('hex') !== savedAvatarBuff?.toString('hex')) {
|
||||
// Save avatar
|
||||
try {
|
||||
await guild.requestSetAvatar(avatarBuff);
|
||||
if (!isMounted.current) return { result: null, errorMessage: null };
|
||||
setSavedAvatarBuff(avatarBuff);
|
||||
} catch (e: unknown) {
|
||||
LOG.error('Error setting guild avatar', e);
|
||||
return { result: null, errorMessage: 'Error setting guild avatar' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
}, { start: 'Submit', pending: 'Submitting...', error: 'Try Again', done: 'Done' }, [ validationErrorMessage, displayName, savedDisplayName, avatarBuff, savedAvatarBuff ]);
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
return { result: null, errorMessage: null };
|
||||
},
|
||||
[ validationErrorMessage, displayName, savedDisplayName, avatarBuff, savedAvatarBuff ]
|
||||
);
|
||||
|
||||
//if (saveFailed) return 'Unable to save personalization';
|
||||
const errorMessage = useMemo(() => {
|
||||
if (validationErrorMessage) return validationErrorMessage;
|
||||
if (submitFailMessage) return submitFailMessage;
|
||||
return null;
|
||||
}, [ validationErrorMessage, submitError ]);
|
||||
}, [ validationErrorMessage, submitFailMessage ]);
|
||||
|
||||
return (
|
||||
<div className="content personalize">
|
||||
@ -128,12 +125,12 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
|
||||
maxLength={Globals.MAX_DISPLAY_NAME_LENGTH}
|
||||
value={displayName} setValue={setDisplayName}
|
||||
setValid={setDisplayNameInputValid} setMessage={setDisplayNameInputMessage}
|
||||
onEnterKeyDown={submitButtonCallback}
|
||||
onEnterKeyDown={submitFunc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SubmitOverlayLower errorMessage={errorMessage} infoMessage={infoMessage}>
|
||||
<Button shaking={submitButtonShaking} onClick={submitButtonCallback}>{submitButtonText}</Button>
|
||||
<Button shaking={submitButtonShaking} onClick={submitFunc}>{submitButtonText}</Button>
|
||||
</SubmitOverlayLower>
|
||||
</div>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import { DependencyList, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { DependencyList, MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import { ShouldNeverHappenError } from "../../data-types";
|
||||
import Util from '../../util';
|
||||
@ -32,7 +32,7 @@ export default class ReactHelper {
|
||||
return isMounted;
|
||||
}
|
||||
|
||||
static useAsyncActionSubscription<T, V>(
|
||||
static useOneTimeAsyncAction<T, V>(
|
||||
actionFunc: () => Promise<T>,
|
||||
initialValue: V,
|
||||
deps: DependencyList
|
||||
@ -64,17 +64,72 @@ export default class ReactHelper {
|
||||
return [ value, error ];
|
||||
}
|
||||
|
||||
static useAsyncButtonSubscription<T>(
|
||||
actionFunc: () => Promise<T>,
|
||||
stateText: { start: string, pending: string, error: string, done: string },
|
||||
deps: DependencyList
|
||||
): [ result: T | null, error: unknown | null, text: string, shaking: boolean, callback: () => void ] {
|
||||
static useSubmitButton<ResultType>(
|
||||
actionFunc: (isMounted: MutableRefObject<boolean>) => Promise<{ errorMessage: string | null, result: ResultType | null }>,
|
||||
deps: DependencyList,
|
||||
stateTextMapping?: { start?: string, pending?: string, error?: string, done?: string }
|
||||
): [ submitFunc: () => void, buttonText: string, buttonShaking: boolean, errorMessage: string | null, result: ResultType | null ] {
|
||||
const fullStateTextMapping = { ...{ start: 'Submit', pending: 'Submitting...', error: 'Try Again', done: 'Done' }, ...stateTextMapping }
|
||||
|
||||
const isMounted = useRef(false);
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
return () => { isMounted.current = false; }
|
||||
});
|
||||
|
||||
const [ result, setResult ] = useState<ResultType | null>(null);
|
||||
const [ errorMessage, setErrorMessage ] = useState<string | null>(null);
|
||||
|
||||
const [ pending, setPending ] = useState<boolean>(false);
|
||||
const [ complete, setComplete ] = useState<boolean>(false);
|
||||
|
||||
const [ buttonShaking, setButtonShaking ] = useState<boolean>(false);
|
||||
|
||||
const buttonText = useMemo(() => {
|
||||
if (errorMessage) return fullStateTextMapping.error;
|
||||
if (pending) return fullStateTextMapping.pending;
|
||||
if (complete) return fullStateTextMapping.done;
|
||||
return fullStateTextMapping.start;
|
||||
}, [ stateTextMapping, errorMessage, pending, complete ]);
|
||||
|
||||
async function shakeButton() {
|
||||
setButtonShaking(true);
|
||||
await Util.sleep(400);
|
||||
if (!isMounted.current) return;
|
||||
setButtonShaking(false);
|
||||
}
|
||||
|
||||
const submitFunc = useCallback(async () => {
|
||||
if (pending) return;
|
||||
setErrorMessage(null);
|
||||
setPending(true);
|
||||
try {
|
||||
const { result, errorMessage } = await actionFunc(isMounted);
|
||||
if (!isMounted.current) return;
|
||||
setResult(result);
|
||||
setErrorMessage(errorMessage);
|
||||
setPending(false);
|
||||
setComplete(true);
|
||||
if (errorMessage) await shakeButton();
|
||||
} catch (e: unknown) {
|
||||
LOG.error('unable to perform submit button actionFunc', e);
|
||||
setResult(null);
|
||||
setErrorMessage('Unknown error');
|
||||
setPending(false);
|
||||
setComplete(true);
|
||||
}
|
||||
}, [ ...deps, pending, actionFunc ]);
|
||||
|
||||
return [ submitFunc, buttonText, buttonShaking, errorMessage, result ];
|
||||
}
|
||||
|
||||
static useAsyncButtonSubscription<T>(
|
||||
actionFunc: () => Promise<T>,
|
||||
stateText: { start: string, pending: string, error: string, done: string },
|
||||
deps: DependencyList
|
||||
): [ result: T | null, error: unknown | null, text: string, shaking: boolean, callback: () => void ] {
|
||||
const isMounted = ReactHelper.useIsMountedRef();
|
||||
|
||||
const [ result, setResult ] = useState<T | null>(null);
|
||||
const [ error, setError ] = useState<unknown | null>(null);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user