slack-block-builder icon indicating copy to clipboard operation
slack-block-builder copied to clipboard

Add support for RichText elements group

Open SnapeEye opened this issue 1 year ago • 5 comments

Is your feature request related to a problem? Please describe. Currently, it is not possible to create and manage RichText elements as they are not supported by this package. So in order to use any RichText element (input, section or block), you have to use builder, build to json, parse the result, manually add RichText elements, convert updated entity to json again. This may be used as a workaround (but still a bad one), but when you have to add such elements multiple times - it's pure hell to manage it manually.

Describe the solution you'd like Add support for RichText (section, input, block, etc.).

Describe alternatives you've considered You can manually add this (as was described in first section), but I thinkits not even a workaround.

Additional context So if you only need f.e. to add it to the end of your UI - it may seem OK. image

But for more complex UI with multiple unsupported elements - it will be pure hell to manage it.

SnapeEye avatar Apr 03 '24 13:04 SnapeEye

Any idea when this may be worked on?

ckrauterlovescoffee avatar Apr 10 '24 16:04 ckrauterlovescoffee

Another related enhancement #85

azmi-plavaga avatar Apr 10 '24 16:04 azmi-plavaga

I'd say there is a better workaround available, as the compositional API makes it really easy to fall back into raw JSON if you need it:

function CustomRichText() { 
    return {
        build: () => ({
            type: 'rich_text',
            elements: [{ ... }],
        })
     } 
}

Message().blocks(CustomRichText() as any).buildToObject();

It's possible that this very bare bones implementation will break some advanced usage (I couldn't get the TS types just right; hence the as any) but for now it seems to work just fine.

tobice avatar May 02 '24 13:05 tobice

I wound up writing some components to support my needs (basic bulleted lists with nesting) for an internal project. If I had to guess, I'd imagine @raycharius (and/or any other maintainers) haven't yet added support because rich text is extremely verbose w.r.t. boilerplate, so it's unlikely to be used unless people absolutely need it. If it were me, I'd want to come up with a builder design that reduces that complexity somewhat for common use cases, otherwise things are likely to get quite unreadable quite quickly in user code.

I'll share the code that I wrote here in case someone wants to use it as a starting point for a PR. Note that I started off using the internal mixins, but I ran into issues and wound up writing the common methods like elements and whatnot myself. I also skipped over setter support in the builder factory functions, and I've probably broken the setter support provided by the base builder type in a couple of my builder constructor overrides.

import {
  Appendable,
  BlockBuilderBase,
  ElementBuilderBase,
  getElementsForContext,
  Settable,
  SlackBlockDto,
  SlackElementDto
} from 'slack-block-builder/dist/internal';

export class RichTextTextElementBuilder extends ElementBuilderBase {
  constructor(options?: { text?: string }) {
    super();
    const { text } = options ?? {};
    this.text(text);
  }

  bold(value: Settable<boolean> = true) {
    return this.set(value, 'bold');
  }

  italic(value: Settable<boolean> = true) {
    return this.set(value, 'italic');
  }

  strike(value: Settable<boolean> = true) {
    return this.set(value, 'strike');
  }

  code(value: Settable<boolean> = true) {
    return this.set(value, 'code');
  }

  text(value: Settable<string>) {
    return this.set(value, 'text');
  }

  build() {
    return this.getResult(SlackElementDto, {
      type: 'text',
      text: this.props.text,
      ...(this.props.bold || this.props.italic || this.props.strike || this.props.code
        ? {
            style: {
              bold: this.props.bold,
              italic: this.props.italic,
              strike: this.props.strike,
              code: this.props.code
            }
          }
        : {})
    });
  }
}

export function RichTextTextElement(options?: { text?: string }) {
  return new RichTextTextElementBuilder(options);
}

export class RichTextSectionBuilder extends ElementBuilderBase {
  elements<T>(...elements: Appendable<T>) {
    return this.append(elements.flat(), 'elements');
  }

