Skip to content
Draft
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: 6 additions & 1 deletion apps/google-docs/src/hooks/useWorkflowAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '../utils/constants/agent';
import {
MappingReviewSuspendPayload,
NeedsReauthSuspendPayload,
ResumePayload,
TabsImagesSuspendPayload,
CompletedWorkflowPayload,
Expand Down Expand Up @@ -111,7 +112,11 @@ const getRunErrorMessage = (runData: AgentRunData): string => {

const getSuspendPayload = (
runData: AgentRunData
): TabsImagesSuspendPayload | MappingReviewSuspendPayload | undefined => {
):
| TabsImagesSuspendPayload
| MappingReviewSuspendPayload
| NeedsReauthSuspendPayload
| undefined => {
return runData.metadata?.suspendPayload;
};

Expand Down
2 changes: 2 additions & 0 deletions apps/google-docs/src/locations/Page/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ const Page = () => {
ref={modalOrchestratorRef}
sdk={sdk}
oauthToken={oauthToken}
onOAuthConnectedChange={handleOAuthConnectedChange}
onOauthTokenChange={handleOauthTokenChange}
onMappingReviewReady={handleMappingReviewReady}
onResetToMain={handleReturnToMainPage}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { PageAppSDK } from '@contentful/app-sdk';
import { Modal } from '@contentful/f36-components';
import { Button, Modal, Paragraph } from '@contentful/f36-components';
import { ContentTypeProps } from 'contentful-management';
import { ConfirmCancelModal } from '../modals/ConfirmCancelModal';
import { ErrorModal } from '../modals/ErrorModal';
Expand All @@ -12,6 +12,7 @@ import { SelectTabsModal } from '../modals/step_3/SelectTabsModal';
import {
DocumentTabProps,
MappingReviewSuspendPayload,
NeedsReauthSuspendPayload,
CompletedWorkflowPayload,
ResumePayload,
TabsImagesSuspendPayload,
Expand All @@ -21,6 +22,7 @@ import {
import { ContentTypePickerModal } from '../modals/step_2/ContentTypePickerModal';
import { IncludeImagesModal } from '../modals/step_4/IncludeImagesModal';
import { useWorkflowAgent } from '@hooks/useWorkflowAgent';
import { OAuthConnector } from './OAuthConnector';

export interface ModalOrchestratorHandle {
startFlow: () => void;
Expand All @@ -32,17 +34,30 @@ enum FlowStep {
SELECT_TABS = 'selectTabs',
INCLUDE_IMAGES = 'includeImages',
LOADING = 'loading',
REAUTH = 'reauth',
}

interface ModalOrchestratorProps {
sdk: PageAppSDK;
oauthToken: string;
onOAuthConnectedChange: (isConnected: boolean) => void;
onOauthTokenChange: (token: string) => void;
onMappingReviewReady: (payload: MappingReviewSuspendPayload, runId: string) => void;
onResetToMain: () => void;
}

export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrchestratorProps>(
({ sdk, oauthToken, onMappingReviewReady, onResetToMain }, ref) => {
(
{
sdk,
oauthToken,
onOAuthConnectedChange,
onOauthTokenChange,
onMappingReviewReady,
onResetToMain,
},
ref
) => {
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const [isConfirmCancelModalOpen, setIsConfirmCancelModalOpen] = useState(false);
const [isErrorPreviewModalOpen, setIsErrorPreviewModalOpen] = useState(false);
Expand All @@ -55,6 +70,7 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
const [includeImages, setIncludeImages] = useState<boolean | null>(null);
const [requiresImageSelection, setRequiresImageSelection] = useState(false);
const [activeRunId, setActiveRunId] = useState<string | null>(null);
const [pendingReauthRunId, setPendingReauthRunId] = useState<string | null>(null);
const { startWorkflow, resumeWorkflow } = useWorkflowAgent({
sdk,
documentId,
Expand Down Expand Up @@ -85,6 +101,7 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
setSelectedContentTypes([]);
resetDocumentScopeReview();
setActiveRunId(null);
setPendingReauthRunId(null);
setFlowStep(null);
setIsUploadModalOpen(false);
};
Expand All @@ -103,9 +120,10 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
const handleConfirmCancel = async () => {
setIsConfirmCancelModalOpen(false);

if (activeRunId) {
const runIdToCancel = activeRunId ?? pendingReauthRunId;
if (runIdToCancel) {
try {
await resumeWorkflow(activeRunId, { cancelled: true });
await resumeWorkflow(runIdToCancel, { cancelled: true });
} catch (error) {
console.error(error);
}
Expand All @@ -117,6 +135,7 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches

const showWorkflowError = () => {
setFlowStep(null);
setPendingReauthRunId(null);
setIsErrorPreviewModalOpen(true);
};

Expand Down Expand Up @@ -157,17 +176,34 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
setFlowStep(null);
};

const showReauthPrompt = (suspendPayload: NeedsReauthSuspendPayload, runId: string) => {
setPendingReauthRunId(runId);
setActiveRunId(null);
setFlowStep(FlowStep.REAUTH);
};

const handleWorkflowResult = (workflowRun: WorkflowRunResult) => {
setActiveRunId(workflowRun.runId);

if (workflowRun.status === RunStatus.PENDING_REVIEW) {
if (workflowRun.suspendPayload.suspendStepId === 'mapping-review') {
setFlowStep(null);
onMappingReviewReady(workflowRun.suspendPayload, workflowRun.runId);
onMappingReviewReady(
workflowRun.suspendPayload as MappingReviewSuspendPayload,
workflowRun.runId
);
return;
}

showDocumentScopeReview(workflowRun.suspendPayload);
if (workflowRun.suspendPayload.suspendStepId === 'needs-google-reauth') {
showReauthPrompt(
workflowRun.suspendPayload as NeedsReauthSuspendPayload,
workflowRun.runId
);
return;
}

showDocumentScopeReview(workflowRun.suspendPayload as TabsImagesSuspendPayload);
return;
}

Expand All @@ -193,6 +229,24 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
handleWorkflowResult(workflowRun);
};

// Called when the user successfully reconnects OAuth during a suspended run
const handleReauthTokenChange = async (newToken: string) => {
onOauthTokenChange(newToken);

if (!pendingReauthRunId || !newToken) return;

const runId = pendingReauthRunId;
setPendingReauthRunId(null);
setFlowStep(FlowStep.LOADING);

try {
const workflowRun = await resumeWorkflow(runId, { oauthToken: newToken });
handleWorkflowResult(workflowRun);
} catch {
showWorkflowError();
}
};

const startWorkflowWithDelayedLoading = async (contentTypeIds: string[]) => {
let isStartPending = true;
const loadingModalTimeout = window.setTimeout(() => {
Expand Down Expand Up @@ -284,6 +338,24 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
contentTypeCount={selectedContentTypes.length}
/>
);
case FlowStep.REAUTH:
return (
<>
<Modal.Header title="Reconnect Google Drive" onClose={showDiscardConfirmation} />
<Modal.Content>
<Paragraph marginBottom="spacingM">
Your Google Drive connection expired. Please reconnect to continue generating your
preview — your progress has been saved.
</Paragraph>
<OAuthConnector
oauthToken={oauthToken}
isOAuthConnected={false}
onOAuthConnectedChange={onOAuthConnectedChange}
onOauthTokenChange={handleReauthTokenChange}
/>
</Modal.Content>
</>
);
default:
return null;
}
Expand All @@ -302,7 +374,7 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
onClose={showDiscardConfirmation}
size={'large'}
shouldCloseOnOverlayClick={false}
shouldCloseOnEscapePress={flowStep !== FlowStep.LOADING}>
shouldCloseOnEscapePress={flowStep !== FlowStep.LOADING && flowStep !== FlowStep.REAUTH}>
{renderFlowStep}
</Modal>

Expand Down
12 changes: 11 additions & 1 deletion apps/google-docs/src/types/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,21 @@ export interface MappingReviewSuspendPayload {
contentTypes: WorkflowContentType[];
}

export interface NeedsReauthSuspendPayload {
reason?: string;
suspendStepId: 'needs-google-reauth';
documentId: string;
}

export type WorkflowRunResult =
| {
status: RunStatus.PENDING_REVIEW;
runId: string;
messages: AgentRunMessage[];
suspendPayload: TabsImagesSuspendPayload | MappingReviewSuspendPayload;
suspendPayload:
| TabsImagesSuspendPayload
| MappingReviewSuspendPayload
| NeedsReauthSuspendPayload;
}
| {
status: RunStatus.COMPLETED;
Expand All @@ -129,4 +138,5 @@ export interface ResumePayload {
editedNormalizedDocument?: NormalizedDocument;
entryBlockGraph?: EntryBlockGraph;
cancelled?: true;
oauthToken?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ vi.mock('@contentful/react-apps-toolkit', () => ({
const defaultProps = {
sdk: mockSdk,
oauthToken: 'mock-oauth-token',
onOAuthConnectedChange: vi.fn(),
onOauthTokenChange: vi.fn(),
onMappingReviewReady: vi.fn(),
onResetToMain: vi.fn(),
};
Expand Down
Loading