feat(ui): memoize all components

This commit is contained in:
psychedelicious 2023-12-29 14:05:56 +11:00 committed by Kent Keirsey
parent ca4b8e65c1
commit 56527da73e
65 changed files with 1165 additions and 970 deletions

View File

@ -1,4 +1,4 @@
import { PropsWithChildren, useEffect } from 'react'; import { PropsWithChildren, memo, useEffect } from 'react';
import { modelChanged } from '../src/features/parameters/store/generationSlice'; import { modelChanged } from '../src/features/parameters/store/generationSlice';
import { useAppDispatch } from '../src/app/store/storeHooks'; import { useAppDispatch } from '../src/app/store/storeHooks';
import { useGlobalModifiersInit } from '../src/common/hooks/useGlobalModifiers'; import { useGlobalModifiersInit } from '../src/common/hooks/useGlobalModifiers';
@ -6,7 +6,7 @@ import { useGlobalModifiersInit } from '../src/common/hooks/useGlobalModifiers';
* Initializes some state for storybook. Must be in a different component * Initializes some state for storybook. Must be in a different component
* so that it is run inside the redux context. * so that it is run inside the redux context.
*/ */
export const ReduxInit = (props: PropsWithChildren) => { export const ReduxInit = memo((props: PropsWithChildren) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useGlobalModifiersInit(); useGlobalModifiersInit();
useEffect(() => { useEffect(() => {
@ -20,4 +20,6 @@ export const ReduxInit = (props: PropsWithChildren) => {
}, []); }, []);
return props.children; return props.children;
}; });
ReduxInit.displayName = 'ReduxInit';

View File

@ -22,7 +22,7 @@ const exit: AnimationProps['exit'] = {
transition: { duration: 0.1 }, transition: { duration: 0.1 },
}; };
export const IAIDropOverlay = (props: Props) => { const IAIDropOverlay = (props: Props) => {
const { isOver, label = 'Drop' } = props; const { isOver, label = 'Drop' } = props;
const motionId = useRef(uuidv4()); const motionId = useRef(uuidv4());
return ( return (

View File

@ -1,6 +1,6 @@
import type { As, FlexProps, StyleProps } from '@chakra-ui/react'; import type { As, FlexProps, StyleProps } from '@chakra-ui/react';
import { Flex, Icon, Skeleton, Spinner } from '@chakra-ui/react'; import { Flex, Icon, Skeleton, Spinner } from '@chakra-ui/react';
import { useMemo } from 'react'; import { memo, useMemo } from 'react';
import { FaImage } from 'react-icons/fa'; import { FaImage } from 'react-icons/fa';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
@ -8,7 +8,7 @@ import { InvText } from './InvText/wrapper';
type Props = { image: ImageDTO | undefined }; type Props = { image: ImageDTO | undefined };
export const IAILoadingImageFallback = (props: Props) => { export const IAILoadingImageFallback = memo((props: Props) => {
if (props.image) { if (props.image) {
return ( return (
<Skeleton <Skeleton
@ -33,7 +33,8 @@ export const IAILoadingImageFallback = (props: Props) => {
<Spinner size="xl" /> <Spinner size="xl" />
</Flex> </Flex>
); );
}; });
IAILoadingImageFallback.displayName = 'IAILoadingImageFallback';
type IAINoImageFallbackProps = FlexProps & { type IAINoImageFallbackProps = FlexProps & {
label?: string; label?: string;
@ -41,7 +42,7 @@ type IAINoImageFallbackProps = FlexProps & {
boxSize?: StyleProps['boxSize']; boxSize?: StyleProps['boxSize'];
}; };
export const IAINoContentFallback = (props: IAINoImageFallbackProps) => { export const IAINoContentFallback = memo((props: IAINoImageFallbackProps) => {
const { icon = FaImage, boxSize = 16, sx, ...rest } = props; const { icon = FaImage, boxSize = 16, sx, ...rest } = props;
const styles = useMemo( const styles = useMemo(
@ -67,37 +68,39 @@ export const IAINoContentFallback = (props: IAINoImageFallbackProps) => {
{props.label && <InvText textAlign="center">{props.label}</InvText>} {props.label && <InvText textAlign="center">{props.label}</InvText>}
</Flex> </Flex>
); );
}; });
IAINoContentFallback.displayName = 'IAINoContentFallback';
type IAINoImageFallbackWithSpinnerProps = FlexProps & { type IAINoImageFallbackWithSpinnerProps = FlexProps & {
label?: string; label?: string;
}; };
export const IAINoContentFallbackWithSpinner = ( export const IAINoContentFallbackWithSpinner = memo(
props: IAINoImageFallbackWithSpinnerProps (props: IAINoImageFallbackWithSpinnerProps) => {
) => { const { sx, ...rest } = props;
const { sx, ...rest } = props; const styles = useMemo(
const styles = useMemo( () => ({
() => ({ w: 'full',
w: 'full', h: 'full',
h: 'full', alignItems: 'center',
alignItems: 'center', justifyContent: 'center',
justifyContent: 'center', borderRadius: 'base',
borderRadius: 'base', flexDir: 'column',
flexDir: 'column', gap: 2,
gap: 2, userSelect: 'none',
userSelect: 'none', opacity: 0.7,
opacity: 0.7, color: 'base.500',
color: 'base.500', ...sx,
...sx, }),
}), [sx]
[sx] );
);
return ( return (
<Flex sx={styles} {...rest}> <Flex sx={styles} {...rest}>
<Spinner size="xl" /> <Spinner size="xl" />
{props.label && <InvText textAlign="center">{props.label}</InvText>} {props.label && <InvText textAlign="center">{props.label}</InvText>}
</Flex> </Flex>
); );
}; }
);
IAINoContentFallbackWithSpinner.displayName = 'IAINoContentFallbackWithSpinner';

View File

@ -2,36 +2,39 @@ import { Box, forwardRef, Textarea as ChakraTextarea } from '@chakra-ui/react';
import { useGlobalModifiersSetters } from 'common/hooks/useGlobalModifiers'; import { useGlobalModifiersSetters } from 'common/hooks/useGlobalModifiers';
import { stopPastePropagation } from 'common/util/stopPastePropagation'; import { stopPastePropagation } from 'common/util/stopPastePropagation';
import type { KeyboardEvent } from 'react'; import type { KeyboardEvent } from 'react';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import ResizeTextarea from 'react-textarea-autosize'; import ResizeTextarea from 'react-textarea-autosize';
import type { InvAutosizeTextareaProps } from './types'; import type { InvAutosizeTextareaProps } from './types';
export const InvAutosizeTextarea = forwardRef< export const InvAutosizeTextarea = memo(
InvAutosizeTextareaProps, forwardRef<InvAutosizeTextareaProps, typeof ResizeTextarea>(
typeof ResizeTextarea (props: InvAutosizeTextareaProps, ref) => {
>((props: InvAutosizeTextareaProps, ref) => { const { setShift } = useGlobalModifiersSetters();
const { setShift } = useGlobalModifiersSetters(); const onKeyUpDown = useCallback(
const onKeyUpDown = useCallback( (e: KeyboardEvent<HTMLTextAreaElement>) => {
(e: KeyboardEvent<HTMLTextAreaElement>) => { setShift(e.shiftKey);
setShift(e.shiftKey); },
}, [setShift]
[setShift] );
); return (
return ( <Box pos="relative">
<Box pos="relative"> <ChakraTextarea
<ChakraTextarea as={ResizeTextarea}
as={ResizeTextarea} ref={ref}
ref={ref} overflow="scroll"
overflow="scroll" w="100%"
w="100%" resize="none"
resize="none" minRows={3}
minRows={3} onPaste={stopPastePropagation}
onPaste={stopPastePropagation} onKeyUp={onKeyUpDown}
onKeyUp={onKeyUpDown} onKeyDown={onKeyUpDown}
onKeyDown={onKeyUpDown} {...props}
{...props} />
/> </Box>
</Box> );
); }
}); )
);
InvAutosizeTextarea.displayName = 'InvAutosizeTextarea';

View File

@ -1,24 +1,33 @@
import { Button, forwardRef } from '@chakra-ui/react'; import { Button, forwardRef } from '@chakra-ui/react';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip'; import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { memo } from 'react';
import type { InvButtonProps } from './types'; import type { InvButtonProps } from './types';
export const InvButton = forwardRef<InvButtonProps, typeof Button>( export const InvButton = memo(
({ isChecked, tooltip, children, ...rest }: InvButtonProps, ref) => { forwardRef<InvButtonProps, typeof Button>(
if (tooltip) { ({ isChecked, tooltip, children, ...rest }: InvButtonProps, ref) => {
if (tooltip) {
return (
<InvTooltip label={tooltip}>
<Button
ref={ref}
colorScheme={isChecked ? 'blue' : 'base'}
{...rest}
>
{children}
</Button>
</InvTooltip>
);
}
return ( return (
<InvTooltip label={tooltip}> <Button ref={ref} colorScheme={isChecked ? 'blue' : 'base'} {...rest}>
<Button ref={ref} colorScheme={isChecked ? 'blue' : 'base'} {...rest}> {children}
{children} </Button>
</Button>
</InvTooltip>
); );
} }
)
return (
<Button ref={ref} colorScheme={isChecked ? 'blue' : 'base'} {...rest}>
{children}
</Button>
);
}
); );
InvButton.displayName = 'InvButton';

View File

@ -1,10 +1,14 @@
import { ButtonGroup, forwardRef } from '@chakra-ui/react'; import { ButtonGroup, forwardRef } from '@chakra-ui/react';
import { memo } from 'react';
import type { InvButtonGroupProps } from './types'; import type { InvButtonGroupProps } from './types';
export const InvButtonGroup = forwardRef< export const InvButtonGroup = memo(
InvButtonGroupProps, forwardRef<InvButtonGroupProps, typeof ButtonGroup>(
typeof ButtonGroup ({ isAttached = true, ...rest }: InvButtonGroupProps, ref) => {
>(({ isAttached = true, ...rest }: InvButtonGroupProps, ref) => { return <ButtonGroup ref={ref} isAttached={isAttached} {...rest} />;
return <ButtonGroup ref={ref} isAttached={isAttached} {...rest} />; }
}); )
);
InvButtonGroup.displayName = 'InvButtonGroup';

View File

@ -7,7 +7,7 @@ import {
InvAlertDialogOverlay, InvAlertDialogOverlay,
} from 'common/components/InvAlertDialog/wrapper'; } from 'common/components/InvAlertDialog/wrapper';
import { InvButton } from 'common/components/InvButton/InvButton'; import { InvButton } from 'common/components/InvButton/InvButton';
import { useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { InvConfirmationAlertDialogProps } from './types'; import type { InvConfirmationAlertDialogProps } from './types';
@ -16,59 +16,61 @@ import type { InvConfirmationAlertDialogProps } from './types';
* This component is a wrapper around InvAlertDialog that provides a confirmation dialog. * This component is a wrapper around InvAlertDialog that provides a confirmation dialog.
* Its state must be managed externally using chakra's `useDisclosure()` hook. * Its state must be managed externally using chakra's `useDisclosure()` hook.
*/ */
export const InvConfirmationAlertDialog = ( export const InvConfirmationAlertDialog = memo(
props: InvConfirmationAlertDialogProps (props: InvConfirmationAlertDialogProps) => {
) => { const { t } = useTranslation();
const { t } = useTranslation();
const { const {
acceptCallback, acceptCallback,
cancelCallback, cancelCallback,
acceptButtonText = t('common.accept'), acceptButtonText = t('common.accept'),
cancelButtonText = t('common.cancel'), cancelButtonText = t('common.cancel'),
children, children,
title, title,
isOpen, isOpen,
onClose, onClose,
} = props; } = props;
const cancelRef = useRef<HTMLButtonElement | null>(null); const cancelRef = useRef<HTMLButtonElement | null>(null);
const handleAccept = useCallback(() => { const handleAccept = useCallback(() => {
acceptCallback(); acceptCallback();
onClose(); onClose();
}, [acceptCallback, onClose]); }, [acceptCallback, onClose]);
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
cancelCallback && cancelCallback(); cancelCallback && cancelCallback();
onClose(); onClose();
}, [cancelCallback, onClose]); }, [cancelCallback, onClose]);
return ( return (
<InvAlertDialog <InvAlertDialog
isOpen={isOpen} isOpen={isOpen}
leastDestructiveRef={cancelRef} leastDestructiveRef={cancelRef}
onClose={onClose} onClose={onClose}
isCentered isCentered
> >
<InvAlertDialogOverlay> <InvAlertDialogOverlay>
<InvAlertDialogContent> <InvAlertDialogContent>
<InvAlertDialogHeader fontSize="lg" fontWeight="bold"> <InvAlertDialogHeader fontSize="lg" fontWeight="bold">
{title} {title}
</InvAlertDialogHeader> </InvAlertDialogHeader>
<InvAlertDialogBody>{children}</InvAlertDialogBody> <InvAlertDialogBody>{children}</InvAlertDialogBody>
<InvAlertDialogFooter> <InvAlertDialogFooter>
<InvButton ref={cancelRef} onClick={handleCancel}> <InvButton ref={cancelRef} onClick={handleCancel}>
{cancelButtonText} {cancelButtonText}
</InvButton> </InvButton>
<InvButton colorScheme="error" onClick={handleAccept} ml={3}> <InvButton colorScheme="error" onClick={handleAccept} ml={3}>
{acceptButtonText} {acceptButtonText}
</InvButton> </InvButton>
</InvAlertDialogFooter> </InvAlertDialogFooter>
</InvAlertDialogContent> </InvAlertDialogContent>
</InvAlertDialogOverlay> </InvAlertDialogOverlay>
</InvAlertDialog> </InvAlertDialog>
); );
}; }
);
InvConfirmationAlertDialog.displayName = 'InvConfirmationAlertDialog';

View File

@ -16,7 +16,7 @@ import type { MenuButtonProps, MenuProps, PortalProps } from '@chakra-ui/react';
import { Portal, useEventListener } from '@chakra-ui/react'; import { Portal, useEventListener } from '@chakra-ui/react';
import { InvMenu, InvMenuButton } from 'common/components/InvMenu/wrapper'; import { InvMenu, InvMenuButton } from 'common/components/InvMenu/wrapper';
import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger'; import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
import { useCallback, useEffect, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useRef, useState } from 'react';
export interface InvContextMenuProps<T extends HTMLElement = HTMLDivElement> { export interface InvContextMenuProps<T extends HTMLElement = HTMLDivElement> {
renderMenu: () => JSX.Element | null; renderMenu: () => JSX.Element | null;
@ -26,89 +26,91 @@ export interface InvContextMenuProps<T extends HTMLElement = HTMLDivElement> {
menuButtonProps?: MenuButtonProps; menuButtonProps?: MenuButtonProps;
} }
export const InvContextMenu = <T extends HTMLElement = HTMLElement>( export const InvContextMenu = memo(
props: InvContextMenuProps<T> <T extends HTMLElement = HTMLElement>(props: InvContextMenuProps<T>) => {
) => { const [isOpen, setIsOpen] = useState(false);
const [isOpen, setIsOpen] = useState(false); const [isRendered, setIsRendered] = useState(false);
const [isRendered, setIsRendered] = useState(false); const [isDeferredOpen, setIsDeferredOpen] = useState(false);
const [isDeferredOpen, setIsDeferredOpen] = useState(false); const [position, setPosition] = useState<[number, number]>([0, 0]);
const [position, setPosition] = useState<[number, number]>([0, 0]); const targetRef = useRef<T>(null);
const targetRef = useRef<T>(null);
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
setTimeout(() => {
setIsRendered(true);
setTimeout(() => { setTimeout(() => {
setIsDeferredOpen(true); setIsRendered(true);
setTimeout(() => {
setIsDeferredOpen(true);
});
}); });
}); } else {
} else { setIsDeferredOpen(false);
setIsDeferredOpen(false); const timeout = setTimeout(() => {
const timeout = setTimeout(() => { setIsRendered(isOpen);
setIsRendered(isOpen); }, 1000);
}, 1000); return () => clearTimeout(timeout);
return () => clearTimeout(timeout); }
} }, [isOpen]);
}, [isOpen]);
const onClose = useCallback(() => { const onClose = useCallback(() => {
setIsOpen(false);
setIsDeferredOpen(false);
setIsRendered(false);
}, []);
// This is the change from the original chakra-ui-contextmenu
// Close all menus when the globalContextMenuCloseTrigger changes
useGlobalMenuCloseTrigger(onClose);
useEventListener('contextmenu', (e) => {
if (
targetRef.current?.contains(e.target as HTMLElement) ||
e.target === targetRef.current
) {
e.preventDefault();
setIsOpen(true);
setPosition([e.pageX, e.pageY]);
} else {
setIsOpen(false); setIsOpen(false);
} setIsDeferredOpen(false);
}); setIsRendered(false);
}, []);
const onCloseHandler = useCallback(() => { // This is the change from the original chakra-ui-contextmenu
props.menuProps?.onClose?.(); // Close all menus when the globalContextMenuCloseTrigger changes
setIsOpen(false); useGlobalMenuCloseTrigger(onClose);
}, [props.menuProps]);
return ( useEventListener('contextmenu', (e) => {
<> if (
{props.children(targetRef)} targetRef.current?.contains(e.target as HTMLElement) ||
{isRendered && ( e.target === targetRef.current
<Portal {...props.portalProps}> ) {
<InvMenu e.preventDefault();
isLazy setIsOpen(true);
isOpen={isDeferredOpen} setPosition([e.pageX, e.pageY]);
gutter={0} } else {
onClose={onCloseHandler} setIsOpen(false);
{...props.menuProps} }
> });
<InvMenuButton
aria-hidden={true} const onCloseHandler = useCallback(() => {
w={1} props.menuProps?.onClose?.();
h={1} setIsOpen(false);
position="absolute" }, [props.menuProps]);
left={position[0]}
top={position[1]} return (
cursor="default" <>
bg="transparent" {props.children(targetRef)}
size="sm" {isRendered && (
_hover={{ bg: 'transparent' }} <Portal {...props.portalProps}>
{...props.menuButtonProps} <InvMenu
/> isLazy
{props.renderMenu()} isOpen={isDeferredOpen}
</InvMenu> gutter={0}
</Portal> onClose={onCloseHandler}
)} {...props.menuProps}
</> >
); <InvMenuButton
}; aria-hidden={true}
w={1}
h={1}
position="absolute"
left={position[0]}
top={position[1]}
cursor="default"
bg="transparent"
size="sm"
_hover={{ bg: 'transparent' }}
{...props.menuButtonProps}
/>
{props.renderMenu()}
</InvMenu>
</Portal>
)}
</>
);
}
);
InvContextMenu.displayName = 'InvContextMenu';

