Skip to content

Commit 4244501

Browse files
authored
Merge pull request #1097 from objectstack-ai/copilot/fix-calendar-duplicate-events
2 parents c7a6750 + 0f09003 commit 4244501

File tree

4 files changed

+62
-19
lines changed

4 files changed

+62
-19
lines changed

packages/plugin-calendar/src/ObjectCalendar.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export interface ObjectCalendarProps {
4747
schema: ObjectGridSchema | CalendarSchema;
4848
dataSource?: DataSource;
4949
className?: string;
50+
/** Pre-fetched records passed by a parent (e.g. ObjectView). When provided, skips internal data fetching. */
51+
data?: any[];
52+
/** Loading state propagated from a parent. Respected only when `data` is also provided. */
53+
loading?: boolean;
5054
onEventClick?: (record: any) => void;
5155
onRowClick?: (record: any) => void;
5256
onDateClick?: (date: Date) => void;
@@ -142,6 +146,8 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
142146
schema,
143147
dataSource,
144148
className,
149+
data: externalData,
150+
loading: externalLoading,
145151
onEventClick,
146152
onRowClick,
147153
onDateClick,
@@ -151,8 +157,12 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
151157
locale,
152158
...rest
153159
}) => {
154-
const [data, setData] = useState<any[]>([]);
155-
const [loading, setLoading] = useState(true);
160+
// When the parent (e.g. ObjectView) pre-fetches data and passes it via the `data` prop,
161+
// we must not trigger a second fetch. Detect external data by checking for an array.
162+
const hasExternalData = Array.isArray(externalData);
163+
164+
const [data, setData] = useState<any[]>(hasExternalData ? externalData! : []);
165+
const [loading, setLoading] = useState(hasExternalData ? (externalLoading ?? false) : true);
156166
const [error, setError] = useState<Error | null>(null);
157167
const [objectSchema, setObjectSchema] = useState<any>(null);
158168
const [currentDate, setCurrentDate] = useState(new Date());
@@ -187,8 +197,24 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
187197
const objectSchemaRef = useRef<any>(null);
188198
objectSchemaRef.current = objectSchema;
189199

200+
// Sync external data/loading changes from parent (e.g. ObjectView re-fetches after filter change)
201+
useEffect(() => {
202+
if (hasExternalData) {
203+
setData(externalData!);
204+
}
205+
}, [externalData, hasExternalData]);
206+
207+
useEffect(() => {
208+
if (hasExternalData && externalLoading !== undefined) {
209+
setLoading(externalLoading);
210+
}
211+
}, [externalLoading, hasExternalData]);
212+
190213
// Fetch data based on provider
191214
useEffect(() => {
215+
// Skip internal fetch when data is managed by a parent component
216+
if (hasExternalData) return;
217+
192218
let isMounted = true;
193219
const fetchData = async () => {
194220
try {
@@ -203,16 +229,6 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
203229
return;
204230
}
205231

206-
// Prioritize data passed from parent (ListView)
207-
if ((schema as any).data || (rest as any).data) {
208-
const passedData = (schema as any).data || (rest as any).data;
209-
if (Array.isArray(passedData)) {
210-
setData(passedData);
211-
setLoading(false);
212-
return;
213-
}
214-
}
215-
216232
if (!dataSource || typeof dataSource.find !== 'function') {
217233
throw new Error('DataSource required for object/api providers');
218234
}
@@ -249,7 +265,7 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
249265

250266
fetchData();
251267
return () => { isMounted = false; };
252-
}, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort, refreshKey]);
268+
}, [hasExternalData, dataConfig, dataSource, hasInlineData, schema.filter, schema.sort, refreshKey]);
253269

