saleor / apps

A central space for Saleor Apps, integrations, and the App Store 🚀
https://apps.saleor.io
Other
112 stars 307 forks source link

[RFC] Resource Picker interface #315

Open lkostrowski opened 1 year ago

lkostrowski commented 1 year ago

Context

Browsing resources in Dashboard is critical for great UX/DX. Users can have very rich data and a huge volume of various products, orders, customers, etc.

To make this experience great, Dashboard grows with advanced data grids with rich filtering, sorting, and configuration.

However, Apps don't share a Dashboard codebase. Each app, despite access to the same Saleor resources and Macaw UI, will have to implement resource lists on its own. Considering scope of app app its not possible to provider such a good UX as the Dashboard.

To avoid solutions based on shared code or code duplication, app can establish an interface with Dashboard and ask Dashboard to provide required UI. This is the scope of this RFC

UI context

Simple drafts:

App that provides control to open a Resource Picker (in this case - customers)

1

Dashboard opens dedicated resource picker outside of the iframe

Zrzut ekranu 2023-03-20 o 15 11 51

Proposed solution

The unit responsible of structured postMessage communication between the Dashboard frontend and the App frontend is AppBridge

AppBridge provides a dispatch() method, and there are strictly typed factories for every event type.

This solution extends the existing interface and adds more actions and responses.

API

postMessage doesn't provide a "response" concept. If the dashboard successfully handles the message, it sends another postMessage with the same ID (that is provided by the app or random), so the App can handle failures.

This requires bi-directional communication.

Request interface

The request will be initialized from the app.

appBridge.dispatch(
  actions.requestResources({
    resourceType: ResourceType,
    options: Options,
    id?: string
  })
)

Under the hood, requestResources is translated to include type: "resources-request"

id can be set to exclusively match requests with a response

ResourceType

The ResourceType is the literal representing every supported resource, for example:

export type ResourceType = "customers" | "orders" | "products" | "categories" | "collections" | "giftcards" | "vouchers" etc.

Each resource can be implemented iteratively.

Options

Options can contain an additional payload that the dashboard can use to pre-configure the resource picker. The simple and accurate use case is to provide initial filters, sort, and search fields that the app assumes should be picked.

Question/to discuss: should the app be able to "lock" the filter, or should the user be able to remove the pre-selected filter and pick any resource?

Use case: app wants to work on fulfilled orders only. It will set the initial filter to status: fulfilled. Users can override it, but the app will filter them anyway.

API for Options TBD, based on Dashboard team input on how data grid is configurable.

Request handling

The dashboard will implement a resource picker on some kind of full-screen (or almost full-screen) overlay/modal. The app can't be unmounted.

Security

The dashboard must validate (on the frontend) if the user has permission to display the resource. The dashboard should return an error response (success: false + message)

Response interface

Dashboard will respond with a postMessage and event will be constructed via factory from the app-sdk

  actions.sendResources({
    resourceType: ResourceType,
    resources: Array<ResourceMeta>,
    id: string
  })

ResourceMeta

It's hard to implement the simple interface to provide all required fields that the app needs. It's where graphQL is powerful, but implementing this in a simple postMessage interface will be hard, especially considering limited access due to permissions

Hence, in the first scope, I suggest only providing very basic data. If an app requires it, it has to fetch it from the API. In the future we can extend it with some dynamic schema building so App can request what it needs

Very simple and initial ResourceMeta can be represented like this:

type ResourceMeta = { type: "order"; id: string }

type field is helpful, because it allows App codebase to implement type guards, without accessing parent object:

For example, further implementations can look more advanced:

type OrderResourceMeta =  { type: "order"; id: string, status: OrderStatus }
type ProductResourceMeta = { type: "products"; variants: string[] }

type ResourceMeta = ProductResourceMeta | OrderResourceMeta

Thanks to the type App can easily write such logic:

resources.map(resource => {
  if(isOrderResource(resource)) {
    console.log(resource.status) // Typescript knows that "status"  is defined
  }
})
andrzejewsky commented 1 year ago

Entire process make sense to me. Very nice idea with discriminating union (type guard by type field) 👍 .

ResourceMeta - so do we want to keep just the ids of resources? If so can we name it accordingly eg. variantId instead of variants - small thing but self-explanatory

Options - I think we can combine options and resource type together and based on the resource type prepare specific options. Here we can also benefit from type guards.

type CustomerResource =  {
  type: "customer",
  options: SomeCustomerRelatedOptions
}

type ProductResource =  {
  type: "product",
  options: SomeProductRelatedOptions
}

type Resource = 
 | CustomerResource
 | ProductResource

  actions.sendResources({
    resource: Resource,
    id: string
  })

Pre-filtering - I think some of them shouldn't be even overridable, or perhaps dashbaord can check permissions and allow it or not (depending on the resource type and its scope)

krzysztofzuraw commented 1 year ago

I also like the idea of Resource Picker - I'm curious if MacawUI components can help reduce the boilerplate needed to serve all use cases e.g loading, error states, etc., or if it should be AppBridge who is responsible for providing such components?

lkostrowski commented 1 year ago

@krzysztofzuraw AppBridge is a communication bus only and the scope of the RFC is the contract only.

In terms of the UI there are a few assumptions

The subject of further discussion is what "business" components to developer and in what package

krzysztofwolski commented 1 year ago

Additional request based on my apps: Attribute picker. It's used by search and product feed to choose which Saleor Attributes should be submitted to the external APIs.