mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-07-21 16:25:27 -04:00
fix(frontend): address QA findings (P0–P3, excluding password policy)
- providers/flow-provider: map "no rows in result set" to a friendly
"Flow not found" toast with a stable id so repeated sibling-query
errors no longer stack raw SQL into the UI
- components/ui/sidebar: skip the global Ctrl+B shortcut when focus is
in an input, textarea, select, or contenteditable — was hijacking the
Tiptap Bold shortcut in the markdown editor
- components/ui/sidebar: mark the mobile drawer with group + data-state
"expanded" so `group-data-[state=...]` selectors collapse correctly
and flow id badges stop rendering twice in the drawer
- pages/templates/template: add aria-label + title to the icon-only Save
button and mark the icon aria-hidden
- pages/settings/settings-api-tokens: render the date-picker label with
date-fns format "d MMM yyyy" to match the table's date format
- pages/settings/settings-providers: drop the fixed Name column width so
the Type column stays visible on narrow viewports; Badge truncates
cleanly with whitespace-nowrap and shrink-0 on the provider icon
- components/shared/confirmation-dialog: derive the title from
`\${confirmText} \${itemType}` so dialogs surface "Delete template"
instead of a generic "Confirm Action", and use the verb in the
description for consistency
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,13 +46,19 @@ function ConfirmationDialog({
|
||||
isOpen,
|
||||
itemName = 'this',
|
||||
itemType = 'item',
|
||||
title = 'Confirm Action',
|
||||
title,
|
||||
}: ConfirmationDialogProps) {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
// Derive a contextual title from confirm verb + item type so callers don't
|
||||
// see "Confirm Action" for a Delete prompt or a Save prompt. Explicit
|
||||
// `title` always wins.
|
||||
const verb = confirmText.trim();
|
||||
const resolvedTitle = title ?? (verb && verb !== 'Confirm' ? `${verb} ${itemType}` : 'Confirm Action');
|
||||
|
||||
const defaultDescription = description || (
|
||||
<>
|
||||
Are you sure you want to perform this action on{' '}
|
||||
Are you sure you want to {verb.toLowerCase() || 'perform this action on'}{' '}
|
||||
<strong className="text-foreground font-semibold">{itemName}</strong> {itemType}?
|
||||
</>
|
||||
);
|
||||
@@ -102,7 +108,7 @@ function ConfirmationDialog({
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogTitle>{resolvedTitle}</DialogTitle>
|
||||
<DialogDescription>{defaultDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -94,9 +94,15 @@ function Sidebar({
|
||||
{...props}
|
||||
>
|
||||
<SheetContent
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
// The mobile drawer always has full label width — mark it as
|
||||
// `data-state="expanded"` so descendants relying on
|
||||
// `group-data-[state=expanded|collapsed]` selectors (e.g.
|
||||
// duplicated id badges in FlowMenuItem) collapse to the
|
||||
// expanded branch instead of rendering both spans.
|
||||
className="group bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
data-mobile="true"
|
||||
data-sidebar="sidebar"
|
||||
data-state="expanded"
|
||||
side={side}
|
||||
style={
|
||||
{
|
||||
@@ -347,12 +353,27 @@ function SidebarProvider({
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
// Skip when focus is in a text input or rich editor — Ctrl+B there means
|
||||
// Bold (markdown editor / Tiptap), not "toggle sidebar". Without this guard
|
||||
// the global handler hijacks every editor's native bold shortcut.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
if (event.key !== SIDEBAR_KEYBOARD_SHORTCUT || !(event.metaKey || event.ctrlKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target as HTMLElement | null;
|
||||
|
||||
if (target) {
|
||||
const tag = target.tagName;
|
||||
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
@@ -646,7 +646,11 @@ function SettingsAPITokens() {
|
||||
variant="outline"
|
||||
>
|
||||
<CalendarIcon className="mr-2 size-4" />
|
||||
{field.value ? field.value.toLocaleDateString() : <span>Pick date</span>}
|
||||
{field.value ? (
|
||||
format(field.value, 'd MMM yyyy', { locale: enUS })
|
||||
) : (
|
||||
<span>Pick date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
|
||||
@@ -135,7 +135,7 @@ function SettingsProviders() {
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
cell: ({ row }) => <div className="font-medium">{row.getValue('name')}</div>,
|
||||
cell: ({ row }) => <div className="truncate font-medium">{row.getValue('name')}</div>,
|
||||
enableHiding: false,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
@@ -143,19 +143,24 @@ function SettingsProviders() {
|
||||
title="Name"
|
||||
/>
|
||||
),
|
||||
// Name flexes to fill remaining width — fixed `size` would push
|
||||
// the Type column off-screen on narrow viewports (e.g. 375px).
|
||||
meta: { searchable: true },
|
||||
size: 400,
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
cell: ({ row }) => {
|
||||
const providerType = row.getValue('type') as ProviderType;
|
||||
const Icon = providerIcons[providerType];
|
||||
const label = providerTypes.find((p) => p.type === providerType)?.label || providerType;
|
||||
|
||||
return (
|
||||
<Badge variant="outline">
|
||||
{Icon && <Icon className="mr-1 size-3" />}
|
||||
{providerTypes.find((p) => p.type === providerType)?.label || providerType}
|
||||
<Badge
|
||||
className="max-w-full whitespace-nowrap"
|
||||
variant="outline"
|
||||
>
|
||||
{Icon && <Icon className="mr-1 size-3 shrink-0" />}
|
||||
<span className="truncate">{label}</span>
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
@@ -166,6 +171,7 @@ function SettingsProviders() {
|
||||
/>
|
||||
),
|
||||
meta: { searchable: true },
|
||||
minSize: 110,
|
||||
size: 160,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -746,6 +746,7 @@ function Template() {
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton
|
||||
aria-label={isNew ? 'Create template' : 'Save template'}
|
||||
className="ml-auto"
|
||||
disabled={
|
||||
isSaving ||
|
||||
@@ -753,10 +754,15 @@ function Template() {
|
||||
(!isNew && !hasUnsavedChanges)
|
||||
}
|
||||
size="icon-xs"
|
||||
title={isNew ? 'Create template' : 'Save template'}
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
{isSaving ? <Spinner variant="circle" /> : <Save />}
|
||||
{isSaving ? (
|
||||
<Spinner variant="circle" />
|
||||
) : (
|
||||
<Save aria-hidden="true" />
|
||||
)}
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
@@ -177,12 +177,18 @@ export function FlowProvider({ children }: FlowProviderProps) {
|
||||
|
||||
const flowStatus = useMemo(() => flowData?.flow?.status, [flowData?.flow?.status]);
|
||||
|
||||
// Show toast notification when flow loading error occurs
|
||||
// Show toast notification when flow loading error occurs.
|
||||
// A single Postgres "no rows in result set" surfaces here every time a sibling
|
||||
// query/subscription retries against an invalid flow id; without a stable
|
||||
// toast id Sonner would stack 8 copies of the same message before the page
|
||||
// redirects. Surface a friendly message and drop the raw SQL detail entirely.
|
||||
useEffect(() => {
|
||||
if (flowError) {
|
||||
const description = flowError.message || 'An error occurred while loading flow';
|
||||
toast.error('Failed to load flow', {
|
||||
description,
|
||||
const raw = flowError.message ?? '';
|
||||
const isNotFound = /no rows in result set|not found/i.test(raw);
|
||||
toast.error(isNotFound ? 'Flow not found' : 'Failed to load flow', {
|
||||
description: isNotFound ? undefined : raw || undefined,
|
||||
id: 'flow-load-error',
|
||||
});
|
||||
Log.error('Error loading flow:', flowError);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user