View File

@ -5,71 +5,75 @@ import {
forwardRef, forwardRef,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { InvControlGroupContext } from 'common/components/InvControl/InvControlGroup'; import { InvControlGroupContext } from 'common/components/InvControl/InvControlGroup';
import { useContext } from 'react'; import { memo, useContext } from 'react';
import { InvLabel } from './InvLabel'; import { InvLabel } from './InvLabel';
import type { InvControlProps } from './types'; import type { InvControlProps } from './types';
export const InvControl = forwardRef<InvControlProps, typeof ChakraFormControl>( export const InvControl = memo(
(props: InvControlProps, ref) => { forwardRef<InvControlProps, typeof ChakraFormControl>(
const { (props: InvControlProps, ref) => {
children, const {
helperText, children,
feature, helperText,
orientation, feature,
renderInfoPopoverInPortal = true, orientation,
isDisabled, renderInfoPopoverInPortal = true,
labelProps, isDisabled,
label, labelProps,
...formControlProps label,
} = props; ...formControlProps
} = props;
const ctx = useContext(InvControlGroupContext); const ctx = useContext(InvControlGroupContext);
if (helperText) {
return (
<ChakraFormControl
ref={ref}
variant="withHelperText"
orientation={orientation ?? ctx.orientation}
isDisabled={isDisabled ?? ctx.isDisabled}
{...formControlProps}
>
<Flex>
{label && (
<InvLabel
feature={feature}
renderInfoPopoverInPortal={renderInfoPopoverInPortal}
{...labelProps}
>
{label}
</InvLabel>
)}
{children}
</Flex>
<ChakraFormHelperText>{helperText}</ChakraFormHelperText>
</ChakraFormControl>
);
}
if (helperText) {
return ( return (
<ChakraFormControl <ChakraFormControl
ref={ref} ref={ref}
variant="withHelperText"
orientation={orientation ?? ctx.orientation}
isDisabled={isDisabled ?? ctx.isDisabled} isDisabled={isDisabled ?? ctx.isDisabled}
orientation={orientation ?? ctx.orientation}
{...formControlProps} {...formControlProps}
> >
<Flex> {label && (
{label && ( <InvLabel
<InvLabel feature={feature}
feature={feature} renderInfoPopoverInPortal={renderInfoPopoverInPortal}
renderInfoPopoverInPortal={renderInfoPopoverInPortal} {...labelProps}
{...labelProps} >
> {label}
{label} </InvLabel>
</InvLabel> )}
)} {children}
{children}
</Flex>
<ChakraFormHelperText>{helperText}</ChakraFormHelperText>
</ChakraFormControl> </ChakraFormControl>
); );
} }
)
return (
<ChakraFormControl
ref={ref}
isDisabled={isDisabled ?? ctx.isDisabled}
orientation={orientation ?? ctx.orientation}
{...formControlProps}
>
{label && (
<InvLabel
feature={feature}
renderInfoPopoverInPortal={renderInfoPopoverInPortal}
{...labelProps}
>
{label}
</InvLabel>
)}
{children}
</ChakraFormControl>
);
}
); );
InvControl.displayName = 'InvControl';

View File

@ -1,6 +1,6 @@
import type { FormLabelProps } from '@chakra-ui/react'; import type { FormLabelProps } from '@chakra-ui/react';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { createContext } from 'react'; import { createContext, memo } from 'react';
export type InvControlGroupProps = { export type InvControlGroupProps = {
labelProps?: FormLabelProps; labelProps?: FormLabelProps;
@ -10,13 +10,14 @@ export type InvControlGroupProps = {
export const InvControlGroupContext = createContext<InvControlGroupProps>({}); export const InvControlGroupContext = createContext<InvControlGroupProps>({});
export const InvControlGroup = ({ export const InvControlGroup = memo(
children, ({ children, ...context }: PropsWithChildren<InvControlGroupProps>) => {
...context return (
}: PropsWithChildren<InvControlGroupProps>) => { <InvControlGroupContext.Provider value={context}>
return ( {children}
<InvControlGroupContext.Provider value={context}> </InvControlGroupContext.Provider>
{children} );
</InvControlGroupContext.Provider> }
); );
};
InvControlGroup.displayName = 'InvControlGroup';

View File

@ -4,7 +4,7 @@ import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover'; import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
import { InvControlGroupContext } from 'common/components/InvControl/InvControlGroup'; import { InvControlGroupContext } from 'common/components/InvControl/InvControlGroup';
import { useContext } from 'react'; import { memo, useContext } from 'react';
import type { InvLabelProps } from './types'; import type { InvLabelProps } from './types';
@ -13,31 +13,35 @@ const selector = createSelector(
({ system }) => system.shouldEnableInformationalPopovers ({ system }) => system.shouldEnableInformationalPopovers
); );
export const InvLabel = forwardRef<InvLabelProps, typeof FormLabel>( export const InvLabel = memo(
( forwardRef<InvLabelProps, typeof FormLabel>(
{ feature, renderInfoPopoverInPortal, children, ...rest }: InvLabelProps, (
ref { feature, renderInfoPopoverInPortal, children, ...rest }: InvLabelProps,
) => { ref
const shouldEnableInformationalPopovers = useAppSelector(selector); ) => {
const ctx = useContext(InvControlGroupContext); const shouldEnableInformationalPopovers = useAppSelector(selector);
if (feature && shouldEnableInformationalPopovers) { const ctx = useContext(InvControlGroupContext);
if (feature && shouldEnableInformationalPopovers) {
return (
<IAIInformationalPopover
feature={feature}
inPortal={renderInfoPopoverInPortal}
>
<Flex as="span">
<FormLabel ref={ref} {...ctx.labelProps} {...rest}>
{children}
</FormLabel>
</Flex>
</IAIInformationalPopover>
);
}
return ( return (
<IAIInformationalPopover <FormLabel ref={ref} {...ctx.labelProps} {...rest}>
feature={feature} {children}
inPortal={renderInfoPopoverInPortal} </FormLabel>
>
<Flex as="span">
<FormLabel ref={ref} {...ctx.labelProps} {...rest}>
{children}
</FormLabel>
</Flex>
</IAIInformationalPopover>
); );
} }
return ( )
<FormLabel ref={ref} {...ctx.labelProps} {...rest}>
{children}
</FormLabel>
);
}
); );
InvLabel.displayName = 'InvLabel';

View File

@ -1,27 +1,32 @@
import { forwardRef, IconButton } from '@chakra-ui/react'; import { forwardRef, IconButton } from '@chakra-ui/react';
import type { InvIconButtonProps } from 'common/components/InvIconButton/types'; import type { InvIconButtonProps } from 'common/components/InvIconButton/types';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip'; import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { memo } from 'react';
export const InvIconButton = memo(
forwardRef<InvIconButtonProps, typeof IconButton>(
({ isChecked, tooltip, ...rest }: InvIconButtonProps, ref) => {
if (tooltip) {
return (
<InvTooltip label={tooltip}>
<IconButton
ref={ref}
colorScheme={isChecked ? 'blue' : 'base'}
{...rest}
/>
</InvTooltip>
);
}
export const InvIconButton = forwardRef<InvIconButtonProps, typeof IconButton>(
({ isChecked, tooltip, ...rest }: InvIconButtonProps, ref) => {
if (tooltip) {
return ( return (
<InvTooltip label={tooltip}> <IconButton
<IconButton ref={ref}
ref={ref} colorScheme={isChecked ? 'blue' : 'base'}
colorScheme={isChecked ? 'blue' : 'base'} {...rest}
{...rest} />
/>
</InvTooltip>
); );
} }
)
return (
<IconButton
ref={ref}
colorScheme={isChecked ? 'blue' : 'base'}
{...rest}
/>
);
}
); );
InvIconButton.displayName = 'InvIconButton';

View File

@ -2,12 +2,12 @@ import { forwardRef, Input } from '@chakra-ui/react';
import { useGlobalModifiersSetters } from 'common/hooks/useGlobalModifiers'; import { useGlobalModifiersSetters } from 'common/hooks/useGlobalModifiers';
import { stopPastePropagation } from 'common/util/stopPastePropagation'; import { stopPastePropagation } from 'common/util/stopPastePropagation';
import type { KeyboardEvent } from 'react'; import type { KeyboardEvent } from 'react';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import type { InvInputProps } from './types'; import type { InvInputProps } from './types';
export const InvInput = forwardRef<InvInputProps, typeof Input>( export const InvInput = memo(
(props: InvInputProps, ref) => { forwardRef<InvInputProps, typeof Input>((props: InvInputProps, ref) => {
const { setShift } = useGlobalModifiersSetters(); const { setShift } = useGlobalModifiersSetters();
const onKeyUpDown = useCallback( const onKeyUpDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => { (e: KeyboardEvent<HTMLInputElement>) => {
@ -24,5 +24,7 @@ export const InvInput = forwardRef<InvInputProps, typeof Input>(
{...props} {...props}
/> />
); );
} })
); );
InvInput.displayName = 'InvInput';

View File

@ -4,6 +4,7 @@ import {
keyframes, keyframes,
MenuItem as ChakraMenuItem, MenuItem as ChakraMenuItem,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import {memo} from'react'
import type { InvMenuItemProps } from './types'; import type { InvMenuItemProps } from './types';
@ -16,29 +17,33 @@ const spin = keyframes`
} }
`; `;
export const InvMenuItem = forwardRef<InvMenuItemProps, typeof ChakraMenuItem>( export const InvMenuItem = memo(
(props: InvMenuItemProps, ref) => { forwardRef<InvMenuItemProps, typeof ChakraMenuItem>(
const { (props: InvMenuItemProps, ref) => {
isDestructive = false, const {
isLoading = false, isDestructive = false,
isDisabled, isLoading = false,
icon, isDisabled,
...rest icon,
} = props; ...rest
return ( } = props;
<ChakraMenuItem return (
ref={ref} <ChakraMenuItem
icon={ ref={ref}
isLoading ? ( icon={
<SpinnerIcon animation={`${spin} 1s linear infinite`} /> isLoading ? (
) : ( <SpinnerIcon animation={`${spin} 1s linear infinite`} />
icon ) : (
) icon
} )
isDisabled={isLoading || isDisabled} }
data-destructive={isDestructive} isDisabled={isLoading || isDisabled}
{...rest} data-destructive={isDestructive}
/> {...rest}
); />
} );
}
)
); );
InvMenuItem.displayName = 'InvMenuItem';

View File

@ -3,20 +3,25 @@ import {
MenuList as ChakraMenuList, MenuList as ChakraMenuList,
Portal, Portal,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { memo } from 'react';
import { menuListMotionProps } from './constants'; import { menuListMotionProps } from './constants';
import type { InvMenuListProps } from './types'; import type { InvMenuListProps } from './types';
export const InvMenuList = forwardRef<InvMenuListProps, typeof ChakraMenuList>( export const InvMenuList = memo(
(props: InvMenuListProps, ref) => { forwardRef<InvMenuListProps, typeof ChakraMenuList>(
return ( (props: InvMenuListProps, ref) => {
<Portal> return (
<ChakraMenuList <Portal>
ref={ref} <ChakraMenuList
motionProps={menuListMotionProps} ref={ref}
{...props} motionProps={menuListMotionProps}
/> {...props}
</Portal> />
); </Portal>
} );
}
)
); );
InvMenuList.displayName = 'InvMenuList';

View File

@ -5,7 +5,7 @@ import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { stopPastePropagation } from 'common/util/stopPastePropagation'; import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import type { FocusEventHandler } from 'react'; import type { FocusEventHandler } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { InvNumberInputField } from './InvNumberInputField'; import { InvNumberInputField } from './InvNumberInputField';
import { InvNumberInputStepper } from './InvNumberInputStepper'; import { InvNumberInputStepper } from './InvNumberInputStepper';
@ -13,109 +13,121 @@ import type { InvNumberInputProps } from './types';
const isValidCharacter = (char: string) => /^[0-9\-.]$/i.test(char); const isValidCharacter = (char: string) => /^[0-9\-.]$/i.test(char);
export const InvNumberInput = forwardRef< export const InvNumberInput = memo(
InvNumberInputProps, forwardRef<InvNumberInputProps, typeof ChakraNumberInput>(
typeof ChakraNumberInput (props: InvNumberInputProps, ref) => {
>((props: InvNumberInputProps, ref) => { const {
const { value,
value, min = 0,
min = 0, max,
max, step: _step = 1,
step: _step = 1, fineStep: _fineStep,
fineStep: _fineStep, onChange: _onChange,
onChange: _onChange, numberInputFieldProps,
numberInputFieldProps, ...rest
...rest } = props;
} = props;
const [valueAsString, setValueAsString] = useState<string>(String(value)); const [valueAsString, setValueAsString] = useState<string>(String(value));
const [valueAsNumber, setValueAsNumber] = useState<number>(value); const [valueAsNumber, setValueAsNumber] = useState<number>(value);
const modifiers = useStore($modifiers); const modifiers = useStore($modifiers);
const step = useMemo( const step = useMemo(
() => (modifiers.shift ? _fineStep ?? _step : _step), () => (modifiers.shift ? _fineStep ?? _step : _step),
[modifiers.shift, _fineStep, _step] [modifiers.shift, _fineStep, _step]
); );
const isInteger = useMemo( const isInteger = useMemo(
() => Number.isInteger(_step) && Number.isInteger(_fineStep ?? 1), () => Number.isInteger(_step) && Number.isInteger(_fineStep ?? 1),
[_step, _fineStep] [_step, _fineStep]
); );
const inputMode = useMemo( const inputMode = useMemo(
() => (isInteger ? 'numeric' : 'decimal'), () => (isInteger ? 'numeric' : 'decimal'),
[isInteger] [isInteger]
); );
const precision = useMemo(() => (isInteger ? 0 : 3), [isInteger]); const precision = useMemo(() => (isInteger ? 0 : 3), [isInteger]);
const onChange = useCallback( const onChange = useCallback(
(valueAsString: string, valueAsNumber: number) => { (valueAsString: string, valueAsNumber: number) => {
setValueAsString(valueAsString); setValueAsString(valueAsString);
if (isNaN(valueAsNumber)) { if (isNaN(valueAsNumber)) {
return; return;
} }
setValueAsNumber(valueAsNumber); setValueAsNumber(valueAsNumber);
_onChange(valueAsNumber); _onChange(valueAsNumber);
}, },
[_onChange] [_onChange]
); );
// This appears to be unnecessary? Cannot figure out what it did but leaving it here in case // This appears to be unnecessary? Cannot figure out what it did but leaving it here in case
// it was important. // it was important.
// const onClickStepper = useCallback( // const onClickStepper = useCallback(
// () => _onChange(Number(valueAsString)), // () => _onChange(Number(valueAsString)),
// [_onChange, valueAsString] // [_onChange, valueAsString]
// ); // );
const onBlur: FocusEventHandler<HTMLInputElement> = useCallback( const onBlur: FocusEventHandler<HTMLInputElement> = useCallback(
(e) => { (e) => {
console.log('blur!'); console.log('blur!');
if (!e.target.value) { if (!e.target.value) {
// If the input is empty, we set it to the minimum value // If the input is empty, we set it to the minimum value
onChange(String(min), min); onChange(String(min), min);
} else { } else {
// Otherwise, we round the value to the nearest multiple if integer, else 3 decimals // Otherwise, we round the value to the nearest multiple if integer, else 3 decimals
const roundedValue = isInteger const roundedValue = isInteger
? roundToMultiple(valueAsNumber, _fineStep ?? _step) ? roundToMultiple(valueAsNumber, _fineStep ?? _step)
: Number(valueAsNumber.toFixed(precision)); : Number(valueAsNumber.toFixed(precision));
// Clamp to min/max // Clamp to min/max
const clampedValue = clamp(roundedValue, min, max); const clampedValue = clamp(roundedValue, min, max);
onChange(String(clampedValue), clampedValue); onChange(String(clampedValue), clampedValue);
} }
}, },
[_fineStep, _step, isInteger, max, min, onChange, precision, valueAsNumber] [
); _fineStep,
_step,
isInteger,
max,
min,
onChange,
precision,
valueAsNumber,
]
);
/** /**
* When `value` changes (e.g. from a diff source than this component), we need * When `value` changes (e.g. from a diff source than this component), we need
* to update the internal `valueAsString`, but only if the actual value is different * to update the internal `valueAsString`, but only if the actual value is different
* from the current value. * from the current value.
*/ */
useEffect(() => { useEffect(() => {
if (value !== valueAsNumber) { if (value !== valueAsNumber) {
setValueAsString(String(value)); setValueAsString(String(value));
setValueAsNumber(value); setValueAsNumber(value);
}
}, [value, valueAsNumber]);
return (
<ChakraNumberInput
ref={ref}
min={min}
max={max}
step={step}
value={valueAsString}
onChange={onChange}
clampValueOnBlur={false}
isValidCharacter={isValidCharacter}
focusInputOnChange={false}
onPaste={stopPastePropagation}
inputMode={inputMode}
precision={precision}
variant="filled"
{...rest}
>
<InvNumberInputField onBlur={onBlur} {...numberInputFieldProps} />
<InvNumberInputStepper />
</ChakraNumberInput>
);
} }
}, [value, valueAsNumber]); )
);
return ( InvNumberInput.displayName = 'InvNumberInput';
<ChakraNumberInput
ref={ref}
min={min}
max={max}
step={step}
value={valueAsString}
onChange={onChange}
clampValueOnBlur={false}
isValidCharacter={isValidCharacter}
focusInputOnChange={false}
onPaste={stopPastePropagation}
inputMode={inputMode}
precision={precision}
variant="filled"
{...rest}
>
<InvNumberInputField onBlur={onBlur} {...numberInputFieldProps} />
<InvNumberInputStepper />
</ChakraNumberInput>
);
});

View File

@ -1,11 +1,11 @@
import { NumberInputField as ChakraNumberInputField } from '@chakra-ui/react'; import { NumberInputField as ChakraNumberInputField } from '@chakra-ui/react';
import { useGlobalModifiersSetters } from 'common/hooks/useGlobalModifiers'; import { useGlobalModifiersSetters } from 'common/hooks/useGlobalModifiers';
import type { KeyboardEventHandler } from 'react'; import type { KeyboardEventHandler } from 'react';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import type { InvNumberInputFieldProps } from './types'; import type { InvNumberInputFieldProps } from './types';
export const InvNumberInputField = (props: InvNumberInputFieldProps) => { export const InvNumberInputField = memo((props: InvNumberInputFieldProps) => {
const { onKeyUp, onKeyDown, children, ...rest } = props; const { onKeyUp, onKeyDown, children, ...rest } = props;
const { setShift } = useGlobalModifiersSetters(); const { setShift } = useGlobalModifiersSetters();
@ -29,4 +29,6 @@ export const InvNumberInputField = (props: InvNumberInputFieldProps) => {
{children} {children}
</ChakraNumberInputField> </ChakraNumberInputField>
); );
}; });
InvNumberInputField.displayName = 'InvNumberInputField';

View File

@ -5,22 +5,26 @@ import {
NumberIncrementStepper as ChakraNumberIncrementStepper, NumberIncrementStepper as ChakraNumberIncrementStepper,
NumberInputStepper as ChakraNumberInputStepper, NumberInputStepper as ChakraNumberInputStepper,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { memo } from 'react';
import { FaMinus, FaPlus } from 'react-icons/fa6'; import { FaMinus, FaPlus } from 'react-icons/fa6';
import type { InvNumberInputStepperProps } from './types'; import type { InvNumberInputStepperProps } from './types';
export const InvNumberInputStepper = forwardRef< export const InvNumberInputStepper = memo(
InvNumberInputStepperProps, forwardRef<InvNumberInputStepperProps, typeof ChakraNumberInputStepper>(
typeof ChakraNumberInputStepper (props: InvNumberInputStepperProps, ref) => {
>((props: InvNumberInputStepperProps, ref) => { return (
return ( <ChakraNumberInputStepper ref={ref} {...props}>
<ChakraNumberInputStepper ref={ref} {...props}> <ChakraNumberIncrementStepper>
<ChakraNumberIncrementStepper> <ChakraIcon as={FaPlus} boxSize={2} />
<ChakraIcon as={FaPlus} boxSize={2} /> </ChakraNumberIncrementStepper>
</ChakraNumberIncrementStepper> <ChakraNumberDecrementStepper>
<ChakraNumberDecrementStepper> <ChakraIcon as={FaMinus} boxSize={2} />
<ChakraIcon as={FaMinus} boxSize={2} /> </ChakraNumberDecrementStepper>
</ChakraNumberDecrementStepper> </ChakraNumberInputStepper>
</ChakraNumberInputStepper> );
); }
}); )
);
InvNumberInputStepper.displayName = 'InvNumberInputStepper';

View File

@ -10,100 +10,113 @@ import type { InvFormattedMark } from 'common/components/InvSlider/types';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip'; import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { $modifiers } from 'common/hooks/useGlobalModifiers'; import { $modifiers } from 'common/hooks/useGlobalModifiers';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { InvRangeSliderMark } from './InvRangeSliderMark'; import { InvRangeSliderMark } from './InvRangeSliderMark';
import type { InvRangeSliderProps } from './types'; import type { InvRangeSliderProps } from './types';
export const InvRangeSlider = (props: InvRangeSliderProps) => { export const InvRangeSlider = memo(
const { forwardRef<InvRangeSliderProps, ChakraRangeSlider>(
value, (props: InvRangeSliderProps, ref) => {
min, const {
max, value,
step: _step = 1, min,
fineStep: _fineStep, max,
onChange, step: _step = 1,
onReset, fineStep: _fineStep,
formatValue = (v: number) => v.toString(), onChange,
marks: _marks, onReset,
withThumbTooltip: withTooltip = false, formatValue = (v: number) => v.toString(),
...sliderProps marks: _marks,
} = props; withThumbTooltip: withTooltip = false,
const [isMouseOverSlider, setIsMouseOverSlider] = useState(false); ...sliderProps
const [isChanging, setIsChanging] = useState(false); } = props;
const [isMouseOverSlider, setIsMouseOverSlider] = useState(false);
const [isChanging, setIsChanging] = useState(false);
const modifiers = useStore($modifiers); const modifiers = useStore($modifiers);
const step = useMemo( const step = useMemo(
() => (modifiers.shift ? _fineStep ?? _step : _step), () => (modifiers.shift ? _fineStep ?? _step : _step),
[modifiers.shift, _fineStep, _step] [modifiers.shift, _fineStep, _step]
); );
const controlProps = useFormControl({}); const controlProps = useFormControl({});
const labels = useMemo<string[]>( const labels = useMemo<string[]>(
() => value.map(formatValue), () => value.map(formatValue),
[formatValue, value] [formatValue, value]
); );
const onMouseEnter = useCallback(() => setIsMouseOverSlider(true), []); const onMouseEnter = useCallback(() => setIsMouseOverSlider(true), []);
const onMouseLeave = useCallback(() => setIsMouseOverSlider(false), []); const onMouseLeave = useCallback(() => setIsMouseOverSlider(false), []);
const onChangeStart = useCallback(() => setIsChanging(true), []); const onChangeStart = useCallback(() => setIsChanging(true), []);
const onChangeEnd = useCallback(() => setIsChanging(false), []); const onChangeEnd = useCallback(() => setIsChanging(false), []);
const marks = useMemo<InvFormattedMark[]>(() => { const marks = useMemo<InvFormattedMark[]>(() => {
if (_marks === true) { if (_marks === true) {
return [min, max].map((m) => ({ value: m, label: formatValue(m) })); return [min, max].map((m) => ({ value: m, label: formatValue(m) }));
} }
if (_marks) { if (_marks) {
return _marks?.map((m) => ({ value: m, label: formatValue(m) })); return _marks?.map((m) => ({ value: m, label: formatValue(m) }));
} }
return []; return [];
}, [_marks, formatValue, max, min]); }, [_marks, formatValue, max, min]);
return ( return (
<ChakraRangeSlider <ChakraRangeSlider
value={value} ref={ref}
min={min} value={value}
max={max} min={min}
step={step} max={max}
onChange={onChange} step={step}
onMouseEnter={onMouseEnter} onChange={onChange}
onMouseLeave={onMouseLeave} onMouseEnter={onMouseEnter}
focusThumbOnChange={false} onMouseLeave={onMouseLeave}
onChangeStart={onChangeStart} focusThumbOnChange={false}
onChangeEnd={onChangeEnd} onChangeStart={onChangeStart}
{...sliderProps} onChangeEnd={onChangeEnd}
{...controlProps} {...sliderProps}
> {...controlProps}
<AnimatePresence> >
{marks?.length && <AnimatePresence>
(isMouseOverSlider || isChanging) && {marks?.length &&
marks.map((m, i) => ( (isMouseOverSlider || isChanging) &&
<InvRangeSliderMark marks.map((m, i) => (
key={m.value} <InvRangeSliderMark
value={m.value} key={m.value}
label={m.label} value={m.value}
index={i} label={m.label}
total={marks.length} index={i}
total={marks.length}
/>
))}
</AnimatePresence>
<ChakraRangeSliderTrack>
<ChakraRangeSliderFilledTrack />
</ChakraRangeSliderTrack>
<InvTooltip
isOpen={withTooltip && (isMouseOverSlider || isChanging)}
label={labels[0]}
>
<ChakraRangeSliderThumb
index={0}
onDoubleClick={onReset}
zIndex={0}
/> />
))} </InvTooltip>
</AnimatePresence> <InvTooltip
isOpen={withTooltip && (isMouseOverSlider || isChanging)}
<ChakraRangeSliderTrack> label={labels[1]}
<ChakraRangeSliderFilledTrack /> >
</ChakraRangeSliderTrack> <ChakraRangeSliderThumb
index={1}
<InvTooltip onDoubleClick={onReset}
isOpen={withTooltip && (isMouseOverSlider || isChanging)} zIndex={0}
label={labels[0]} />
> </InvTooltip>
<ChakraRangeSliderThumb index={0} onDoubleClick={onReset} zIndex={0} /> </ChakraRangeSlider>
</InvTooltip> );
<InvTooltip }
isOpen={withTooltip && (isMouseOverSlider || isChanging)} )
label={labels[1]} );
>
<ChakraRangeSliderThumb index={1} onDoubleClick={onReset} zIndex={0} />
</InvTooltip>
</ChakraRangeSlider>
);
};

View File

@ -2,55 +2,55 @@ import { RangeSliderMark as ChakraRangeSliderMark } from '@chakra-ui/react';
import type { InvRangeSliderMarkProps } from 'common/components/InvRangeSlider/types'; import type { InvRangeSliderMarkProps } from 'common/components/InvRangeSlider/types';
import { sliderMarkAnimationConstants } from 'common/components/InvSlider/InvSliderMark'; import { sliderMarkAnimationConstants } from 'common/components/InvSlider/InvSliderMark';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { memo } from 'react';
export const InvRangeSliderMark = memo(
({ value, label, index, total }: InvRangeSliderMarkProps) => {
if (index === 0) {
return (
<ChakraRangeSliderMark
as={motion.div}
initial={sliderMarkAnimationConstants.initialFirstLast}
animate={sliderMarkAnimationConstants.animateFirstLast}
exit={sliderMarkAnimationConstants.exitFirstLast}
key={value}
value={value}
sx={sliderMarkAnimationConstants.firstMarkStyle}
>
{label}
</ChakraRangeSliderMark>
);
}
if (index === total - 1) {
return (
<ChakraRangeSliderMark
as={motion.div}
initial={sliderMarkAnimationConstants.initialFirstLast}
animate={sliderMarkAnimationConstants.animateFirstLast}
exit={sliderMarkAnimationConstants.exitFirstLast}
key={value}
value={value}
sx={sliderMarkAnimationConstants.lastMarkStyle}
>
{label}
</ChakraRangeSliderMark>
);
}
export const InvRangeSliderMark = ({
value,
label,
index,
total,
}: InvRangeSliderMarkProps) => {
if (index === 0) {
return ( return (
<ChakraRangeSliderMark <ChakraRangeSliderMark
as={motion.div} as={motion.div}
initial={sliderMarkAnimationConstants.initialFirstLast} initial={sliderMarkAnimationConstants.initialOther}
animate={sliderMarkAnimationConstants.animateFirstLast} animate={sliderMarkAnimationConstants.animateOther}
exit={sliderMarkAnimationConstants.exitFirstLast} exit={sliderMarkAnimationConstants.exitOther}
key={value} key={value}
value={value} value={value}
sx={sliderMarkAnimationConstants.firstMarkStyle}
> >
{label} {label}
</ChakraRangeSliderMark> </ChakraRangeSliderMark>
); );
} }
);
if (index === total - 1) { InvRangeSliderMark.displayName = 'InvRangeSliderMark';
return (
<ChakraRangeSliderMark
as={motion.div}
initial={sliderMarkAnimationConstants.initialFirstLast}
animate={sliderMarkAnimationConstants.animateFirstLast}
exit={sliderMarkAnimationConstants.exitFirstLast}
key={value}
value={value}
sx={sliderMarkAnimationConstants.lastMarkStyle}
>
{label}
</ChakraRangeSliderMark>
);
}
return (
<ChakraRangeSliderMark
as={motion.div}
initial={sliderMarkAnimationConstants.initialOther}
animate={sliderMarkAnimationConstants.animateOther}
exit={sliderMarkAnimationConstants.exitOther}
key={value}
value={value}
>
{label}
</ChakraRangeSliderMark>
);
};

View File

@ -6,7 +6,7 @@ import { cloneDeep, merge } from 'lodash-es';
import type { UseOverlayScrollbarsParams } from 'overlayscrollbars-react'; import type { UseOverlayScrollbarsParams } from 'overlayscrollbars-react';
import { useOverlayScrollbars } from 'overlayscrollbars-react'; import { useOverlayScrollbars } from 'overlayscrollbars-react';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { useEffect, useRef, useState } from 'react'; import { memo, useEffect, useRef, useState } from 'react';
import type { InvSelectOption } from './types'; import type { InvSelectOption } from './types';
@ -25,59 +25,61 @@ const osParams = merge(
overlayScrollbarsParamsOverrides overlayScrollbarsParamsOverrides
); );
const Scrollable = ( const Scrollable = memo(
props: PropsWithChildren<{ viewport: HTMLDivElement | null }> (props: PropsWithChildren<{ viewport: HTMLDivElement | null }>) => {
) => { const { children, viewport } = props;
const { children, viewport } = props;
const targetRef = useRef<HTMLDivElement>(null); const targetRef = useRef<HTMLDivElement>(null);
const [initialize, getInstance] = useOverlayScrollbars(osParams); const [initialize, getInstance] = useOverlayScrollbars(osParams);
useEffect(() => { useEffect(() => {
if (targetRef.current && viewport) { if (targetRef.current && viewport) {
initialize({ initialize({
target: targetRef.current, target: targetRef.current,
elements: { elements: {
viewport, viewport,
}, },
}); });
} }
return () => getInstance()?.destroy(); return () => getInstance()?.destroy();
}, [viewport, initialize, getInstance]); }, [viewport, initialize, getInstance]);
return ( return (
<Box <Box
ref={targetRef} ref={targetRef}
data-overlayscrollbars="" data-overlayscrollbars=""
border="none" border="none"
shadow="dark-lg" shadow="dark-lg"
borderRadius="md" borderRadius="md"
p={1} p={1}
> >
{children}
</Box>
);
};
export const CustomMenuList = ({
children,
innerRef,
...other
}: CustomMenuListProps) => {
const [viewport, setViewport] = useState<HTMLDivElement | null>(null);
useEffect(() => {
if (!innerRef || !(innerRef instanceof Function)) {
return;
}
innerRef(viewport);
}, [innerRef, viewport]);
return (
<Scrollable viewport={viewport}>
<chakraComponents.MenuList {...other} innerRef={setViewport}>
{children} {children}
</chakraComponents.MenuList> </Box>
</Scrollable> );
); }
}; );
Scrollable.displayName = 'Scrollable';
export const CustomMenuList = memo(
({ children, innerRef, ...other }: CustomMenuListProps) => {
const [viewport, setViewport] = useState<HTMLDivElement | null>(null);
useEffect(() => {
if (!innerRef || !(innerRef instanceof Function)) {
return;
}
innerRef(viewport);
}, [innerRef, viewport]);
return (
<Scrollable viewport={viewport}>
<chakraComponents.MenuList {...other} innerRef={setViewport}>
{children}
</chakraComponents.MenuList>
</Scrollable>
);
}
);
CustomMenuList.displayName = 'CustomMenuList';

View File

@ -3,6 +3,7 @@ import type { GroupBase, OptionProps } from 'chakra-react-select';
import { chakraComponents } from 'chakra-react-select'; import { chakraComponents } from 'chakra-react-select';
import { InvText } from 'common/components/InvText/wrapper'; import { InvText } from 'common/components/InvText/wrapper';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip'; import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { memo } from 'react';
import type { InvSelectOption } from './types'; import type { InvSelectOption } from './types';
@ -12,58 +13,62 @@ type CustomOptionProps = OptionProps<
GroupBase<InvSelectOption> GroupBase<InvSelectOption>
>; >;
export const CustomOption = ({ children, ...props }: CustomOptionProps) => { export const CustomOption = memo(
// On large lists, perf really takes a hit :/ ({ children, ...props }: CustomOptionProps) => {
// This improves it drastically and doesn't seem to break anything... // On large lists, perf really takes a hit :/
delete props.innerProps.onMouseMove; // This improves it drastically and doesn't seem to break anything...
delete props.innerProps.onMouseOver; delete props.innerProps.onMouseMove;
delete props.innerProps.onMouseOver;
if (props.data.icon) {
return (
<chakraComponents.Option {...props}>
<InvTooltip label={props.data.tooltip}>
<Flex w="full" h="full" p={1} ps={2} pe={2}>
<Flex ps={1} pe={3} alignItems="center" justifyContent="center">
{props.data.icon}
</Flex>
<Flex flexDir="column">
<InvText>{children}</InvText>
{props.data.description && (
<InvText
data-option-desc
fontSize="sm"
colorScheme="base"
variant="subtext"
noOfLines={1}
>
{props.data.description}
</InvText>
)}
</Flex>
</Flex>
</InvTooltip>
</chakraComponents.Option>
);
}
if (props.data.icon) {
return ( return (
<chakraComponents.Option {...props}> <chakraComponents.Option {...props}>
<InvTooltip label={props.data.tooltip}> <InvTooltip label={props.data.tooltip}>
<Flex w="full" h="full" p={1} ps={2} pe={2}> <Flex w="full" h="full" flexDir="column" p={1} px={4}>
<Flex ps={1} pe={3} alignItems="center" justifyContent="center"> <InvText>{children}</InvText>
{props.data.icon} {props.data.description && (
</Flex> <InvText
<Flex flexDir="column"> data-option-desc
<InvText>{children}</InvText> fontSize="sm"
{props.data.description && ( colorScheme="base"
<InvText variant="subtext"
data-option-desc noOfLines={1}
fontSize="sm" >
colorScheme="base" {props.data.description}
variant="subtext" </InvText>
noOfLines={1} )}
>
{props.data.description}
</InvText>
)}
</Flex>
</Flex> </Flex>
</InvTooltip> </InvTooltip>
</chakraComponents.Option> </chakraComponents.Option>
); );
} }
);
return ( CustomOption.displayName = 'CustomOption';
<chakraComponents.Option {...props}>
<InvTooltip label={props.data.tooltip}>
<Flex w="full" h="full" flexDir="column" p={1} px={4}>
<InvText>{children}</InvText>
{props.data.description && (
<InvText
data-option-desc
fontSize="sm"
colorScheme="base"
variant="subtext"
noOfLines={1}
>
{props.data.description}
</InvText>
)}
</Flex>
</InvTooltip>
</chakraComponents.Option>
);
};

View File

@ -1,12 +1,15 @@
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { InvText } from 'common/components/InvText/wrapper'; import { InvText } from 'common/components/InvText/wrapper';
import { memo } from 'react';
import type { InvSelectFallbackProps } from './types'; import type { InvSelectFallbackProps } from './types';
export const InvSelectFallback = (props: InvSelectFallbackProps) => ( export const InvSelectFallback = memo((props: InvSelectFallbackProps) => (
<Flex h={8} alignItems="center" justifyContent="center"> <Flex h={8} alignItems="center" justifyContent="center">
<InvText fontSize="sm" color="base.500"> <InvText fontSize="sm" color="base.500">
{props.label} {props.label}
</InvText> </InvText>
</Flex> </Flex>
); ));
InvSelectFallback.displayName = 'InvSelectFallback';

View File

@ -4,10 +4,11 @@ import {
InvAccordionItem, InvAccordionItem,
InvAccordionPanel, InvAccordionPanel,
} from 'common/components/InvAccordion/wrapper'; } from 'common/components/InvAccordion/wrapper';
import { memo } from 'react';
import type { InvSingleAccordionProps } from './types'; import type { InvSingleAccordionProps } from './types';
export const InvSingleAccordion = (props: InvSingleAccordionProps) => { export const InvSingleAccordion = memo((props: InvSingleAccordionProps) => {
return ( return (
<InvAccordion <InvAccordion
allowToggle allowToggle
@ -21,4 +22,6 @@ export const InvSingleAccordion = (props: InvSingleAccordionProps) => {
</InvAccordionItem> </InvAccordionItem>
</InvAccordion> </InvAccordion>
); );
}; });
InvSingleAccordion.displayName = 'InvSingleAccordion';

View File

@ -10,12 +10,12 @@ import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput'
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip'; import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { $modifiers } from 'common/hooks/useGlobalModifiers'; import { $modifiers } from 'common/hooks/useGlobalModifiers';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { InvSliderMark } from './InvSliderMark'; import { InvSliderMark } from './InvSliderMark';
import type { InvFormattedMark, InvSliderProps } from './types'; import type { InvFormattedMark, InvSliderProps } from './types';
export const InvSlider = (props: InvSliderProps) => { export const InvSlider = memo((props: InvSliderProps) => {
const { const {
value, value,
min, min,
@ -112,4 +112,6 @@ export const InvSlider = (props: InvSliderProps) => {
)} )}
</> </>
); );
}; });
InvSlider.displayName = 'InvSlider';

View File

@ -3,6 +3,7 @@ import type { SystemStyleObject } from '@chakra-ui/styled-system';
import type { InvSliderMarkProps } from 'common/components/InvSlider/types'; import type { InvSliderMarkProps } from 'common/components/InvSlider/types';
import type { MotionProps } from 'framer-motion'; import type { MotionProps } from 'framer-motion';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { memo } from 'react';
const initialFirstLast: MotionProps['initial'] = { opacity: 0, y: 10 }; const initialFirstLast: MotionProps['initial'] = { opacity: 0, y: 10 };
const initialOther = { ...initialFirstLast, x: '-50%' }; const initialOther = { ...initialFirstLast, x: '-50%' };
@ -41,54 +42,53 @@ export const sliderMarkAnimationConstants = {
lastMarkStyle, lastMarkStyle,
}; };
export const InvSliderMark = ({ export const InvSliderMark = memo(
value, ({ value, label, index, total }: InvSliderMarkProps) => {
label, if (index === 0) {
index, return (
total, <ChakraSliderMark
}: InvSliderMarkProps) => { as={motion.div}
if (index === 0) { initial={sliderMarkAnimationConstants.initialFirstLast}
animate={sliderMarkAnimationConstants.animateFirstLast}
exit={sliderMarkAnimationConstants.exitFirstLast}
key={value}
value={value}
sx={sliderMarkAnimationConstants.firstMarkStyle}
>
{label}
</ChakraSliderMark>
);
}
if (index === total - 1) {
return (
<ChakraSliderMark
as={motion.div}
initial={sliderMarkAnimationConstants.initialFirstLast}
animate={sliderMarkAnimationConstants.animateFirstLast}
exit={sliderMarkAnimationConstants.exitFirstLast}
key={value}
value={value}
sx={sliderMarkAnimationConstants.lastMarkStyle}
>
{label}
</ChakraSliderMark>
);
}
return ( return (
<ChakraSliderMark <ChakraSliderMark
as={motion.div} as={motion.div}
initial={sliderMarkAnimationConstants.initialFirstLast} initial={sliderMarkAnimationConstants.initialOther}
animate={sliderMarkAnimationConstants.animateFirstLast} animate={sliderMarkAnimationConstants.animateOther}
exit={sliderMarkAnimationConstants.exitFirstLast} exit={sliderMarkAnimationConstants.exitOther}
key={value} key={value}
value={value} value={value}
sx={sliderMarkAnimationConstants.firstMarkStyle}
> >
{label} {label}
</ChakraSliderMark> </ChakraSliderMark>
); );
} }
);
if (index === total - 1) { InvSliderMark.displayName = 'InvSliderMark';
return (
<ChakraSliderMark
as={motion.div}
initial={sliderMarkAnimationConstants.initialFirstLast}
animate={sliderMarkAnimationConstants.animateFirstLast}
exit={sliderMarkAnimationConstants.exitFirstLast}
key={value}
value={value}
sx={sliderMarkAnimationConstants.lastMarkStyle}
>
{label}
</ChakraSliderMark>
);
}
return (
<ChakraSliderMark
as={motion.div}
initial={sliderMarkAnimationConstants.initialOther}
animate={sliderMarkAnimationConstants.animateOther}
exit={sliderMarkAnimationConstants.exitOther}
key={value}
value={value}
>
{label}
</ChakraSliderMark>
);
};

View File

@ -2,9 +2,10 @@ import { Spacer } from '@chakra-ui/layout';
import { forwardRef, Tab as ChakraTab } from '@chakra-ui/react'; import { forwardRef, Tab as ChakraTab } from '@chakra-ui/react';
import { InvBadge } from 'common/components/InvBadge/wrapper'; import { InvBadge } from 'common/components/InvBadge/wrapper';
import type { InvTabProps } from 'common/components/InvTabs/types'; import type { InvTabProps } from 'common/components/InvTabs/types';
import { memo } from 'react';
export const InvTab = forwardRef<InvTabProps, typeof ChakraTab>( export const InvTab = memo(
(props: InvTabProps, ref) => { forwardRef<InvTabProps, typeof ChakraTab>((props: InvTabProps, ref) => {
const { children, badges, ...rest } = props; const { children, badges, ...rest } = props;
return ( return (
<ChakraTab ref={ref} {...rest}> <ChakraTab ref={ref} {...rest}>
@ -17,5 +18,7 @@ export const InvTab = forwardRef<InvTabProps, typeof ChakraTab>(
))} ))}
</ChakraTab> </ChakraTab>
); );
} })
); );
InvTab.displayName = 'InvTab';

View File

@ -2,28 +2,32 @@ import { forwardRef, Textarea as ChakraTextarea } from '@chakra-ui/react';
import { useGlobalModifiersSetters } from 'common/hooks/useGlobalModifiers'; import { useGlobalModifiersSetters } from 'common/hooks/useGlobalModifiers';
import { stopPastePropagation } from 'common/util/stopPastePropagation'; import { stopPastePropagation } from 'common/util/stopPastePropagation';
import type { KeyboardEvent } from 'react'; import type { KeyboardEvent } from 'react';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import type { InvTextareaProps } from './types'; import type { InvTextareaProps } from './types';
export const InvTextarea = forwardRef<InvTextareaProps, typeof ChakraTextarea>( export const InvTextarea = memo(
(props: InvTextareaProps, ref) => { forwardRef<InvTextareaProps, typeof ChakraTextarea>(
const { ...rest } = props; (props: InvTextareaProps, ref) => {
const { setShift } = useGlobalModifiersSetters(); const { ...rest } = props;
const onKeyUpDown = useCallback( const { setShift } = useGlobalModifiersSetters();
(e: KeyboardEvent<HTMLTextAreaElement>) => { const onKeyUpDown = useCallback(
setShift(e.shiftKey); (e: KeyboardEvent<HTMLTextAreaElement>) => {
}, setShift(e.shiftKey);
[setShift] },
); [setShift]
return ( );
<ChakraTextarea return (
ref={ref} <ChakraTextarea
onPaste={stopPastePropagation} ref={ref}
onKeyUp={onKeyUpDown} onPaste={stopPastePropagation}
onKeyDown={onKeyUpDown} onKeyUp={onKeyUpDown}
{...rest} onKeyDown={onKeyUpDown}
/> {...rest}
); />
} );
}
)
); );
InvTextarea.displayName = 'InvTextarea';

View File

@ -1,19 +1,24 @@
import { forwardRef, Tooltip as ChakraTooltip } from '@chakra-ui/react'; import { forwardRef, Tooltip as ChakraTooltip } from '@chakra-ui/react';
import { memo } from 'react';
import type { InvTooltipProps } from './types'; import type { InvTooltipProps } from './types';
export const InvTooltip = forwardRef<InvTooltipProps, typeof ChakraTooltip>( export const InvTooltip = memo(
(props: InvTooltipProps, ref) => { forwardRef<InvTooltipProps, typeof ChakraTooltip>(
const { children, hasArrow = true, placement = 'top', ...rest } = props; (props: InvTooltipProps, ref) => {
return ( const { children, hasArrow = true, placement = 'top', ...rest } = props;
<ChakraTooltip return (
ref={ref} <ChakraTooltip
hasArrow={hasArrow} ref={ref}
placement={placement} hasArrow={hasArrow}
{...rest} placement={placement}
> {...rest}
{children} >
</ChakraTooltip> {children}
); </ChakraTooltip>
} );
}
)
); );
InvTooltip.displayName = 'InvTooltip';

View File

@ -1,6 +1,7 @@
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import type { InvIconButtonProps } from 'common/components/InvIconButton/types'; import type { InvIconButtonProps } from 'common/components/InvIconButton/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa'; import { FaTrash } from 'react-icons/fa';
@ -8,7 +9,7 @@ type DeleteImageButtonProps = Omit<InvIconButtonProps, 'aria-label'> & {
onClick: () => void; onClick: () => void;
}; };
export const DeleteImageButton = (props: DeleteImageButtonProps) => { export const DeleteImageButton = memo((props: DeleteImageButtonProps) => {
const { onClick, isDisabled } = props; const { onClick, isDisabled } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const isConnected = useAppSelector((state) => state.system.isConnected); const isConnected = useAppSelector((state) => state.system.isConnected);
@ -23,4 +24,6 @@ export const DeleteImageButton = (props: DeleteImageButtonProps) => {
colorScheme="error" colorScheme="error"
/> />
); );
}; });
DeleteImageButton.displayName = 'DeleteImageButton';

View File

@ -8,13 +8,14 @@ import {
InvModalOverlay, InvModalOverlay,
} from 'common/components/InvModal/wrapper'; } from 'common/components/InvModal/wrapper';
import { useDynamicPromptsModal } from 'features/dynamicPrompts/hooks/useDynamicPromptsModal'; import { useDynamicPromptsModal } from 'features/dynamicPrompts/hooks/useDynamicPromptsModal';
import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ParamDynamicPromptsMaxPrompts from './ParamDynamicPromptsMaxPrompts'; import ParamDynamicPromptsMaxPrompts from './ParamDynamicPromptsMaxPrompts';
import ParamDynamicPromptsPreview from './ParamDynamicPromptsPreview'; import ParamDynamicPromptsPreview from './ParamDynamicPromptsPreview';
import ParamDynamicPromptsSeedBehaviour from './ParamDynamicPromptsSeedBehaviour'; import ParamDynamicPromptsSeedBehaviour from './ParamDynamicPromptsSeedBehaviour';
export const DynamicPromptsModal = () => { export const DynamicPromptsModal = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isOpen, onClose } = useDynamicPromptsModal(); const { isOpen, onClose } = useDynamicPromptsModal();
@ -41,4 +42,6 @@ export const DynamicPromptsModal = () => {
</InvModalContent> </InvModalContent>
</InvModal> </InvModal>
); );
}; });
DynamicPromptsModal.displayName = 'DynamicPromptsModal';

View File

@ -1,10 +1,11 @@
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip'; import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { useDynamicPromptsModal } from 'features/dynamicPrompts/hooks/useDynamicPromptsModal'; import { useDynamicPromptsModal } from 'features/dynamicPrompts/hooks/useDynamicPromptsModal';
import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BsBracesAsterisk } from 'react-icons/bs'; import { BsBracesAsterisk } from 'react-icons/bs';
export const ShowDynamicPromptsPreviewButton = () => { export const ShowDynamicPromptsPreviewButton = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isOpen, onOpen } = useDynamicPromptsModal(); const { isOpen, onOpen } = useDynamicPromptsModal();
return ( return (
@ -19,4 +20,6 @@ export const ShowDynamicPromptsPreviewButton = () => {
/> />
</InvTooltip> </InvTooltip>
); );
}; });
ShowDynamicPromptsPreviewButton.displayName = 'ShowDynamicPromptsPreviewButton';

View File

@ -1,5 +1,6 @@
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip'; import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCode } from 'react-icons/fa'; import { FaCode } from 'react-icons/fa';
@ -8,7 +9,7 @@ type Props = {
onOpen: () => void; onOpen: () => void;
}; };
export const AddEmbeddingButton = (props: Props) => { export const AddEmbeddingButton = memo((props: Props) => {
const { onOpen, isOpen } = props; const { onOpen, isOpen } = props;
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -22,4 +23,6 @@ export const AddEmbeddingButton = (props: Props) => {
/> />
</InvTooltip> </InvTooltip>
); );
}; });
AddEmbeddingButton.displayName = 'AddEmbeddingButton';

View File

@ -6,9 +6,10 @@ import {
} from 'common/components/InvPopover/wrapper'; } from 'common/components/InvPopover/wrapper';
import { EmbeddingSelect } from 'features/embedding/EmbeddingSelect'; import { EmbeddingSelect } from 'features/embedding/EmbeddingSelect';
import type { EmbeddingPopoverProps } from 'features/embedding/types'; import type { EmbeddingPopoverProps } from 'features/embedding/types';
import { memo } from 'react';
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
export const EmbeddingPopover = (props: EmbeddingPopoverProps) => { export const EmbeddingPopover = memo((props: EmbeddingPopoverProps) => {
const { const {
onSelect, onSelect,
isOpen, isOpen,
@ -43,4 +44,6 @@ export const EmbeddingPopover = (props: EmbeddingPopoverProps) => {
</InvPopoverContent> </InvPopoverContent>
</InvPopover> </InvPopover>
); );
}; });
EmbeddingPopover.displayName = 'EmbeddingPopover';

View File

@ -6,74 +6,75 @@ import { InvSelectFallback } from 'common/components/InvSelect/InvSelectFallback
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect'; import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
import type { EmbeddingSelectProps } from 'features/embedding/types'; import type { EmbeddingSelectProps } from 'features/embedding/types';
import { t } from 'i18next'; import { t } from 'i18next';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { TextualInversionModelConfigEntity } from 'services/api/endpoints/models'; import type { TextualInversionModelConfigEntity } from 'services/api/endpoints/models';
import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models'; import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models';
const noOptionsMessage = () => t('embedding.noMatchingEmbedding'); const noOptionsMessage = () => t('embedding.noMatchingEmbedding');
export const EmbeddingSelect = ({ export const EmbeddingSelect = memo(
onSelect, ({ onSelect, onClose }: EmbeddingSelectProps) => {
onClose, const { t } = useTranslation();
}: EmbeddingSelectProps) => {
const { t } = useTranslation();
const currentBaseModel = useAppSelector( const currentBaseModel = useAppSelector(
(state) => state.generation.model?.base_model (state) => state.generation.model?.base_model
); );
const getIsDisabled = ( const getIsDisabled = (
embedding: TextualInversionModelConfigEntity embedding: TextualInversionModelConfigEntity
): boolean => { ): boolean => {
const isCompatible = currentBaseModel === embedding.base_model; const isCompatible = currentBaseModel === embedding.base_model;
const hasMainModel = Boolean(currentBaseModel); const hasMainModel = Boolean(currentBaseModel);
return !hasMainModel || !isCompatible; return !hasMainModel || !isCompatible;
}; };
const { data, isLoading } = useGetTextualInversionModelsQuery(); const { data, isLoading } = useGetTextualInversionModelsQuery();
const _onChange = useCallback( const _onChange = useCallback(
(embedding: TextualInversionModelConfigEntity | null) => { (embedding: TextualInversionModelConfigEntity | null) => {
if (!embedding) { if (!embedding) {
return; return;
} }
onSelect(embedding.model_name); onSelect(embedding.model_name);
}, },
[onSelect] [onSelect]
); );
const { options, onChange } = useGroupedModelInvSelect({ const { options, onChange } = useGroupedModelInvSelect({
modelEntities: data, modelEntities: data,
getIsDisabled, getIsDisabled,
onChange: _onChange, onChange: _onChange,
}); });
if (isLoading) { if (isLoading) {
return <InvSelectFallback label={t('common.loading')} />; return <InvSelectFallback label={t('common.loading')} />;
}
if (options.length === 0) {
return <InvSelectFallback label={t('embedding.noEmbeddingsLoaded')} />;
}
return (
<InvControl isDisabled={!options.length}>
<InvSelect
placeholder={t('embedding.addEmbedding')}
defaultMenuIsOpen
autoFocus
value={null}
options={options}
isDisabled={!options.length}
noOptionsMessage={noOptionsMessage}
onChange={onChange}
onMenuClose={onClose}
data-testid="add-embedding"
sx={selectStyles}
/>
</InvControl>
);
} }
);
if (options.length === 0) { EmbeddingSelect.displayName = 'EmbeddingSelect';
return <InvSelectFallback label={t('embedding.noEmbeddingsLoaded')} />;
}
return (
<InvControl isDisabled={!options.length}>
<InvSelect
placeholder={t('embedding.addEmbedding')}
defaultMenuIsOpen
autoFocus
value={null}
options={options}
isDisabled={!options.length}
noOptionsMessage={noOptionsMessage}
onChange={onChange}
onMenuClose={onClose}
data-testid="add-embedding"
sx={selectStyles}
/>
</InvControl>
);
};
const selectStyles: ChakraProps['sx'] = { const selectStyles: ChakraProps['sx'] = {
w: 'full', w: 'full',

View File

@ -1,29 +1,34 @@
import { InvButton } from 'common/components/InvButton/InvButton'; import { InvButton } from 'common/components/InvButton/InvButton';
import type { InvButtonProps } from 'common/components/InvButton/types'; import type { InvButtonProps } from 'common/components/InvButton/types';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaSync } from 'react-icons/fa'; import { FaSync } from 'react-icons/fa';
import { useSyncModels } from './useSyncModels'; import { useSyncModels } from './useSyncModels';
export const SyncModelsButton = (props: Omit<InvButtonProps, 'children'>) => { export const SyncModelsButton = memo(
const { t } = useTranslation(); (props: Omit<InvButtonProps, 'children'>) => {
const { syncModels, isLoading } = useSyncModels(); const { t } = useTranslation();
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled; const { syncModels, isLoading } = useSyncModels();
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;
if (!isSyncModelEnabled) { if (!isSyncModelEnabled) {
return null; return null;
}
return (
<InvButton
isLoading={isLoading}
onClick={syncModels}
leftIcon={<FaSync />}
minW="max-content"
{...props}
>
{t('modelManager.syncModels')}
</InvButton>
);
} }
);
return ( SyncModelsButton.displayName = 'SyncModelsButton';
<InvButton
isLoading={isLoading}
onClick={syncModels}
leftIcon={<FaSync />}
minW="max-content"
{...props}
>
{t('modelManager.syncModels')}
</InvButton>
);
};

View File

@ -1,32 +1,35 @@
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import type { InvIconButtonProps } from 'common/components/InvIconButton/types'; import type { InvIconButtonProps } from 'common/components/InvIconButton/types';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaSync } from 'react-icons/fa'; import { FaSync } from 'react-icons/fa';
import { useSyncModels } from './useSyncModels'; import { useSyncModels } from './useSyncModels';
export const SyncModelsIconButton = ( export const SyncModelsIconButton = memo(
props: Omit<InvIconButtonProps, 'aria-label'> (props: Omit<InvIconButtonProps, 'aria-label'>) => {
) => { const { t } = useTranslation();
const { t } = useTranslation(); const { syncModels, isLoading } = useSyncModels();
const { syncModels, isLoading } = useSyncModels(); const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;
if (!isSyncModelEnabled) { if (!isSyncModelEnabled) {
return null; return null;
}
return (
<InvIconButton
icon={<FaSync />}
tooltip={t('modelManager.syncModels')}
aria-label={t('modelManager.syncModels')}
isLoading={isLoading}
onClick={syncModels}
size="sm"
variant="ghost"
{...props}
/>
);
} }
);
return ( SyncModelsIconButton.displayName = 'SyncModelsIconButton';
<InvIconButton
icon={<FaSync />}
tooltip={t('modelManager.syncModels')}
aria-label={t('modelManager.syncModels')}
isLoading={isLoading}
onClick={syncModels}
size="sm"
variant="ghost"
{...props}
/>
);
};

View File

@ -24,7 +24,7 @@ import {
import { $flow } from 'features/nodes/store/reactFlowInstance'; import { $flow } from 'features/nodes/store/reactFlowInstance';
import { bumpGlobalMenuCloseTrigger } from 'features/ui/store/uiSlice'; import { bumpGlobalMenuCloseTrigger } from 'features/ui/store/uiSlice';
import type { CSSProperties, MouseEvent } from 'react'; import type { CSSProperties, MouseEvent } from 'react';
import { useCallback, useMemo, useRef } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import type { import type {
OnConnect, OnConnect,
@ -77,7 +77,7 @@ const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
const snapGrid: [number, number] = [25, 25]; const snapGrid: [number, number] = [25, 25];
export const Flow = () => { export const Flow = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const nodes = useAppSelector((state) => state.nodes.nodes); const nodes = useAppSelector((state) => state.nodes.nodes);
const edges = useAppSelector((state) => state.nodes.edges); const edges = useAppSelector((state) => state.nodes.edges);
@ -287,4 +287,6 @@ export const Flow = () => {
<Background /> <Background />
</ReactFlow> </ReactFlow>
); );
}; });
Flow.displayName = 'Flow';

View File

@ -5,7 +5,7 @@ import { InvControl } from 'common/components/InvControl/InvControl';
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput'; import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
import { InvSlider } from 'common/components/InvSlider/InvSlider'; import { InvSlider } from 'common/components/InvSlider/InvSlider';
import { heightChanged } from 'features/parameters/store/generationSlice'; import { heightChanged } from 'features/parameters/store/generationSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selector = createMemoizedSelector( const selector = createMemoizedSelector(
@ -32,7 +32,7 @@ const selector = createMemoizedSelector(
} }
); );
export const ParamHeight = () => { export const ParamHeight = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { initial, height, min, max, inputMax, step, fineStep } = const { initial, height, min, max, inputMax, step, fineStep } =
@ -71,4 +71,6 @@ export const ParamHeight = () => {
/> />
</InvControl> </InvControl>
); );
}; });
ParamHeight.displayName = 'ParamHeight';

View File

@ -6,10 +6,10 @@ import { EmbeddingPopover } from 'features/embedding/EmbeddingPopover';
import { usePrompt } from 'features/embedding/usePrompt'; import { usePrompt } from 'features/embedding/usePrompt';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { setNegativePrompt } from 'features/parameters/store/generationSlice'; import { setNegativePrompt } from 'features/parameters/store/generationSlice';
import { useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const ParamNegativePrompt = () => { export const ParamNegativePrompt = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const prompt = useAppSelector((state) => state.generation.negativePrompt); const prompt = useAppSelector((state) => state.generation.negativePrompt);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -55,4 +55,6 @@ export const ParamNegativePrompt = () => {
</Box> </Box>
</EmbeddingPopover> </EmbeddingPopover>
); );
}; });
ParamNegativePrompt.displayName = 'ParamNegativePrompt';

View File

@ -8,12 +8,12 @@ import { usePrompt } from 'features/embedding/usePrompt';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { setPositivePrompt } from 'features/parameters/store/generationSlice'; import { setPositivePrompt } from 'features/parameters/store/generationSlice';
import { SDXLConcatButton } from 'features/sdxl/components/SDXLPrompts/SDXLConcatButton'; import { SDXLConcatButton } from 'features/sdxl/components/SDXLPrompts/SDXLConcatButton';
import { useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import type { HotkeyCallback } from 'react-hotkeys-hook'; import type { HotkeyCallback } from 'react-hotkeys-hook';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const ParamPositivePrompt = () => { export const ParamPositivePrompt = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const prompt = useAppSelector((state) => state.generation.positivePrompt); const prompt = useAppSelector((state) => state.generation.positivePrompt);
const baseModel = useAppSelector((state) => state.generation.model) const baseModel = useAppSelector((state) => state.generation.model)
@ -79,4 +79,6 @@ export const ParamPositivePrompt = () => {
</Box> </Box>
</EmbeddingPopover> </EmbeddingPopover>
); );
}; });
ParamPositivePrompt.displayName = 'ParamPositivePrompt';

View File

@ -5,7 +5,7 @@ import { InvControl } from 'common/components/InvControl/InvControl';
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput'; import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
import { InvSlider } from 'common/components/InvSlider/InvSlider'; import { InvSlider } from 'common/components/InvSlider/InvSlider';
import { widthChanged } from 'features/parameters/store/generationSlice'; import { widthChanged } from 'features/parameters/store/generationSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selector = createMemoizedSelector( const selector = createMemoizedSelector(
@ -31,7 +31,7 @@ const selector = createMemoizedSelector(
}; };
} }
); );
export const ParamWidth = () => { export const ParamWidth = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { initial, width, min, max, inputMax, step, fineStep } = const { initial, width, min, max, inputMax, step, fineStep } =
@ -70,4 +70,6 @@ export const ParamWidth = () => {
/> />
</InvControl> </InvControl>
); );
}; });
ParamWidth.displayName = 'ParamWidth';

View File

@ -1,9 +1,12 @@
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { AspectRatioPreview } from 'common/components/AspectRatioPreview/AspectRatioPreview'; import { AspectRatioPreview } from 'common/components/AspectRatioPreview/AspectRatioPreview';
import { memo } from 'react';
export const AspectRatioPreviewWrapper = () => { export const AspectRatioPreviewWrapper = memo(() => {
const width = useAppSelector((state) => state.generation.width); const width = useAppSelector((state) => state.generation.width);
const height = useAppSelector((state) => state.generation.height); const height = useAppSelector((state) => state.generation.height);
return <AspectRatioPreview width={width} height={height} />; return <AspectRatioPreview width={width} height={height} />;
}; });
AspectRatioPreviewWrapper.displayName = 'AspectRatioPreviewWrapper';

View File

@ -7,14 +7,14 @@ import type { InvSelectOption } from 'common/components/InvSelect/types';
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/ImageSize/constants'; import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/ImageSize/constants';
import { isAspectRatioID } from 'features/parameters/components/ImageSize/types'; import { isAspectRatioID } from 'features/parameters/components/ImageSize/types';
import { aspectRatioSelected } from 'features/parameters/store/generationSlice'; import { aspectRatioSelected } from 'features/parameters/store/generationSlice';
import { useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { LockAspectRatioButton } from './LockAspectRatioButton'; import { LockAspectRatioButton } from './LockAspectRatioButton';
import { SetOptimalSizeButton } from './SetOptimalSizeButton'; import { SetOptimalSizeButton } from './SetOptimalSizeButton';
import { SwapDimensionsButton } from './SwapDimensionsButton'; import { SwapDimensionsButton } from './SwapDimensionsButton';
export const AspectRatioSelect = () => { export const AspectRatioSelect = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const aspectRatioID = useAppSelector( const aspectRatioID = useAppSelector(
@ -49,6 +49,8 @@ export const AspectRatioSelect = () => {
<SetOptimalSizeButton /> <SetOptimalSizeButton />
</InvControl> </InvControl>
); );
}; });
AspectRatioSelect.displayName = 'AspectRatioSelect';
const selectStyles: SystemStyleObject = { minW: 48 }; const selectStyles: SystemStyleObject = { minW: 48 };

View File

@ -1,11 +1,11 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { isLockedToggled } from 'features/parameters/store/generationSlice'; import { isLockedToggled } from 'features/parameters/store/generationSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaLock, FaLockOpen } from 'react-icons/fa6'; import { FaLock, FaLockOpen } from 'react-icons/fa6';
export const LockAspectRatioButton = () => { export const LockAspectRatioButton = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isLocked = useAppSelector( const isLocked = useAppSelector(
@ -24,4 +24,6 @@ export const LockAspectRatioButton = () => {
icon={isLocked ? <FaLock /> : <FaLockOpen />} icon={isLocked ? <FaLock /> : <FaLockOpen />}
/> />
); );
}; });
LockAspectRatioButton.displayName = 'LockAspectRatioButton';

View File

@ -1,11 +1,11 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { sizeReset } from 'features/parameters/store/generationSlice'; import { sizeReset } from 'features/parameters/store/generationSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IoSparkles } from 'react-icons/io5'; import { IoSparkles } from 'react-icons/io5';
export const SetOptimalSizeButton = () => { export const SetOptimalSizeButton = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const optimalDimension = useAppSelector((state) => const optimalDimension = useAppSelector((state) =>
state.generation.model?.base_model === 'sdxl' ? 1024 : 512 state.generation.model?.base_model === 'sdxl' ? 1024 : 512
@ -24,4 +24,6 @@ export const SetOptimalSizeButton = () => {
icon={<IoSparkles />} icon={<IoSparkles />}
/> />
); );
}; });
SetOptimalSizeButton.displayName = 'SetOptimalSizeButton';

View File

@ -1,11 +1,11 @@
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { dimensionsSwapped } from 'features/parameters/store/generationSlice'; import { dimensionsSwapped } from 'features/parameters/store/generationSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IoSwapVertical } from 'react-icons/io5'; import { IoSwapVertical } from 'react-icons/io5';
export const SwapDimensionsButton = () => { export const SwapDimensionsButton = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const onClick = useCallback(() => { const onClick = useCallback(() => {
@ -20,4 +20,6 @@ export const SwapDimensionsButton = () => {
icon={<IoSwapVertical />} icon={<IoSwapVertical />}
/> />
); );
}; });
SwapDimensionsButton.displayName = 'SwapDimensionsButton';

View File

@ -1,7 +1,7 @@
import { Flex } from '@chakra-ui/layout'; import { Flex } from '@chakra-ui/layout';
import type { PropsWithChildren } from 'react'; import { memo, type PropsWithChildren } from 'react';
export const PromptOverlayButtonWrapper = (props: PropsWithChildren) => ( export const PromptOverlayButtonWrapper = memo((props: PropsWithChildren) => (
<Flex <Flex
pos="absolute" pos="absolute"
insetBlockStart={0} insetBlockStart={0}
@ -14,4 +14,6 @@ export const PromptOverlayButtonWrapper = (props: PropsWithChildren) => (
> >
{props.children} {props.children}
</Flex> </Flex>
); ));
PromptOverlayButtonWrapper.displayName = 'PromptOverlayButtonWrapper';

View File

@ -1,12 +1,15 @@
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt'; import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt'; import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
import { memo } from 'react';
export const Prompts = () => { export const Prompts = memo(() => {
return ( return (
<Flex flexDir="column" gap={2}> <Flex flexDir="column" gap={2}>
<ParamPositivePrompt /> <ParamPositivePrompt />
<ParamNegativePrompt /> <ParamNegativePrompt />
</Flex> </Flex>
); );
}; });
Prompts.displayName = 'Prompts';

View File

@ -3,10 +3,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvControl } from 'common/components/InvControl/InvControl'; import { InvControl } from 'common/components/InvControl/InvControl';
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput'; import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
import { setSeed } from 'features/parameters/store/generationSlice'; import { setSeed } from 'features/parameters/store/generationSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const ParamSeedNumberInput = () => { export const ParamSeedNumberInput = memo(() => {
const seed = useAppSelector((state) => state.generation.seed); const seed = useAppSelector((state) => state.generation.seed);
const shouldRandomizeSeed = useAppSelector( const shouldRandomizeSeed = useAppSelector(
(state) => state.generation.shouldRandomizeSeed (state) => state.generation.shouldRandomizeSeed
@ -34,4 +34,6 @@ export const ParamSeedNumberInput = () => {
/> />
</InvControl> </InvControl>
); );
}; });
ParamSeedNumberInput.displayName = 'ParamSeedNumberInput';

View File

@ -4,10 +4,10 @@ import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSwitch } from 'common/components/InvSwitch/wrapper'; import { InvSwitch } from 'common/components/InvSwitch/wrapper';
import { setShouldRandomizeSeed } from 'features/parameters/store/generationSlice'; import { setShouldRandomizeSeed } from 'features/parameters/store/generationSlice';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const ParamSeedRandomize = () => { export const ParamSeedRandomize = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -29,4 +29,6 @@ export const ParamSeedRandomize = () => {
/> />
</InvControl> </InvControl>
); );
}; });
ParamSeedRandomize.displayName = 'ParamSeedRandomize';

View File

@ -4,11 +4,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvButton } from 'common/components/InvButton/InvButton'; import { InvButton } from 'common/components/InvButton/InvButton';
import randomInt from 'common/util/randomInt'; import randomInt from 'common/util/randomInt';
import { setSeed } from 'features/parameters/store/generationSlice'; import { setSeed } from 'features/parameters/store/generationSlice';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaShuffle } from 'react-icons/fa6'; import { FaShuffle } from 'react-icons/fa6';
export const ParamSeedShuffle = () => { export const ParamSeedShuffle = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const shouldRandomizeSeed = useAppSelector( const shouldRandomizeSeed = useAppSelector(
(state: RootState) => state.generation.shouldRandomizeSeed (state: RootState) => state.generation.shouldRandomizeSeed
@ -31,4 +31,6 @@ export const ParamSeedShuffle = () => {
{t('parameters.shuffle')} {t('parameters.shuffle')}
</InvButton> </InvButton>
); );
}; });
ParamSeedShuffle.displayName = 'ParamSeedShuffle';

View File

@ -6,7 +6,7 @@ import { InvSelect } from 'common/components/InvSelect/InvSelect';
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect'; import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
import { vaeSelected } from 'features/parameters/store/generationSlice'; import { vaeSelected } from 'features/parameters/store/generationSlice';
import { pick } from 'lodash-es'; import { pick } from 'lodash-es';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { VaeModelConfigEntity } from 'services/api/endpoints/models'; import type { VaeModelConfigEntity } from 'services/api/endpoints/models';
import { useGetVaeModelsQuery } from 'services/api/endpoints/models'; import { useGetVaeModelsQuery } from 'services/api/endpoints/models';
@ -60,4 +60,4 @@ const ParamVAEModelSelect = () => {
); );
}; };
export default ParamVAEModelSelect; export default memo(ParamVAEModelSelect);

View File

@ -8,7 +8,7 @@ import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput'
import type { InvNumberInputFieldProps } from 'common/components/InvNumberInput/types'; import type { InvNumberInputFieldProps } from 'common/components/InvNumberInput/types';
import { setIterations } from 'features/parameters/store/generationSlice'; import { setIterations } from 'features/parameters/store/generationSlice';
import { useQueueBack } from 'features/queue/hooks/useQueueBack'; import { useQueueBack } from 'features/queue/hooks/useQueueBack';
import { useCallback } from 'react'; import { memo, useCallback } from 'react';
import { IoSparkles } from 'react-icons/io5'; import { IoSparkles } from 'react-icons/io5';
import { QueueButtonTooltip } from './QueueButtonTooltip'; import { QueueButtonTooltip } from './QueueButtonTooltip';
@ -40,7 +40,7 @@ const selector = createMemoizedSelector([stateSelector], (state) => {
}; };
}); });
export const InvokeQueueBackButton = () => { export const InvokeQueueBackButton = memo(() => {
const { queueBack, isLoading, isDisabled } = useQueueBack(); const { queueBack, isLoading, isDisabled } = useQueueBack();
const { iterations, step, fineStep } = useAppSelector(selector); const { iterations, step, fineStep } = useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -89,4 +89,6 @@ export const InvokeQueueBackButton = () => {
</InvButton> </InvButton>
</Flex> </Flex>
); );
}; });
InvokeQueueBackButton.displayName = 'InvokeQueueBackButton';

View File

@ -9,12 +9,13 @@ import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrent
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor'; import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor'; import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaPause, FaPlay, FaTimes } from 'react-icons/fa'; import { FaPause, FaPlay, FaTimes } from 'react-icons/fa';
import { FaList } from 'react-icons/fa6'; import { FaList } from 'react-icons/fa6';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue'; import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
export const QueueActionsMenuButton = () => { export const QueueActionsMenuButton = memo(() => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { t } = useTranslation(); const { t } = useTranslation();
const isPauseEnabled = useFeatureStatus('pauseQueue').isFeatureEnabled; const isPauseEnabled = useFeatureStatus('pauseQueue').isFeatureEnabled;
@ -100,4 +101,6 @@ export const QueueActionsMenuButton = () => {
)} )}
</Box> </Box>
); );
}; });
QueueActionsMenuButton.displayName = 'QueueActionsMenuButton';

View File

@ -4,6 +4,7 @@ import ClearQueueButton from 'features/queue/components/ClearQueueButton';
import QueueFrontButton from 'features/queue/components/QueueFrontButton'; import QueueFrontButton from 'features/queue/components/QueueFrontButton';
import ProgressBar from 'features/system/components/ProgressBar'; import ProgressBar from 'features/system/components/ProgressBar';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
import { InvokeQueueBackButton } from './InvokeQueueBackButton'; import { InvokeQueueBackButton } from './InvokeQueueBackButton';
import { QueueActionsMenuButton } from './QueueActionsMenuButton'; import { QueueActionsMenuButton } from './QueueActionsMenuButton';
@ -35,7 +36,7 @@ const QueueControls = () => {
); );
}; };
export default QueueControls; export default memo(QueueControls);
// const QueueCounts = () => { // const QueueCounts = () => {
// const { t } = useTranslation(); // const { t } = useTranslation();

View File

@ -6,11 +6,11 @@ import { EmbeddingPopover } from 'features/embedding/EmbeddingPopover';
import { usePrompt } from 'features/embedding/usePrompt'; import { usePrompt } from 'features/embedding/usePrompt';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { setNegativeStylePromptSDXL } from 'features/sdxl/store/sdxlSlice'; import { setNegativeStylePromptSDXL } from 'features/sdxl/store/sdxlSlice';
import { useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const ParamSDXLNegativeStylePrompt = () => { export const ParamSDXLNegativeStylePrompt = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const prompt = useAppSelector((state) => state.sdxl.negativeStylePrompt); const prompt = useAppSelector((state) => state.sdxl.negativeStylePrompt);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -65,4 +65,6 @@ export const ParamSDXLNegativeStylePrompt = () => {
</Box> </Box>
</EmbeddingPopover> </EmbeddingPopover>
); );
}; });
ParamSDXLNegativeStylePrompt.displayName = 'ParamSDXLNegativeStylePrompt';

View File

@ -6,10 +6,10 @@ import { EmbeddingPopover } from 'features/embedding/EmbeddingPopover';
import { usePrompt } from 'features/embedding/usePrompt'; import { usePrompt } from 'features/embedding/usePrompt';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { setPositiveStylePromptSDXL } from 'features/sdxl/store/sdxlSlice'; import { setPositiveStylePromptSDXL } from 'features/sdxl/store/sdxlSlice';
import { useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const ParamSDXLPositiveStylePrompt = () => { export const ParamSDXLPositiveStylePrompt = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const prompt = useAppSelector((state) => state.sdxl.positiveStylePrompt); const prompt = useAppSelector((state) => state.sdxl.positiveStylePrompt);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -55,4 +55,6 @@ export const ParamSDXLPositiveStylePrompt = () => {
</Box> </Box>
</EmbeddingPopover> </EmbeddingPopover>
); );
}; });
ParamSDXLPositiveStylePrompt.displayName = 'ParamSDXLPositiveStylePrompt';

View File

@ -2,11 +2,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip'; import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { setShouldConcatSDXLStylePrompt } from 'features/sdxl/store/sdxlSlice'; import { setShouldConcatSDXLStylePrompt } from 'features/sdxl/store/sdxlSlice';
import { useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaLink, FaUnlink } from 'react-icons/fa'; import { FaLink, FaUnlink } from 'react-icons/fa';
export const SDXLConcatButton = () => { export const SDXLConcatButton = memo(() => {
const shouldConcatSDXLStylePrompt = useAppSelector( const shouldConcatSDXLStylePrompt = useAppSelector(
(state) => state.sdxl.shouldConcatSDXLStylePrompt (state) => state.sdxl.shouldConcatSDXLStylePrompt
); );
@ -38,4 +38,6 @@ export const SDXLConcatButton = () => {
/> />
</InvTooltip> </InvTooltip>
); );
}; });
SDXLConcatButton.displayName = 'SDXLConcatButton';

View File

@ -2,11 +2,12 @@ import { Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt'; import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt'; import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
import { memo } from 'react';
import { ParamSDXLNegativeStylePrompt } from './ParamSDXLNegativeStylePrompt'; import { ParamSDXLNegativeStylePrompt } from './ParamSDXLNegativeStylePrompt';
import { ParamSDXLPositiveStylePrompt } from './ParamSDXLPositiveStylePrompt'; import { ParamSDXLPositiveStylePrompt } from './ParamSDXLPositiveStylePrompt';
export const SDXLPrompts = () => { export const SDXLPrompts = memo(() => {
const shouldConcatSDXLStylePrompt = useAppSelector( const shouldConcatSDXLStylePrompt = useAppSelector(
(state) => state.sdxl.shouldConcatSDXLStylePrompt (state) => state.sdxl.shouldConcatSDXLStylePrompt
); );
@ -18,4 +19,6 @@ export const SDXLPrompts = () => {
{!shouldConcatSDXLStylePrompt && <ParamSDXLNegativeStylePrompt />} {!shouldConcatSDXLStylePrompt && <ParamSDXLNegativeStylePrompt />}
</Flex> </Flex>
); );
}; });
SDXLPrompts.displayName = 'SDXLPrompts';

View File

@ -8,6 +8,7 @@ import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSea
import ParamSeamlessYAxis from 'features/parameters/components/Seamless/ParamSeamlessYAxis'; import ParamSeamlessYAxis from 'features/parameters/components/Seamless/ParamSeamlessYAxis';
import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect'; import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect';
import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision'; import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision';
import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const labelProps: InvLabelProps = { const labelProps: InvLabelProps = {
@ -18,7 +19,7 @@ const labelProps2: InvLabelProps = {
flexGrow: 1, flexGrow: 1,
}; };
export const AdvancedSettingsAccordion = () => { export const AdvancedSettingsAccordion = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -41,4 +42,6 @@ export const AdvancedSettingsAccordion = () => {
</Flex> </Flex>
</InvSingleAccordion> </InvSingleAccordion>
); );
}; });
AdvancedSettingsAccordion.displayName = 'AdvancedSettingsAccordion';

View File

@ -21,6 +21,7 @@ import ParamScheduler from 'features/parameters/components/Core/ParamScheduler';
import ParamSteps from 'features/parameters/components/Core/ParamSteps'; import ParamSteps from 'features/parameters/components/Core/ParamSteps';
import ParamMainModelSelect from 'features/parameters/components/MainModel/ParamMainModelSelect'; import ParamMainModelSelect from 'features/parameters/components/MainModel/ParamMainModelSelect';
import { size, truncate } from 'lodash-es'; import { size, truncate } from 'lodash-es';
import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const labelProps: InvLabelProps = { const labelProps: InvLabelProps = {
@ -43,7 +44,7 @@ const badgesSelector = createMemoizedSelector(
} }
); );
export const GenerationSettingsAccordion = () => { export const GenerationSettingsAccordion = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const { loraTabBadges, accordionBadges } = useAppSelector(badgesSelector); const { loraTabBadges, accordionBadges } = useAppSelector(badgesSelector);
@ -86,4 +87,6 @@ export const GenerationSettingsAccordion = () => {
</InvTabs> </InvTabs>
</InvSingleAccordion> </InvSingleAccordion>
); );
}; });
GenerationSettingsAccordion.displayName = 'GenerationSettingsAccordion';

View File

@ -23,6 +23,7 @@ import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSee
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
import type { InvokeTabName } from 'features/ui/store/tabMap'; import type { InvokeTabName } from 'features/ui/store/tabMap';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selector = createMemoizedSelector( const selector = createMemoizedSelector(
@ -49,7 +50,7 @@ const scalingLabelProps: InvLabelProps = {
w: '4.5rem', w: '4.5rem',
}; };
export const ImageSettingsAccordion = () => { export const ImageSettingsAccordion = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const { badges, activeTabName } = useAppSelector(selector); const { badges, activeTabName } = useAppSelector(selector);
@ -95,9 +96,11 @@ export const ImageSettingsAccordion = () => {
</Flex> </Flex>
</InvSingleAccordion> </InvSingleAccordion>
); );
}; });
const WidthHeight = (props: { activeTabName: InvokeTabName }) => { ImageSettingsAccordion.displayName = 'ImageSettingsAccordion';
const WidthHeight = memo((props: { activeTabName: InvokeTabName }) => {
if (props.activeTabName === 'unifiedCanvas') { if (props.activeTabName === 'unifiedCanvas') {
return ( return (
<> <>
@ -113,4 +116,6 @@ const WidthHeight = (props: { activeTabName: InvokeTabName }) => {
<ParamHeight /> <ParamHeight />
</> </>
); );
}; });
WidthHeight.displayName = 'WidthHeight';

View File

@ -5,10 +5,10 @@ import type { InvSelectOnChange } from 'common/components/InvSelect/types';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { languageChanged } from 'features/system/store/systemSlice'; import { languageChanged } from 'features/system/store/systemSlice';
import { isLanguage } from 'features/system/store/types'; import { isLanguage } from 'features/system/store/types';
import { useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const SettingsLanguageSelect = () => { export const SettingsLanguageSelect = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const language = useAppSelector((state) => state.system.language); const language = useAppSelector((state) => state.system.language);
@ -58,4 +58,6 @@ export const SettingsLanguageSelect = () => {
<InvSelect value={value} options={options} onChange={onChange} /> <InvSelect value={value} options={options} onChange={onChange} />
</InvControl> </InvControl>
); );
}; });
SettingsLanguageSelect.displayName = 'SettingsLanguageSelect';

View File

@ -4,10 +4,10 @@ import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect'; import { InvSelect } from 'common/components/InvSelect/InvSelect';
import type { InvSelectOnChange } from 'common/components/InvSelect/types'; import type { InvSelectOnChange } from 'common/components/InvSelect/types';
import { consoleLogLevelChanged } from 'features/system/store/systemSlice'; import { consoleLogLevelChanged } from 'features/system/store/systemSlice';
import { useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const SettingsLogLevelSelect = () => { export const SettingsLogLevelSelect = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const consoleLogLevel = useAppSelector( const consoleLogLevel = useAppSelector(
@ -43,4 +43,6 @@ export const SettingsLogLevelSelect = () => {
<InvSelect value={value} options={options} onChange={onChange} /> <InvSelect value={value} options={options} onChange={onChange} />
</InvControl> </InvControl>
); );
}; });
SettingsLogLevelSelect.displayName = 'SettingsLogLevelSelect';