Major frontend code cleanup (#452)

Many large changes, including:
- TypeScript enums are now string unions throughout
- Strong type-checking throughout the TS and Vue codebase
- Vue component props now all specify `as PropType<...>`
- Usage of annotated return types on all functions
- Sorting of JS import statements
- Explicit usage of Vue bind attribute function call arguments (`@click="foo"` is now `@click=(e) => foo(e)`)
- Much improved code quality related to the color picker
- Consistent camelCase Vue bind and v-model attributes
- Consistent Vue HTML attribute strings with single quotes
- Bug fix and clarity improvement with incorrect hint class parameters
- Empty Vue component objects like `props: {}` and `components: {}` removed
This commit is contained in:
Keavon Chambers
2022-01-02 06:00:02 -08:00
parent 6662a9a04f
commit 2c8d70acb4
53 changed files with 842 additions and 946 deletions
+12 -11
View File
@@ -1,10 +1,11 @@
import { reactive, readonly } from "vue";
import { TextButtonWidget } from "@/components/widgets/widgets";
import { EditorState } from "@/state/wasm-loader";
import { DisplayAboutGraphiteDialog } from "@/dispatcher/js-messages";
import { EditorState } from "@/state/wasm-loader";
import { stripIndents } from "@/utilities/strip-indents";
import { TextButtonWidget } from "@/utilities/widgets";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createDialogState(editor: EditorState) {
const state = reactive({
visible: false,
@@ -14,7 +15,7 @@ export function createDialogState(editor: EditorState) {
buttons: [] as TextButtonWidget[],
});
const createDialog = (icon: string, heading: string, details: string, buttons: TextButtonWidget[]) => {
const createDialog = (icon: string, heading: string, details: string, buttons: TextButtonWidget[]): void => {
state.visible = true;
state.icon = icon;
state.heading = heading;
@@ -22,11 +23,11 @@ export function createDialogState(editor: EditorState) {
state.buttons = buttons;
};
const dismissDialog = () => {
const dismissDialog = (): void => {
state.visible = false;
};
const submitDialog = () => {
const submitDialog = (): void => {
const firstEmphasizedButton = state.buttons.find((button) => button.props.emphasized && button.callback);
if (firstEmphasizedButton) {
// If statement satisfies TypeScript
@@ -38,7 +39,7 @@ export function createDialogState(editor: EditorState) {
return state.visible;
};
const comingSoon = (issueNumber?: number) => {
const comingSoon = (issueNumber?: number): void => {
const bugMessage = `— but you can help add it!\nSee issue #${issueNumber} on GitHub.`;
const details = `This feature is not implemented yet${issueNumber ? bugMessage : ""}`;
@@ -58,7 +59,7 @@ export function createDialogState(editor: EditorState) {
createDialog("Warning", "Coming soon", details, buttons);
};
const onAboutHandler = () => {
const onAboutHandler = (): void => {
const date = new Date(process.env.VUE_APP_COMMIT_DATE || "");
const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
const timeString = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
@@ -80,22 +81,22 @@ export function createDialogState(editor: EditorState) {
const buttons: TextButtonWidget[] = [
{
kind: "TextButton",
callback: () => window.open("https://www.graphite.design", "_blank"),
callback: (): unknown => window.open("https://www.graphite.design", "_blank"),
props: { label: "Website", emphasized: false, minWidth: 0 },
},
{
kind: "TextButton",
callback: () => window.open("https://github.com/GraphiteEditor/Graphite/graphs/contributors", "_blank"),
callback: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite/graphs/contributors", "_blank"),
props: { label: "Credits", emphasized: false, minWidth: 0 },
},
{
kind: "TextButton",
callback: () => window.open("https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/LICENSE.txt", "_blank"),
callback: (): unknown => window.open("https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/LICENSE.txt", "_blank"),
props: { label: "License", emphasized: false, minWidth: 0 },
},
{
kind: "TextButton",
callback: () => window.open("/third-party-licenses.txt", "_blank"),
callback: (): unknown => window.open("/third-party-licenses.txt", "_blank"),
props: { label: "Third-Party Licenses", emphasized: false, minWidth: 0 },
},
];
+11 -10
View File
@@ -1,9 +1,6 @@
/* eslint-disable max-classes-per-file */
import { reactive, readonly } from "vue";
import { DialogState } from "@/state/dialog";
import { download, upload } from "@/utilities/files";
import { EditorState } from "@/state/wasm-loader";
import {
DisplayConfirmationToCloseAllDocuments,
DisplayConfirmationToCloseDocument,
@@ -14,7 +11,11 @@ import {
SetActiveDocument,
UpdateOpenDocumentsList,
} from "@/dispatcher/js-messages";
import { DialogState } from "@/state/dialog";
import { EditorState } from "@/state/wasm-loader";
import { download, upload } from "@/utilities/files";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createDocumentsState(editor: EditorState, dialogState: DialogState) {
const state = reactive({
unsaved: false,
@@ -22,7 +23,7 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
activeDocumentIndex: 0,
});
const closeDocumentWithConfirmation = async (documentId: BigInt) => {
const closeDocumentWithConfirmation = async (documentId: BigInt): Promise<void> => {
// Assume we receive a correct document_id
const targetDocument = state.documents.find((doc) => doc.id === documentId) as FrontendDocumentDetails;
const tabLabel = targetDocument.displayName;
@@ -31,7 +32,7 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
dialogState.createDialog("File", "Save changes before closing?", tabLabel, [
{
kind: "TextButton",
callback: async () => {
callback: async (): Promise<void> => {
editor.instance.save_document();
dialogState.dismissDialog();
},
@@ -39,7 +40,7 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
},
{
kind: "TextButton",
callback: async () => {
callback: async (): Promise<void> => {
editor.instance.close_document(targetDocument.id);
dialogState.dismissDialog();
},
@@ -47,7 +48,7 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
},
{
kind: "TextButton",
callback: async () => {
callback: async (): Promise<void> => {
dialogState.dismissDialog();
},
props: { label: "Cancel", minWidth: 96 },
@@ -55,11 +56,11 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
]);
};
const closeAllDocumentsWithConfirmation = () => {
const closeAllDocumentsWithConfirmation = (): void => {
dialogState.createDialog("Copy", "Close all documents?", "Unsaved work will be lost!", [
{
kind: "TextButton",
callback: () => {
callback: (): void => {
editor.instance.close_all_documents();
dialogState.dismissDialog();
},
@@ -67,7 +68,7 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
},
{
kind: "TextButton",
callback: () => {
callback: (): void => {
dialogState.dismissDialog();
},
props: { label: "Cancel", minWidth: 96 },
+5 -4
View File
@@ -1,12 +1,13 @@
import { reactive, readonly } from "vue";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createFullscreenState() {
const state = reactive({
windowFullscreen: false,
keyboardLocked: false,
});
const fullscreenModeChanged = () => {
const fullscreenModeChanged = (): void => {
state.windowFullscreen = Boolean(document.fullscreenElement);
if (!state.windowFullscreen) state.keyboardLocked = false;
};
@@ -15,7 +16,7 @@ export function createFullscreenState() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const keyboardLockApiSupported: Readonly<boolean> = "keyboard" in navigator && "lock" in (navigator as any).keyboard;
const enterFullscreen = async () => {
const enterFullscreen = async (): Promise<void> => {
await document.documentElement.requestFullscreen();
if (keyboardLockApiSupported) {
@@ -26,11 +27,11 @@ export function createFullscreenState() {
};
// eslint-disable-next-line class-methods-use-this
const exitFullscreen = async () => {
const exitFullscreen = async (): Promise<void> => {
await document.exitFullscreen();
};
const toggleFullscreen = async () => {
const toggleFullscreen = async (): Promise<void> => {
if (state.windowFullscreen) await exitFullscreen();
else await enterFullscreen();
};
+7 -6
View File
@@ -7,7 +7,7 @@ export type WasmInstance = typeof import("@/../wasm/pkg");
export type RustEditorInstance = InstanceType<WasmInstance["JsEditorHandle"]>;
let wasmImport: WasmInstance | null = null;
export async function initWasm() {
export async function initWasm(): Promise<void> {
if (wasmImport !== null) return;
// Separating in two lines satisfies typescript when used below
@@ -32,7 +32,7 @@ function panicProxy<T extends object>(module: T): T {
// Special handling to wrap the return of a constructor in the proxy
const isClass = isFunction && /^\s*class\s+/.test(targetValue.toString());
if (isClass) {
return function (...args: unknown[]) {
return function (...args: unknown[]): unknown {
// eslint-disable-next-line new-cap
const result = new targetValue(...args);
return panicProxy(result);
@@ -40,7 +40,7 @@ function panicProxy<T extends object>(module: T): T {
}
// Replace the original function with a wrapper function that runs the original in a try-catch block
return function (...args: unknown[]) {
return function (...args: unknown[]): unknown {
let result;
try {
// @ts-expect-error TypeScript does not know what `this` is, since it should be able to be anything
@@ -57,16 +57,17 @@ function panicProxy<T extends object>(module: T): T {
return new Proxy<T>(module, proxyHandler);
}
function getWasmInstance() {
function getWasmInstance(): WasmInstance {
if (wasmImport) return wasmImport;
throw new Error("Editor WASM backend was not initialized at application startup");
}
export function createEditorState() {
type CreateEditorStateType = { dispatcher: ReturnType<typeof createJsDispatcher>; rawWasm: WasmInstance; instance: RustEditorInstance };
export function createEditorState(): CreateEditorStateType {
const dispatcher = createJsDispatcher();
const rawWasm = getWasmInstance();
const rustCallback = (messageType: JsMessageType, data: Record<string, unknown>) => {
const rustCallback = (messageType: JsMessageType, data: Record<string, unknown>): void => {
dispatcher.handleJsMessage(messageType, data, rawWasm, instance);
};