implement a config error button + message modal that shows the errors (#1030)

This commit is contained in:
Mike Sawka 2024-10-14 14:57:12 -07:00 committed by GitHub
parent a15b339f39
commit a629b28194
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 183 additions and 5 deletions

View File

@ -0,0 +1,6 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.message-modal {
min-width: 400px;
}

View File

@ -0,0 +1,24 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Modal } from "@/app/modals/modal";
import { modalsModel } from "@/app/store/modalmodel";
import { ReactNode } from "react";
import "./messagemodal.less";
const MessageModal = ({ children }: { children: ReactNode }) => {
function closeModal() {
modalsModel.popModal();
}
return (
<Modal className="message-modal" onOk={() => closeModal()} onClose={() => closeModal()}>
{children}
</Modal>
);
};
MessageModal.displayName = "MessageModal";
export { MessageModal };

View File

@ -1,6 +1,7 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { MessageModal } from "@/app/modals/messagemodal";
import { AboutModal } from "./about"; import { AboutModal } from "./about";
import { TosModal } from "./tos"; import { TosModal } from "./tos";
import { UserInputModal } from "./userinputmodal"; import { UserInputModal } from "./userinputmodal";
@ -9,6 +10,7 @@ const modalRegistry: { [key: string]: React.ComponentType<any> } = {
[TosModal.displayName || "TosModal"]: TosModal, [TosModal.displayName || "TosModal"]: TosModal,
[UserInputModal.displayName || "UserInputModal"]: UserInputModal, [UserInputModal.displayName || "UserInputModal"]: UserInputModal,
[AboutModal.displayName || "AboutModal"]: AboutModal, [AboutModal.displayName || "AboutModal"]: AboutModal,
[MessageModal.displayName || "MessageModal"]: MessageModal,
}; };
export const getModalComponent = (key: string): React.ComponentType<any> | undefined => { export const getModalComponent = (key: string): React.ComponentType<any> | undefined => {

View File

@ -12,6 +12,16 @@
} }
} }
.config-error-message {
max-width: 500px;
h3 {
margin-bottom: 10px;
}
margin-bottom: 20px;
}
.tab-bar-wrapper { .tab-bar-wrapper {
position: relative; position: relative;
user-select: none; user-select: none;
@ -53,6 +63,13 @@
color: var(--accent-color); color: var(--accent-color);
} }
.config-error-button {
height: 80%;
margin: auto 4px;
color: black;
flex: 0 0 fit-content;
}
.add-tab-btn { .add-tab-btn {
width: 22px; width: 22px;
height: 100%; height: 100%;

View File

@ -1,6 +1,8 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Button } from "@/app/element/button";
import { modalsModel } from "@/app/store/modalmodel";
import { WindowDrag } from "@/element/windowdrag"; import { WindowDrag } from "@/element/windowdrag";
import { deleteLayoutModelForTab } from "@/layout/index"; import { deleteLayoutModelForTab } from "@/layout/index";
import { atoms, getApi, isDev, PLATFORM } from "@/store/global"; import { atoms, getApi, isDev, PLATFORM } from "@/store/global";
@ -37,6 +39,69 @@ interface TabBarProps {
workspace: Workspace; workspace: Workspace;
} }
const ConfigErrorMessage = () => {
const fullConfig = useAtomValue(atoms.fullConfigAtom);
if (fullConfig?.configerrors == null || fullConfig?.configerrors.length == 0) {
return (
<div className="config-error-message">
<h3>Configuration Clean</h3>
<p>There are no longer any errors detected in your config.</p>
</div>
);
}
if (fullConfig?.configerrors.length == 1) {
const singleError = fullConfig.configerrors[0];
return (
<div className="config-error-message">
<h3>Configuration Error</h3>
<div>
{singleError.file}: {singleError.err}
</div>
</div>
);
}
return (
<div className="config-error-message">
<h3>Configuration Error</h3>
<ul>
{fullConfig.configerrors.map((error, index) => (
<li key={index}>
{error.file}: {error.err}
</li>
))}
</ul>
</div>
);
};
const ConfigErrorIcon = ({ buttonRef }: { buttonRef: React.RefObject<HTMLElement> }) => {
const fullConfig = useAtomValue(atoms.fullConfigAtom);
function handleClick() {
modalsModel.pushModal("MessageModal", { children: <ConfigErrorMessage /> });
}
if (fullConfig?.configerrors == null || fullConfig?.configerrors.length == 0) {
return null;
}
return (
<Button
ref={buttonRef as React.RefObject<HTMLButtonElement>}
className="config-error-button red"
onClick={handleClick}
>
<i className="fa fa-solid fa-exclamation-triangle" />
Config Error
</Button>
);
return (
<div className="config-error" ref={buttonRef as React.RefObject<HTMLDivElement>}>
<i className="fa fa-solid fa-exclamation-triangle" />
</div>
);
};
const TabBar = React.memo(({ workspace }: TabBarProps) => { const TabBar = React.memo(({ workspace }: TabBarProps) => {
const [tabIds, setTabIds] = useState<string[]>([]); const [tabIds, setTabIds] = useState<string[]>([]);
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]); const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
@ -67,6 +132,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
const tabWidthRef = useRef<number>(TAB_DEFAULT_WIDTH); const tabWidthRef = useRef<number>(TAB_DEFAULT_WIDTH);
const scrollableRef = useRef<boolean>(false); const scrollableRef = useRef<boolean>(false);
const updateStatusButtonRef = useRef<HTMLButtonElement>(null); const updateStatusButtonRef = useRef<HTMLButtonElement>(null);
const configErrorButtonRef = useRef<HTMLElement>(null);
const prevAllLoadedRef = useRef<boolean>(false); const prevAllLoadedRef = useRef<boolean>(false);
const windowData = useAtomValue(atoms.waveWindow); const windowData = useAtomValue(atoms.waveWindow);
@ -124,8 +190,10 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
const windowDragLeftWidth = draggerLeftRef.current.getBoundingClientRect().width; const windowDragLeftWidth = draggerLeftRef.current.getBoundingClientRect().width;
const addBtnWidth = addBtnRef.current.getBoundingClientRect().width; const addBtnWidth = addBtnRef.current.getBoundingClientRect().width;
const updateStatusLabelWidth = updateStatusButtonRef.current?.getBoundingClientRect().width ?? 0; const updateStatusLabelWidth = updateStatusButtonRef.current?.getBoundingClientRect().width ?? 0;
const configErrorWidth = configErrorButtonRef.current?.getBoundingClientRect().width ?? 0;
const spaceForTabs = const spaceForTabs =
tabbarWrapperWidth - (windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth + updateStatusLabelWidth); tabbarWrapperWidth -
(windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth + updateStatusLabelWidth + configErrorWidth);
const numberOfTabs = tabIds.length; const numberOfTabs = tabIds.length;
const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH; const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH;
@ -510,6 +578,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
</div> </div>
<WindowDrag ref={draggerRightRef} className="right" /> <WindowDrag ref={draggerRightRef} className="right" />
<UpdateStatusBanner buttonRef={updateStatusButtonRef} /> <UpdateStatusBanner buttonRef={updateStatusButtonRef} />
<ConfigErrorIcon buttonRef={configErrorButtonRef} />
</div> </div>
); );
}); });

