feat(ux): Sticky Save/Cancel buttons on forms (#38)
- Add sticky positioning to form action buttons (always visible at bottom) - Add Ctrl+S / Cmd+S keyboard shortcut to save forms - Create reusable useFormShortcuts hook - Apply to all create forms: spool, filament, vendor Fixes: https://github.com/Donkie/Spoolman/issues/274 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { PlusOutlined } from "@ant-design/icons";
|
|||||||
import TextArea from "antd/es/input/TextArea";
|
import TextArea from "antd/es/input/TextArea";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
||||||
import { FilamentImportModal } from "../../components/filamentImportModal";
|
import { FilamentImportModal } from "../../components/filamentImportModal";
|
||||||
import { VendorCreateModal } from "../../components/vendorCreateModal";
|
import { VendorCreateModal } from "../../components/vendorCreateModal";
|
||||||
@@ -20,6 +20,7 @@ import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from ".
|
|||||||
import { ExternalFilament } from "../../utils/queryExternalDB";
|
import { ExternalFilament } from "../../utils/queryExternalDB";
|
||||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||||
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
|
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
|
||||||
|
import { useFormShortcuts } from "../../utils/useFormShortcuts";
|
||||||
import { getOrCreateVendorFromExternal } from "../vendors/functions";
|
import { getOrCreateVendorFromExternal } from "../vendors/functions";
|
||||||
import { IVendor } from "../vendors/model";
|
import { IVendor } from "../vendors/model";
|
||||||
import { IFilament, IFilamentParsedExtras } from "./model";
|
import { IFilament, IFilamentParsedExtras } from "./model";
|
||||||
@@ -83,6 +84,12 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
|
|||||||
redirect(redirectTo);
|
redirect(redirectTo);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keyboard shortcut: Ctrl+S / Cmd+S to save
|
||||||
|
const handleSaveShortcut = useCallback(() => {
|
||||||
|
handleSubmit("list");
|
||||||
|
}, []);
|
||||||
|
useFormShortcuts(handleSaveShortcut);
|
||||||
|
|
||||||
const { selectProps: vendorSelect } = useSelect<IVendor>({
|
const { selectProps: vendorSelect } = useSelect<IVendor>({
|
||||||
resource: "vendor",
|
resource: "vendor",
|
||||||
optionLabel: "name",
|
optionLabel: "name",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Alert, Button, DatePicker, Divider, Form, Input, InputNumber, Radio, Se
|
|||||||
import TextArea from "antd/es/input/TextArea";
|
import TextArea from "antd/es/input/TextArea";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
||||||
import { FilamentCreateModal } from "../../components/filamentCreateModal";
|
import { FilamentCreateModal } from "../../components/filamentCreateModal";
|
||||||
import { useSpoolmanLocations } from "../../components/otherModels";
|
import { useSpoolmanLocations } from "../../components/otherModels";
|
||||||
@@ -14,6 +14,7 @@ import "../../utils/overrides.css";
|
|||||||
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
|
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
|
||||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||||
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
|
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
|
||||||
|
import { useFormShortcuts } from "../../utils/useFormShortcuts";
|
||||||
import { createFilamentFromExternal } from "../filaments/functions";
|
import { createFilamentFromExternal } from "../filaments/functions";
|
||||||
import { IFilament } from "../filaments/model";
|
import { IFilament } from "../filaments/model";
|
||||||
import { useGetFilamentSelectOptions } from "./functions";
|
import { useGetFilamentSelectOptions } from "./functions";
|
||||||
@@ -140,6 +141,12 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
|
|||||||
redirect(redirectTo);
|
redirect(redirectTo);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keyboard shortcut: Ctrl+S / Cmd+S to save
|
||||||
|
const handleSaveShortcut = useCallback(() => {
|
||||||
|
handleSubmit("list");
|
||||||
|
}, []);
|
||||||
|
useFormShortcuts(handleSaveShortcut);
|
||||||
|
|
||||||
// Use useEffect to update the form's initialValues when the extra fields are loaded
|
// Use useEffect to update the form's initialValues when the extra fields are loaded
|
||||||
// This is necessary because the form is rendered before the extra fields are loaded
|
// This is necessary because the form is rendered before the extra fields are loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
9
client/src/pages/vendors/create.tsx
vendored
9
client/src/pages/vendors/create.tsx
vendored
@@ -4,9 +4,10 @@ import { Button, Form, Input, InputNumber, Typography } from "antd";
|
|||||||
import TextArea from "antd/es/input/TextArea";
|
import TextArea from "antd/es/input/TextArea";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import { useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
||||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||||
|
import { useFormShortcuts } from "../../utils/useFormShortcuts";
|
||||||
import { IVendor, IVendorParsedExtras } from "./model";
|
import { IVendor, IVendorParsedExtras } from "./model";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
@@ -41,6 +42,12 @@ export const VendorCreate: React.FC<IResourceComponentsProps & CreateOrCloneProp
|
|||||||
redirect(redirectTo, (values as IVendor).id);
|
redirect(redirectTo, (values as IVendor).id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keyboard shortcut: Ctrl+S / Cmd+S to save
|
||||||
|
const handleSaveShortcut = useCallback(() => {
|
||||||
|
handleSubmit("list");
|
||||||
|
}, []);
|
||||||
|
useFormShortcuts(handleSaveShortcut);
|
||||||
|
|
||||||
// Use useEffect to update the form's initialValues when the extra fields are loaded
|
// Use useEffect to update the form's initialValues when the extra fields are loaded
|
||||||
// This is necessary because the form is rendered before the extra fields are loaded
|
// This is necessary because the form is rendered before the extra fields are loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -6,3 +6,15 @@
|
|||||||
.ant-form {
|
.ant-form {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sticky Save/Cancel buttons on forms (#38) */
|
||||||
|
.ant-card-actions {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--ant-color-bg-container, #fff);
|
||||||
|
border-top: 1px solid var(--ant-color-border-secondary, #f0f0f0);
|
||||||
|
margin: 0 -24px -24px -24px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
|||||||
26
client/src/utils/useFormShortcuts.ts
Normal file
26
client/src/utils/useFormShortcuts.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to add keyboard shortcuts to forms.
|
||||||
|
* - Ctrl+S / Cmd+S: Save (calls onSave callback)
|
||||||
|
* - Escape: Cancel (calls onCancel callback if provided)
|
||||||
|
*/
|
||||||
|
export function useFormShortcuts(onSave: () => void, onCancel?: () => void) {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// Ctrl+S or Cmd+S to save
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||||
|
e.preventDefault();
|
||||||
|
onSave();
|
||||||
|
}
|
||||||
|
// Escape to cancel (if handler provided)
|
||||||
|
if (e.key === "Escape" && onCancel) {
|
||||||
|
e.preventDefault();
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [onSave, onCancel]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user