firecmsco / firecms

Awesome Firebase/Firestore-based CMS. The missing admin panel for your Firebase project!
https://firecms.co
Other
1.15k stars 184 forks source link

Reference field for non-firebase reference field types: Data is not a reference error #502

Open michi88 opened 1 year ago

michi88 commented 1 year ago

This may be a bit confusing but bear with me here.

While trying out this project I was stumped for a long time why a reference field was not working. I would always get Data is not a reference.

What I actually didn't know, and is not specified in the docs anywhere, is that this requires a Firebase reference field type. I'm guessing this field type is not being used a lot as it is not pushed in any Google docs as well. At least I'm not using it. The default in projects I see is to just use an ID of the referenced objects and build the paths in code to fetch data etc.

I understand that we need to provide some more data for FireCMS to be able to correctly fetch the referenced object(s) etc. but from a user perspective one would only need to provide a function to build the path maybe? Or if the path is just a collection and an entity ID, we'd only need that info.

Did a quick POC how it can work for reading/displaying these types of fields:

You could make an helper like this:

/**
 * Apparently a 'reference' field requires an actual firestore reference field
 * to work. So we need to create a field that will fetch the referenced object and
 * then render a reference preview. Then is can be added as an additional field.
 *
 * TODO: Ask FireCMS team why a reference field is actually necessary for this.
 *  it seems like a regular use-case to me and it would be great if updating
 *  a field like this would also work.
 */
function referenceFromIdFieldBuilder<
  EntityT extends { [K in ReferenceFieldId]: string },
  ReferenceFieldId extends Extract<keyof EntityT, string>,
  ReferencedCollectionT
>({
  referenceId,
  name,
  width,
  collection,
  referencePreviewProps
}: {
  referenceId: ReferenceFieldId;
  name: string;
  width?: number;
  collection: EntityCollection<ReferencedCollectionT>;
  referencePreviewProps: Omit<ReferencePreviewProps, "reference">;
}): AdditionalFieldDelegate<EntityT> {
  return {
    id: referenceId,
    name: name,
    width,
    Builder: ({ entity, context }) => {
      const obj = entity.values;
      return (
        <AsyncPreviewComponent
          builder={context.dataSource
            .fetchEntity({
              path: collection.path,
              entityId: obj[referenceId],
              collection: collection
            })
            .then(refEntity => {
              if (!refEntity) {
                return undefined;
              }
              const reference = new EntityReference(
                refEntity.id,
                refEntity.path
              );
              return (
                <ReferencePreview
                  disabled={referencePreviewProps.disabled}
                  size={referencePreviewProps.size}
                  reference={reference}
                  previewProperties={referencePreviewProps.previewProperties}
                ></ReferencePreview>
              );
            })}
        />
      );
    },
    dependencies: [referenceId]
  };
}

Suppose you have an Organization{name: string} collection and an Account{name: string, organizationId: string} collection. You can then use it in additional fields like so:

const orgsCollection = buildCollection<Organization>({
  name: "Organizations",
  singularName: "Organization",
  path: dbPaths.organizations,
  properties: {
    name: {
      name: "Name",
      readOnly: true,
      dataType: "string"
    }
  }
});

const accountCollection = buildCollection<Account>({
  name: "Accounts",
  singularName: "Account",
  path: dbPaths.accounts,
  properties: {
    name: {
      name: "Name",
      readOnly: true,
      dataType: "string"
    },
  },
  additionalFields: [
    referenceFromIdFieldBuilder<Account, "organizationId", Organization>({
      referenceId: "organizationId",
      name: "Organization",
      collection: orgsCollection,
      referencePreviewProps: {
        disabled: false,
        size: "regular",
        previewProperties: ["name"]
      }
    })
  ]
});

This way at least we can render these fields but updating of course does not work.

Questions:

  1. Am I missing something here / is there a better way?
  2. Could we add a data type that supports updating if we can have the user configure the path in some way?
  3. Can we update the docs to make it clear that dataType: 'reference' requires a firebase reference?

For number 2. Say we have this fictional referenceById datatype:

const accountCollection = buildCollection<Account>({
  name: "Accounts",
  singularName: "Account",
  path: dbPaths.accounts,
  properties: {
    name: {
      name: "Name",
      readOnly: true,
      dataType: "string"
    },
    organizationId: {
      dataType: "referenceById",
      path: "organizations",
      name: "Related organization"
    }
  }
});

Then we'd know the base path ("organizations"), the field that holds the ID ("organizationId") and the value we have while rendering the entity. I understand this does not always work when dealing with subcollections and stuff so maybe a pathBuilder func needs to be provided by the user that takes the entity being rendered / updated or something but initially this was why I was confused as I thought this should work.

Hoping this issue will also save some googlers (and future ChatGPT ;-)) time figuring out what is going on.

Last but not least, thanks for the efforts here, I think FireCMS is something the Firebase ecosystem is lacking. We haven't decided yet if we'll adopt this in our projects but if we will, we will be sponsoring. Keep up the good work!

fgatti675 commented 1 year ago

Hi @michi88 Before jumping into your code to investigate, if I understand correctly, you are looking for a solution for storing a reference path like a string, but still be able to use the reference functionality of the CMS. If that is the case, the simplest way I can think of making it work is using EntityCallbacks: onFetch and onPreSave for adapting your data model. Also, keep in mind that you don't need to fetch the entity for building the reference as you do inside AsyncPreviewComponent, the path and id retrieved will be the same ones you input. Hope this helps

michi88 commented 1 year ago

Thanks for leaving a comment here @fgatti675 What do you mean by that I do not need to fetch the entity? I'm inputting entityId: obj[referenceId] which would be the id stored in the main document together with the path from the config, right?

michi88 commented 1 year ago

Also, when I'm using EntityCallbacks: onFetch and set the fetched values on the object as a map, they are only rendered in the list when I open the details dialog. Any way to get around that? I guess AsyncPreviewComponent should be used somehow? As I want to show multiple fields of a doc that needs to be fetched, I'd like to avoid loading the related doc for every field.