payloadcms / payload

Payload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.
https://payloadcms.com
MIT License
24.57k stars 1.56k forks source link

Cannot exit/clear "Draft Preview" in Frontend #7848

Closed CodeKarl closed 1 month ago

CodeKarl commented 1 month ago

Link to reproduction

https://demo.payloadcms.com/

Describe the Bug

Once having pressed the Preview-button on a Draft- or Changed-status Page-Collection item. You go to the frontend to view it in Draft Preview mode.

There is then no way to return to the normal frontend in the unedited state.

Opening a new tab and going to the frontend again will also trigger the Preview mode now, making the published version inaccessible.

Is there any way to clear the Draft Preview-mode in the frontend to return to the current state?

I did notice I can change the history to the currently published version, but this will not clear the admin bar or other pages in Draft mode.

To Reproduce

  1. Login to Payload CMS
  2. Go to the "Pages"-collection
  3. Edit a page to trigger the "changed"-status
  4. Press "Preview"
  5. You will now enter the Frontend in Preview mode with the Admin bar visible
  6. There is no way to exit Preview mode in the Frontend
  7. Opening the website in another tab will also trigger Preview mode making you unable to show the current website unless you open a new incognito browser.

Payload Version

Seen in the 2.0 demo + The 3.0 beta Website Example Demo

Adapters and Plugins

No response

neverether commented 1 month ago

I believe at present the developer is responsible for revoking the draft mode cookie, see: https://github.com/payloadcms/payload/blob/ea48cfbfe9acabaf75c7adb008ce15f8103744a1/templates/website/src/app/next/exit-preview/route.ts

I ended up creating a banner that displays on my page but not within the livepreview iframe when a user has the draft mode cookie active:

const onExitPreview = () => {
  fetch("/exit-preview", {
    method: "GET",
  }).then(() => {
    window.location.reload();
  });
};

export const ExitPreviewBanner = () => {
  const [showBanner, setShowBanner] = useState<boolean>(false);

  useEffect(() => {
    // Prevent hydration error in SSR
    if (typeof window === "undefined" || window.frameElement) {
      setShowBanner(false);
    } else {
      setShowBanner(true);
    }
  }, []);

  if (!showBanner) {
    return null;
  }

  return (
    // ...
        <button onClick={onExitPreview}>
          Exit Preview
        </button>
    // ...
  )
CodeKarl commented 1 month ago
onExitPreview 

Thank for the response :)

I found a solution based on what you mentioned:

components/AdminBar/index.tsx

'use client'

import type { PayloadAdminBarProps, PayloadMeUser } from 'payload-admin-bar'
import React, { useEffect, useState } from 'react'

const CMS_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:8000';
const API_PATH = '/api';
const ADMIN_PATH = '/admin';

export const AdminBar: React.FC<{
  adminBarProps?: PayloadAdminBarProps
}> = (props) => {
  const [showDraftPreview, setShowDraftPreview] = useState(false)
  const [draftUser, setDraftUser] = useState<PayloadMeUser | undefined>(undefined);
  const [isExitingDraftMode, setIsExitingDraftMode] = useState(false)

  async function fetchDraftPreviewUser() {
    try {
      const draftModeResponse = await fetch('/next/check-draft-mode', { method: 'GET' });
      if (!draftModeResponse.ok) {
        throw new Error('Failed to check draft mode');
      }

      const { isDraft } = await draftModeResponse.json();
      if (!isDraft) {
        setShowDraftPreview(false);
        setDraftUser(undefined);

        // Exit early if not in draft mode as there is no need to fetch the user
        return;
      }

      const userResponse = await fetch(`${CMS_URL}${API_PATH}/users/me`, {
        method: 'GET',
        credentials: 'include',
      });
      if (!userResponse.ok) {
        throw new Error('Failed to fetch user');
      }

      const { user } = await userResponse.json();
      if (user) {
        setDraftUser(user);
        setShowDraftPreview(true);
      } else {
        setDraftUser(undefined);
        setShowDraftPreview(false);
      }
    } catch (error) {
      console.error('Error fetching draft preview user:', error);
      setShowDraftPreview(false);
    }
  }

  useEffect(() => {
    if (!isExitingDraftMode) fetchDraftPreviewUser();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    CMS_URL,
    ADMIN_PATH,
    API_PATH,
  ])

  async function onPreviewExit() {
    try {
      setIsExitingDraftMode(true);

      const response = await fetch('/next/exit-preview', { method: 'GET' });
      if (!response.ok) {
        throw new Error('Failed to exit preview');
      }

      // Hide the admin bar as you have exited draft mode
      setShowDraftPreview(false);

      // Reload the page to display the non-draft content
      window.location.reload();
    } catch (error) {
      console.error('Failed to exit preview:', error);
    } finally {
      setIsExitingDraftMode(false);
    }
  }

  if (!showDraftPreview) return null;

  return (
    <>
      <div className='z-50 fixed right-4 bottom-4 rounded-full px-4 py-2 shadow-[rgba(0,_0,_0,_0.25)_0px_16px_32px_0px]'>
        <div
          className='bg-[#1C1C1E] rounded-full absolute inset-0'
        />
        <div className='relative flex gap-2 items-center'>
          {isExitingDraftMode ? (
            <>
              <span className='loading loading-spinner loading-sm' />
              <span className='text-white'>
                Exiting Preview Mode...
              </span>
            </>
          ) : (
            <>
              <span className='bg-yellow-500 aspect-square h-3 rounded-full animate-pulse' />
              <span className='text-white'>
                Preview Mode{draftUser ? ` as ${draftUser.email}` : ''}
              </span>
              <button onClick={onPreviewExit}>
                X
              </button>
            </>
          )}
        </div>
      </div>
    </>
  )
}

In addition to the exit-preview-route I also added a check-draft-mode-route. This one just return if draft mode is active or not. As this cannot be done in the client-component of AdminBar. Then this is used to show the preview overlay or not.

next/check-draft-mode/route.ts:

import { draftMode } from 'next/headers'

export async function GET(): Promise<Response> {
  const { isEnabled: isDraft } = draftMode()

  return new Response(JSON.stringify({ isDraft }))
}

This code successfully disables the preview mode, no longer shows the preview overlay, returns the live page data instead of showing the draft version, and also keeps the admin user logged into payload cms in any other tab.

You can then re-enable draft mode by pressing the preview button in the payload cms admin panel.

github-actions[bot] commented 1 month ago

This issue has been automatically locked. Please open a new issue if this issue persists with any additional detail.