Fix infinite hook call loop
Regression a7dcdd5651
"usePrevious" uses "useEffect" to update the ref while the fix of the commit updated the state during the render function call.
This causes the component to not actually render and immediately call the render function again. Which then leads to the "usePrevious" hooks "useEffect" to never getting called.
This commit is contained in:
@@ -23,18 +23,22 @@ import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
|||||||
import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts';
|
import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts';
|
||||||
import { languageSortComparator, toUniqueLanguageCodes } from '@/base/utils/Languages.ts';
|
import { languageSortComparator, toUniqueLanguageCodes } from '@/base/utils/Languages.ts';
|
||||||
import { usePrevious } from '@mantine/hooks';
|
import { usePrevious } from '@mantine/hooks';
|
||||||
|
import { AwaitableComponent, type AwaitableComponentProps } from 'awaitable-component';
|
||||||
|
|
||||||
interface IProps {
|
const LanguageSelectDialog = ({
|
||||||
|
isVisible,
|
||||||
|
onDismiss,
|
||||||
|
onSubmit,
|
||||||
|
onExitComplete,
|
||||||
|
selectedLanguages,
|
||||||
|
languages,
|
||||||
|
}: AwaitableComponentProps<string[]> & {
|
||||||
selectedLanguages: string[];
|
selectedLanguages: string[];
|
||||||
setSelectedLanguages: (languages: string[]) => void;
|
|
||||||
languages: string[];
|
languages: string[];
|
||||||
}
|
}) => {
|
||||||
|
|
||||||
export function LanguageSelect({ selectedLanguages, setSelectedLanguages, languages }: IProps) {
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueLanguageCodes(selectedLanguages));
|
const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueLanguageCodes(selectedLanguages));
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const previousSelectedLanguages = usePrevious(selectedLanguages);
|
const previousSelectedLanguages = usePrevious(selectedLanguages);
|
||||||
|
|
||||||
@@ -53,14 +57,8 @@ export function LanguageSelect({ selectedLanguages, setSelectedLanguages, langua
|
|||||||
[languages, tmpSelectedLanguages],
|
[languages, tmpSelectedLanguages],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setOpen(false);
|
|
||||||
setTmpSelectedLanguages(toUniqueLanguageCodes(selectedLanguages));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
setOpen(false);
|
onSubmit(toUniqueLanguageCodes(tmpSelectedLanguages));
|
||||||
setSelectedLanguages(toUniqueLanguageCodes(tmpSelectedLanguages));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (language: string, selected: boolean) => {
|
const handleChange = (language: string, selected: boolean) => {
|
||||||
@@ -72,45 +70,70 @@ export function LanguageSelect({ selectedLanguages, setSelectedLanguages, langua
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dialog fullWidth maxWidth="xs" open={isVisible} onClose={onDismiss} onTransitionExited={onExitComplete}>
|
||||||
<CustomTooltip title={t`Settings`}>
|
<DialogTitle>{t`Allowed Languages`}</DialogTitle>
|
||||||
<IconButton onClick={() => setOpen(true)} aria-label="display more actions" edge="end" color="inherit">
|
<DialogContent dividers sx={{ padding: 0 }}>
|
||||||
<FilterListIcon />
|
<Virtuoso
|
||||||
</IconButton>
|
style={{
|
||||||
</CustomTooltip>
|
height: languagesSortedBySelectState.length * 54,
|
||||||
<Dialog fullWidth maxWidth="xs" open={open} onClose={handleCancel}>
|
minHeight: '25vh',
|
||||||
<DialogTitle>{t`Allowed Languages`}</DialogTitle>
|
maxHeight: '50vh',
|
||||||
<DialogContent dividers sx={{ padding: 0 }}>
|
}}
|
||||||
<Virtuoso
|
data={languagesSortedBySelectState}
|
||||||
style={{
|
increaseViewportBy={400}
|
||||||
height: languagesSortedBySelectState.length * 54,
|
computeItemKey={(index) => languagesSortedBySelectState[index]}
|
||||||
minHeight: '25vh',
|
itemContent={(_index, language) => (
|
||||||
maxHeight: '50vh',
|
<ListItem>
|
||||||
}}
|
<ListItemText primary={translateExtensionLanguage(language)} />
|
||||||
data={languagesSortedBySelectState}
|
|
||||||
increaseViewportBy={400}
|
|
||||||
computeItemKey={(index) => languagesSortedBySelectState[index]}
|
|
||||||
itemContent={(_index, language) => (
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary={translateExtensionLanguage(language)} />
|
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={tmpSelectedLanguages.includes(language)}
|
checked={tmpSelectedLanguages.includes(language)}
|
||||||
onChange={(e) => handleChange(language, e.target.checked)}
|
onChange={(e) => handleChange(language, e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button autoFocus onClick={handleCancel} color="primary">
|
<Button autoFocus onClick={onDismiss} color="primary">
|
||||||
{t`Cancel`}
|
{t`Cancel`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOk} color="primary">
|
<Button onClick={handleOk} color="primary">
|
||||||
{t`Ok`}
|
{t`Ok`}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LanguageSelect({
|
||||||
|
setSelectedLanguages,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
selectedLanguages: string[];
|
||||||
|
setSelectedLanguages: (languages: string[]) => void;
|
||||||
|
languages: string[];
|
||||||
|
}) {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomTooltip title={t`Settings`}>
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const updatedSelectedLanguages = await AwaitableComponent.show(LanguageSelectDialog, props);
|
||||||
|
|
||||||
|
setSelectedLanguages(updatedSelectedLanguages);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label="display more actions"
|
||||||
|
edge="end"
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
<FilterListIcon />
|
||||||
|
</IconButton>
|
||||||
|
</CustomTooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,16 +40,21 @@ import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx
|
|||||||
import { makeToast } from '@/base/utils/Toast.ts';
|
import { makeToast } from '@/base/utils/Toast.ts';
|
||||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||||
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
|
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
|
||||||
import { usePrevious } from '@mantine/hooks';
|
import { AwaitableComponent, type AwaitableComponentProps } from 'awaitable-component';
|
||||||
|
|
||||||
export const SourceLanguageSelect = ({
|
const SourceLanguageSelectDialog = ({
|
||||||
|
isVisible,
|
||||||
|
onDismiss,
|
||||||
|
onSubmit,
|
||||||
|
onExitComplete,
|
||||||
selectedLanguages,
|
selectedLanguages,
|
||||||
setSelectedLanguages,
|
|
||||||
languages,
|
languages,
|
||||||
sources,
|
sources,
|
||||||
}: {
|
}: AwaitableComponentProps<{
|
||||||
|
selectedLanguages: string[];
|
||||||
|
sourceEnabledStateMetaUpdatePayload: Parameters<typeof batchUpdateSourceMetadata>[0];
|
||||||
|
}> & {
|
||||||
selectedLanguages: string[];
|
selectedLanguages: string[];
|
||||||
setSelectedLanguages: (languages: string[]) => void;
|
|
||||||
languages: string[];
|
languages: string[];
|
||||||
sources: (SourceIdInfo &
|
sources: (SourceIdInfo &
|
||||||
SourceLanguageInfo &
|
SourceLanguageInfo &
|
||||||
@@ -63,12 +68,6 @@ export const SourceLanguageSelect = ({
|
|||||||
const [tmpSourceIdToEnabledState, setTmpSourceIdToEnabledState] = useState<Record<SourceIdInfo['id'], boolean>>({});
|
const [tmpSourceIdToEnabledState, setTmpSourceIdToEnabledState] = useState<Record<SourceIdInfo['id'], boolean>>({});
|
||||||
|
|
||||||
const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueLanguageCodes(selectedLanguages));
|
const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueLanguageCodes(selectedLanguages));
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const previousSelectedLanguages = usePrevious(selectedLanguages);
|
|
||||||
if (previousSelectedLanguages && previousSelectedLanguages !== selectedLanguages) {
|
|
||||||
setTmpSelectedLanguages(toUniqueLanguageCodes(selectedLanguages));
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourcesByLanguage = useMemo(() => Sources.groupByLanguage(sources), [sources]);
|
const sourcesByLanguage = useMemo(() => Sources.groupByLanguage(sources), [sources]);
|
||||||
|
|
||||||
@@ -109,18 +108,10 @@ export const SourceLanguageSelect = ({
|
|||||||
useCallback((index) => flattenedSourcesByLanguages[index].id, [flattenedSourcesByLanguages]),
|
useCallback((index) => flattenedSourcesByLanguages[index].id, [flattenedSourcesByLanguages]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setOpen(false);
|
|
||||||
|
|
||||||
setTmpSourceIdToEnabledState({});
|
|
||||||
setTmpSelectedLanguages(toUniqueLanguageCodes(selectedLanguages));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
setOpen(false);
|
onSubmit({
|
||||||
|
selectedLanguages: toUniqueLanguageCodes(tmpSelectedLanguages),
|
||||||
batchUpdateSourceMetadata(
|
sourceEnabledStateMetaUpdatePayload: Object.entries(tmpSourceIdToEnabledState)
|
||||||
Object.entries(tmpSourceIdToEnabledState)
|
|
||||||
.map(([sourceId, enabled]) => {
|
.map(([sourceId, enabled]) => {
|
||||||
const source = sources.find((sourceToEnable) => sourceToEnable.id === sourceId);
|
const source = sources.find((sourceToEnable) => sourceToEnable.id === sourceId);
|
||||||
|
|
||||||
@@ -134,9 +125,7 @@ export const SourceLanguageSelect = ({
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((entry) => entry !== null),
|
.filter((entry) => entry !== null),
|
||||||
).catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
|
});
|
||||||
setTmpSourceIdToEnabledState({});
|
|
||||||
setSelectedLanguages(toUniqueLanguageCodes(tmpSelectedLanguages));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (language: string, selected: boolean) => {
|
const handleChange = (language: string, selected: boolean) => {
|
||||||
@@ -148,92 +137,127 @@ export const SourceLanguageSelect = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dialog fullWidth maxWidth="xs" open={isVisible} onClose={onDismiss} onTransitionExited={onExitComplete}>
|
||||||
<CustomTooltip title={t`Settings`}>
|
<DialogTitle>{t`Allowed Languages`}</DialogTitle>
|
||||||
<IconButton onClick={() => setOpen(true)} aria-label="display more actions" edge="end" color="inherit">
|
<DialogContent dividers sx={{ padding: 0 }}>
|
||||||
<FilterListIcon />
|
{!languages.length && <Box sx={{ p: 1 }}>{t`No sources installed`}</Box>}
|
||||||
</IconButton>
|
<GroupedVirtuoso
|
||||||
</CustomTooltip>
|
style={{
|
||||||
<Dialog fullWidth maxWidth="xs" open={open} onClose={handleCancel}>
|
height: languagesSortedBySelectState.length * 54,
|
||||||
<DialogTitle>{t`Allowed Languages`}</DialogTitle>
|
minHeight: '25vh',
|
||||||
<DialogContent dividers sx={{ padding: 0 }}>
|
maxHeight: '50vh',
|
||||||
{!languages.length && <Box sx={{ p: 1 }}>{t`No sources installed`}</Box>}
|
}}
|
||||||
<GroupedVirtuoso
|
groupCounts={groupCounts}
|
||||||
style={{
|
increaseViewportBy={400}
|
||||||
height: languagesSortedBySelectState.length * 54,
|
computeItemKey={computeItemKey}
|
||||||
minHeight: '25vh',
|
groupContent={(index) => {
|
||||||
maxHeight: '50vh',
|
const language = languagesSortedBySelectState[index];
|
||||||
}}
|
const isEnabled = tmpSelectedLanguages.includes(language);
|
||||||
groupCounts={groupCounts}
|
|
||||||
increaseViewportBy={400}
|
|
||||||
computeItemKey={computeItemKey}
|
|
||||||
groupContent={(index) => {
|
|
||||||
const language = languagesSortedBySelectState[index];
|
|
||||||
const isEnabled = tmpSelectedLanguages.includes(language);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: 'background.paper',
|
backgroundColor: 'background.paper',
|
||||||
backgroundImage: 'var(--Paper-overlay)',
|
backgroundImage: 'var(--Paper-overlay)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemText primary={translateExtensionLanguage(language)} />
|
<ListItemText primary={translateExtensionLanguage(language)} />
|
||||||
<Switch
|
<Switch
|
||||||
checked={isEnabled}
|
checked={isEnabled}
|
||||||
onChange={(e) => handleChange(language, e.target.checked)}
|
onChange={(e) => handleChange(language, e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
itemContent={(index) => {
|
itemContent={(index) => {
|
||||||
const source = flattenedSourcesByLanguages[index];
|
const source = flattenedSourcesByLanguages[index];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem sx={{ pl: 3 }}>
|
<ListItem sx={{ pl: 3 }}>
|
||||||
<ListItemAvatar sx={{ minWidth: 32, mr: 1 }}>
|
<ListItemAvatar sx={{ minWidth: 32, mr: 1 }}>
|
||||||
<ListCardAvatar
|
<ListCardAvatar
|
||||||
iconUrl={requestManager.getValidImgUrlFor(source.iconUrl)}
|
iconUrl={requestManager.getValidImgUrlFor(source.iconUrl)}
|
||||||
alt={source.name}
|
alt={source.name}
|
||||||
slots={{
|
slots={{
|
||||||
avatarProps: {
|
avatarProps: {
|
||||||
sx: {
|
sx: {
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
spinnerImageProps: {
|
},
|
||||||
ignoreQueue: true,
|
spinnerImageProps: {
|
||||||
},
|
ignoreQueue: true,
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary={source.name} />
|
|
||||||
<Checkbox
|
|
||||||
checked={
|
|
||||||
tmpSourceIdToEnabledState[source.id] ?? getSourceMetadata(source).isEnabled
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
setTmpSourceIdToEnabledState({
|
|
||||||
...tmpSourceIdToEnabledState,
|
|
||||||
[source.id]: e.target.checked,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItemAvatar>
|
||||||
);
|
<ListItemText primary={source.name} />
|
||||||
}}
|
<Checkbox
|
||||||
/>
|
checked={
|
||||||
</DialogContent>
|
tmpSourceIdToEnabledState[source.id] ?? getSourceMetadata(source).isEnabled
|
||||||
<DialogActions>
|
}
|
||||||
<Button autoFocus onClick={handleCancel} color="primary">
|
onChange={(e) =>
|
||||||
{t`Cancel`}
|
setTmpSourceIdToEnabledState({
|
||||||
</Button>
|
...tmpSourceIdToEnabledState,
|
||||||
<Button onClick={handleOk} color="primary">
|
[source.id]: e.target.checked,
|
||||||
{t`Ok`}
|
})
|
||||||
</Button>
|
}
|
||||||
</DialogActions>
|
/>
|
||||||
</Dialog>
|
</ListItem>
|
||||||
</>
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button autoFocus onClick={onDismiss} color="primary">
|
||||||
|
{t`Cancel`}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleOk} color="primary">
|
||||||
|
{t`Ok`}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SourceLanguageSelect = ({
|
||||||
|
setSelectedLanguages,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
selectedLanguages: string[];
|
||||||
|
setSelectedLanguages: (languages: string[]) => Promise<void>;
|
||||||
|
languages: string[];
|
||||||
|
sources: (SourceIdInfo &
|
||||||
|
SourceLanguageInfo &
|
||||||
|
SourceNameInfo &
|
||||||
|
SourceDisplayNameInfo &
|
||||||
|
SourceIconInfo &
|
||||||
|
SourceMetaInfo)[];
|
||||||
|
}) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomTooltip title={t`Settings`}>
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const { selectedLanguages: updatedSelectedLanguages, sourceEnabledStateMetaUpdatePayload } =
|
||||||
|
await AwaitableComponent.show(SourceLanguageSelectDialog, props);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
setSelectedLanguages(updatedSelectedLanguages),
|
||||||
|
batchUpdateSourceMetadata(sourceEnabledStateMetaUpdatePayload),
|
||||||
|
]).catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label="display more actions"
|
||||||
|
edge="end"
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
<FilterListIcon />
|
||||||
|
</IconButton>
|
||||||
|
</CustomTooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export class Sources {
|
|||||||
|
|
||||||
static useLanguages(): {
|
static useLanguages(): {
|
||||||
languages: string[];
|
languages: string[];
|
||||||
setLanguages: (languages: string[]) => void;
|
setLanguages: (languages: string[]) => Promise<void>;
|
||||||
} {
|
} {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const {
|
const {
|
||||||
|
|||||||
Reference in New Issue
Block a user