  build() {
    return this.getResult(SlackElementDto, {
      type: 'rich_text_section',
      elements: getElementsForContext(this.props.elements)
    });
  }
}

export function RichTextSection() {
  return new RichTextSectionBuilder();
}

export class RichTextListBuilder extends ElementBuilderBase {
  style(style: Settable<'bullet' | 'ordered'>) {
    return this.set(style, 'style');
  }

  indent(indent: Settable<number>) {
    return this.set(indent, 'indent');
  }

  border(border: Settable<number>) {
    return this.set(border, 'border');
  }

  offset(offset: Settable<number>) {
    return this.set(offset, 'offset');
  }

  elements<T>(...elements: Appendable<T>) {
    return this.append(elements.flat(), 'elements');
  }

  build() {
    return this.getResult(SlackElementDto, {
      type: 'rich_text_list',
      style: this.props.style ?? 'bullet',
      indent: this.props.indent,
      border: this.props.border,
      offset: this.props.offset,
      elements: getElementsForContext(this.props.elements)
    });
  }
}

export function RichTextList() {
  return new RichTextListBuilder();
}

export class RichTextBuilder extends BlockBuilderBase {
  blockId(value: Settable<string>) {
    return this.set(value, 'block_id');
  }

  elements<T>(...elements: Appendable<T>) {
    return this.append(elements.flat(), 'elements');
  }

  end() {
    return this;
  }

  build() {
    return this.getResult(SlackBlockDto, {
      type: 'rich_text',
      elements: getElementsForContext(this.props.elements)
    });
  }
}

export function RichText() {
  return new RichTextBuilder();
}

Example usage (simple bulleted list with nesting and text styling):

RichText().elements(
  RichTextSection().elements(
    RichTextTextElement().text('Title of list').bold(),
    RichTextTextElement().text('\n')
  ),
  RichTextList().elements(
    // top-level bullet
    RichTextSection().elements(
      // styled and unstyled text on the same line, just here to show how verbose this stuff is
      RichTextTextElement().text('list item:').bold(),
      RichTextTextElement().text('1')
    ),
    // sub-list
    RichTextSection().elements(
      RichTextList().indent(1).elements(
        RichTextSection().elements(
          RichTextTextElement().text('sub item:').bold(),
          RichTextTextElement().text('1.1')
        ),
        RichTextSection().elements(
          RichTextTextElement().text('sub item:').bold(),
          RichTextTextElement().text('1.2')
        )
      )
    ),
    // top-level bullet
    RichTextSection().elements(
      RichTextTextElement().text('list item:').bold(),
      RichTextTextElement().text('2')
    ),
    // top-level bullet
    RichTextSection().elements(
      RichTextTextElement().text('list item:').bold(),
      RichTextTextElement().text('3')
    )
  )
)

benjamincburns avatar Jun 09 '24 23:06 benjamincburns

I wound up writing some components to support my needs (basic bulleted lists with nesting) for an internal project. If I had to guess, I'd imagine @raycharius (and/or any other maintainers) haven't yet added support because rich text is extremely verbose w.r.t. boilerplate, so it's unlikely to be used unless people absolutely need it. If it were me, I'd want to come up with a builder design that reduces that complexity somewhat for common use cases, otherwise things are likely to get quite unreadable quite quickly in user code.

I'll share the code that I wrote here in case someone wants to use it as a starting point for a PR. Note that I started off using the internal mixins, but I ran into issues and wound up writing the common methods like elements and whatnot myself. I also skipped over setter support in the builder factory functions, and I've probably broken the setter support provided by the base builder type in a couple of my builder constructor overrides.

import {
  Appendable,
  BlockBuilderBase,
  ElementBuilderBase,
  getElementsForContext,
  Settable,
  SlackBlockDto,
  SlackElementDto
} from 'slack-block-builder/dist/internal';

export class RichTextTextElementBuilder extends ElementBuilderBase {
  constructor(options?: { text?: string }) {
    super();
    const { text } = options ?? {};
    this.text(text);
  }

  bold(value: Settable<boolean> = true) {
    return this.set(value, 'bold');
  }

