-
Notifications
You must be signed in to change notification settings - Fork 3
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
feat(common): new subscription model #260
feat(common): new subscription model #260
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
let assetSelectedCallbackRef: undefined | ((filename: Asset) => void) = | ||
undefined | ||
let assetSelectedCallbackId: undefined | string = undefined | ||
const { pushCallback, popCallback } = callbackQueue() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function returns two functions that lets you manage callback functions.
When pushing, you specify the type of the callback function, and the callback function itself. It returns an id that you can include in the message to the visual editor.
When popping, you specify the callbackId and the type of the callback function, this way, typescript is able to guarantee that the callback function is of the expected type.
const onStateChange: OnStateChangeMessage = (data) => { | ||
state = { | ||
...state, | ||
...partialPluginStateFromStateChangeMessage(data), | ||
} | ||
onUpdateState(state) | ||
popCallback('state', data.callbackId)?.(data) | ||
onUpdateState(pluginStateFromStateChangeMessage(data)) | ||
} | ||
const onLoaded: OnLoadedMessage = (data) => { | ||
onUpdateState(pluginStateFromStateChangeMessage(data)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a new message. Whenever the state in the field plugin editor changes, it will send an event with the new state to the field plugin. The state is identical to the state in the loaded
message.
Two things happen:
onUpdateState
is called, as before, except we no longer need to store any state, sinceisModalOpen
is included.- The callback function that is associated with the
callbackId
in the message is invoked (if found)
@@ -114,45 +96,42 @@ export const createPluginActions: CreatePluginActions = ( | |||
|
|||
return { | |||
actions: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As seen here, all actions return a promise that resolves when a confirmation of the message has been received.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love how these actions are uniform now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed this from StateChangedMessage
to LoadedMessage
, because the StateChangedMessage
is now referring to a different message (the one that is sent during the plugin's lifecycle)
@@ -3,6 +3,7 @@ import { hasKey } from '../../../utils' | |||
export type MessageToPlugin<Action extends string> = { | |||
action: Action | |||
uid: string | |||
callbackId?: string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every message has potentially a callbackId
. It is optional because sometimes, the event was not triggered by the plugin, so there would be no callback to refer to
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you give an example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following MessageToContainer
messages resolve a promise: loaded, asset-selected
, update
, toggle-modal
, request-context
. Therefore, all these messages can contain a callbackId
.
The heightChange
message does not resolve a promise. Right now, I don't see a reason why it would. However, it is conceivable. Not sure if we should omit callbackId
from MessageToContainer
because of this.
For MessageToPlugin
, all messages could resolve a promise: loaded
, state-changed
, asset-selected
, context
, so all can contain a callbackId
.
Actually, right now, there is no real example of a MessageToPlugin
message that does not contain a callbackId
... but, I can invent one!
Let's say the user clicks outside the modal window: ideally, this would close the modal window. Upon such click event, the Visual Editor sends a state-updated
message to the field plugin. The event originated from the Visual Editor, so there is no promise to resolve, therefore the callbackId
is omitted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, thanks for the explanation. Yeah it makes sense to have it optional. We cannot revert it if we being with it being required :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@johannes-lindgren @eunjae-lee Can you explain what how we will use the callbackId, or why does it need to be present?
clarified
@@ -18,17 +18,18 @@ export type StateChangedMessage = MessageToPlugin<'loaded'> & { | |||
// Related to the field type itself | |||
schema: FieldPluginSchema | |||
model: unknown | |||
isModalOpen: boolean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The state changed message includes the state of the modal window
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to add tests for the new StateChangedMessage
As you can see in the pull request, I intend to make each action return a promise, but I am not sure how to implement this in Storydfront yet; the code there is a bit messy. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I absolutely love this. Let's keep going! 🚀
type CallbackMap = { | ||
asset: CallbackRef<AssetSelectedMessage>[] | ||
context: CallbackRef<ContextRequestMessage>[] | ||
state: CallbackRef<LoadedMessage>[] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's say
const plugin1 = createFieldPlugin();
const plugin2 = createFieldPlugin();
const plugin3 = createFieldPlugin();
...
plugin1.actions.setContent({...});
After that, the container will send the new state. In this case, I think all of plugin 1 ~ 3 should receive the state update regardless of callbackId. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, all of them would. And the promise that was created in one of them will be resolved. These lines are for handling the callback function in the promise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see. All the plugins get updated by onLoaded
event, but only the caller's promise gets resolved by the callback.
I thought valueChangeMessage and modalChangeMessage contained the states, but they're just return value from the Container, and I assume the Container send a separate loaded
event, so that all the plugins' onLoaded
event gets triggered?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onLoaded
is called only once after the library is loaded.
And there's onStateChange
, which can be returned after setContent
or setModalOpen
.
If plugin1
calls setContent
, and Storyfront will stateChanged
event back to all of plugin1 ~ 3. Only plugin1 will resolve the promise. However, all of plugin1 ~ 3 will still call the updater function.
popCallback('stateChanged', data.callbackId)?.(data)
onUpdateState(pluginStateFromStateChangeMessage(data))
I was writing down a question, and in the middle of it, I understood. But still left it as a record for others :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, precisely 🙂
callbackId: CallbackId | undefined, | ||
): CallbackMap[T][number]['callback'] | undefined => { | ||
// TODO remove callback when popping | ||
return callbackMap[callbackType].findLast( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure but don't we need to find the first one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on whether we push items to the front or to the back of the queue. Since each callbackId
is unique, it will find the element regardless of how it traverses the list, but it's slightly better to check from the end of the array, since the array is sorted by order of insertion.
resolve(pluginStateFromStateChangeMessage(message)), | ||
) | ||
postToContainer( | ||
valueChangeMessage({ uid, callbackId, model: content }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it'd be nice to rename this to contentChangeMessage in the future (not in this PR). It's a note for myself :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is true. (@demetriusfeijoo We started with the word "value" and switched to "content")
@@ -114,45 +96,42 @@ export const createPluginActions: CreatePluginActions = ( | |||
|
|||
return { | |||
actions: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love how these actions are uniform now.
@@ -4,6 +4,7 @@ import { hasKey } from '../../../utils' | |||
export type PluginLoadedMessage = MessageToContainer<'loaded'> & { | |||
// signals that the field plugin is responsible for its own scrolling in modal mode | |||
fullHeight?: boolean | |||
subscribeState?: boolean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it to inform something to the container?
packages/field-plugin/src/createFieldPlugin/createPluginActions/createPluginActions.ts
Outdated
Show resolved
Hide resolved
I'd love to get your reviews @demetriusfeijoo @BibiSebi on this :) |
Current dependencies on/for this PR:
This comment was auto-generated by Graphite. |
// Previously, debounced message was the default behavior. | ||
// That debouncing implementation can be problematic, for example, | ||
// when multiple field plugin instances request for context. | ||
debounce: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never saw this before, is the debouncing implemented inside the storyfront?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BibiSebi yes, but only for requestContext
. The debouncing is enabled by default for requestContext
, but neither Johannes nor I know why it exists. And it seems troublesome especially with multiple field plugin instances. So I've added this new property to bypass debouncing, which is used only for the Field Plugin SDK (not the legacy ones).
This is huge, thank you for your efforts @johannes-lindgren and @eunjae-lee 👏 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @eunjae-lee, I've left some notes.
Let me know if they make sense.
Related to the new approach, everything look nice to me.
I didn't find any particular or possible issue.
setHeight, | ||
setModalOpen: setModalOpen, | ||
setHeight: onHeightChange, | ||
setModalOpen: onModalChange, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
|
||
dispatchStateChanged(stateChangedData) | ||
}, [dispatchStateChanged, stateChangedData]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
}), | ||
[uid, content, language, schema, story], | ||
[uid, content, language, schema, story, isModalOpen], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @eunjae-lee, here eslint is warning for missing dependencies:
@@ -1,24 +1,27 @@ | |||
import { | |||
AssetModalChangeMessage, | |||
ContextRequestMessage, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was imported but it's not being used @eunjae-lee.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @demetriusfeijoo ! My vim didn't show me this lint errors 😅 And we also need to fix the CI to check these. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for all the effort on this one @johannes-lindgren and @eunjae-lee.
Aewsome!
Merge Activity
|
## What? This PR adds `parseContent` as the option to `createFieldPlugin()` function. This makes sure that we always pass around correctly formed payload. This is almost the exact porting of #241. @johannes-lindgren did the job there, but since #260, it was too hard to resolve conflicts. So I've created a new PR here. ## Why? JIRA: EXT-1918 more discussion on #241 ## Notice It's expected to see tests broken in this pull request, as the demo and the helpers are not taking this change into account. It will be fixed in the separate pull request.
What?
This PR introduces callback queue
callbackId
to messages to ContainercallbackId
callbackId
matches.This enables us to
createFieldPlugin()
Why?
JIRA: EXT-1957
How to test? (optional)