From dc928828e235038ecac74549ca857bd10fbcc4ce Mon Sep 17 00:00:00 2001 From: danielyxie Date: Mon, 1 Jul 2019 22:28:24 -0700 Subject: [PATCH] Fix GH Issue #616: Stock Market UI throws error for certain locales because the price format length is too high --- src/StockMarket/StockMarket.tsx | 6 ++ src/StockMarket/ui/Root.tsx | 3 + src/StockMarket/ui/StockTickerHeaderText.tsx | 13 ++- src/StockMarket/ui/StockTickers.tsx | 13 ++- src/ui/React/ErrorBoundary.tsx | 103 +++++++++++++++++++ 5 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/ui/React/ErrorBoundary.tsx diff --git a/src/StockMarket/StockMarket.tsx b/src/StockMarket/StockMarket.tsx index 30d2b8b1b..97802e56f 100644 --- a/src/StockMarket/StockMarket.tsx +++ b/src/StockMarket/StockMarket.tsx @@ -20,6 +20,7 @@ import { CONSTANTS } from "../Constants"; import { WorkerScript } from "../Netscript/WorkerScript"; import { Player } from "../Player"; import { IMap } from "../types"; +import { EventEmitter } from "../utils/EventEmitter"; import { Page, routing } from ".././ui/navigationTracking"; import { numeralWrapper } from ".././ui/numeralFormat"; @@ -290,11 +291,15 @@ function initStockMarketFnForReact() { initSymbolToStockMap(); } +const eventEmitterForUiReset = new EventEmitter(); + export function displayStockMarketContent() { if (!routing.isOn(Page.StockMarket)) { return; } + eventEmitterForUiReset.emitEvent(); + if (stockMarketContainer instanceof HTMLElement) { const castedStockMarket = StockMarket as IStockMarket; ReactDOM.render( @@ -302,6 +307,7 @@ export function displayStockMarketContent() { buyStockLong={buyStock} buyStockShort={shortStock} cancelOrder={cancelOrder} + eventEmitterForReset={eventEmitterForUiReset} initStockMarket={initStockMarketFnForReact} p={Player} placeOrder={placeOrder} diff --git a/src/StockMarket/ui/Root.tsx b/src/StockMarket/ui/Root.tsx index 59daeebc7..771a61e28 100644 --- a/src/StockMarket/ui/Root.tsx +++ b/src/StockMarket/ui/Root.tsx @@ -12,6 +12,7 @@ import { OrderTypes } from "../data/OrderTypes"; import { PositionTypes } from "../data/PositionTypes"; import { IPlayer } from "../../PersonObjects/IPlayer"; +import { EventEmitter } from "../../utils/EventEmitter"; type txFn = (stock: Stock, shares: number) => boolean; export type placeOrderFn = (stock: Stock, shares: number, price: number, ordType: OrderTypes, posType: PositionTypes) => boolean; @@ -20,6 +21,7 @@ type IProps = { buyStockLong: txFn; buyStockShort: txFn; cancelOrder: (params: object) => void; + eventEmitterForReset?: EventEmitter; initStockMarket: () => void; p: IPlayer; placeOrder: placeOrderFn; @@ -65,6 +67,7 @@ export class StockMarketRoot extends React.Component { buyStockLong={this.props.buyStockLong} buyStockShort={this.props.buyStockShort} cancelOrder={this.props.cancelOrder} + eventEmitterForReset={this.props.eventEmitterForReset} p={this.props.p} placeOrder={this.props.placeOrder} sellStockLong={this.props.sellStockLong} diff --git a/src/StockMarket/ui/StockTickerHeaderText.tsx b/src/StockMarket/ui/StockTickerHeaderText.tsx index 387e0b96d..95ab96939 100644 --- a/src/StockMarket/ui/StockTickerHeaderText.tsx +++ b/src/StockMarket/ui/StockTickerHeaderText.tsx @@ -9,6 +9,7 @@ import { Stock } from "../Stock"; import { TickerHeaderFormatData } from "../data/TickerHeaderFormatData"; import { IPlayer } from "../../PersonObjects/IPlayer"; +import { Settings } from "../../Settings/Settings"; import { numeralWrapper } from "../../ui/numeralFormat"; type IProps = { @@ -16,12 +17,22 @@ type IProps = { stock: Stock; } +const localesWithLongPriceFormat = [ + "cs", + "lv", + "pl", + "ru", +]; + export function StockTickerHeaderText(props: IProps): React.ReactElement { const stock = props.stock; const stockPriceFormat = numeralWrapper.formatMoney(stock.price); + const spacesAllottedForStockPrice = localesWithLongPriceFormat.includes(Settings.Locale) ? 15 : 12; + const spacesAfterStockName = " ".repeat(1 + TickerHeaderFormatData.longestName - stock.name.length + (TickerHeaderFormatData.longestSymbol - stock.symbol.length)); + const spacesBeforePrice = " ".repeat(spacesAllottedForStockPrice - stockPriceFormat.length); - let hdrText = `${stock.name}${" ".repeat(1 + TickerHeaderFormatData.longestName - stock.name.length + (TickerHeaderFormatData.longestSymbol - stock.symbol.length))}${stock.symbol} -${" ".repeat(10 - stockPriceFormat.length)}${stockPriceFormat}`; + let hdrText = `${stock.name}${spacesAfterStockName}${stock.symbol} -${spacesBeforePrice}${stockPriceFormat}`; if (props.p.has4SData) { hdrText += ` - Volatility: ${numeralWrapper.format(stock.mv, '0,0.00')}% - Price Forecast: `; let plusOrMinus = stock.b; // True for "+", false for "-" diff --git a/src/StockMarket/ui/StockTickers.tsx b/src/StockMarket/ui/StockTickers.tsx index 935737ec2..1771b7679 100644 --- a/src/StockMarket/ui/StockTickers.tsx +++ b/src/StockMarket/ui/StockTickers.tsx @@ -14,6 +14,9 @@ import { OrderTypes } from "../data/OrderTypes"; import { PositionTypes } from "../data/PositionTypes"; import { IPlayer } from "../../PersonObjects/IPlayer"; +import { EventEmitter } from "../../utils/EventEmitter"; + +import { ErrorBoundary } from "../../ui/React/ErrorBoundary"; export type txFn = (stock: Stock, shares: number) => boolean; export type placeOrderFn = (stock: Stock, shares: number, price: number, ordType: OrderTypes, posType: PositionTypes) => boolean; @@ -22,6 +25,7 @@ type IProps = { buyStockLong: txFn; buyStockShort: txFn; cancelOrder: (params: object) => void; + eventEmitterForReset?: EventEmitter; p: IPlayer; placeOrder: placeOrderFn; sellStockLong: txFn; @@ -169,8 +173,13 @@ export class StockTickers extends React.Component { } } + const errorBoundaryProps = { + eventEmitterForReset: this.props.eventEmitterForReset, + id: "StockTickersErrorBoundary", + } + return ( -
+ {
    {tickers}
-
+ ) } } diff --git a/src/ui/React/ErrorBoundary.tsx b/src/ui/React/ErrorBoundary.tsx new file mode 100644 index 000000000..203f0c5fe --- /dev/null +++ b/src/ui/React/ErrorBoundary.tsx @@ -0,0 +1,103 @@ +/** + * React Component for a simple Error Boundary. The fallback UI for + * this error boundary is simply a bordered text box + */ +import * as React from "react"; + +import { EventEmitter } from "../../utils/EventEmitter"; + +type IProps = { + eventEmitterForReset?: EventEmitter; + id?: string; +}; + +type IState = { + errorInfo: string; + hasError: boolean; +} + +type IErrorInfo = { + componentStack: string; +} + +// TODO: Move this out to a css file +const styleMarkup = { + border: "1px solid red", + display: "inline-block", + margin: "4px", + padding: "4px", +} + +export class ErrorBoundary extends React.Component { + constructor(props: IProps) { + super(props); + this.state = { + errorInfo: "", + hasError: false, + } + } + + static getDerivedStateFromError(error: Error) { + return { + errorInfo: error.message, + hasError: true, + }; + } + + componentDidCatch(error: Error, info: IErrorInfo) { + console.error(`Caught error in React ErrorBoundary. Component stack:`); + console.error(info.componentStack); + } + + componentDidMount() { + const cb = () => { + this.setState({ + hasError: false, + }); + } + + if (this.hasEventEmitter()) { + this.props.eventEmitterForReset!.addSubscriber({ + cb: cb, + id: this.props.id!, + }); + } + } + + componentWillUnmount() { + if (this.hasEventEmitter()) { + this.props.eventEmitterForReset!.removeSubscriber(this.props.id!); + } + } + + hasEventEmitter(): boolean { + return this.props.eventEmitterForReset instanceof EventEmitter + && typeof this.props.id === "string"; + } + + render() { + if (this.state.hasError) { + return ( +
+

+ { + `Error rendering UI. This is (probably) a bug. Please report to game developer.` + } +

+

+ { + `In the meantime, try refreshing the game WITHOUT saving.` + } +

+

+ { + `Error info: ${this.state.errorInfo}` + } +

+
+ ) + } + + return this.props.children; + } +}