mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-08-26 13:06:32 +00:00
refactor(app): unify the header save action on AppHeaderAction with a loading prop
The four detail-page saves rendered two ways: knowledge as AppHeaderAction (a primary CTA that collapses to an icon on mobile), the other three as a secondary FormSubmitButton. FormSubmitButton buys nothing here — all three sit in the header, outside the form's FormProvider, so its useFormContext read returns null and they already submit via form= + a manual loading prop. Its real value is the in-<Form> subscription, which its 8 dialog/auth consumers keep using untouched. AppHeaderAction gains an optional loading (icon->spinner + disable); the three header saves become AppHeaderAction like knowledge, so all four are now the same primary CTA that collapses to an icon-only button (aria-label preserved) below md. type="submit" is explicit because Button defaults type to "button". Verified at 480px: label hidden, 32px icon button, aria-label "Create"; at 1440: primary fill, label shown. Researched the alternatives first: React 19 useFormStatus can't read a form=-associated button (react.dev; facebook/react#27980) and targets native form actions not RHF; and a Radix Slot asChild compose throws on FormSubmitButton's two-child array and can't inject the spinner past child-wins prop merging. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
672851ef32
commit
1adcbec0f9
@@ -1,5 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb';
|
||||
import { Button, type ButtonProps } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
@@ -10,6 +12,7 @@ interface AppHeaderActionProps extends Omit<ButtonProps, 'children'> {
|
||||
endIcon?: ReactNode;
|
||||
icon: ReactNode;
|
||||
label: ReactNode;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function AppHeader({ children, className }: { children: ReactNode; className?: string }) {
|
||||
@@ -28,9 +31,11 @@ export function AppHeader({ children, className }: { children: ReactNode; classN
|
||||
export function AppHeaderAction({
|
||||
'aria-label': ariaLabel,
|
||||
className,
|
||||
disabled,
|
||||
endIcon,
|
||||
icon,
|
||||
label,
|
||||
loading = false,
|
||||
size = 'sm',
|
||||
...props
|
||||
}: AppHeaderActionProps) {
|
||||
@@ -40,10 +45,11 @@ export function AppHeaderAction({
|
||||
<Button
|
||||
aria-label={accessibleLabel}
|
||||
className={cn('w-8 px-0 md:w-auto md:px-3', className)}
|
||||
disabled={disabled || loading}
|
||||
size={size}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
{loading ? <Loader2 className="size-4 animate-spin" /> : icon}
|
||||
<span className="hidden md:inline">{label}</span>
|
||||
{endIcon ? <span className="hidden md:inline-flex">{endIcon}</span> : null}
|
||||
</Button>
|
||||
|
||||
@@ -16,7 +16,6 @@ import { AppHeaderAction } from '@/components/layouts/app/app-header';
|
||||
import { type EditorViewMode } from '@/components/shared/markdown-editor';
|
||||
import { UnsavedChangesDialog, useUnsavedChangesGuard } from '@/components/shared/unsaved-changes';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { AnonymizeTextDocument, KnowledgeAnswerType, KnowledgeDocType, KnowledgeGuideType } from '@/graphql/types';
|
||||
import { useAppForm } from '@/hooks/use-app-form';
|
||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||
@@ -319,8 +318,9 @@ export function KnowledgeForm({ initialValues, isNew, knowledge, onSubmit }: Kno
|
||||
const saveButton = (
|
||||
<AppHeaderAction
|
||||
disabled={!canSubmit}
|
||||
icon={isSaving ? <Spinner variant="circle" /> : <Save aria-hidden="true" />}
|
||||
icon={<Save aria-hidden="true" />}
|
||||
label={isNew ? 'Create' : 'Save'}
|
||||
loading={isSaving}
|
||||
type="submit"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -71,7 +71,6 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
|
||||
import { Form, FormControl, FormItem, FormMessage } from '@/components/ui/form';
|
||||
import { FormSubmitButton } from '@/components/ui/form-submit-button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import {
|
||||
@@ -675,15 +674,13 @@ function SettingsPrompt() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
/>
|
||||
<FormSubmitButton
|
||||
<AppHeaderAction
|
||||
form={activeFormId}
|
||||
icon={<Save className="size-4" />}
|
||||
label="Save"
|
||||
loading={isLoading}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
Save
|
||||
</FormSubmitButton>
|
||||
type="submit"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||
import { FormSubmitButton } from '@/components/ui/form-submit-button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
@@ -1881,15 +1880,13 @@ function SettingsProvider() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
/>
|
||||
<FormSubmitButton
|
||||
<AppHeaderAction
|
||||
form="provider-form"
|
||||
icon={<Save className="size-4" />}
|
||||
label={isNew ? 'Create' : 'Save'}
|
||||
loading={isLoading}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
{isNew ? 'Create' : 'Save'}
|
||||
</FormSubmitButton>
|
||||
type="submit"
|
||||
/>
|
||||
{!isNew && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppHeader, AppHeaderActions, AppHeaderContent } from '@/components/layouts/app/app-header';
|
||||
import { AppHeader, AppHeaderAction, AppHeaderActions, AppHeaderContent } from '@/components/layouts/app/app-header';
|
||||
import ConfirmationDialog from '@/components/shared/confirmation-dialog';
|
||||
import {
|
||||
DetailNavigationButtons,
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||
import { FormSubmitButton } from '@/components/ui/form-submit-button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
@@ -476,16 +475,14 @@ function Template() {
|
||||
/>
|
||||
)}
|
||||
{(isNew || !!templateData?.flowTemplate) && (
|
||||
<FormSubmitButton
|
||||
disabled={isSaving || (!isNew && !hasUnsavedChanges)}
|
||||
<AppHeaderAction
|
||||
disabled={!isNew && !hasUnsavedChanges}
|
||||
form="template-form"
|
||||
icon={<Save className="size-4" />}
|
||||
label={isNew ? 'Create' : 'Save'}
|
||||
loading={isSaving}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
{isNew ? 'Create' : 'Save'}
|
||||
</FormSubmitButton>
|
||||
type="submit"
|
||||
/>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
Reference in New Issue
Block a user