Skip to content

Commit 03196ef

Browse files
fix: FIT-792: Task action menu sometimes loads only reduced number of action items (#8679)
Co-authored-by: yyassi-heartex <[email protected]>
1 parent 109cfea commit 03196ef

File tree

4 files changed

+267
-21
lines changed

4 files changed

+267
-21
lines changed

web/libs/datamanager/src/components/DataManager/Toolbar/ActionsButton.jsx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { IconChevronDown, IconChevronRight, IconTrash } from "@humansignal/icons";
22
import { Button, Spinner, Tooltip } from "@humansignal/ui";
33
import { inject, observer } from "mobx-react";
4-
import { useCallback, useEffect, useRef, useState } from "react";
4+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5+
import { useActions } from "../../../hooks/useActions";
56
import { Block, Elem } from "../../../utils/bem";
67
import { FF_LOPS_E_3, isFF } from "../../../utils/feature-flags";
78
import { Dropdown } from "../../Common/Dropdown/DropdownComponent";
@@ -204,21 +205,20 @@ export const ActionsButton = injector(
204205
const formRef = useRef();
205206
const selectedCount = store.currentView.selectedCount;
206207
const [isOpen, setIsOpen] = useState(false);
207-
const [isLoading, setIsLoading] = useState(false);
208-
const fetchedActionsRef = useRef(false);
209208

210-
const actions = store.availableActions.filter((a) => !a.hidden).sort((a, b) => a.order - b.order);
211-
212-
useEffect(() => {
213-
if (isOpen && !fetchedActionsRef.current) {
214-
setIsLoading(true);
215-
store.fetchActions().finally(() => {
216-
setIsLoading(false);
217-
});
218-
fetchedActionsRef.current = true;
219-
}
220-
}, [isOpen, store]);
209+
// Use TanStack Query hook for fetching actions
210+
const {
211+
actions: serverActions,
212+
isLoading,
213+
isFetching,
214+
} = useActions({
215+
enabled: isOpen,
216+
projectId: store.SDK.projectId,
217+
});
221218

219+
const actions = useMemo(() => {
220+
return [...store.availableActions, ...serverActions].filter((a) => !a.hidden).sort((a, b) => a.order - b.order);
221+
}, [store.availableActions, serverActions]);
222222
const actionButtons = actions.map((action) => (
223223
<ActionButton key={action.id} action={action} parentRef={formRef} store={store} formRef={formRef} />
224224
));
@@ -228,8 +228,8 @@ export const ActionsButton = injector(
228228
<Dropdown.Trigger
229229
content={
230230
<Menu size="compact">
231-
{isLoading ? (
232-
<Menu.Item disabled data-testid="loading-actions">
231+
{isLoading || isFetching ? (
232+
<Menu.Item data-testid="loading-actions" disabled>
233233
Loading actions...
234234
</Menu.Item>
235235
) : (
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# DataManager Hooks
2+
3+
This directory contains React hooks for the DataManager library, following modern React patterns with TanStack Query for data fetching.
4+
5+
## Available Hooks
6+
7+
### `useActions`
8+
9+
A hook for fetching available actions from the DataManager API using TanStack Query.
10+
11+
#### Features
12+
13+
- **Automatic caching**: Actions are cached for 5 minutes by default
14+
- **Lazy loading**: Only fetches when enabled (e.g., when dropdown is opened)
15+
- **Error handling**: Built-in error states
16+
- **Loading states**: Provides both initial loading and refetching states
17+
- **Type safety**: Full TypeScript support
18+
19+
#### Usage
20+
21+
```tsx
22+
import { useActions } from "../hooks/useActions";
23+
24+
function ActionsDropdown({ projectId }) {
25+
const {
26+
actions,
27+
isLoading,
28+
isError,
29+
error,
30+
refetch,
31+
isFetching,
32+
} = useActions({
33+
projectId, // Optional: Used for cache scoping per project
34+
enabled: isOpen, // Only fetch when dropdown is opened
35+
staleTime: 5 * 60 * 1000, // Optional: 5 minutes
36+
cacheTime: 10 * 60 * 1000, // Optional: 10 minutes
37+
});
38+
39+
if (isLoading) return <div>Loading...</div>;
40+
if (isError) return <div>Error: {error.message}</div>;
41+
42+
return (
43+
<div>
44+
{actions.map((action) => (
45+
<button key={action.id}>{action.title}</button>
46+
))}
47+
</div>
48+
);
49+
}
50+
```
51+
52+
#### Parameters
53+
54+
- `options.projectId` (string, optional): Project ID for scoping the query cache. When provided, actions are cached per project, preventing cache conflicts in multi-project scenarios
55+
- `options.enabled` (boolean, default: `true`): Whether to enable the query
56+
- `options.staleTime` (number, default: `5 * 60 * 1000`): Time in ms before data is considered stale
57+
- `options.cacheTime` (number, default: `10 * 60 * 1000`): Time in ms before unused data is garbage collected
58+
59+
#### Return Value
60+
61+
- `actions` (Action[]): Array of available actions
62+
- `isLoading` (boolean): True on first load
63+
- `isFetching` (boolean): True whenever data is being fetched
64+
- `isError` (boolean): True if the query failed
65+
- `error` (Error): The error object if query failed
66+
- `refetch` (function): Function to manually refetch the data
67+
68+
### `useDataManagerUsers`
69+
70+
A hook for fetching users from the DataManager API with infinite pagination support.
71+
72+
See `useUsers.ts` for documentation.
73+
74+
### Other Hooks
75+
76+
- `useFirstMountState`: Utility hook to detect first mount
77+
- `useUpdateEffect`: Effect hook that skips the first render
78+
79+
## Migration from MobX to TanStack Query
80+
81+
The DataManager is gradually migrating from MobX State Tree flows to TanStack Query hooks for better performance, caching, and developer experience.
82+
83+
### Why TanStack Query?
84+
85+
1. **Automatic caching**: Reduces unnecessary API calls
86+
2. **Better loading states**: Built-in loading, error, and refetching states
87+
3. **Background refetching**: Keeps data fresh automatically
88+
4. **Query invalidation**: Easy cache management
89+
5. **TypeScript support**: Full type safety out of the box
90+
6. **React best practices**: Follows modern React patterns recommended in project rules
91+
92+
### Coexistence with MobX
93+
94+
The hooks replace the need for MobX flows for data fetching:
95+
96+
- **Old code**: `store.fetchActions()` is now deprecated but kept for backward compatibility
97+
- **New code**: Should always use `useActions()` hook
98+
- **Migration complete**: The actions endpoint is now only called via the `useActions` hook, preventing duplicate API calls
99+
100+
### Example Migration
101+
102+
**Before (MobX):**
103+
```javascript
104+
useEffect(() => {
105+
if (isOpen && actions.length === 0) {
106+
setIsLoading(true);
107+
store.fetchActions().finally(() => {
108+
setIsLoading(false);
109+
});
110+
}
111+
}, [isOpen, actions.length, store]);
112+
```
113+
114+
**After (TanStack Query):**
115+
```typescript
116+
const { actions, isLoading, isFetching } = useActions({
117+
projectId, // Optional: for cache scoping
118+
enabled: isOpen,
119+
});
120+
```
121+
122+
## Best Practices
123+
124+
1. **Use lazy loading**: Set `enabled: false` for data that's not immediately needed
125+
2. **Scope by projectId**: Always pass `projectId` when working with project-specific data to prevent cache conflicts
126+
3. **Configure cache times**: Adjust `staleTime` and `cacheTime` based on data freshness requirements
127+
4. **Handle loading states**: Always provide loading UI for better UX
128+
5. **Handle errors**: Display user-friendly error messages
129+
6. **Type everything**: Use TypeScript interfaces for type safety
130+
131+
## QueryClient Setup
132+
133+
The QueryClient is configured at the app level in `App.tsx`:
134+
135+
```typescript
136+
import { QueryClientProvider } from "@tanstack/react-query";
137+
import { queryClient } from "@humansignal/core/lib/utils/query-client";
138+
139+
<QueryClientProvider client={queryClient}>
140+
<Provider store={app}>
141+
{/* App content */}
142+
</Provider>
143+
</QueryClientProvider>
144+
```
145+
146+
This ensures all hooks have access to the shared query cache.
147+
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
3+
// Extend Window interface to include DataManager properties
4+
declare global {
5+
interface Window {
6+
DM?: {
7+
store?: {
8+
apiCall: (method: string, params?: any) => Promise<any>;
9+
};
10+
apiCall?: (method: string, params?: any) => Promise<any>;
11+
};
12+
}
13+
}
14+
15+
interface Action {
16+
id: string;
17+
title: string;
18+
order: number;
19+
hidden?: boolean;
20+
dialog?: {
21+
type?: string;
22+
text?: string;
23+
form?: any;
24+
title?: string;
25+
};
26+
children?: Action[];
27+
disabled?: boolean;
28+
disabledReason?: string;
29+
isSeparator?: boolean;
30+
isTitle?: boolean;
31+
callback?: (selection: any, action: Action) => void;
32+
}
33+
34+
interface UseActionsOptions {
35+
projectId?: string;
36+
enabled?: boolean;
37+
staleTime?: number;
38+
cacheTime?: number;
39+
}
40+
41+
/**
42+
* Hook to fetch available actions from the DataManager API
43+
* Uses TanStack Query for data fetching and caching
44+
*
45+
* @param options - Configuration options for the query
46+
* @returns Object containing actions data, loading state, error state, and refetch function
47+
*/
48+
export const useActions = (options: UseActionsOptions = {}) => {
49+
const {
50+
enabled = true,
51+
staleTime = 5 * 60 * 1000, // 5 minutes
52+
cacheTime = 10 * 60 * 1000, // 10 minutes
53+
projectId,
54+
} = options;
55+
56+
const queryKey = ["actions", projectId];
57+
58+
const { data, isLoading, isError, error, refetch, isFetching } = useQuery({
59+
queryKey,
60+
queryFn: async () => {
61+
// Use the correct DataManager API pattern - window.DM is the AppStore
62+
const store = window?.DM?.store || window?.DM;
63+
64+
if (!store) {
65+
throw new Error("DataManager store not available");
66+
}
67+
68+
const response = await store.apiCall?.("actions");
69+
70+
if (!response) {
71+
throw new Error("No actions found in response or response is invalid");
72+
}
73+
74+
return response as Action[];
75+
},
76+
enabled,
77+
staleTime,
78+
cacheTime,
79+
});
80+
81+
const actions = data ?? [];
82+
83+
return {
84+
actions,
85+
isLoading,
86+
isError,
87+
error,
88+
refetch,
89+
isFetching,
90+
};
91+
};

web/libs/datamanager/src/stores/AppStore.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -542,14 +542,22 @@ export const AppStore = types
542542
return true;
543543
}),
544544

545+
/**
546+
* @deprecated Use the useActions hook instead for better caching and performance
547+
* This method is kept for backward compatibility but is no longer actively used
548+
*/
545549
fetchActions: flow(function* () {
546-
const serverActions = yield self.apiCall("actions");
550+
try {
551+
const serverActions = yield self.apiCall("actions");
547552

548-
const actions = (serverActions ?? []).map((action) => {
549-
return [action, undefined];
550-
});
553+
const actions = (serverActions ?? []).map((action) => {
554+
return [action, undefined];
555+
});
551556

552-
self.SDK.updateActions(actions);
557+
self.SDK.updateActions(actions);
558+
} catch (error) {
559+
console.error("Error fetching actions:", error);
560+
}
553561
}),
554562

555563
fetchActionForm: flow(function* (actionId) {

0 commit comments

Comments
 (0)