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:
Sergey Kozyrenko
2026-05-20 13:41:20 +07:00
parent 1debdd9fdf
commit 18069565d9
6 changed files with 67 additions and 18 deletions
@@ -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>
+25 -4
View File
@@ -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,
},
{
+7 -1
View File
@@ -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>
+10 -4
View File
@@ -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);
}