diff --git a/src/controls/DynDiv.tsx b/src/controls/DynDiv.tsx index 6d93882bd..62993689a 100644 --- a/src/controls/DynDiv.tsx +++ b/src/controls/DynDiv.tsx @@ -5,6 +5,8 @@ import { extend } from '../util/ExtensionProvider'; import * as React from 'react'; import { IExtensibleProps } from '../types/IExtensionProvider'; +import ErrorBoundary from '../controls/ErrorBoundary'; + interface IDynDivDefinition { component: React.ComponentClass; options: IDynDivOptions; @@ -41,9 +43,11 @@ class DynDiv extends React.Component { } return ( -
- {visible.map((comp, idx) => this.renderComponent(comp, idx))} -
+ +
+ {visible.map((comp, idx) => this.renderComponent(comp, idx))} +
+
); } diff --git a/src/controls/ErrorBoundary.tsx b/src/controls/ErrorBoundary.tsx index 7fa29831b..12210590f 100644 --- a/src/controls/ErrorBoundary.tsx +++ b/src/controls/ErrorBoundary.tsx @@ -30,6 +30,7 @@ export interface IErrorBoundaryProps extends WithTranslation { } interface IErrorBoundaryState { + hasError: boolean; error: Error; errorInfo?: React.ErrorInfo; } @@ -42,6 +43,7 @@ class ErrorBoundary extends ComponentEx {React.Children.only(this.props.children)} @@ -153,7 +161,7 @@ class ErrorBoundary extends ComponentEx { - this.setState({ error: undefined, errorInfo: undefined }); + this.setState({ hasError: false, error: undefined, errorInfo: undefined }); } } diff --git a/src/extensions/mod_management/util/deploy.ts b/src/extensions/mod_management/util/deploy.ts index c7a28fba9..9e845620d 100644 --- a/src/extensions/mod_management/util/deploy.ts +++ b/src/extensions/mod_management/util/deploy.ts @@ -1,7 +1,6 @@ import { startActivity, stopActivity } from '../../../actions/session'; import { IDeployedFile, IDeploymentMethod, IExtensionApi } from '../../../types/IExtensionContext'; import { IGame } from '../../../types/IGame'; -import { INotification } from '../../../types/INotification'; import { IProfile } from '../../../types/IState'; import { ProcessCanceled, TemporaryError } from '../../../util/CustomErrors'; import { log } from '../../../util/log'; @@ -10,7 +9,7 @@ import { getSafe } from '../../../util/storeHelper'; import { truthy } from '../../../util/util'; import { IModType } from '../../gamemode_management/types/IModType'; import { getGame } from '../../gamemode_management/util/getGame'; -import { installPath, installPathForGame } from '../selectors'; +import { installPathForGame } from '../selectors'; import { IMod } from '../types/IMod'; import { fallbackPurgeType, getManifest, loadActivation, saveActivation, withActivationLock } from './activationStore'; import { getActivator, getCurrentActivator } from './deploymentMethods'; diff --git a/src/extensions/mod_management/views/ExternalChangeDialog.tsx b/src/extensions/mod_management/views/ExternalChangeDialog.tsx index b5e025d95..d4ff256a6 100644 --- a/src/extensions/mod_management/views/ExternalChangeDialog.tsx +++ b/src/extensions/mod_management/views/ExternalChangeDialog.tsx @@ -231,17 +231,17 @@ class ExternalChangeDialog extends ComponentEx { return (
{text} -

{actions.map(action => ( - <> - {t(action.allText)} -   |   - +

{actions.map(action => ( + )) - }

+ }
{ } private updateModGrouping(modsWithState) { - const modList = Object.keys(modsWithState).map(key => modsWithState[key]); + const modList = Object.keys(modsWithState).reduce((accum, key) => { + const mod = modsWithState[key]; + if (mod) { + accum.push(mod); + } + return accum; + }, []); const grouped = groupMods(modList, { groupBy: 'file', multipleEnabled: false }); const groupedMods = grouped.reduce((prev: { [id: string]: IModWithState[] }, value) => { diff --git a/src/extensions/symlink_activator_elevate/index.ts b/src/extensions/symlink_activator_elevate/index.ts index 7f271e26b..927f72ca2 100644 --- a/src/extensions/symlink_activator_elevate/index.ts +++ b/src/extensions/symlink_activator_elevate/index.ts @@ -1,8 +1,9 @@ +/* eslint-disable */ import { clearUIBlocker, setUIBlocker } from '../../actions'; import {IExtensionApi, IExtensionContext} from '../../types/IExtensionContext'; import { IGame } from '../../types/IGame'; import { IState } from '../../types/IState'; -import {ProcessCanceled, TemporaryError, UserCanceled} from '../../util/CustomErrors'; +import {ProcessCanceled, UserCanceled} from '../../util/CustomErrors'; import * as fs from '../../util/fs'; import { Normalize } from '../../util/getNormalizeFunc'; import getVortexPath from '../../util/getVortexPath'; @@ -101,7 +102,7 @@ class DeploymentMethod extends LinkingDeployment { private mOpenRequests: { [num: number]: { resolve: () => void, reject: (err: Error) => void } }; private mIPCServer: net.Server; private mDone: () => void; - private mWaitForUser: () => Promise; + // private mWaitForUser: () => Promise; private mOnReport: (report: string) => void; private mTmpFilePath: string; @@ -114,18 +115,18 @@ class DeploymentMethod extends LinkingDeployment { api); this.mElevatedClient = null; - this.mWaitForUser = () => new Promise((resolve, reject) => api.sendNotification({ - type: 'info', - message: 'Deployment requires elevation', - noDismiss: true, - actions: [{ - title: 'Elevate', - action: dismiss => { dismiss(); resolve(); }, - }, { - title: 'Cancel', - action: dismiss => { dismiss(); reject(new UserCanceled()); }, - }], - })); + // this.mWaitForUser = () => new Promise((resolve, reject) => api.sendNotification({ + // type: 'info', + // message: 'Deployment requires elevation', + // noDismiss: true, + // actions: [{ + // title: 'Elevate', + // action: dismiss => { dismiss(); resolve(); }, + // }, { + // title: 'Cancel', + // action: dismiss => { dismiss(); reject(new UserCanceled()); }, + // }], + // })); let lastReport: string; this.mOnReport = (report: string) => { @@ -173,11 +174,21 @@ class DeploymentMethod extends LinkingDeployment { } public userGate(): Promise { - const state: IState = this.api.store.getState(); - - return state.settings.workarounds.userSymlinks - ? Promise.resolve() - : this.mWaitForUser(); + // In the past, we used to block the user from deploying/purging his mods + // until he would give us consent to elevate permissions to do so. + // That is a redundant anti-pattern as the elevation UI itself will already inform the user + // of this requirement and give him the opportunity to cancel or proceed with the deployment! + // + // Additionally - blocking the deployment behind a collapsible notification is extremely bad UX + // as it is not necessarily obvious to the user that we require him to click on the notification. + // Finally, this will block the user from switching to other games while Vortex awaits for elevation + // causing the "Are you stuck?" overlay to appear and remain there, waiting for the user to click on an + // invisible notification button. + // + // I could add a Promise.race([this.waitForUser(), this.waitForElevation()]) to replace the mWaitForUser + // functor - but what's the point - if the user clicked deploy, he surely wants to elevate his instance + // as well. (And if not, he can always cancel the Windows API dialog!) + return Promise.resolve(); } public prepare(dataPath: string, clean: boolean, lastActivation: IDeployedFile[], diff --git a/src/views/MainWindow.tsx b/src/views/MainWindow.tsx index e803e2792..8a89d2720 100644 --- a/src/views/MainWindow.tsx +++ b/src/views/MainWindow.tsx @@ -150,20 +150,6 @@ export class MainWindow extends React.Component { }; this.applicationButtons = []; - - this.props.api.events.on('show-main-page', pageId => { - this.setMainPage(pageId, false); - }); - - this.props.api.events.on('refresh-main-page', () => { - this.forceUpdate(); - }); - - this.props.api.events.on('show-modal', id => { - this.updateState({ - showLayer: { $set: id }, - }); - }); } public getChildContext(): IComponentContext { @@ -183,6 +169,20 @@ export class MainWindow extends React.Component { this.updateSize(); + this.props.api.events.on('show-main-page', pageId => { + this.setMainPage(pageId, false); + }); + + this.props.api.events.on('refresh-main-page', () => { + this.forceUpdate(); + }); + + this.props.api.events.on('show-modal', id => { + this.updateState({ + showLayer: { $set: id }, + }); + }); + window.addEventListener('resize', this.updateSize); window.addEventListener('keydown', this.updateModifiers); window.addEventListener('keyup', this.updateModifiers); @@ -191,6 +191,9 @@ export class MainWindow extends React.Component { } public componentWillUnmount() { + this.props.api.events.removeAllListeners('show-main-page'); + this.props.api.events.removeAllListeners('refresh-main-page'); + this.props.api.events.removeAllListeners('show-modal'); window.removeEventListener('resize', this.updateSize); window.removeEventListener('keydown', this.updateModifiers); window.removeEventListener('keyup', this.updateModifiers); @@ -436,7 +439,7 @@ export class MainWindow extends React.Component { const state = this.props.api.getState(); const profile = profileById(state, this.props.activeProfileId); const game = profile !== undefined ? getGame(profile.gameId) : undefined; - const gameName = game?.name || 'Mods'; + const gameName = game?.shortName || game?.name || 'Mods'; const pageGroups = [ { title: undefined, key: 'dashboard' }, { title: 'General', key: 'global' }, diff --git a/src/views/QuickLauncher.tsx b/src/views/QuickLauncher.tsx index ccfadd305..49e0f7500 100644 --- a/src/views/QuickLauncher.tsx +++ b/src/views/QuickLauncher.tsx @@ -60,10 +60,12 @@ interface IComponentState { } class QuickLauncher extends ComponentEx { + private mMounted = false; private mCacheDebouncer: Debouncer = new Debouncer(() => { + if (!this.mMounted) { return Promise.resolve(); } this.nextState.gameIconCache = this.genGameIconCache(); return Promise.resolve(); - }, 100); + }, 250); constructor(props: IProps) { super(props); @@ -71,11 +73,14 @@ class QuickLauncher extends ComponentEx { } public componentDidMount() { + this.mMounted = true; this.context.api.events.on('quick-launch', this.start); } public componentWillUnmount() { + this.mMounted = false; this.context.api.events.removeListener('quick-launch', this.start); + this.mCacheDebouncer.clear(); } public UNSAFE_componentWillReceiveProps(nextProps: IProps) {