From 18839b3487617e3091bbaf3fdee33f8323e583e5 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sun, 28 Jun 2026 01:41:26 +0700 Subject: [PATCH] refactor(templates): adopt the 2-panel resizable layout of the other detail pages templates/{id} now mirrors knowledges/{id} and settings/prompts/{id}: a left ResizablePanel (intro + title + the Presets panel, in the spot where the prompt page shows "Available variables") and a right panel holding the Code/Plain editor that fills the space. Save moves to the header (FormSubmitButton form="template-form"); the right-side Presets sidebar/Sheet, the PanelRight toggle, the in-input save button, and the Enter-to-submit handler are removed. Live-verified on the stand: 2-panel render (create + edit), preset apply, create->DB->reload byte-fidelity (incl. the blank line, reconstructed from .cm-line), the Code editor filling the right panel, delete. tsc/eslint/build + 652 frontend tests green; console clean. Co-Authored-By: Claude Opus 4.8 --- frontend/src/pages/templates/template.tsx | 442 ++++++++++------------ 1 file changed, 196 insertions(+), 246 deletions(-) diff --git a/frontend/src/pages/templates/template.tsx b/frontend/src/pages/templates/template.tsx index c76a2546..ea810b0b 100644 --- a/frontend/src/pages/templates/template.tsx +++ b/frontend/src/pages/templates/template.tsx @@ -8,9 +8,8 @@ import { Ellipsis, FileSymlink, FileText, + GripVertical, Loader2, - PanelRightClose, - PanelRightOpen, Pencil, Save, Trash, @@ -43,10 +42,11 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Form, FormControl, FormField, FormItem } from '@/components/ui/form'; +import { FormSubmitButton } from '@/components/ui/form-submit-button'; import { Input } from '@/components/ui/input'; -import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextareaAutosize } from '@/components/ui/input-group'; -import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; import { Spinner } from '@/components/ui/spinner'; +import { Textarea } from '@/components/ui/textarea'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useTemplateDetailNavigation } from '@/features/templates/use-template-detail-navigation'; import { FlowTemplateDocument } from '@/graphql/types'; @@ -253,7 +253,7 @@ function Template() { const { templateId } = useParams<{ templateId?: string }>(); const { createTemplate, deleteTemplate, updateTemplate } = useTemplates(); - const { isMobile } = useBreakpoint(); + const { isDesktop, isMobile } = useBreakpoint(); const isNew = templateId === 'new'; // Pass `null` while creating a new template — there is no "current item" @@ -261,7 +261,6 @@ function Template() { // below by `canShowActions`). const templateNav = useTemplateDetailNavigation(isNew ? null : templateId); - const [isAsideOpen, setIsAsideOpen] = useState(false); const [expandedPresetIndex, setExpandedPresetIndex] = useState(null); const [isReplaceConfirmOpen, setIsReplaceConfirmOpen] = useState(false); const [isSaving, setIsSaving] = useState(false); @@ -372,17 +371,6 @@ function Template() { } }; - const handleKeyDown = (event: React.KeyboardEvent) => { - const { ctrlKey, key, metaKey, shiftKey } = event; - - if (isSaving || key !== 'Enter' || shiftKey || ctrlKey || metaKey) { - return; - } - - event.preventDefault(); - handleFormSubmit(handleSubmit)(); - }; - const handleApplyPreset = useCallback( (preset: { text: string; title: string }) => { const current = getValues(); @@ -456,13 +444,18 @@ function Template() { sheetTitle="Templates" /> )} - + {(isNew || !!templateData?.flowTemplate) && ( + } + loading={isSaving} + size="sm" + variant="secondary" + > + {isNew ? 'Create' : 'Save'} + + )} - +
+
+

+ Preset templates + + {PRESET_TEMPLATES.length} + +

+

+ Click a preset to fill the form, or expand it to preview the content. +

+
+
+ {PRESET_TEMPLATES.map((preset, index) => ( + setExpandedPresetIndex(open ? index : null)} + open={expandedPresetIndex === index} + > + +
- -
- - -

- {preset.text} -

-
-
-
-
- ))} + + + +
+ + +

+ {preset.text} +

+
+
+ + + ))} +
), [expandedPresetIndex, handleApplyPreset], ); - const aside = useMemo( - () => - isMobile ? ( - - - - - - Preset templates - - {PRESET_TEMPLATES.length} - - - - {/* Plain overflow-y-auto instead of Radix ScrollArea — - ScrollArea's Viewport wraps children in a - `display: table` div whose width grows to fit - intrinsic content, defeating `w-full min-w-0` on - inner flex rows and pushing the chevron buttons - off-screen. */} -
{asideContent}
-
-
- ) : ( - - ), - [isMobile, isAsideOpen, asideContent], + const introBlock = ( +
+

{isNew ? 'Create a new template' : 'Edit template'}

+

Add a title and content, or start from a preset.

+
+ ); + + const titleField = ( + ( + + + + + + )} + /> + ); + + const textEditor = ( + ( + + + {viewMode === 'code' ? ( + + + + } + > + + + ) : ( +