254270
// Fetch object schema for field metadata
255271
useEffect(() => {

packages/plugin-calendar/src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type { CalendarViewProps, CalendarEvent } from './CalendarView';
2424
import './calendar-view-renderer';
2525

2626
// Register object-calendar component
27-
export const ObjectCalendarRenderer: React.FC<{ schema: any; [key: string]: any }> = ({ schema, data: _data, loading: _loading, ...props }) => {
27+
export const ObjectCalendarRenderer: React.FC<{ schema: any; [key: string]: any }> = ({ schema, ...props }) => {
2828
const { dataSource } = useSchemaContext() || {};
2929
return <ObjectCalendar schema={schema} dataSource={dataSource} {...props} />;
3030
};

packages/plugin-calendar/src/registration.test.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ vi.mock('@object-ui/react', () => ({
1010

1111
// Mock the implementation
1212
vi.mock('./ObjectCalendar', () => ({
13-
ObjectCalendar: ({ dataSource }: any) => (
13+
ObjectCalendar: ({ dataSource, data, loading }: any) => (
1414
<div data-testid="calendar-mock">
1515
{dataSource ? `DataSource: ${dataSource.type}` : 'No DataSource'}
16+
{data !== undefined ? ` Data: ${JSON.stringify(data)}` : ''}
17+
{loading !== undefined ? ` Loading: ${loading}` : ''}
1618
</div>
1719
)
1820
}));
@@ -22,4 +24,18 @@ describe('Plugin Calendar Registration', () => {
2224
render(<ObjectCalendarRenderer schema={{ type: 'object-calendar' }} />);
2325
expect(screen.getByTestId('calendar-mock')).toHaveTextContent('DataSource: mock-datasource');
2426
});
27+
28+
it('renderer passes data and loading props through to ObjectCalendar (no double-fetch)', () => {
29+
const preloadedData = [{ id: 1, name: 'Event A' }];
30+
render(
31+
<ObjectCalendarRenderer
32+
schema={{ type: 'object-calendar' }}
33+
data={preloadedData}
34+
loading={false}
35+
/>
36+
);
37+
const el = screen.getByTestId('calendar-mock');
38+
expect(el).toHaveTextContent('Data: [{"id":1,"name":"Event A"}]');
39+
expect(el).toHaveTextContent('Loading: false');
40+
});
2541
});

packages/plugin-view/src/ObjectView.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* - ViewSwitcher for toggling between view types
2323
*/
2424

25-
import React, { useEffect, useState, useCallback, useMemo } from 'react';
25+
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
2626
import type {
2727
ObjectViewSchema,
2828
ObjectGridSchema,
@@ -219,6 +219,11 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
219219
onViewAction,
220220
}) => {
221221
const [objectSchema, setObjectSchema] = useState<Record<string, unknown> | null>(null);
222+
// Assigned in the render body (not in an effect) so the fetchData effect always
223+
// reads the latest objectSchema without needing it as a dependency. This matches
224+
// the same pattern used in ObjectCalendar's objectSchemaRef.
225+
const objectSchemaRef = useRef<Record<string, unknown> | null>(null);
226+
objectSchemaRef.current = objectSchema;
222227
const [isFormOpen, setIsFormOpen] = useState(false);
223228
const [formMode, setFormMode] = useState<FormMode>('create');
224229
const [selectedRecord, setSelectedRecord] = useState<Record<string, unknown> | null>(null);
@@ -322,8 +327,11 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
322327
? sortConfig.map(s => ({ field: s.field, order: s.direction }))
323328
: (currentNamedViewConfig?.sort || activeView?.sort || schema.table?.defaultSort || undefined);
324329

325-
// Auto-inject $expand for lookup/master_detail fields
326-
const expand = buildExpandFields((objectSchema as any)?.fields);
330+
// Auto-inject $expand for lookup/master_detail fields.
331+
// Use a ref instead of the state variable to avoid re-running this effect
332+
// every time the object schema loads — that would cause a double-fetch and
333+
// duplicate events in child views like the calendar.
334+
const expand = buildExpandFields((objectSchemaRef.current as any)?.fields);
327335
const results = await dataSource.find(schema.objectName, {
328336
$filter: finalFilter.length > 0 ? finalFilter : undefined,
329337
$orderby: sort,
@@ -339,6 +347,8 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
339347
items = (results as any).data;
340348
} else if (Array.isArray((results as any).records)) {
341349
items = (results as any).records;
350+
} else if (Array.isArray((results as any).value)) {
351+
items = (results as any).value;
342352
}
343353
}
344354

@@ -352,8 +362,9 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
352362

353363
fetchData();
354364
return () => { isMounted = false; };
365+
// objectSchema intentionally omitted from deps — read via ref to prevent double-fetch
355366
// eslint-disable-next-line react-hooks/exhaustive-deps
356-
}, [schema.objectName, dataSource, currentViewType, filterValues, sortConfig, refreshKey, currentNamedViewConfig, activeView, renderListView, objectSchema]);
367+
}, [schema.objectName, dataSource, currentViewType, filterValues, sortConfig, refreshKey, currentNamedViewConfig, activeView, renderListView]);
357368

358369
// Determine layout mode
359370
const layout = schema.layout || 'drawer';

0 commit comments

Comments
 (0)