diff --git a/renderer/src/modules/api/index.js b/renderer/src/modules/api/index.js index 2a68654a..ffca6ab8 100644 --- a/renderer/src/modules/api/index.js +++ b/renderer/src/modules/api/index.js @@ -29,6 +29,9 @@ import SwitchInput from "@ui/settings/components/switch"; import TextInput from "@ui/settings/components/textbox"; import SettingGroup from "@ui/settings/group"; import ErrorBoundary from "@ui/errorboundary"; +import Text from "@ui/base/text"; +import Flex from "@ui/base/flex"; +import Button from "@ui/base/button"; const bounded = new Map(); const PluginAPI = new AddonAPI(PluginManager); @@ -39,6 +42,31 @@ const DOMAPI = new DOM(); const ContextMenuAPI = new ContextMenu(); const DefaultLogger = new Logger(); +/** + * `Components` is a namespace holding a series of React components. It is available under {@link BdApi}. + * @type Components + * @summary {@link Components} a namespace holding a series of React components + * @name Components + */ +const Components = { + get Tooltip() {return DiscordModules.Tooltip;}, + get ColorInput() {return ColorInput;}, + get DropdownInput() {return DropdownInput;}, + get SettingItem() {return SettingItem;}, + get KeybindInput() {return KeybindInput;}, + get NumberInput() {return NumberInput;}, + get RadioInput() {return RadioInput;}, + get SearchInput() {return SearchInput;}, + get SliderInput() {return SliderInput;}, + get SwitchInput() {return SwitchInput;}, + get TextInput() {return TextInput;}, + get SettingGroup() {return SettingGroup;}, + get ErrorBoundary() {return ErrorBoundary;}, + get Text() {return Text;}, + get Flex() {return Flex;}, + get Button() {return Button;}, +}; + /** * `BdApi` is a globally (`window.BdApi`) accessible object for use by plugins and developers to make their lives easier. * @name BdApi @@ -72,21 +100,7 @@ export default class BdApi { get UI() {return UI;} get ReactUtils() {return ReactUtils;} get ContextMenu() {return ContextMenuAPI;} - Components = { - get Tooltip() {return DiscordModules.Tooltip;}, - get ColorInput() {return ColorInput;}, - get DropdownInput() {return DropdownInput;}, - get SettingItem() {return SettingItem;}, - get KeybindInput() {return KeybindInput;}, - get NumberInput() {return NumberInput;}, - get RadioInput() {return RadioInput;}, - get SearchInput() {return SearchInput;}, - get SliderInput() {return SliderInput;}, - get SwitchInput() {return SwitchInput;}, - get TextInput() {return TextInput;}, - get SettingGroup() {return SettingGroup;}, - get ErrorBoundary() {return ErrorBoundary;}, - }; + get Components() {return Components;} Net = {fetch}; } @@ -157,21 +171,7 @@ BdApi.ContextMenu = ContextMenuAPI; * An set of react components plugins can make use of. * @type Components */ -BdApi.Components = { - get Tooltip() {return DiscordModules.Tooltip;}, - get ColorInput() {return ColorInput;}, - get DropdownInput() {return DropdownInput;}, - get SettingItem() {return SettingItem;}, - get KeybindInput() {return KeybindInput;}, - get NumberInput() {return NumberInput;}, - get RadioInput() {return RadioInput;}, - get SearchInput() {return SearchInput;}, - get SliderInput() {return SliderInput;}, - get SwitchInput() {return SwitchInput;}, - get TextInput() {return TextInput;}, - get SettingGroup() {return SettingGroup;}, - get ErrorBoundary() {return ErrorBoundary;}, -}; +BdApi.Components = Components; /** * An instance of {@link Net} for using network related tools. diff --git a/renderer/src/modules/api/ui.js b/renderer/src/modules/api/ui.js index 9ba2f8ab..8a54f083 100644 --- a/renderer/src/modules/api/ui.js +++ b/renderer/src/modules/api/ui.js @@ -189,7 +189,7 @@ const UI = { buildSettingsPanel({settings, onChange, onDrawerToggle, getDrawerState}) { if (!settings?.length) throw new Error("No settings provided!"); - return React.createElement(ErrorBoundary, null, settings.map(setting => { + return React.createElement(ErrorBoundary, {id: "buildSettingsPanel", name: "BdApi.UI"}, settings.map(setting => { if (!setting.id || !setting.type) throw new Error(`Setting item missing id or type`); if (setting.type === "category") { diff --git a/renderer/src/ui/errorboundary.jsx b/renderer/src/ui/errorboundary.jsx index 4587411e..8f712c7e 100644 --- a/renderer/src/ui/errorboundary.jsx +++ b/renderer/src/ui/errorboundary.jsx @@ -4,19 +4,37 @@ import IPC from "@modules/ipc"; export default class ErrorBoundary extends React.Component { + /** + * Creates an error boundary with optional fallbacks and debug info. + * @param {object} props + * @param {ReactElement[]} props.children - An optional id for debugging purposes + * @param {string} [props.id="Unknown"] - An optional id for debugging purposes + * @param {string} [props.name="Unknown"] - An optional name for debugging purposes + * @param {boolean} [props.hideError=false] - Whether to hide the default error message in the ui (never shown if there is a fallback) + * @param {ReactElement} [props.fallback] - A fallback to show on error + * @param {function} [props.onError] - A callback called with the error when it happens + */ constructor(props) { - super(props); - this.state = {hasError: false}; + super(props); + this.state = {hasError: false}; } componentDidCatch(error) { - this.setState({hasError: true}); - if (typeof this.props.onError === "function") this.props.onError(error); + this.setState({hasError: true}); + Logger.stacktrace("ErrorBoundary", `React error detected for {name: ${this.props.name ?? "Unknown"}, id: ${this.props.id ?? "Unknown"}}`, error); + if (typeof this.props.onError === "function") this.props.onError(error); } render() { - if (this.state.hasError && !this.props.hideError) return
IPC.openDevTools()} className="react-error">There was an unexpected Error. Click to open console for more details.
; - return this.props.children; + if (this.state.hasError && this.props.fallback) { + return this.props.fallback; + } + else if (this.state.hasError && !this.props.hideError) { + return
IPC.openDevTools()} className="react-error"> + There was an unexpected Error. Click to open console for more details. +
; + } + return this.props.children; } } @@ -24,6 +42,6 @@ const originalRender = ErrorBoundary.prototype.render; Object.defineProperty(ErrorBoundary.prototype, "render", { enumerable: false, configurable: false, - set: function() {Logger.warn("ErrorBoundary", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");}, + set: function() {Logger.warn("ErrorBoundary", "Addon policy for plugins https://docs.betterdiscord.app/plugins/introduction/guidelines#scope");}, get: () => originalRender }); diff --git a/renderer/src/ui/modals.js b/renderer/src/ui/modals.js index 06da2551..6233acb8 100644 --- a/renderer/src/ui/modals.js +++ b/renderer/src/ui/modals.js @@ -199,7 +199,7 @@ export default class Modals { onCloseCallback: () => { if (props?.transitionState === 2) onClose?.(); } - }, props), React.createElement(ErrorBoundary, {}, content))); + }, props), React.createElement(ErrorBoundary, {id: "showConfirmationModal", name: "Modals"}, content))); }, {modalKey: key}); return modalKey; } @@ -214,13 +214,13 @@ export default class Modals { themeErrors: Array.isArray(themeErrors) ? themeErrors : [] }; this.openModal(props => { - return React.createElement(ErrorBoundary, null, React.createElement(AddonErrorModal, Object.assign(options, props))); + return React.createElement(ErrorBoundary, {id: "showAddonErrors", name: "Modals"}, React.createElement(AddonErrorModal, Object.assign(options, props))); }); } static showChangelogModal(options = {}) { const key = this.openModal(props => { - return React.createElement(ErrorBoundary, null, React.createElement(ChangelogModal, Object.assign(options, props))); + return React.createElement(ErrorBoundary, {id: "showChangelogModal", name: "Modals"}, React.createElement(ChangelogModal, Object.assign(options, props))); }); return key; } @@ -267,7 +267,7 @@ export default class Modals { }; return this.openModal(props => { - return React.createElement(ErrorBoundary, null, React.createElement(ConfirmationModal, Object.assign(options, props), child)); + return React.createElement(ErrorBoundary, {id: "showAddonSettingsModal", name: "Modals"}, React.createElement(ConfirmationModal, Object.assign(options, props), child)); }); } @@ -276,7 +276,7 @@ export default class Modals { static makeStack() { const div = DOMManager.parseHTML(`
`); DOMManager.bdBody.append(div); - ReactDOM.render(, div); + ReactDOM.render(, div); this.hasInitialized = true; } diff --git a/renderer/src/ui/settings/addonlist.jsx b/renderer/src/ui/settings/addonlist.jsx index cf44d781..77527506 100644 --- a/renderer/src/ui/settings/addonlist.jsx +++ b/renderer/src/ui/settings/addonlist.jsx @@ -183,7 +183,9 @@ export default function AddonList({prefix, type, title, folder, addonList, addon return sorted.map(addon => { const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function"; const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance); - return triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />; + return + triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /> + ; }); }, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, prefix, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps