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 dayjs from "dayjs";
|
||||
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 { FilamentImportModal } from "../../components/filamentImportModal";
|
||||
import { VendorCreateModal } from "../../components/vendorCreateModal";
|
||||
@@ -20,6 +20,7 @@ import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from ".
|
||||
import { ExternalFilament } from "../../utils/queryExternalDB";
|
||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
|
||||
import { useFormShortcuts } from "../../utils/useFormShortcuts";
|
||||
import { getOrCreateVendorFromExternal } from "../vendors/functions";
|
||||
import { IVendor } from "../vendors/model";
|
||||
import { IFilament, IFilamentParsedExtras } from "./model";
|
||||
@@ -83,6 +84,12 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
|
||||
redirect(redirectTo);
|
||||
};
|
||||
|
||||
// Keyboard shortcut: Ctrl+S / Cmd+S to save
|
||||
const handleSaveShortcut = useCallback(() => {
|
||||
handleSubmit("list");
|
||||
}, []);
|
||||
useFormShortcuts(handleSaveShortcut);
|
||||
|
||||
const { selectProps: vendorSelect } = useSelect<IVendor>({
|
||||
resource: "vendor",
|
||||
optionLabel: "name",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Alert, Button, DatePicker, Divider, Form, Input, InputNumber, Radio, Se
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import dayjs from "dayjs";
|
||||
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 { FilamentCreateModal } from "../../components/filamentCreateModal";
|
||||
import { useSpoolmanLocations } from "../../components/otherModels";
|
||||
@@ -14,6 +14,7 @@ import "../../utils/overrides.css";
|
||||
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
|
||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
|
||||
import { useFormShortcuts } from "../../utils/useFormShortcuts";
|
||||
import { createFilamentFromExternal } from "../filaments/functions";
|
||||
import { IFilament } from "../filaments/model";
|
||||
import { useGetFilamentSelectOptions } from "./functions";
|
||||
@@ -140,6 +141,12 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
|
||||
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
|
||||
// This is necessary because the form is rendered before the extra fields are loaded
|
||||
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 dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||
import { useFormShortcuts } from "../../utils/useFormShortcuts";
|
||||
import { IVendor, IVendorParsedExtras } from "./model";
|
||||
|
||||
dayjs.extend(utc);
|
||||
@@ -41,6 +42,12 @@ export const VendorCreate: React.FC<IResourceComponentsProps & CreateOrCloneProp
|
||||
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
|
||||
// This is necessary because the form is rendered before the extra fields are loaded
|
||||
useEffect(() => {
|
||||
|
||||
@@ -6,3 +6,15 @@
|
||||
.ant-form {
|
||||
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