  italic(value: Settable<boolean> = true) {
    return this.set(value, 'italic');
  }

  strike(value: Settable<boolean> = true) {
    return this.set(value, 'strike');
  }

  code(value: Settable<boolean> = true) {
    return this.set(value, 'code');
  }

  text(value: Settable<string>) {
    return this.set(value, 'text');
  }

  build() {
    return this.getResult(SlackElementDto, {
      type: 'text',
      text: this.props.text,
      ...(this.props.bold || this.props.italic || this.props.strike || this.props.code
        ? {
            style: {
              bold: this.props.bold,
              italic: this.props.italic,
              strike: this.props.strike,
              code: this.props.code
            }
          }
        : {})
    });
  }
}

export function RichTextTextElement(options?: { text?: string }) {
  return new RichTextTextElementBuilder(options);
}

export class RichTextSectionBuilder extends ElementBuilderBase {
  elements<T>(...elements: Appendable<T>) {
    return this.append(elements.flat(), 'elements');
  }

  build() {
    return this.getResult(SlackElementDto, {
      type: 'rich_text_section',
      elements: getElementsForContext(this.props.elements)
    });
  }
}

export function RichTextSection() {
  return new RichTextSectionBuilder();
}

export class RichTextListBuilder extends ElementBuilderBase {
  style(style: Settable<'bullet' | 'ordered'>) {
    return this.set(style, 'style');
  }

  indent(indent: Settable<number>) {
    return this.set(indent, 'indent');
  }

  border(border: Settable<number>) {
    return this.set(border, 'border');
  }

  offset(offset: Settable<number>) {
    return this.set(offset, 'offset');
  }

  elements<T>(...elements: Appendable<T>) {
    return this.append(elements.flat(), 'elements');
  }

  build() {
    return this.getResult(SlackElementDto, {
      type: 'rich_text_list',
      style: this.props.style ?? 'bullet',
      indent: this.props.indent,
      border: this.props.border,
      offset: this.props.offset,
      elements: getElementsForContext(this.props.elements)
    });
  }
}

export function RichTextList() {
  return new RichTextListBuilder();
}

export class RichTextBuilder extends BlockBuilderBase {
  blockId(value: Settable<string>) {
    return this.set(value, 'block_id');
  }

  elements<T>(...elements: Appendable<T>) {
    return this.append(elements.flat(), 'elements');
  }

  end() {
    return this;
  }

  build() {
    return this.getResult(SlackBlockDto, {
      type: 'rich_text',
      elements: getElementsForContext(this.props.elements)
    });
  }
}

export function RichText() {
  return new RichTextBuilder();
}

Example usage (simple bulleted list with nesting and text styling):

RichText().elements(
  RichTextSection().elements(
    RichTextTextElement().text('Title of list').bold(),
    RichTextTextElement().text('\n')
  ),
  RichTextList().elements(
    // top-level bullet
    RichTextSection().elements(
      // styled and unstyled text on the same line, just here to show how verbose this stuff is
      RichTextTextElement().text('list item:').bold(),
      RichTextTextElement().text('1')
    ),
    // sub-list
    RichTextSection().elements(
      RichTextList().indent(1).elements(
        RichTextSection().elements(
          RichTextTextElement().text('sub item:').bold(),
          RichTextTextElement().text('1.1')
        ),
        RichTextSection().elements(
          RichTextTextElement().text('sub item:').bold(),
          RichTextTextElement().text('1.2')
        )
      )
    ),
    // top-level bullet
    RichTextSection().elements(
      RichTextTextElement().text('list item:').bold(),
      RichTextTextElement().text('2')
    ),
    // top-level bullet
    RichTextSection().elements(
      RichTextTextElement().text('list item:').bold(),
      RichTextTextElement().text('3')
    )
  )
)

Yeah, a really good code sample to start with. It could be expanded to support even more functionality (like user and channel tags), but still it's pretty good. Thanks!

SnapeEye avatar Jun 10 '24 05:06 SnapeEye