diff --git a/.env.local.example b/.env.local.example index af2cb17ac..dcc07c130 100644 --- a/.env.local.example +++ b/.env.local.example @@ -27,8 +27,9 @@ OTEL_SERVICE_NAME=common OTEL_RESOURCE_ATTRIBUTES="deployment.environment=local" ## TipTap Cloud (Real-time collaborative editing) -# Get your App ID from https://cloud.tiptap.dev/ +# Get your App ID and Secret from https://cloud.tiptap.dev/ NEXT_PUBLIC_TIPTAP_APP_ID=your-tiptap-app-id +TIPTAP_SECRET=your-tiptap-secret # TIPTAP_PRO_TOKEN is needed for pnpm to install @tiptap-pro packages. # Run before install: export TIPTAP_PRO_TOKEN="your-token" && pnpm install # Or add to your shell profile for persistence. diff --git a/apps/app/src/components/decisions/DocumentNotAvailable.tsx b/apps/app/src/components/decisions/DocumentNotAvailable.tsx new file mode 100644 index 000000000..b5060702a --- /dev/null +++ b/apps/app/src/components/decisions/DocumentNotAvailable.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { cn } from '@op/ui/utils'; +import { LuFileQuestion } from 'react-icons/lu'; + +import { useTranslations } from '@/lib/i18n'; + +/** Shown when document content failed to load from the collaboration server. */ +export function DocumentNotAvailable({ className }: { className?: string }) { + const t = useTranslations(); + + return ( +
+ {t('Content could not be loaded')} +
+${t('No content available')}
`; + const proposalContent = getProposalContent(currentProposal.documentContent); return (HTML
' }, + }), + testData.createProposal({ + callerEmail: setup.userEmail, + processInstanceId: instance.instance.id, + proposalData: { title: 'Empty' }, + }), + ]); + + const { collaborationDocId } = collabProposal.proposalData as { + collaborationDocId: string; + }; + const mockTipTapContent = { type: 'doc', content: [ { type: 'paragraph', content: [{ type: 'text', text: 'TipTap' }] }, ], }; + // Only set up mock for the collab proposal (empty and legacy won't have valid responses) mockCollab.setDocResponse(collaborationDocId, mockTipTapContent); - const [collabProposal, legacyProposal, emptyProposal, caller] = - await Promise.all([ - testData.createProposal({ - callerEmail: setup.userEmail, - processInstanceId: instance.instance.id, - proposalData: { title: 'Collab', collaborationDocId }, - }), - testData.createProposal({ - callerEmail: setup.userEmail, - processInstanceId: instance.instance.id, - proposalData: { title: 'Legacy', description: 'HTML
' }, - }), - testData.createProposal({ - callerEmail: setup.userEmail, - processInstanceId: instance.instance.id, - proposalData: { title: 'Empty' }, // No content - }), - createAuthenticatedCaller(setup.userEmail), - ]); + const caller = await createAuthenticatedCaller(setup.userEmail); const result = await caller.decision.listProposals({ processInstanceId: instance.instance.id, @@ -789,6 +798,7 @@ describe.concurrent('listProposals', () => { type: 'html', content: 'HTML
', }); + // Empty proposal has a collaborationDocId but no mock response, so documentContent is undefined expect(foundEmpty?.documentContent).toBeUndefined(); }); }); diff --git a/services/api/src/test/helpers/TestDecisionsDataManager.ts b/services/api/src/test/helpers/TestDecisionsDataManager.ts index 2bb81a99e..74cabd83b 100644 --- a/services/api/src/test/helpers/TestDecisionsDataManager.ts +++ b/services/api/src/test/helpers/TestDecisionsDataManager.ts @@ -8,6 +8,7 @@ import { profileUserToAccessRoles, profileUsers, profiles, + proposals, users, } from '@op/db/schema'; import { ROLES } from '@op/db/seedData/accessControl'; @@ -530,6 +531,7 @@ export class TestDecisionsDataManager { /** * Creates a proposal via the tRPC router and tracks its profile for cleanup. + * If `description` is provided, removes `collaborationDocId` to simulate legacy proposals. */ async createProposal({ callerEmail, @@ -557,6 +559,18 @@ export class TestDecisionsDataManager { this.createdProfileIds.push(proposal.profileId); } + // Simulate legacy proposal by removing collaborationDocId when description is provided + if (proposalData.description) { + const { collaborationDocId: _, ...legacyProposalData } = + proposal.proposalData as Record