decaporg / decap-cms

A Git-based CMS for Static Site Generators
https://decapcms.org
MIT License
17.91k stars 3.04k forks source link

fieldsMetaData empty #4819

Open logemann opened 3 years ago

logemann commented 3 years ago

Describe the bug fieldsMetaData attribute is empty on registerPreviewTemplate() handler.

To Reproduce register own PreviewTemplate and print out the incoming data.

CMS.registerPreviewTemplate('blog', BlogPostPreview)

const BlogPostPreview = (data) => {

    console.log(data);
    const {entry, widgetFor, getAsset, fieldsMetaData} = data;
    ...
}

Expected behavior fieldsMetaData with data about the collection and more importantly about the "relation" collections. Thats the reason why i need it.

Screenshots

Bildschirmfoto 2021-01-09 um 14 15 32

Applicable Versions: netlify-cms-app 2.14.10 netlify-cms-core 2.36.9 together with latest Gatsby.

CMS configuration

backend:
  name: github
  repo: logemann/okaycloudweb2
  branch: main

local_backend: true

logo_url: https://okaycloud.de/static/okaycloud_logo-f77199ecfa7a21de8ce142f51ab6009f.svg
site_url: https://okaycloud.de
display_url: https://okaycloud.de
locale: 'de'
media_folder: static/images
public_folder: /images
slug:
  encoding: "ascii"
  clean_accents: true

collections:
  - name: authors
    label: Authors
    folder: src/pages/blog/_authors/
    extension: .md
    media_folder: ''
    public_folder: ''
    format: frontmatter
    create: true
    fields:
      - { label: Name, name: name, widget: string }
      - { label: Title, name: title, widget: string }
      - { label: Email, name: email, widget: string }
      - { label: Shortbio, name: shortbio, widget: text }
      - { label: Image, name: authorimage, widget: image }
      - { label: "Template Key", name: "templateKey", widget: "hidden", default: "author" }

  - name: "blog"
    label: "Blog"
    folder: "src/pages/blog"
    create: true
    slug: "{{title}}"
    media_folder: ''
    public_folder: ''
    path: '{{year}}-{{month}}-{{day}}/{{title}}/index'
    fields:
      - { label: "Template Key", name: "templateKey", widget: "hidden", default: "single-blog-post" }
      - { label: "Title", name: "title", widget: "string" }
      - { label: "Author", name: "author", widget: "relation", collection: "authors", value_field: "email", display_fields: [ "name" ], search_fields: [ "name", "email" ] }
      - { label: "Publish Date", name: "date", widget: "datetime", date_format: 'DD.MM.YYYY', time_format: "HH:mm U\\hr" }
      - { label: "Description", name: "description", widget: "text" }
      - { label: "Featured Post", name: "featuredpost", widget: "boolean", required: false }
      - { label: "Featured Image (AR 1,0)", name: "featuredimage", widget: image }
      - { label: "Social Media Image (AR 2,0)", name: "socialmediaimage", widget: image }
      - { label: "Body", name: "body", widget: "markdown" }
      - {
        label: "Tags", name: "tags", widget: "select",
        options: [ "Daimler", "BMW", "Fiat", "VW", "Porsche", "Bankrecht", "Kapitalmarktrecht", "Versicherungsrecht",
                   "Handelsrecht", "Gesellschaftsrecht", "Video", "Podcast" ],
        multiple: true, max: 3 }
      - {
        label: "Category", name: "category", widget: "select",
        options: [ "General", "Websites / SEO", "Cloud Infrastructure", "Software Development" ],
        multiple: false }

Additional context

As written. For preview reason i need the authors image, which is stored in the "relation" collection. The only way to get those related attributes is via fieldsMetaData according to Docs.

One more note to the docs at https://www.netlifycms.org/docs/customization/ . I appreciate the specific section about MetaData but the example is pretty non-selfexplaining cause no one knows the data model behind this example. I mean, the obfuscation resulting by using Immutable.js is ok as long as the docs make it clear what kind of object "data" is in data.author. To me this snippet wouldnt even work because of that phantom "data" object.

Then in the document its written: "react_component: A React component that renders the collection data. Six props will be passed to your component during render:"

But with fieldsMetaData object, its 7 isnt it? Its not mentioned there as a bullet point.

I think tons of issues could be prevented by having proper API docs.

Pearce-Ropion commented 3 years ago

Fields meta data is always empty on the first render. You have to wait for the author data to be present since the call is async. Here's an example of how we do it with your config. Basically you need a bunch of if statements to get the required data.

// BlogPostPreview.js
const BlogPostPreview = ({ entry, fieldsMetaData, widgetFor, getAsset }) => {
    const props = entry.toJS().data;
    const relations = fieldsMetaData.toJS();

    const selectedAuthorEmail = props.author; // save the selected author email

    // remove the author email from the props (this makes it easier to handle
    // the preview since the preview is expecting an author object 
    props.author = {}; 

    let isCMSAuthorLoading; // Initialize this as undefined (see the component for why)

    // Only go into a loading state if the user has selected an author from the
    // relation dropdown
    if (selectedAuthorEmail && selectedAuthorEmail.length) {
        isCMSAuthorLoading = true;
    }

    // This requires some knowledge of how the fieldsMetaData object is structured.
    if (relations) {
        const authorRelationObj = relations.author;
        if (authorRelationObj) {
            const allAuthors = Object.values(authorRelationObj.authors);

            if (allAuthors.length) {
                // authorRelationObj.authors will have all of the authors the
                // were selected or previously selected during the user's
                // interaction with this blog post within the current session.
                // So we need to find the correct author.
                const author = allAuthors.find(({ email }) => email === selectedAuthorEmail);

                if (author) {
                    isCMSAuthorLoading = false;

                    // set the found author relation "in-place" within the
                    // entry's props
                    props.author = author;
                }
            }
        }
    }

    return (
        <BlogPost isCMSPreview isCMSAuthorLoading={isCMSAuthorLoading} {...props} />
    )
};

// BlogPost.js
const BlogPost = ({ isCMSPreview, isCMSAuthorLoading, author, ...otherBlogPostProps }) => {

    // The `author` prop is now an object at all times so we can always do this spread `...` operator
    let authorComponent = <AuthorComponent {...author} />;    

    // Do some logic using the `isCMSAuthorLoading` prop to determine if the
    // author relation data has started/finished loading and render some basic
    // informational components if the info isn't available yet.
    if (isCMSPreview) {
        if (isCMSAuthorLoading === false) {
            authorComponent = <AuthorComponent {...author} />;
        } else if (isCMSAuthorLoading === true) {
            authorComponent = (
                <div style={{ margin: "40px 0" }}>
                    {/* Render a loading component if you have one */}
                    {/* <LoadingIcon />  */} 
                    <p style={{ textAlign: "center" }}>
                        Author Loading. Please Wait...
                    </p>
                </div>
            );
        } else if (isCMSAuthorLoading === undefined) {
            authorComponent = (
                <div style={{ margin: "40px 0" }}>
                    <p style={{ textAlign: "center", color: "red" }}>
                        Select an author from the author dropdown
                    </p>
                </div>
            );
        }
    }

    return (
        <div className="blog-post">
            {authorComponent}
            -- Content --
        </div>
    )
}

You can also replace the .find() with a .filter() and a .every() if your relation widget is set to multiple: true.