Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/decap-cms-core/src/actions/deploys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,17 @@ export function loadDeployPreview(
slug: string,
entry: Entry,
published: boolean,
opts?: { maxAttempts?: number; interval?: number },
opts?: { maxAttempts?: number; interval?: number; signal?: AbortSignal },
) {
return async (dispatch: ThunkDispatch<State, undefined, AnyAction>, getState: () => State) => {
const state = getState();
const backend = currentBackend(state.config);
const collectionName = collection.get('name');

// Exit if currently fetching
// Exit if currently fetching, unless the caller provides a signal
// (indicating it manages cancellation of the previous poll externally).
const deployState = selectDeployPreview(state, collectionName, slug);
if (deployState && deployState.isFetching) {
if (deployState && deployState.isFetching && !opts?.signal) {
return;
}

Expand Down
9 changes: 8 additions & 1 deletion packages/decap-cms-core/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,7 +1046,11 @@ export class Backend {
collection: Collection,
slug: string,
entry: EntryMap,
{ maxAttempts = 1, interval = 5000 } = {},
{
maxAttempts = 1,
interval = 5000,
signal,
}: { maxAttempts?: number; interval?: number; signal?: AbortSignal } = {},
) {
/**
* If the registered backend does not provide a `getDeployPreview` method, or
Expand All @@ -1063,6 +1067,9 @@ export class Backend {
let deployPreview,
count = 0;
while (!deployPreview && count < maxAttempts) {
if (signal?.aborted) {
return;
}
count++;
deployPreview = await this.implementation.getDeployPreview(collection.get('name'), slug);
if (!deployPreview) {
Expand Down
19 changes: 17 additions & 2 deletions packages/decap-cms-core/src/components/Editor/EditorToolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,17 +303,32 @@ export class EditorToolbar extends React.Component {

const { isNewEntry, loadDeployPreview } = this.props;
if (!isNewEntry) {
loadDeployPreview({ maxAttempts: 3 });
// 24 attempts × 5s interval = ~2 min polling window.
// With editorial workflow, saving remounts the component (navigates to
// the unpublished entry view), so componentDidMount is the primary
// polling trigger — not componentDidUpdate.
this._pollController = new AbortController();
loadDeployPreview({ maxAttempts: 24, signal: this._pollController.signal });
}
}

componentDidUpdate(prevProps) {
const { isNewEntry, isPersisting, loadDeployPreview } = this.props;
if (!isNewEntry && prevProps.isPersisting && !isPersisting) {
loadDeployPreview({ maxAttempts: 3 });
// Abort any in-flight poll before starting a new one.
this._pollController?.abort();
this._pollController = new AbortController();
// Fires on subsequent saves when the component survives (no remount).
// In editorial workflow the first save remounts, so this mainly
// covers the second-save-and-beyond case.
loadDeployPreview({ maxAttempts: 3, signal: this._pollController.signal });
}
}

componentWillUnmount() {
this._pollController?.abort();
}

renderSimpleControls = () => {
const { collection, hasChanged, isNewEntry, showDelete, onDelete, t } = this.props;
const canCreate = collection.get('create');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,50 @@ describe('EditorToolbar', () => {
expect(asFragment()).toMatchSnapshot();
});
});

describe('deploy preview polling', () => {
it('should poll with maxAttempts: 24 and an AbortSignal on mount for existing entries', () => {
render(<EditorToolbar {...props} isNewEntry={false} />);
expect(props.loadDeployPreview).toHaveBeenCalledTimes(1);
const opts = props.loadDeployPreview.mock.calls[0][0];
expect(opts.maxAttempts).toBe(24);
expect(opts.signal).toBeInstanceOf(AbortSignal);
});

it('should not poll on mount for new entries', () => {
render(<EditorToolbar {...props} isNewEntry={true} />);
expect(props.loadDeployPreview).not.toHaveBeenCalled();
});

it('should poll with maxAttempts: 3 after a save completes', () => {
const { rerender } = render(<EditorToolbar {...props} isPersisting={true} />);
props.loadDeployPreview.mockClear();
rerender(<EditorToolbar {...props} isPersisting={false} />);
expect(props.loadDeployPreview).toHaveBeenCalledTimes(1);
const opts = props.loadDeployPreview.mock.calls[0][0];
expect(opts.maxAttempts).toBe(3);
expect(opts.signal).toBeInstanceOf(AbortSignal);
});

it('should abort polling on unmount', () => {
const { unmount } = render(<EditorToolbar {...props} isNewEntry={false} />);
const signal = props.loadDeployPreview.mock.calls[0][0].signal;
expect(signal.aborted).toBe(false);
unmount();
expect(signal.aborted).toBe(true);
});

it('should abort previous poll when a new save triggers a new poll', () => {
const { rerender } = render(<EditorToolbar {...props} isPersisting={false} />);
const firstSignal = props.loadDeployPreview.mock.calls[0][0].signal;

// Simulate save completing
rerender(<EditorToolbar {...props} isPersisting={true} />);
rerender(<EditorToolbar {...props} isPersisting={false} />);

expect(firstSignal.aborted).toBe(true);
const secondSignal = props.loadDeployPreview.mock.calls[1][0].signal;
expect(secondSignal.aborted).toBe(false);
});
});
});
Loading