diff --git a/services/platform/app/components/theme/theme-provider.tsx b/services/platform/app/components/theme/theme-provider.tsx
index ce99f13c8..c86334ca0 100644
--- a/services/platform/app/components/theme/theme-provider.tsx
+++ b/services/platform/app/components/theme/theme-provider.tsx
@@ -4,6 +4,7 @@ import {
createContext,
useContext,
useEffect,
+ useMemo,
useState,
useCallback,
type ReactNode,
@@ -90,11 +91,10 @@ export function ThemeProvider({
[updateResolvedTheme],
);
- const value: ThemeContextValue = {
- theme,
- resolvedTheme,
- setTheme,
- };
+ const value = useMemo(
+ () => ({ theme, resolvedTheme, setTheme }),
+ [theme, resolvedTheme, setTheme],
+ );
return (
{children}
diff --git a/services/platform/app/features/approvals/components/approvals-client.tsx b/services/platform/app/features/approvals/components/approvals-client.tsx
index 903cf64eb..6f1d7072d 100644
--- a/services/platform/app/features/approvals/components/approvals-client.tsx
+++ b/services/platform/app/features/approvals/components/approvals-client.tsx
@@ -422,6 +422,10 @@ export function ApprovalsClient({
return (
{list.map((p, index) => {
+ const id =
+ safeGetString(p, 'productId', '') ||
+ safeGetString(p, 'id', '') ||
+ String(index);
const name =
safeGetString(p, 'name', '') ||
safeGetString(p, 'productName', '');
@@ -431,7 +435,7 @@ export function ApprovalsClient({
'/assets/placeholder-image.png';
return (
-
+
{
+ const [edgePath, defaultLabelX, defaultLabelY] = useMemo(() => {
if (type === 'smoothstep' || type === 'default') {
return getSmoothStepPath({
sourceX,
@@ -62,7 +62,6 @@ export function AutomationEdge({
targetY,
});
}
- // bezier (or fallback)
return getBezierPath({
sourceX,
sourceY,
@@ -71,42 +70,34 @@ export function AutomationEdge({
targetY,
targetPosition,
});
- })();
+ }, [type, sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition]);
- // Calculate smart label position
- const calculateLabelPosition = () => {
+ const { labelX, labelY } = useMemo(() => {
const labelPosition = data?.labelPosition || 'center';
- let labelX = defaultLabelX;
- let labelY = defaultLabelY;
+ let lx = defaultLabelX;
+ let ly = defaultLabelY;
- // Adjust position based on labelPosition setting
if (labelPosition === 'source') {
- // Position label 25% from source
- labelX = sourceX + (targetX - sourceX) * 0.25;
- labelY = sourceY + (targetY - sourceY) * 0.25;
+ lx = sourceX + (targetX - sourceX) * 0.25;
+ ly = sourceY + (targetY - sourceY) * 0.25;
} else if (labelPosition === 'target') {
- // Position label 75% toward target
- labelX = sourceX + (targetX - sourceX) * 0.75;
- labelY = sourceY + (targetY - sourceY) * 0.75;
+ lx = sourceX + (targetX - sourceX) * 0.75;
+ ly = sourceY + (targetY - sourceY) * 0.75;
}
- // Special handling for backward connections
if (data?.isBackwardConnection) {
- // Offset label to the side for backward connections to avoid overlap
- const offsetX = 30; // Offset to the right
- labelX += offsetX;
+ lx += 30;
}
- // Apply manual offset if provided
- if (data?.labelOffset) {
- labelX += data.labelOffset.x;
- labelY += data.labelOffset.y;
+ if (data?.labelOffset?.x) {
+ lx += data.labelOffset.x;
+ }
+ if (data?.labelOffset?.y) {
+ ly += data.labelOffset.y;
}
- return { labelX, labelY };
- };
-
- const { labelX, labelY } = calculateLabelPosition();
+ return { labelX: lx, labelY: ly };
+ }, [defaultLabelX, defaultLabelY, sourceX, sourceY, targetX, targetY, data?.labelPosition, data?.isBackwardConnection, data?.labelOffset?.x, data?.labelOffset?.y]);
return (
<>
diff --git a/services/platform/app/features/automations/components/automation-sidepanel.tsx b/services/platform/app/features/automations/components/automation-sidepanel.tsx
index 1175ddf6e..dcbd575b2 100644
--- a/services/platform/app/features/automations/components/automation-sidepanel.tsx
+++ b/services/platform/app/features/automations/components/automation-sidepanel.tsx
@@ -382,8 +382,8 @@ export function AutomationSidePanel({
{t('sidePanel.validationErrors')}
- {errors.map((error, i) => (
- - • {error}
+ {errors.map((error, index) => (
+ - • {error}
))}
@@ -396,8 +396,8 @@ export function AutomationSidePanel({
{t('sidePanel.validationWarnings')}
- {warnings.map((warning, i) => (
- - • {warning}
+ {warnings.map((warning, index) => (
+ - • {warning}
))}
diff --git a/services/platform/app/features/automations/components/automation-tester.tsx b/services/platform/app/features/automations/components/automation-tester.tsx
index 2cb7cc76c..ddfc4a5d7 100644
--- a/services/platform/app/features/automations/components/automation-tester.tsx
+++ b/services/platform/app/features/automations/components/automation-tester.tsx
@@ -205,8 +205,8 @@ export function AutomationTester({
{t('tester.dryRun.errors')}:
- {dryRunResult.errors.map((err, i) => (
- - • {err}
+ {dryRunResult.errors.map((err, index) => (
+ - • {err}
))}
@@ -218,8 +218,8 @@ export function AutomationTester({
{t('tester.dryRun.warnings')}:
- {dryRunResult.warnings.map((warn, i) => (
- - • {warn}
+ {dryRunResult.warnings.map((warn, index) => (
+ - • {warn}
))}
diff --git a/services/platform/app/features/chat/components/chat-search-dialog.tsx b/services/platform/app/features/chat/components/chat-search-dialog.tsx
index fdd3803cc..658d59ff8 100644
--- a/services/platform/app/features/chat/components/chat-search-dialog.tsx
+++ b/services/platform/app/features/chat/components/chat-search-dialog.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from '@tanstack/react-router';
import { useQuery } from 'convex/react';
import { X } from 'lucide-react';
@@ -30,6 +30,8 @@ export function ChatSearchDialog({
const navigate = useNavigate();
const [query, setQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(-1);
+ const selectedIndexRef = useRef(selectedIndex);
+ selectedIndexRef.current = selectedIndex;
const inputRef = useRef(null);
const debouncedQuery = useDebounce(query, 300);
@@ -37,12 +39,19 @@ export function ChatSearchDialog({
const threadsData = useQuery(api.threads.queries.listThreads, {
search: debouncedQuery || undefined,
});
- const chats =
- threadsData?.map((thread) => ({
- _id: thread._id,
- title: thread.title ?? t('searchChat.untitledChat'),
- createdAt: thread._creationTime,
- })) || [];
+
+ const chats = useMemo(
+ () =>
+ threadsData?.map((thread) => ({
+ _id: thread._id,
+ title: thread.title ?? t('searchChat.untitledChat'),
+ createdAt: thread._creationTime,
+ formattedDate: thread._creationTime
+ ? formatDateSmart(new Date(thread._creationTime))
+ : '',
+ })) ?? [],
+ [threadsData, t, formatDateSmart],
+ );
useEffect(() => {
if (isOpen) {
@@ -68,30 +77,34 @@ export function ChatSearchDialog({
return () => window.removeEventListener('keydown', onKeyDown, true);
}, [isOpen, onOpenChange]);
+ // Reset selection when results change (debounced query), not on every keystroke
useEffect(() => {
if (isOpen) setSelectedIndex(-1);
- }, [isOpen, query]);
-
- const close = () => onOpenChange(false);
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === 'ArrowDown') {
- e.preventDefault();
- setSelectedIndex((i) => Math.min(i + 1, chats.length - 1));
- } else if (e.key === 'ArrowUp') {
- e.preventDefault();
- setSelectedIndex((i) => Math.max(i - 1, 0));
- } else if (e.key === 'Enter' && chats[selectedIndex]) {
- navigate({
- to: '/dashboard/$id/chat/$threadId',
- params: { id: organizationId, threadId: chats[selectedIndex]._id },
- });
- onOpenChange(false);
- } else if (e.key === 'Escape') {
- e.preventDefault();
- onOpenChange(false);
- }
- };
+ }, [isOpen, debouncedQuery]);
+
+ const close = useCallback(() => onOpenChange(false), [onOpenChange]);
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setSelectedIndex((i) => Math.min(i + 1, chats.length - 1));
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setSelectedIndex((i) => Math.max(i - 1, 0));
+ } else if (e.key === 'Enter' && chats[selectedIndexRef.current]) {
+ navigate({
+ to: '/dashboard/$id/chat/$threadId',
+ params: { id: organizationId, threadId: chats[selectedIndexRef.current]._id },
+ });
+ onOpenChange(false);
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ onOpenChange(false);
+ }
+ },
+ [chats, navigate, organizationId, onOpenChange],
+ );
return (