diff --git a/apps/google-docs/src/hooks/useWorkflowAgent.ts b/apps/google-docs/src/hooks/useWorkflowAgent.ts index 74118418bf..b27d551964 100644 --- a/apps/google-docs/src/hooks/useWorkflowAgent.ts +++ b/apps/google-docs/src/hooks/useWorkflowAgent.ts @@ -8,6 +8,7 @@ import { } from '../utils/constants/agent'; import { MappingReviewSuspendPayload, + NeedsReauthSuspendPayload, ResumePayload, TabsImagesSuspendPayload, CompletedWorkflowPayload, @@ -111,7 +112,11 @@ const getRunErrorMessage = (runData: AgentRunData): string => { const getSuspendPayload = ( runData: AgentRunData -): TabsImagesSuspendPayload | MappingReviewSuspendPayload | undefined => { +): + | TabsImagesSuspendPayload + | MappingReviewSuspendPayload + | NeedsReauthSuspendPayload + | undefined => { return runData.metadata?.suspendPayload; }; diff --git a/apps/google-docs/src/locations/Page/Page.tsx b/apps/google-docs/src/locations/Page/Page.tsx index 82cd3cde58..6039081984 100644 --- a/apps/google-docs/src/locations/Page/Page.tsx +++ b/apps/google-docs/src/locations/Page/Page.tsx @@ -134,6 +134,8 @@ const Page = () => { ref={modalOrchestratorRef} sdk={sdk} oauthToken={oauthToken} + onOAuthConnectedChange={handleOAuthConnectedChange} + onOauthTokenChange={handleOauthTokenChange} onMappingReviewReady={handleMappingReviewReady} onResetToMain={handleReturnToMainPage} /> diff --git a/apps/google-docs/src/locations/Page/components/mainpage/ModalOrchestrator.tsx b/apps/google-docs/src/locations/Page/components/mainpage/ModalOrchestrator.tsx index 94adc2c318..92387f78ea 100644 --- a/apps/google-docs/src/locations/Page/components/mainpage/ModalOrchestrator.tsx +++ b/apps/google-docs/src/locations/Page/components/mainpage/ModalOrchestrator.tsx @@ -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'; @@ -12,6 +12,7 @@ import { SelectTabsModal } from '../modals/step_3/SelectTabsModal'; import { DocumentTabProps, MappingReviewSuspendPayload, + NeedsReauthSuspendPayload, CompletedWorkflowPayload, ResumePayload, TabsImagesSuspendPayload, @@ -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; @@ -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( - ({ 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); @@ -55,6 +70,7 @@ export const ModalOrchestrator = forwardRef(null); const [requiresImageSelection, setRequiresImageSelection] = useState(false); const [activeRunId, setActiveRunId] = useState(null); + const [pendingReauthRunId, setPendingReauthRunId] = useState(null); const { startWorkflow, resumeWorkflow } = useWorkflowAgent({ sdk, documentId, @@ -85,6 +101,7 @@ export const ModalOrchestrator = forwardRef { 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); } @@ -117,6 +135,7 @@ export const ModalOrchestrator = forwardRef { setFlowStep(null); + setPendingReauthRunId(null); setIsErrorPreviewModalOpen(true); }; @@ -157,17 +176,34 @@ export const ModalOrchestrator = forwardRef { + 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; } @@ -193,6 +229,24 @@ export const ModalOrchestrator = forwardRef { + 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(() => { @@ -284,6 +338,24 @@ export const ModalOrchestrator = forwardRef ); + case FlowStep.REAUTH: + return ( + <> + + + + Your Google Drive connection expired. Please reconnect to continue generating your + preview — your progress has been saved. + + + + + ); default: return null; } @@ -302,7 +374,7 @@ export const ModalOrchestrator = forwardRef + shouldCloseOnEscapePress={flowStep !== FlowStep.LOADING && flowStep !== FlowStep.REAUTH}> {renderFlowStep} diff --git a/apps/google-docs/src/types/workflow.ts b/apps/google-docs/src/types/workflow.ts index 6901a7b001..dff62bde1e 100644 --- a/apps/google-docs/src/types/workflow.ts +++ b/apps/google-docs/src/types/workflow.ts @@ -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; @@ -129,4 +138,5 @@ export interface ResumePayload { editedNormalizedDocument?: NormalizedDocument; entryBlockGraph?: EntryBlockGraph; cancelled?: true; + oauthToken?: string; } diff --git a/apps/google-docs/test/locations/Page/components/mainpage/ModalOrchestrator.spec.tsx b/apps/google-docs/test/locations/Page/components/mainpage/ModalOrchestrator.spec.tsx index 7b37333225..29fc562802 100644 --- a/apps/google-docs/test/locations/Page/components/mainpage/ModalOrchestrator.spec.tsx +++ b/apps/google-docs/test/locations/Page/components/mainpage/ModalOrchestrator.spec.tsx @@ -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(), };