Improve errorboundary and expose more components

This commit is contained in:
Zerebos 2024-12-11 22:21:37 -05:00
parent 8ddce94f7e
commit a608786ac3
No known key found for this signature in database
5 changed files with 64 additions and 44 deletions

View File

@ -29,6 +29,9 @@ import SwitchInput from "@ui/settings/components/switch";
import TextInput from "@ui/settings/components/textbox"; import TextInput from "@ui/settings/components/textbox";
import SettingGroup from "@ui/settings/group"; import SettingGroup from "@ui/settings/group";
import ErrorBoundary from "@ui/errorboundary"; 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 bounded = new Map();
const PluginAPI = new AddonAPI(PluginManager); const PluginAPI = new AddonAPI(PluginManager);
@ -39,6 +42,31 @@ const DOMAPI = new DOM();
const ContextMenuAPI = new ContextMenu(); const ContextMenuAPI = new ContextMenu();
const DefaultLogger = new Logger(); 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. * `BdApi` is a globally (`window.BdApi`) accessible object for use by plugins and developers to make their lives easier.
* @name BdApi * @name BdApi
@ -72,21 +100,7 @@ export default class BdApi {
get UI() {return UI;} get UI() {return UI;}
get ReactUtils() {return ReactUtils;} get ReactUtils() {return ReactUtils;}
get ContextMenu() {return ContextMenuAPI;} get ContextMenu() {return ContextMenuAPI;}
Components = { get Components() {return 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;},
};
Net = {fetch}; Net = {fetch};
} }
@ -157,21 +171,7 @@ BdApi.ContextMenu = ContextMenuAPI;
* An set of react components plugins can make use of. * An set of react components plugins can make use of.
* @type Components * @type Components
*/ */
BdApi.Components = { BdApi.Components = 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;},
};
/** /**
* An instance of {@link Net} for using network related tools. * An instance of {@link Net} for using network related tools.

View File

@ -189,7 +189,7 @@ const UI = {
buildSettingsPanel({settings, onChange, onDrawerToggle, getDrawerState}) { buildSettingsPanel({settings, onChange, onDrawerToggle, getDrawerState}) {
if (!settings?.length) throw new Error("No settings provided!"); 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.id || !setting.type) throw new Error(`Setting item missing id or type`);
if (setting.type === "category") { if (setting.type === "category") {

View File

@ -4,19 +4,37 @@ import IPC from "@modules/ipc";
export default class ErrorBoundary extends React.Component { 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) { constructor(props) {
super(props); super(props);
this.state = {hasError: false}; this.state = {hasError: false};
} }
componentDidCatch(error) { componentDidCatch(error) {
this.setState({hasError: true}); this.setState({hasError: true});
if (typeof this.props.onError === "function") this.props.onError(error); 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() { render() {
if (this.state.hasError && !this.props.hideError) return <div onClick={() => IPC.openDevTools()} className="react-error">There was an unexpected Error. Click to open console for more details.</div>; if (this.state.hasError && this.props.fallback) {
return this.props.children; return this.props.fallback;
}
else if (this.state.hasError && !this.props.hideError) {
return <div onClick={() => IPC.openDevTools()} className="react-error">
There was an unexpected Error. Click to open console for more details.
</div>;
}
return this.props.children;
} }
} }
@ -24,6 +42,6 @@ const originalRender = ErrorBoundary.prototype.render;
Object.defineProperty(ErrorBoundary.prototype, "render", { Object.defineProperty(ErrorBoundary.prototype, "render", {
enumerable: false, enumerable: false,
configurable: 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 get: () => originalRender
}); });

View File

@ -199,7 +199,7 @@ export default class Modals {
onCloseCallback: () => { onCloseCallback: () => {
if (props?.transitionState === 2) onClose?.(); if (props?.transitionState === 2) onClose?.();
} }
}, props), React.createElement(ErrorBoundary, {}, content))); }, props), React.createElement(ErrorBoundary, {id: "showConfirmationModal", name: "Modals"}, content)));
}, {modalKey: key}); }, {modalKey: key});
return modalKey; return modalKey;
} }
@ -214,13 +214,13 @@ export default class Modals {
themeErrors: Array.isArray(themeErrors) ? themeErrors : [] themeErrors: Array.isArray(themeErrors) ? themeErrors : []
}; };
this.openModal(props => { 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 = {}) { static showChangelogModal(options = {}) {
const key = this.openModal(props => { 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; return key;
} }
@ -267,7 +267,7 @@ export default class Modals {
}; };
return this.openModal(props => { 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() { static makeStack() {
const div = DOMManager.parseHTML(`<div id="bd-modal-container">`); const div = DOMManager.parseHTML(`<div id="bd-modal-container">`);
DOMManager.bdBody.append(div); DOMManager.bdBody.append(div);
ReactDOM.render(<ErrorBoundary hideError={true}><ModalStack /></ErrorBoundary>, div); ReactDOM.render(<ErrorBoundary id="makeStack" name="Modals" hideError={true}><ModalStack /></ErrorBoundary>, div);
this.hasInitialized = true; this.hasInitialized = true;
} }

View File

@ -183,7 +183,9 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
return sorted.map(addon => { return sorted.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function"; const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance); const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} prefix={prefix} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>; return <ErrorBoundary id={addon.id} name="AddonCard">
<AddonCard disabled={addon.partial} type={type} prefix={prefix} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />
</ErrorBoundary>;
}); });
}, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, prefix, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps }, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, prefix, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps