Shopify / ui-extensions

MIT License
269 stars 35 forks source link

Implement Rich Text Section Component to Match rich_text_field metafield Checkout UI Setting #2162

Open Jrschellenberg opened 4 months ago

Jrschellenberg commented 4 months ago

Please list the related package(s)

@shopify/ui-extensions-react/checkout

If this related to specific APIs or components, please list them here

Components

Is your feature request related to a problem? Please describe.

Currently there is a Metafield for a Rich Text Editor

[[extensions.settings.fields]]
key = "banner_message"
type = "rich_text_field"
name = "Banner Message"
default = "<p>thisistest</p>"

However To my knowledge (I spent hours trying to find something closest I could is this Hydrogen implementation)

https://github.com/Shopify/hydrogen/blob/788291c391993fac62f168db5940015b5ae1faf2/packages/hydrogen-react/CHANGELOG.md?plain=1#L9

However, I couldn't find a component within the @shopify/ui-extensions-react/checkout to implement the payload return from this rich_text_field Example:

{"type":"root","children":[{"type":"heading","children":[{"type":"text","value":"Heading1"}],"level":"1"},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"heading","children":[{"type":"text","value":"Heading2"}],"level":"2"},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"heading","children":[{"type":"text","value":"Heading3"}],"level":"3"},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"heading","children":[{"type":"text","value":"Heading4"}],"level":"4"},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"heading","children":[{"type":"text","value":"Heading5"}],"level":"5"},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"heading","children":[{"type":"text","value":"Heading6"}],"level":"6"},{"type":"paragraph","children":[{"type":"text","value":"asdf","bold":"true"},{"type":"text","value":"a"},{"type":"text","value":"sdfs","italic":"true"},{"type":"text","value":"d"}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"paragraph","children":[{"type":"text","value":"asdfasdfsd"}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"paragraph","children":[{"type":"text","value":"asdfas"},{"url":"https://google.com","title":"test","target":"_blank","type":"link","children":[{"type":"text","value":"dfsd"}]},{"type":"text","value":""}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"listType":"unordered","type":"list","children":[{"type":"list-item","children":[{"type":"text","value":"1"}]},{"type":"list-item","children":[{"type":"text","value":"2"}]},{"type":"list-item","children":[{"type":"text","value":"3"}]}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"listType":"ordered","type":"list","children":[{"type":"list-item","children":[{"type":"text","value":"1"}]},{"type":"list-item","children":[{"type":"text","value":"2"}]},{"type":"list-item","children":[{"type":"text","value":"3"}]}]},{"type":"paragraph","children":[{"type":"text","value":"dddd"}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"paragraph","children":[{"type":"text","value":""}]}]}

Describe the changes you are looking for

Adding a Rich Text Component similar to Hydrogen to the @shopify/ui-extensions-react/checkout Library

Note: You cannot use the hydrogen one, as it implements regular HTML, and this needs to follow the SDK rules of the @shopify/ui-extensions-react/checkout Library

A RichText Component implemented to work out of the box with the rich_text_field

Describe alternatives you’ve considered

I implemented one on my own Feel free to use this as a starting point, or at the very least hope it can help someone if they have a similar issue in the future

Example.extension.toml

[extensions.settings]
[[extensions.settings.fields]]
key = "banner_message"
type = "rich_text_field"
name = "Banner Message"
default = "<p>thisistest</p>"

[[extensions.settings.fields]]
key = "banner_title"
type = "single_line_text_field"
name = "Banner Title"

[[extensions.settings.fields]]
key = "banner_style"
type = "single_line_text_field"
name = "Banner Title Pick 1 of: 'info' | 'success' | 'warning' | 'critical'"

extension.jsx

import {
  Banner,
  reactExtension,
  useSubscription,
  useApi,
  Text,
  Link,
  BlockStack,
  List,
  View,
  ListItem,
  Heading,
} from '@shopify/ui-extensions-react/checkout';
import React from 'react';

// 1. Choose an extension target
export default reactExtension(
  'purchase.checkout.block.render',
  () => <Extension />,
);

function Extension() {
  const { settings } = useApi();
  const _settings = useSubscription(settings);
  let message = _settings?.banner_message || `{"type":"root","children":[{"type":"heading","children":[{"type":"text","value":"Heading1"}],"level":"1"},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"heading","children":[{"type":"text","value":"Heading2"}],"level":"2"},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"heading","children":[{"type":"text","value":"Heading3"}],"level":"3"},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"heading","children":[{"type":"text","value":"Heading4"}],"level":"4"},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"heading","children":[{"type":"text","value":"Heading5"}],"level":"5"},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"heading","children":[{"type":"text","value":"Heading6"}],"level":"6"},{"type":"paragraph","children":[{"type":"text","value":"asdf","bold":"true"},{"type":"text","value":"a"},{"type":"text","value":"sdfs","italic":"true"},{"type":"text","value":"d"}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"paragraph","children":[{"type":"text","value":"asdfasdfsd"}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"paragraph","children":[{"type":"text","value":"asdfas"},{"url":"https://google.com","title":"test","target":"_blank","type":"link","children":[{"type":"text","value":"dfsd"}]},{"type":"text","value":""}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"listType":"unordered","type":"list","children":[{"type":"list-item","children":[{"type":"text","value":"1"}]},{"type":"list-item","children":[{"type":"text","value":"2"}]},{"type":"list-item","children":[{"type":"text","value":"3"}]}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"listType":"ordered","type":"list","children":[{"type":"list-item","children":[{"type":"text","value":"1"}]},{"type":"list-item","children":[{"type":"text","value":"2"}]},{"type":"list-item","children":[{"type":"text","value":"3"}]}]},{"type":"paragraph","children":[{"type":"text","value":"dddd"}]},{"type":"paragraph","children":[{"type":"text","value":""}]},{"type":"paragraph","children":[{"type":"text","value":""}]}]}`;
  const title = _settings?.banner_title || 'Notice:';
  const style = _settings?.banner_style || 'warning';

  if(typeof message === 'string') {
    message = JSON.parse(message);
  }

  const renderRichText = (node) => {
    switch (node?.type) {
      case 'root':
        return (
          <BlockStack>
            {node?.children?.map((child, index) => (
              <BlockStack key={index}>{renderRichText(child)}</BlockStack>
            ))}
          </BlockStack>
        );
      case 'paragraph':
        return <View>{node?.children?.map(renderRichText)}</View>;
      case 'text':
        return (
          <Text emphasis={node?.bold ? 'bold' : (node?.italic ? 'italic' : undefined)}>
            {node?.value}
          </Text>
        );
      case 'link':
        return (
          <Link to={node?.url} title={node?.title} external={node?.target === '_blank'}>
            {node?.children?.map(renderRichText)}
          </Link>
        );
      case 'list':
        return (
          <List marker={node?.listType === 'ordered' ? 'number' : 'bullet'}>
            {node?.children?.map((child, index) => (
              <ListItem key={index}>{renderRichText(child)}</ListItem>
            ))}
          </List>
        );
      case 'list-item':
        return <ListItem>{node?.value}</ListItem>;
      case 'heading':
        const level = parseInt(node?.level, 10);
        if (level >= 1 && level <= 3) {
          const child = node?.children?.length === 1 && node?.children?.[0]?.type === 'text' ? node?.children?.[0].value : node?.children?.map(renderRichText);
          return (
            <Heading level={level}>
              {child}
            </Heading>
          );
        } else {
          const size = level === 4 ? 'base' : level === 5 ? 'small' : 'extraSmall';
          const child = node?.children?.length === 1 && node?.children?.[0]?.type === 'text' ? node?.children?.[0]?.value : node?.children?.map(renderRichText);
          return (
            <Text size={size}>
              {child}
            </Text>
          );
        }
      default:
        return null;
    }
  };

  return (
    <Banner status={style} title={title}>
      {renderRichText(message)}
    </Banner>
  );
}

Additional context

GorgonFreeman commented 4 months ago

Agreed, tried to use this but no idea how to render the result. None of the components seem to take anything like what it outputs.

mattgast commented 4 months ago

I ran into the exact same thing yesterday and it would be great to see some more features coming to the Checkout UI components.

Borisav21 commented 10 hours ago

@Jrschellenberg You are the best, now I have rich text in my extension app.