gregsmirnov

yes, thank you. I was looking for this in silverstripe-framework 🙂

Scopey

The OS team uses ZenHub (browser extension / web app) to add some product mangement features to GitHub

Scopey

https://github.com/silverstripe/silverstripe-admin/issues/556

Show 1 attachment(s)
unclecheese

Overview

This RFC proposes a series of changes to the API and developer experience to support GridFields rendered with React and composed by GraphQL/Apollo data fetching.

Proof of concept

https://github.com/open-sausages/react-gridfield-poc|https://github.com/open-sausages/react-gridfield-poc

Key Challenges

• GraphQL queries are presumed static, bound to components at compile time. • Mandating that developers to hook into compile time operations is a non-starter (i.e. requiring node) • While developers will have to write Javascript, GridField remains primarily a PHP API. • Extensibility to both the UI and data layer are fully supported to the extent that they are in the current implementation • getCMSFields() is assumed non-deterministic, and therefore a non-qualifier for static analysis or evaluation (i.e. through singletons).

Proposal

Most of this can be done without major changes to the API, but the changes to the developer experience, particularly around getCMSFields will be non-trivial. The fundamental idea behind this proposal is that, while we can't inject at compile time, we can do quite a bit at boot time. This requires that every GridField declares itself on boot and this information is provided to the client to process the necessary transformations in Injector. Because every GridField needs to declare itself, all GridFields must be uniquely identified.

Create a GridField registry

A singleton instance of a GridFieldRegistry is used to store all GridFields in the system by their identifiers. This registry is populated using a method on a common interface, e.g. provideGridFields(GridFieldRegistry $registry), much like providePermissions.

class GridFieldRegistry
{
    public function add(string $identifier, GridField $gridField): void;

    public function getAll(): array;

    public function get(string $id): ?GridFieldRegistration;

}
class GridFieldRegistration
{
    public function __construct(string $identifier, GridField $gridField);
}

Classes that use GridField become providers

class MyDataObject extends DataObject implements GridFieldProvider
{
    public function provideGridFields(GridFieldRegistry $registry)
    {
        $registry->add(
            'MyGridIdentifier',
            GridField::create(
                'Products',
                'View / edit products ',
                $this->Products()
            )
        );
    }
}

The grid field is then fetched from the registry instead of instantiated in getCMSFields().

class MyDataObject extends DataObject implements GridFieldProvider
{
    public function provideGridFields(GridFieldRegistry $registry)
    {
        //...
    }

    public function getCMSFields()
    {
        return FieldList::create(
            TextField::create('Title'),
            Injector::inst()->get(GridFieldRegistry::class)->get('AllNotes')
        );
    } 
}

Ideally, the explicit call to Injector could be made more user-friendly with a trait (e.g. getGridField(string $id). Or even GridField::getByIdentifier($id)

Expose all dataobjects to graphql

A simple ScaffoldingProvider implementation will suffice.

class UniversalScaffolder implements ScaffoldingProvider
{
    public function provideGraphQLScaffolding(SchemaScaffolder $scaffolder)
    {
        foreach(ClassInfo::subclassesFor(DataObject::class) as $dataObjectClass) {
            if ($dataObjectClass === DataObject::class) continue;
            $dataObjectScaffold = $scaffolder
                ->type($dataObjectClass)
                ->addAllFields();
            foreach(OperationScaffolder::getOperations() as $identifier => $class) {
                $dataObjectScaffold->operation($identifier);
            }
        }

        return $scaffolder;
    }
}

A GridField ScaffoldingProvider runs through the registry and adds the requirements of each GridField

class GridFieldScaffolder implements ScaffoldingProvider
{
    public function __construct(GridFieldRegistry $registry);

    public function provideGraphQLScaffolding(SchemaScaffolder $scaffolder)
    {
        foreach ($this->registry->getAll() as $gridFieldRegistration) {
            $gridField = $gridFieldRegistration->getGridField();
            $identifier = $gridFieldRegistration->getIdentifier();
            $scaffolder->query(
                'readGridField' . $identifier,
                $gridField->getList()->dataClass(),
                function ($object, array $args, $context, ResolveInfo $info) use ($gridField) {
                    return $gridField->getList();
                }
            )
        }

        return $scaffolder;
    }
}

This presumes that GridField lists are DataList instances. If they're arbitrary ArrayList / ArrayData compositions, the developer would need to create his or her own custom GraphQL types and use a custom resolver.

Ideally this would be extensible through all the current channels, so that the SortComponent could jump in here and add ->addSortableColumns(array $fields) or something like that.

LeftAndMain provides client configuration about the registered gridfields.

        $combinedClientConfig['gridFieldQueries'] = [];
        $registry = Injector::inst()->get(GridFieldRegistry::class);
        foreach($registry->getAll() as $gridFieldRegistration) {
            $combinedClientConfig['gridFieldQueries'][] = [
                'name' => $gridFieldRegistration->getIdentifier(),
                'fields' => $gridFieldRegistration->getFields(),
                'components' => $gridFieldRegistration->getGridField()->getComponents(),
            ];
        }

getFields() is effectively GridField::getColumns(). getComponents() would be a serialised representation of the component configuration:

[
  { type: 'AddNewButton', position: 'before', title: 'Add a new record' }
]

A boot function transforms all of the GridField registrations with the injectGraphql HOC.

  Config.get('gridFieldQueries').forEach(gridFieldQuery => {
    const { name, fields, components } = gridFieldQuery;
    const query = {
      apolloConfig: {
         // map query data to props
      },
      templateName: READ,
      pluralName: `GridField${name}`,
      // ...
      fields,
    };

    const queryName = `${name}Query`;
    Injector.query.register(queryName, query);
    Injector.transform(
      `gridfield-graphql-${name}`,
      (updater) => {
        updater.component(context, injectGraphql(queryName));
      }
    );
});

This results in the creation of a query readGridField<MyGridFieldIdentifier>. Ideally this would use a custom template, other than read so it could be disambiguated as a GridField-specific operation in the schema, and we could name it something like get<GridFieldIdentifier>Data

GridField declares "slots" for component injection

      <div>
        <BeforeComponents />
        <table>
          <HeaderComponents />
          <tbody />
          <FooterComponents />
        </table>
        <AfterComponents />
      </div>

These slots are registered with Injector. (Injector.component.register('GridFieldBeforeComponentSlot', GridFieldComponentSlot))

The assigned components for each GridField are in the config and injected at boot time

    Injector.transform(
      `gridfield-${name}-components`,
      (updater) => {
        ['Before', 'Header', 'After'].forEach(key => {
          updater.component(
            `GridField${key}Components.${name}`,
            inject(
              appliedComponents,
              (...injectedComponents) => ({
                children: injectedComponents,
              })
            )
          );
        });
      }
    );

*GridField components can transform the query via inject…

Hide attachment content
gregsmirnov

Hello. Is there any wip or rfc for implementing gridfield in react?