View File

@ -6,7 +6,7 @@ import "./updatebanner.less";
const UpdateStatusBannerComponent = ({ buttonRef }: { buttonRef: React.RefObject<HTMLButtonElement> }) => { const UpdateStatusBannerComponent = ({ buttonRef }: { buttonRef: React.RefObject<HTMLButtonElement> }) => {
const appUpdateStatus = useAtomValue(atoms.updaterStatusAtom); const appUpdateStatus = useAtomValue(atoms.updaterStatusAtom);
const [updateStatusMessage, setUpdateStatusMessage] = useState<string>(); let [updateStatusMessage, setUpdateStatusMessage] = useState<string>();
const [dismissBannerTimeout, setDismissBannerTimeout] = useState<NodeJS.Timeout>(); const [dismissBannerTimeout, setDismissBannerTimeout] = useState<NodeJS.Timeout>();
useEffect(() => { useEffect(() => {
@ -52,7 +52,6 @@ const UpdateStatusBannerComponent = ({ buttonRef }: { buttonRef: React.RefObject
function onClick() { function onClick() {
getApi().installAppUpdate(); getApi().installAppUpdate();
} }
if (updateStatusMessage) { if (updateStatusMessage) {
return ( return (
<Button <Button

View File

@ -932,3 +932,17 @@ func WriteFileIfDifferent(fileName string, contents []byte) (bool, error) {
} }
return true, nil return true, nil
} }
func GetLineColFromOffset(barr []byte, offset int) (int, int) {
line := 1
col := 1
for i := 0; i < offset && i < len(barr); i++ {
if barr[i] == '\n' {
line++
col = 1
} else {
col++
}
}
return line, col
}

View File

@ -20,6 +20,13 @@ import (
const SettingsFile = "settings.json" const SettingsFile = "settings.json"
const AnySchema = `
{
"type": "object",
"additionalProperties": true
}
`
type MetaSettingsType struct { type MetaSettingsType struct {
waveobj.MetaMapType waveobj.MetaMapType
} }
@ -117,10 +124,37 @@ type FullConfigType struct {
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"` ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
} }
func goBackWS(barr []byte, offset int) int {
if offset >= len(barr) {
offset = offset - 1
}
for i := offset - 1; i >= 0; i-- {
if barr[i] == ' ' || barr[i] == '\t' || barr[i] == '\n' || barr[i] == '\r' {
continue
}
return i
}
return 0
}
func isTrailingCommaError(barr []byte, offset int) bool {
if offset >= len(barr) {
offset = offset - 1
}
offset = goBackWS(barr, offset)
if barr[offset] == '}' {
offset = goBackWS(barr, offset)
if barr[offset] == ',' {
return true
}
}
return false
}
func readConfigHelper(fileName string, barr []byte, readErr error) (waveobj.MetaMapType, []ConfigError) { func readConfigHelper(fileName string, barr []byte, readErr error) (waveobj.MetaMapType, []ConfigError) {
var cerrs []ConfigError var cerrs []ConfigError
if readErr != nil && !os.IsNotExist(readErr) { if readErr != nil && !os.IsNotExist(readErr) {
cerrs = append(cerrs, ConfigError{File: "defaults:" + fileName, Err: readErr.Error()}) cerrs = append(cerrs, ConfigError{File: fileName, Err: readErr.Error()})
} }
if len(barr) == 0 { if len(barr) == 0 {
return nil, cerrs return nil, cerrs
@ -128,7 +162,20 @@ func readConfigHelper(fileName string, barr []byte, readErr error) (waveobj.Meta
var rtn waveobj.MetaMapType var rtn waveobj.MetaMapType
err := json.Unmarshal(barr, &rtn) err := json.Unmarshal(barr, &rtn)
if err != nil { if err != nil {
cerrs = append(cerrs, ConfigError{File: "defaults:" + fileName, Err: err.Error()}) if syntaxErr, ok := err.(*json.SyntaxError); ok {
offset := syntaxErr.Offset
if offset > 0 {
offset = offset - 1
}
lineNum, colNum := utilfn.GetLineColFromOffset(barr, int(offset))
isTrailingComma := isTrailingCommaError(barr, int(offset))
if isTrailingComma {
err = fmt.Errorf("json syntax error at line %d, col %d: probably an extra trailing comma: %v", lineNum, colNum, syntaxErr)
} else {
err = fmt.Errorf("json syntax error at line %d, col %d: %v", lineNum, colNum, syntaxErr)
}
}
cerrs = append(cerrs, ConfigError{File: fileName, Err: err.Error()})
} }
return rtn, cerrs return rtn, cerrs
} }