Skip to content
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(dashboard): restore email editor 'for' block #7483

Merged
merged 11 commits into from
Jan 14, 2025

Conversation

ChmaraX
Copy link
Contributor

@ChmaraX ChmaraX commented Jan 10, 2025

What changed? Why was the change needed?

  • DX: iterable needs to be referenced in full, e.g. when looping over {{payload.comments}}, item is {{payload.comments.id}}
  • the preview always generates 3 example items in the array
  • I've tried to restore the functionality with the least amount of code change, but in my opinion heavy refactoring is required for the entire preview

High-level overview of how the parsing flow goes so we can reference it later when refactoring:
image

for-loop.mov

Copy link

netlify bot commented Jan 10, 2025

Deploy Preview for dev-web-novu ready!

Name Link
🔨 Latest commit 9c8d4a5
🔍 Latest deploy log https://app.netlify.com/sites/dev-web-novu/deploys/67867809d4881d000968df6a
😎 Deploy Preview https://deploy-preview-7483.dashboard.novu-staging.co
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

netlify bot commented Jan 10, 2025

Deploy Preview for dashboard-v2-novu-staging ready!

Name Link
🔨 Latest commit 9c8d4a5
🔍 Latest deploy log https://app.netlify.com/sites/dashboard-v2-novu-staging/deploys/678678093c45d10009f9d9b1
😎 Deploy Preview https://deploy-preview-7483.dashboard-v2.novu-staging.co
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@ChmaraX ChmaraX changed the title feat(dashboard): restore 'for' block; refactor feat(dashboard): restore email editor 'for' block Jan 10, 2025
Copy link
Contributor

@djabarovgeorge djabarovgeorge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work, i left a couple of comments about my opinion of responsibility spread in the code and the meaning of it.

