Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bug(admin): Admin view of Blocks nested within a Page don't load default locale data #6292

Open
lynndylanhurley opened this issue May 9, 2024 · 5 comments

Comments

@lynndylanhurley
Copy link

lynndylanhurley commented May 9, 2024

Expected behavior:

When switching to non-default locales, if a translated value has not been provided, the admin editor should load the content from the default locale.

Current behavior:

When switching to non-default locales, nested block data is empty in the admin view.

Setup:

I was able to reproduce with the latest from the main branch of this repo, using payload version 3.0.0-beta.24.

I made a couple of changes to the payload.config.ts file to test nested blocks with locales. Here is the full config with the changes commented:

import path from 'path'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { en } from 'payload/i18n/en'
import {
  AlignFeature,
  BlockQuoteFeature,
  BlocksFeature,
  BoldFeature,
  ChecklistFeature,
  HeadingFeature,
  IndentFeature,
  InlineCodeFeature,
  ItalicFeature,
  lexicalEditor,
  LinkFeature,
  OrderedListFeature,
  ParagraphFeature,
  RelationshipFeature,
  UnorderedListFeature,
  UploadFeature,
} from '@payloadcms/richtext-lexical'
import { buildConfig } from 'payload/config'
import sharp from 'sharp'
import { fileURLToPath } from 'url'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

export default buildConfig({
  editor: lexicalEditor(),
  collections: [
    {
      slug: 'users',
      auth: true,
      access: {
        delete: () => false,
        update: () => false,
      },
      fields: [],
    },
    {
      slug: 'pages',
      admin: {
        useAsTitle: 'title',
      },
      fields: [
        {
          name: 'title',
          type: 'text',
        },
        {
          name: 'content',
          type: 'richText',
        },
        // added this nested blocks field
        {
          name: 'blocks',
          label: 'Blocks',
          type: 'blocks',
          blocks: [
            {
              slug: 'textBlock',
              fields: [
                {
                  type: 'text',
                  name: 'content',
                  localized: true, // <-- this field should be localized
                  label: 'Content'
                }
              ]
            }
          ]
        }
      ],
    },
    {
      slug: 'media',
      upload: true,
      fields: [
        {
          name: 'text',
          type: 'text',
        },
      ],
    },
  ],
  secret: process.env.PAYLOAD_SECRET || '',
  typescript: {
    outputFile: path.resolve(dirname, 'payload-types.ts'),
  },
  // using postgers instead of mongo
  db: postgresAdapter({
    pool: {
      connectionString: process.env.POSTGRES_URI || ''
    }
  }),
  i18n: {
    supportedLanguages: { en },
  },

  // added localization config
  localization: {
    defaultLocale: 'en',
    locales: ['en', 'es']
  },

  admin: {
    autoLogin: {
      email: 'dev@payloadcms.com',
      password: 'test',
      prefillOnly: true,
    },
  },
  async onInit(payload) {
    const existingUsers = await payload.find({
      collection: 'users',
      limit: 1,
    })

    if (existingUsers.docs.length === 0) {
      await payload.create({
        collection: 'users',
        data: {
          email: 'dev@payloadcms.com',
          password: 'test',
        },
      })
    }
  },
  sharp,
})

Notes

When loading the content into the front-end of my app, it does seem to pull in the default locale data.

It's only the admin view where the localized values are empty.

@jmikrut
Copy link
Member

jmikrut commented May 9, 2024

Hey there! This is intended functionality actually. We could make it configurable in the future of course but right now the admin UI explicitly calls for fallback-locale=null.

Are you interested in us making this configurable for the admin panel? Open a feature request on the main Payload repository if so and we will do what we can!

@jmikrut jmikrut closed this as completed May 9, 2024
@lynndylanhurley
Copy link
Author

Hi @jmikrut ! Thank you for the quick response.

The fact that the content fields are blank isn't so much the issue.

The real problem is that the live update seems to pull directly from the content fields. So when they're blank they make it look like the live preview has no content at all, which is different than what you see on the actual live site.

Here's what we're using for the useLivePreview hook, which i think was lifted from the docs:

import { useCallback, useEffect, useRef, useState } from 'react';
import { ready, subscribe, unsubscribe } from '@payloadcms/live-preview';

const useLivePreview = <T>(props: {
  depth?: number;
  initialData: T;
  serverURL: string;
}): {
  data: T;
  isLoading: boolean;
} => {
  const { depth = 0, initialData } = props;
  const [data, setData] = useState<T>(initialData);
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const hasSentReadyMessage = useRef<boolean>(false);

  const onChange = useCallback((mergedData: T) => {
    // When a change is made, the `onChange` callback will be called with the merged data
    // Set this merged data into state so that React will re-render the UI
    setData(mergedData);
    setIsLoading(false);
  }, []);

  useEffect(() => {
    // Listen for `window.postMessage` events from the Admin panel
    // When a change is made, the `onChange` callback will be called with the merged data
    const serverURL = window.location.origin;

    const subscription = subscribe({
      callback: onChange,
      depth,
      initialData,
      serverURL,
      apiRoute: '/api'
    });

    // Once subscribed, send a `ready` message back up to the Admin panel
    // This will indicate that the front-end is ready to receive messages
    if (!hasSentReadyMessage.current) {
      hasSentReadyMessage.current = true;

      ready({
        serverURL
      });
    }

    // When the component unmounts, unsubscribe from the `window.postMessage` events
    return () => {
      unsubscribe(subscription);
    };
  }, [onChange, depth, initialData]);

  return {
    data,
    isLoading
  };
};

export default useLivePreview;

Correct me if I'm wrong, but I believe the mergedData value in the onChange callback used to contain the default locale values when there were no translations present. Currently it seems to contain exactly what's filled out in the CMS fields with no fallbacks.

@jmikrut
Copy link
Member

jmikrut commented May 10, 2024

Ohhhhhh I see. I will convert this to an issue on the main Payload repo and we will solve for it there. Also, today we released server component support for live preview which would not have this problem. You can check that out in the beta branch documentation - but this is definitely something that should be solved in the useLivePreview hook for sure.

On it.

@jmikrut jmikrut transferred this issue from payloadcms/payload-3.0-demo May 10, 2024
@jmikrut jmikrut reopened this May 10, 2024
@lynndylanhurley
Copy link
Author

Oh excellent. I'll check out the server component support. Thank you @jmikrut !!!

@lynndylanhurley
Copy link
Author

I was able to get it kind of working with this code:

import { useCallback, useEffect, useRef, useState } from 'react';
import { ready, subscribe, unsubscribe } from '@payloadcms/live-preview';
import * as R from 'ramda';

const isObject = (val: unknown) =>
  val && typeof val === 'object' && !Array.isArray(val);

const mergeArrays = (arr1: unknown[], arr2: unknown[]): unknown[] => {
  return R.range(0, Math.max(arr1.length, arr2.length)).map((i: number) => {
    if (arr1[i] === undefined) {
      return arr2[i];
    }
    if (arr2[i] === undefined) {
      return arr1[i];
    }
    if (isObject(arr1[i]) || isObject(arr2[i])) {
      // eslint-disable-next-line
      return deepMerge(arr1[i], arr2[i]);
    }
    return arr2[i];
  });
};

const deepMerge = <T, U>(obj1: T, obj2: U): T => {
  return R.mergeWithKey(
    (_key: string, left: object, right: object) => {
      if (isObject(left) || isObject(right)) {
        // lexical content merge
        if (Object.hasOwn(right ?? left, 'root')) {
          if (right) {
            return right;
          }

          return left;
        }

        return deepMerge(left, right);
      }
      if (Array.isArray(left) && Array.isArray(right)) {
        return mergeArrays(left, right);
      }
      return right;
    },
    obj1,
    obj2
  );
};

const useLivePreview = <T>(props: {
  depth?: number;
  initialData: T;
  serverURL: string;
}): {
  data: T;
  isLoading: boolean;
} => {
  const { depth = 0, initialData } = props;
  const [data, setData] = useState<T>(initialData);
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const hasSentReadyMessage = useRef<boolean>(false);

  const onChange = useCallback((mergedData: T) => {
    // When a change is made, the `onChange` callback will be called with the merged data
    // Set this merged data into state so that React will re-render the UI
    setData(deepMerge(R.clone(initialData), R.clone(mergedData)));
    setIsLoading(false);
  }, []);

  useEffect(() => {
    // Listen for `window.postMessage` events from the Admin panel
    // When a change is made, the `onChange` callback will be called with the merged data
    const serverURL = window.location.origin;

    const subscription = subscribe({
      callback: onChange,
      depth,
      initialData: R.clone(initialData),
      serverURL,
      apiRoute: '/api'
    });

    // Once subscribed, send a `ready` message back up to the Admin panel
    // This will indicate that the front-end is ready to receive messages
    if (!hasSentReadyMessage.current) {
      hasSentReadyMessage.current = true;

      ready({
        serverURL
      });
    }

    // When the component unmounts, unsubscribe from the `window.postMessage` events
    return () => {
      unsubscribe(subscription);
    };
  }, [onChange, depth, initialData]);

  return {
    data,
    isLoading
  };
};

export default useLivePreview;

This isn't ideal but it's working as a stop-gap until someone pushes a real fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants