Skip to content

Commit

Permalink
feat: add user-friendly error reporting with TResult
Browse files Browse the repository at this point in the history
  • Loading branch information
CSharperMantle committed Jul 10, 2023
1 parent c5c3392 commit 5415c0a
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 57 deletions.
18 changes: 18 additions & 0 deletions src/common/TResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (C) 2021-present Rong "Mantle" Bao
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see https://www.gnu.org/licenses/ .
*/

export type TResult<T, E> = { ok: true; value?: T } | { ok: false; err?: E }
3 changes: 2 additions & 1 deletion src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { waitForEvent } from "./waitForEvent"

import type { ISize } from "./ISize"
import type { TPosition } from "./TPosition"
import type { TResult } from "./TResult"

export {
AutoplaySentinel,
Expand All @@ -61,4 +62,4 @@ export {
rearrange,
waitForEvent,
}
export type { ISize, TPosition }
export type { ISize, TPosition, TResult }
13 changes: 8 additions & 5 deletions src/components/FileFormControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import TextField from "@mui/material/TextField"
import Tooltip from "@mui/material/Tooltip"
import styled from "@mui/system/styled"

import { isNil } from "../common"
import type { TResult } from "../common"

const HiddenInput = styled("input")({
display: "none",
Expand Down Expand Up @@ -79,13 +79,14 @@ interface IFileFormControlProps {
readonly helperText: string
readonly readOnly?: boolean
readonly disabled?: boolean
readonly onFileChange: (newContent: string) => boolean | void
readonly onFileChange: (newContent: string) => TResult<never, string>
readonly contentPreprocessor?: (content: string) => string
}

export const FileFormControl = (props: IFileFormControlProps) => {
const [fileContent, setFileContent] = React.useState(props.initialFileContent)
const [error, setError] = React.useState(false)
const [helperText, setHelperText] = React.useState(props.helperText)

return (
<Grid container spacing={0} direction="row" alignItems="center">
Expand All @@ -95,7 +96,7 @@ export const FileFormControl = (props: IFileFormControlProps) => {
fullWidth
value={fileContent}
label={props.label}
helperText={props.helperText}
helperText={helperText}
disabled={props.disabled ?? false}
error={error}
InputProps={{
Expand All @@ -113,9 +114,11 @@ export const FileFormControl = (props: IFileFormControlProps) => {
let content = (await head(event.target.files)?.text()) ?? ""
content = props.contentPreprocessor?.(content) ?? content
const result = props.onFileChange(content)
const isSuccessful = isNil(result) || result
setFileContent(content)
setError(!isSuccessful)
setError(!result.ok)
setHelperText(
result.ok ? props.helperText : result.err ?? props.helperText
)
}}
/>
</Grid>
Expand Down
14 changes: 10 additions & 4 deletions src/components/NumberFormControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import TextField from "@mui/material/TextField"

import { isNil } from "../common"

import type { TResult } from "../common"

interface INumberFormControlProps {
readonly id: string
readonly initialContent: string
Expand All @@ -34,23 +36,25 @@ interface INumberFormControlProps {
readonly min?: number
readonly max?: number
readonly disabled?: boolean
readonly onChange: (newContent: string) => boolean | void
readonly onChange: (newContent: string) => TResult<never, string>
readonly contentPreprocessor?: (content: string) => string
}

export const NumberFormControl = (props: INumberFormControlProps) => {
const [content, setContent] = React.useState(props.initialContent)
const [error, setError] = React.useState(false)
const [helperText, setHelperText] = React.useState(props.helperText)

return (
<TextField
id={props.id}
value={content}
label={props.label}
type="number"
helperText={props.helperText}
helperText={helperText}
InputProps={{ ...props.adornments }}
inputProps={{
inputMode: "numeric",
step: props.step,
min: props.min,
max: props.max,
Expand All @@ -62,9 +66,11 @@ export const NumberFormControl = (props: INumberFormControlProps) => {
? event.target.value
: props.contentPreprocessor(event.target.value)
const result = props.onChange(content)
const isSuccessful = isNil(result) || result
setContent(content)
setError(!isSuccessful)
setError(!result.ok)
setHelperText(
result.ok ? props.helperText : result.err ?? props.helperText
)
}}
/>
)
Expand Down
70 changes: 23 additions & 47 deletions src/pages/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@ import { queryPath } from "../common"
import { CommonHead, FileFormControl, NumberFormControl } from "../components"
import { customizationFacade } from "../customization"

const INT_REGEX = /^[0-9]+$/

const tryParseInt = (s: string): readonly [boolean, number] => {
const v = parseInt(s, 10)
return [!isNaN(v), v]
const isInt = INT_REGEX.test(s)
const v = isInt ? parseInt(s, 10) : Number.NaN
return [isInt, v]
}

const App = ({ data }: PageProps<Queries.SettingsPageQuery>) => {
Expand Down Expand Up @@ -172,12 +175,9 @@ const App = ({ data }: PageProps<Queries.SettingsPageQuery>) => {
const [isValid, value] = tryParseInt(newContent)
if (isValid) {
customizationFacade.settings.gridLineThickness = value
return true
return { ok: true }
} else {
enqueueSnackbar(t("msg_int_expected"), {
variant: "error",
})
return false
return { ok: false, err: t("msg_int_expected") }
}
}}
/>
Expand Down Expand Up @@ -210,12 +210,9 @@ const App = ({ data }: PageProps<Queries.SettingsPageQuery>) => {
const obj = JSON.parse(newContent)
if (validateColorScheme(obj)) {
customizationFacade.settings.colorScheme = obj
return true
return { ok: true }
} else {
enqueueSnackbar(t("msg_color_scheme_invalid"), {
variant: "error",
})
return false
return { ok: false, err: t("msg_color_scheme_invalid") }
}
}}
contentPreprocessor={jsonMinifyPreprocessor}
Expand Down Expand Up @@ -245,12 +242,9 @@ const App = ({ data }: PageProps<Queries.SettingsPageQuery>) => {
const obj = JSON.parse(newContent)
if (validateMap(obj)) {
customizationFacade.settings.gameMap = obj
return true
return { ok: true }
} else {
enqueueSnackbar(t("msg_game_map_invalid"), {
variant: "error",
})
return false
return { ok: false, err: t("msg_game_map_invalid") }
}
}}
contentPreprocessor={jsonMinifyPreprocessor}
Expand All @@ -270,12 +264,9 @@ const App = ({ data }: PageProps<Queries.SettingsPageQuery>) => {
if (isValid) {
customizationFacade.settings.gameUpdateIntervalMilliseconds =
value
return true
return { ok: true }
} else {
enqueueSnackbar(t("msg_int_expected"), {
variant: "error",
})
return false
return { ok: false, err: t("msg_int_expected") }
}
}}
/>
Expand Down Expand Up @@ -304,12 +295,9 @@ const App = ({ data }: PageProps<Queries.SettingsPageQuery>) => {
const [isValid, value] = tryParseInt(newContent)
if (isValid) {
customizationFacade.settings.swipeThreshold = value
return true
return { ok: true }
} else {
enqueueSnackbar(t("msg_int_expected"), {
variant: "error",
})
return false
return { ok: false, err: t("msg_int_expected") }
}
}}
/>
Expand All @@ -327,12 +315,9 @@ const App = ({ data }: PageProps<Queries.SettingsPageQuery>) => {
const [isValid, value] = tryParseInt(newContent)
if (isValid) {
customizationFacade.settings.swipeDeltaX = value
return true
return { ok: true }
} else {
enqueueSnackbar(t("msg_int_expected"), {
variant: "error",
})
return false
return { ok: false, err: t("msg_int_expected") }
}
}}
/>
Expand All @@ -350,12 +335,9 @@ const App = ({ data }: PageProps<Queries.SettingsPageQuery>) => {
const [isValid, value] = tryParseInt(newContent)
if (isValid) {
customizationFacade.settings.swipeDeltaY = value
return true
return { ok: true }
} else {
enqueueSnackbar(t("msg_int_expected"), {
variant: "error",
})
return false
return { ok: false, err: t("msg_int_expected") }
}
}}
/>
Expand All @@ -373,12 +355,9 @@ const App = ({ data }: PageProps<Queries.SettingsPageQuery>) => {
const [isValid, value] = tryParseInt(newContent)
if (isValid) {
customizationFacade.settings.pressThreshold = value
return true
return { ok: true }
} else {
enqueueSnackbar(t("msg_int_expected"), {
variant: "error",
})
return false
return { ok: false, err: t("msg_int_expected") }
}
}}
/>
Expand All @@ -404,12 +383,9 @@ const App = ({ data }: PageProps<Queries.SettingsPageQuery>) => {
const [isValid, value] = tryParseInt(newContent)
if (isValid) {
customizationFacade.settings.concurrency = value
return true
return { ok: true }
} else {
enqueueSnackbar(t("msg_int_expected"), {
variant: "error",
})
return false
return { ok: false, err: t("msg_int_expected") }
}
}}
/>
Expand Down

0 comments on commit 5415c0a

Please sign in to comment.