Convert App to TypeScript

This commit is contained in:
Mark McDowall 2024-07-24 16:39:01 -07:00 committed by Mark McDowall
parent d46f4b2154
commit d6d90a64a3
18 changed files with 258 additions and 456 deletions

View File

@ -1,13 +1,19 @@
import { ConnectedRouter } from 'connected-react-router'; import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import DocumentTitle from 'react-document-title'; import DocumentTitle from 'react-document-title';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { Store } from 'redux';
import PageConnector from 'Components/Page/PageConnector'; import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme'; import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes'; import AppRoutes from './AppRoutes';
function App({ store, history }) { interface AppProps {
store: Store;
history: ConnectedRouterProps['history'];
}
function App({ store, history }: AppProps) {
return ( return (
<DocumentTitle title={window.Sonarr.instanceName}> <DocumentTitle title={window.Sonarr.instanceName}>
<Provider store={store}> <Provider store={store}>
@ -24,7 +30,7 @@ function App({ store, history }) {
App.propTypes = { App.propTypes = {
store: PropTypes.object.isRequired, store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired history: PropTypes.object.isRequired,
}; };
export default App; export default App;

View File

@ -35,60 +35,37 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector'; import MissingConnector from 'Wanted/Missing/MissingConnector';
function AppRoutes(props) { function AppRoutes() {
const {
app
} = props;
return ( return (
<Switch> <Switch>
{/* {/*
Series Series
*/} */}
<Route <Route exact={true} path="/" component={SeriesIndex} />
exact={true}
path="/"
component={SeriesIndex}
/>
{ {window.Sonarr.urlBase && (
window.Sonarr.urlBase && <Route
<Route exact={true}
exact={true} path="/"
path="/" // eslint-disable-next-line @typescript-eslint/ban-ts-comment
addUrlBase={false} // @ts-ignore
render={() => { addUrlBase={false}
return ( render={() => {
<Redirect return <Redirect to={getPathWithUrlBase('/')} />;
to={getPathWithUrlBase('/')} }}
component={app} />
/> )}
);
}}
/>
}
<Route <Route path="/add/new" component={AddNewSeriesConnector} />
path="/add/new"
component={AddNewSeriesConnector}
/>
<Route <Route path="/add/import" component={ImportSeries} />
path="/add/import"
component={ImportSeries}
/>
<Route <Route
path="/serieseditor" path="/serieseditor"
exact={true} exact={true}
render={() => { render={() => {
return ( return <Redirect to={getPathWithUrlBase('/')} />;
<Redirect
to={getPathWithUrlBase('/')}
component={app}
/>
);
}} }}
/> />
@ -96,96 +73,57 @@ function AppRoutes(props) {
path="/seasonpass" path="/seasonpass"
exact={true} exact={true}
render={() => { render={() => {
return ( return <Redirect to={getPathWithUrlBase('/')} />;
<Redirect
to={getPathWithUrlBase('/')}
component={app}
/>
);
}} }}
/> />
<Route <Route path="/series/:titleSlug" component={SeriesDetailsPageConnector} />
path="/series/:titleSlug"
component={SeriesDetailsPageConnector}
/>
{/* {/*
Calendar Calendar
*/} */}
<Route <Route path="/calendar" component={CalendarPageConnector} />
path="/calendar"
component={CalendarPageConnector}
/>
{/* {/*
Activity Activity
*/} */}
<Route <Route path="/activity/history" component={History} />
path="/activity/history"
component={History}
/>
<Route <Route path="/activity/queue" component={Queue} />
path="/activity/queue"
component={Queue}
/>
<Route <Route path="/activity/blocklist" component={Blocklist} />
path="/activity/blocklist"
component={Blocklist}
/>
{/* {/*
Wanted Wanted
*/} */}
<Route <Route path="/wanted/missing" component={MissingConnector} />
path="/wanted/missing"
component={MissingConnector}
/>
<Route <Route path="/wanted/cutoffunmet" component={CutoffUnmetConnector} />
path="/wanted/cutoffunmet"
component={CutoffUnmetConnector}
/>
{/* {/*
Settings Settings
*/} */}
<Route <Route exact={true} path="/settings" component={Settings} />
exact={true}
path="/settings"
component={Settings}
/>
<Route <Route
path="/settings/mediamanagement" path="/settings/mediamanagement"
component={MediaManagementConnector} component={MediaManagementConnector}
/> />
<Route <Route path="/settings/profiles" component={Profiles} />
path="/settings/profiles"
component={Profiles}
/>
<Route <Route path="/settings/quality" component={QualityConnector} />
path="/settings/quality"
component={QualityConnector}
/>
<Route <Route
path="/settings/customformats" path="/settings/customformats"
component={CustomFormatSettingsPage} component={CustomFormatSettingsPage}
/> />
<Route <Route path="/settings/indexers" component={IndexerSettingsConnector} />
path="/settings/indexers"
component={IndexerSettingsConnector}
/>
<Route <Route
path="/settings/downloadclients" path="/settings/downloadclients"
@ -197,84 +135,48 @@ function AppRoutes(props) {
component={ImportListSettingsConnector} component={ImportListSettingsConnector}
/> />
<Route <Route path="/settings/connect" component={NotificationSettings} />
path="/settings/connect"
component={NotificationSettings}
/>
<Route <Route path="/settings/metadata" component={MetadataSettings} />
path="/settings/metadata"
component={MetadataSettings}
/>
<Route <Route
path="/settings/metadatasource" path="/settings/metadatasource"
component={MetadataSourceSettings} component={MetadataSourceSettings}
/> />
<Route <Route path="/settings/tags" component={TagSettings} />
path="/settings/tags"
component={TagSettings}
/>
<Route <Route path="/settings/general" component={GeneralSettingsConnector} />
path="/settings/general"
component={GeneralSettingsConnector}
/>
<Route <Route path="/settings/ui" component={UISettingsConnector} />
path="/settings/ui"
component={UISettingsConnector}
/>
{/* {/*
System System
*/} */}
<Route <Route path="/system/status" component={Status} />
path="/system/status"
component={Status}
/>
<Route <Route path="/system/tasks" component={Tasks} />
path="/system/tasks"
component={Tasks}
/>
<Route <Route path="/system/backup" component={BackupsConnector} />
path="/system/backup"
component={BackupsConnector}
/>
<Route <Route path="/system/updates" component={Updates} />
path="/system/updates"
component={Updates}
/>
<Route <Route path="/system/events" component={LogsTableConnector} />
path="/system/events"
component={LogsTableConnector}
/>
<Route <Route path="/system/logs/files" component={Logs} />
path="/system/logs/files"
component={Logs}
/>
{/* {/*
Not Found Not Found
*/} */}
<Route <Route path="*" component={NotFound} />
path="*"
component={NotFound}
/>
</Switch> </Switch>
); );
} }
AppRoutes.propTypes = { AppRoutes.propTypes = {
app: PropTypes.func.isRequired app: PropTypes.func.isRequired,
}; };
export default AppRoutes; export default AppRoutes;

View File

@ -1,30 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector';
function AppUpdatedModal(props) {
const {
isOpen,
onModalClose
} = props;
return (
<Modal
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<AppUpdatedModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
AppUpdatedModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AppUpdatedModal;

View File

@ -0,0 +1,28 @@
import React, { useCallback } from 'react';
import Modal from 'Components/Modal/Modal';
import AppUpdatedModalContent from './AppUpdatedModalContent';
interface AppUpdatedModalProps {
isOpen: boolean;
onModalClose: (...args: unknown[]) => unknown;
}
function AppUpdatedModal(props: AppUpdatedModalProps) {
const { isOpen, onModalClose } = props;
const handleModalClose = useCallback(() => {
location.reload();
}, []);
return (
<Modal
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<AppUpdatedModalContent onModalClose={handleModalClose} />
</Modal>
);
}
export default AppUpdatedModal;

View File

@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import AppUpdatedModal from './AppUpdatedModal';
function createMapDispatchToProps(dispatch, props) {
return {
onModalClose() {
location.reload();
}
};
}
export default connect(null, createMapDispatchToProps)(AppUpdatedModal);

View File

@ -1,139 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import UpdateChanges from 'System/Updates/UpdateChanges';
import translate from 'Utilities/String/translate';
import styles from './AppUpdatedModalContent.css';
function mergeUpdates(items, version, prevVersion) {
let installedIndex = items.findIndex((u) => u.version === version);
let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
if (installedIndex === -1) {
installedIndex = 0;
}
if (installedPreviouslyIndex === -1) {
installedPreviouslyIndex = items.length;
} else if (installedPreviouslyIndex === installedIndex && items.length) {
installedPreviouslyIndex += 1;
}
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
if (!appliedUpdates.length) {
return null;
}
const appliedChanges = { new: [], fixed: [] };
appliedUpdates.forEach((u) => {
if (u.changes) {
appliedChanges.new.push(... u.changes.new);
appliedChanges.fixed.push(... u.changes.fixed);
}
});
const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
mergedUpdate.changes = null;
}
return mergedUpdate;
}
function AppUpdatedModalContent(props) {
const {
version,
prevVersion,
isPopulated,
error,
items,
onSeeChangesPress,
onModalClose
} = props;
const update = mergeUpdates(items, version, prevVersion);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AppUpdated')}
</ModalHeader>
<ModalBody>
<div>
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
</div>
{
isPopulated && !error && !!update &&
<div>
{
!update.changes &&
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
}
{
!!update.changes &&
<div>
<div className={styles.changes}>
{translate('WhatsNew')}
</div>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
}
</div>
}
{
!isPopulated && !error &&
<LoadingIndicator />
}
</ModalBody>
<ModalFooter>
<Button
onPress={onSeeChangesPress}
>
{translate('RecentChanges')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={onModalClose}
>
{translate('Reload')}
</Button>
</ModalFooter>
</ModalContent>
);
}
AppUpdatedModalContent.propTypes = {
version: PropTypes.string.isRequired,
prevVersion: PropTypes.string,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onSeeChangesPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AppUpdatedModalContent;

View File

@ -0,0 +1,145 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { kinds } from 'Helpers/Props';
import { fetchUpdates } from 'Store/Actions/systemActions';
import UpdateChanges from 'System/Updates/UpdateChanges';
import Update from 'typings/Update';
import translate from 'Utilities/String/translate';
import AppState from './State/AppState';
import styles from './AppUpdatedModalContent.css';
function mergeUpdates(items: Update[], version: string, prevVersion?: string) {
let installedIndex = items.findIndex((u) => u.version === version);
let installedPreviouslyIndex = items.findIndex(
(u) => u.version === prevVersion
);
if (installedIndex === -1) {
installedIndex = 0;
}
if (installedPreviouslyIndex === -1) {
installedPreviouslyIndex = items.length;
} else if (installedPreviouslyIndex === installedIndex && items.length) {
installedPreviouslyIndex += 1;
}
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
if (!appliedUpdates.length) {
return null;
}
const appliedChanges: Update['changes'] = { new: [], fixed: [] };
appliedUpdates.forEach((u: Update) => {
if (u.changes) {
appliedChanges.new.push(...u.changes.new);
appliedChanges.fixed.push(...u.changes.fixed);
}
});
const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], {
changes: appliedChanges,
});
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
mergedUpdate.changes = null;
}
return mergedUpdate;
}
interface AppUpdatedModalContentProps {
onModalClose: () => void;
}
function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
const dispatch = useDispatch();
const { version, prevVersion } = useSelector((state: AppState) => state.app);
const { isPopulated, error, items } = useSelector(
(state: AppState) => state.system.updates
);
const previousVersion = usePrevious(version);
const { onModalClose } = props;
const update = mergeUpdates(items, version, prevVersion);
const handleSeeChangesPress = useCallback(() => {
window.location.href = `${window.Sonarr.urlBase}/system/updates`;
}, []);
useEffect(() => {
dispatch(fetchUpdates());
}, [dispatch]);
useEffect(() => {
if (version !== previousVersion) {
dispatch(fetchUpdates());
}
}, [version, previousVersion, dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('AppUpdated')}</ModalHeader>
<ModalBody>
<div>
<InlineMarkdown
data={translate('AppUpdatedVersion', { version })}
blockClassName={styles.version}
/>
</div>
{isPopulated && !error && !!update ? (
<div>
{update.changes ? (
<div className={styles.maintenance}>
{translate('MaintenanceRelease')}
</div>
) : null}
{update.changes ? (
<div>
<div className={styles.changes}>{translate('WhatsNew')}</div>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
) : null}
</div>
) : null}
{!isPopulated && !error ? <LoadingIndicator /> : null}
</ModalBody>
<ModalFooter>
<Button onPress={handleSeeChangesPress}>
{translate('RecentChanges')}
</Button>
<Button kind={kinds.PRIMARY} onPress={onModalClose}>
{translate('Reload')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default AppUpdatedModalContent;

View File

@ -1,78 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchUpdates } from 'Store/Actions/systemActions';
import AppUpdatedModalContent from './AppUpdatedModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.app.version,
(state) => state.app.prevVersion,
(state) => state.system.updates,
(version, prevVersion, updates) => {
const {
isPopulated,
error,
items
} = updates;
return {
version,
prevVersion,
isPopulated,
error,
items
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchUpdates() {
dispatch(fetchUpdates());
},
onSeeChangesPress() {
window.location = `${window.Sonarr.urlBase}/system/updates`;
}
};
}
class AppUpdatedModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchUpdates();
}
componentDidUpdate(prevProps) {
if (prevProps.version !== this.props.version) {
this.props.dispatchFetchUpdates();
}
}
//
// Render
render() {
const {
dispatchFetchUpdates,
...otherProps
} = this.props;
return (
<AppUpdatedModalContent {...otherProps} />
);
}
}
AppUpdatedModalContentConnector.propTypes = {
version: PropTypes.string.isRequired,
dispatchFetchUpdates: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector);

View File

@ -1,13 +1,9 @@
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import themes from 'Styles/Themes'; import themes from 'Styles/Themes';
import AppState from './State/AppState'; import AppState from './State/AppState';
interface ApplyThemeProps {
children: ReactNode;
}
function createThemeSelector() { function createThemeSelector() {
return createSelector( return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme, (state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
@ -17,7 +13,7 @@ function createThemeSelector() {
); );
} }
function ApplyTheme({ children }: ApplyThemeProps) { function ApplyTheme() {
const theme = useSelector(createThemeSelector()); const theme = useSelector(createThemeSelector());
const updateCSSVariables = useCallback(() => { const updateCSSVariables = useCallback(() => {
@ -31,7 +27,7 @@ function ApplyTheme({ children }: ApplyThemeProps) {
updateCSSVariables(); updateCSSVariables();
}, [updateCSSVariables, theme]); }, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>; return null;
} }
export default ApplyTheme; export default ApplyTheme;

View File

@ -1,5 +1,4 @@
import PropTypes from 'prop-types'; import React, { useCallback } from 'react';
import React from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
@ -10,36 +9,31 @@ import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './ConnectionLostModal.css'; import styles from './ConnectionLostModal.css';
function ConnectionLostModal(props) { interface ConnectionLostModalProps {
const { isOpen: boolean;
isOpen, }
onModalClose
} = props; function ConnectionLostModal(props: ConnectionLostModalProps) {
const { isOpen } = props;
const handleModalClose = useCallback(() => {
location.reload();
}, []);
return ( return (
<Modal <Modal isOpen={isOpen} onModalClose={handleModalClose}>
isOpen={isOpen} <ModalContent onModalClose={handleModalClose}>
onModalClose={onModalClose} <ModalHeader>{translate('ConnectionLost')}</ModalHeader>
>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('ConnectionLost')}
</ModalHeader>
<ModalBody> <ModalBody>
<div> <div>{translate('ConnectionLostToBackend')}</div>
{translate('ConnectionLostToBackend')}
</div>
<div className={styles.automatic}> <div className={styles.automatic}>
{translate('ConnectionLostReconnect')} {translate('ConnectionLostReconnect')}
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button kind={kinds.PRIMARY} onPress={handleModalClose}>
kind={kinds.PRIMARY}
onPress={onModalClose}
>
{translate('Reload')} {translate('Reload')}
</Button> </Button>
</ModalFooter> </ModalFooter>
@ -48,9 +42,4 @@ function ConnectionLostModal(props) {
); );
} }
ConnectionLostModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ConnectionLostModal; export default ConnectionLostModal;

View File

@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import ConnectionLostModal from './ConnectionLostModal';
function createMapDispatchToProps(dispatch, props) {
return {
onModalClose() {
location.reload();
}
};
}
export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal);

View File

@ -49,6 +49,7 @@ export interface AppSectionState {
isConnected: boolean; isConnected: boolean;
isReconnecting: boolean; isReconnecting: boolean;
version: string; version: string;
prevVersion?: string;
dimensions: { dimensions: {
isSmallScreen: boolean; isSmallScreen: boolean;
width: number; width: number;

View File

@ -1,8 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector'; import AppUpdatedModal from 'App/AppUpdatedModal';
import ColorImpairedContext from 'App/ColorImpairedContext'; import ColorImpairedContext from 'App/ColorImpairedContext';
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector'; import ConnectionLostModal from 'App/ConnectionLostModal';
import SignalRConnector from 'Components/SignalRConnector'; import SignalRConnector from 'Components/SignalRConnector';
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal'; import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
import locationShape from 'Helpers/Props/Shapes/locationShape'; import locationShape from 'Helpers/Props/Shapes/locationShape';
@ -101,12 +101,12 @@ class Page extends Component {
{children} {children}
</div> </div>
<AppUpdatedModalConnector <AppUpdatedModal
isOpen={this.state.isUpdatedModalOpen} isOpen={this.state.isUpdatedModalOpen}
onModalClose={this.onUpdatedModalClose} onModalClose={this.onUpdatedModalClose}
/> />
<ConnectionLostModalConnector <ConnectionLostModal
isOpen={this.state.isConnectionLostModalOpen} isOpen={this.state.isConnectionLostModalOpen}
onModalClose={this.onConnectionLostModalClose} onModalClose={this.onConnectionLostModalClose}
/> />

View File

@ -198,8 +198,6 @@ function Updates() {
{hasUpdates && ( {hasUpdates && (
<div> <div>
{items.map((update) => { {items.map((update) => {
const hasChanges = !!update.changes;
return ( return (
<div key={update.version} className={styles.update}> <div key={update.version} className={styles.update}>
<div className={styles.info}> <div className={styles.info}>
@ -249,7 +247,7 @@ function Updates() {
) : null} ) : null}
</div> </div>
{hasChanges ? ( {update.changes ? (
<div> <div>
<UpdateChanges <UpdateChanges
title={translate('New')} title={translate('New')}

View File

@ -13,7 +13,7 @@ interface Update {
installedOn: string; installedOn: string;
installable: boolean; installable: boolean;
latest: boolean; latest: boolean;
changes: Changes; changes: Changes | null;
hash: string; hash: string;
} }

View File

@ -93,6 +93,7 @@
"@babel/preset-typescript": "7.24.1", "@babel/preset-typescript": "7.24.1",
"@types/lodash": "4.14.194", "@types/lodash": "4.14.194",
"@types/qs": "6.9.15", "@types/qs": "6.9.15",
"@types/react-document-title": "2.0.9",
"@types/react-lazyload": "3.2.0", "@types/react-lazyload": "3.2.0",
"@types/react-router-dom": "5.3.3", "@types/react-router-dom": "5.3.3",
"@types/react-text-truncate": "0.14.1", "@types/react-text-truncate": "0.14.1",

View File

@ -1463,6 +1463,13 @@
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce"
integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==
"@types/react-document-title@2.0.9":
version "2.0.9"
resolved "https://registry.yarnpkg.com/@types/react-document-title/-/react-document-title-2.0.9.tgz#65fd57e6086ef8ced5ad45ac8a439dd878ca71d6"
integrity sha512-Q8bSnESgyVoMCo0vAKJj2N4wD/yl7EnutaFntKpaL/edUUo4kTNH8M6A5iCoje9sknRdLx7cfB39cpdTNr5Z+Q==
dependencies:
"@types/react" "*"
"@types/react-dom@18.2.25": "@types/react-dom@18.2.25":
version "18.2.25" version "18.2.25"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521"