@@ -39,12 +39,12 @@ export class HydrateEmailSchemaUseCase {
index: number
) {
content[index] = {
type: 'text',
type: 'variable',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was there a specific reason for this change? When parsing the variable type from Maily, we need to convert it to text because Liquid is responsible for variable parsing.
in addition, this helps us avoid any side effects from Maily's variable parsing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required for the 'for' logic down the line. The output from this hydrate schema usecase looks something like this:

{
      "type": "for",
      "content": [
        {
          "type": "paragraph",
          "content": [
            {
              "type": "text",
              "text": "comments: "
            },
            {
              "type": "variable", <--- This needs to be variable not text
              "text": "{{ payload.comments.text }}"
            }
          ],
          "attrs": {
            "textAlign": "left"
          }
        }
      ],
 }

If it would be replaced by type: text here, I wouldn't be able to know which node is variable and which text, therefore I wouldn't be able to transform the variable here from {{ payload.comments.text }} to {{ payload.comments[0].text }}

Copy link

pkg-pr-new bot commented Jan 13, 2025

Open in Stackblitz

@novu/client

npm i https://pkg.pr.new/novuhq/novu/@novu/client@7483

@novu/node

npm i https://pkg.pr.new/novuhq/novu/@novu/node@7483

@novu/notification-center

npm i https://pkg.pr.new/novuhq/novu/@novu/notification-center@7483

@novu/headless

npm i https://pkg.pr.new/novuhq/novu/@novu/headless@7483

novu

npm i https://pkg.pr.new/novuhq/novu@7483

@novu/providers

npm i https://pkg.pr.new/novuhq/novu/@novu/providers@7483

@novu/shared

npm i https://pkg.pr.new/novuhq/novu/@novu/shared@7483

commit: 9c8d4a5

}

private hasEach(node: TipTapNode): node is TipTapNode & { attrs: { each: unknown } } {
return !!(node.attrs && 'each' in node.attrs);
private async handleShowNode(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show is an Attribute, not a node. So to be precise we should name this as handleShowAttr

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even more precise would be handleNodeByShowAttr because here we decide if to show a Node based on an attribute.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to disagree here, I think this way the naming is more common and understandable.

We are processing nodes and the for and show are a specific type of nodes. We are not processing the attribute only, we are processing an entire node which has attr = "for", similar as step.type = "email".

When you first time look at this method, its clear that if the node is of type for you do something with the node - you dont need to know about some internal property name such as attr or anything:

  private async processNode(node: TipTapNode, variables: FullPayloadForRender, parent?: TipTapNode): Promise<void> {
    if (this.hasShow(node)) {
      await this.handleShowNode(node, variables, parent);
    }

    if (this.hasEach(node)) {
      await this.handleEachNode(node, variables, parent);
    }
  }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about handleEach, handleShow?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the validation became even more complex, this is fine for now but i wonder if we could make the validation work without this change.


return this.regularExpansion(eachObject, templateContent);
}
const iterableArray = this.getValueByPath(variables, iterablePath) as [{ [key: string]: unknown }];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we do not need to getValueByPath, id suggest doing the following:
go to variableAttributeConfig
add a new entry, { attr: MailyAttrsEnum.EACH, flag: MailyAttrsEnum.EACH }
the result will be that in the pipeline we will wrap each attribute (node.attrs.each) with {{...}}
then you could get the value from the variable using liquid js.

Suggested change
const iterableArray = this.getValueByPath(variables, iterablePath) as [{ [key: string]: unknown }];
const iterableArray = await parseLiquid(iterablePath as string, variables);

let me know what you think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, iterableArray accepts and returns this:

// variables, iterablePath = "payload.comments"
const iterableArray = this.getValueByPath(variables, iterablePath) as [{ [key: string]: unknown }];
/**
* iterableArray = [
*  {
*    name: '{{payload.comments.name}}',
* },
* {
*   name: '{{payload.comments.name}}',
* },
* {
*   name: '{{payload.comments.name}}',
* }
*]
**/

What you are suggesting, would return just the iterable path string, not the actual array:

const iterableArray = await parseLiquid(iterablePath as string, variables);
// iterableArray = payload.comments

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if i understand it should return an array.

Editor:

for block {{ payload.comments }}

Variable:

{"payload" : {
    "comments": [
      " a_1 ",
      " a_2 ",
      " a_3 "
    ]
  }
}

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It currently doesnt work like this because we dont have for block {{ payload.comments }} anywhere in the maily json, we have just each: {{ payload.comments}}. That said its interesting idea to explore I will have a look if we can do this separately.

Copy link
Contributor

@djabarovgeorge djabarovgeorge Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for semantics representation aside, this one should work, doesn't it?
we have each: {{ payload.comments}}
with data variables: {"payload":{"comments":[" a_1 "," a_2 "," a_3 "]}}
once we execute const iterableArray = await parseLiquid(iterablePath as string, variables);
we should get the array [" a_1 "," a_2 "," a_3 "]

if (showIfKey === undefined) {
return;
}
private addIndexesToPlaceholders(nodes: TipTapNode[], iterablePath: string, index: number): TipTapNode[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about?

Suggested change
private addIndexesToPlaceholders(nodes: TipTapNode[], iterablePath: string, index: number): TipTapNode[] {
private addIndexesToLiquidOutput(nodes: TipTapNode[], iterablePath: string, index: number): TipTapNode[] {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the placeholders ({{payload.something}}) are not liquid output right? They are more Liquid input, since the parsing by Liquid happens later and this is usecase is an input for that:

 const parsedTipTap = await this.parseTipTapNodeByLiquid(expandedMailyContent, renderCommand);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Liquid JS calls the value with filters wrapped with {{...}} an Output i using Output instead of placeholder would help us unify and help us to understand each other better.

and the main logic in this function is
newNode.text = nodePlaceholder.replace(iterablePath, ${iterablePath}[${index}]);
which creates a liquid js Output.
Let me know what you think about it.

In addition, apologies for missing this earlier I noticed that we are using string.replace. Instead, we should use wrapInLiquidOutput whenever we have a Maily fallback attribute.

Copy link
Contributor

@djabarovgeorge djabarovgeorge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work, looks super clean! please go over the comments

as a side thought, what do we think about adding indexes tp the preview? At first glance, it’s hard to understand that each value in the payload corresponds to a separate line in the preview, especially since the strings are identical.
image

@ChmaraX ChmaraX merged commit fe98e77 into next Jan 14, 2025
42 checks passed
@ChmaraX ChmaraX deleted the nv-5034-NV-5138-fix-preview-of-maily-for-block branch January 14, 2025 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants