From b83fabd85523b431aca363b8f351751a26b1a32a Mon Sep 17 00:00:00 2001 From: Ray East Date: Sat, 16 Jul 2022 17:37:26 +0100 Subject: [PATCH 01/14] :sparkles: Add Video function and VideoBuilder to support the video block by Slack --- src/blocks/index.ts | 8 +++ src/blocks/video.ts | 72 ++++++++++++++++++++ src/internal/constants/block-types.ts | 1 + src/internal/constants/props.ts | 6 ++ src/internal/dto/slack-dto.ts | 6 ++ src/internal/methods/set-methods.ts | 94 +++++++++++++++++++++++++++ src/internal/types/index.ts | 7 +- 7 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 src/blocks/video.ts diff --git a/src/blocks/index.ts b/src/blocks/index.ts index 66ccf85..9ca4c8d 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -8,6 +8,7 @@ import { HeaderBuilder, HeaderParams } from './header'; import { ImageBuilder, ImageParams } from './image'; import { InputBuilder, InputParams } from './input'; import { SectionBuilder, SectionParams } from './section'; +import { VideoBuilder, VideoParams } from './video'; export type { ActionsBuilder, @@ -26,6 +27,8 @@ export type { InputParams, SectionBuilder, SectionParams, + VideoBuilder, + VideoParams, }; /** @@ -128,6 +131,10 @@ export function Section(params?: SectionParams): SectionBuilder { return new SectionBuilder(params); } +export function Video(params?: VideoParams): VideoBuilder { + return new VideoBuilder(params); +} + const blocks = { Actions, Context, @@ -137,6 +144,7 @@ const blocks = { Image, Input, Section, + Video, }; // Strange export. I know. For IDE highlighting purposes. diff --git a/src/blocks/video.ts b/src/blocks/video.ts new file mode 100644 index 0000000..1cd2fd7 --- /dev/null +++ b/src/blocks/video.ts @@ -0,0 +1,72 @@ +import { BlockBuilderBase } from '../internal/base'; +import { BlockType } from '../internal/constants'; +import { SlackBlockDto } from '../internal/dto'; +import { applyMixins, getPlainTextObject } from '../internal/helpers'; +import { + AltText, + AuthorName, + BlockId, + Description, + End, + ProviderIconUrl, + ProviderName, + Title, + TitleUrl, + ThumbnailUrl, + VideoUrl, +} from '../internal/methods'; + +export interface VideoParams { + blockId?: string; + description?: string; + providerIconUrl?: string; + providerName?: string; + title?: string; + titleUrl?: string; + thumbnailUrl?: string; + videoUrl?: string; +} + +export interface VideoBuilder extends AltText, + AuthorName, + BlockId, + Description, + End, + ProviderIconUrl, + ProviderName, + Title, + TitleUrl, + ThumbnailUrl, + VideoUrl { +} + +/** + * @@link https://api.slack.com/reference/block-kit/blocks#video + * @@displayName Video + */ + +export class VideoBuilder extends BlockBuilderBase { + /** @internal */ + + build(): Readonly { + return this.getResult(SlackBlockDto, { + type: BlockType.Video, + description: getPlainTextObject(this.props.description), + title: getPlainTextObject(this.props.title), + }); + } +} + +applyMixins(VideoBuilder, [ + AltText, + AuthorName, + BlockId, + Description, + End, + ProviderIconUrl, + ProviderName, + Title, + TitleUrl, + ThumbnailUrl, + VideoUrl, +]); diff --git a/src/internal/constants/block-types.ts b/src/internal/constants/block-types.ts index 31ab7a6..8044d44 100644 --- a/src/internal/constants/block-types.ts +++ b/src/internal/constants/block-types.ts @@ -7,4 +7,5 @@ export enum BlockType { Divider = 'divider', Image = 'image', Header = 'header', + Video = 'video', } diff --git a/src/internal/constants/props.ts b/src/internal/constants/props.ts index 4e82760..cfb8364 100644 --- a/src/internal/constants/props.ts +++ b/src/internal/constants/props.ts @@ -1,4 +1,5 @@ export enum Prop { + AuthorName = 'authorName', Blocks = 'blocks', Elements = 'elements', BlockId = 'blockId', @@ -77,4 +78,9 @@ export enum Prop { SubmitDisabled = 'submitDisabled', FocusOnLoad = 'focusOnLoad', AccessibilityLabel = 'accessibilityLabel', + ProviderIconUrl = 'providerIconUrl', + ProviderName = 'providerName', + TitleUrl = 'titleUrl', + ThumbnailUrl = 'thumbnailUrl', + VideoUrl = 'videoUrl', } diff --git a/src/internal/dto/slack-dto.ts b/src/internal/dto/slack-dto.ts index 8c7a2d6..e2a6596 100644 --- a/src/internal/dto/slack-dto.ts +++ b/src/internal/dto/slack-dto.ts @@ -75,6 +75,12 @@ export enum Param { type = 'type', focusOnLoad = 'focus_on_load', accessibilityLabel = 'accessibility_label', + authorName = 'author_name', + providerIconUrl = 'provider_icon_url', + providerName = 'provider_name', + titleUrl = 'title_url', + thumbnailUrl = 'thumbnail_url', + videoUrl = 'video_url', } export class SlackDto implements ObjectLiteral { diff --git a/src/internal/methods/set-methods.ts b/src/internal/methods/set-methods.ts index 19da557..9c2b3b5 100644 --- a/src/internal/methods/set-methods.ts +++ b/src/internal/methods/set-methods.ts @@ -77,6 +77,23 @@ export abstract class AltText extends Builder { } } +export abstract class AuthorName extends Builder { + /** + * @description This a plain-text representation of the author of a video. + * + * **Slack Validation Rules and Tips:** + * * Maximum of 50 characters. + * + * + * {@link https://api.slack.com/block-kit|Open Official Slack Block Kit Documentation} + * {@link https://www.blockbuilder.dev|Open Block Builder Documentation} + */ + + public authorName(authorName: Settable): this { + return this.set(authorName, Prop.AuthorName); + } +} + export abstract class BlockId extends Builder { /** * @description Sets a string to be an identifier for any given block in a view or message. This is sent back to your app in interaction payloads and view submissions for your app to process. @@ -500,6 +517,32 @@ export abstract class PrivateMetaData extends Builder { } } +export abstract class ProviderIconUrl extends Builder { + /** + * @description Icon for the video provider - ex. YouTube or Vimeo icon. + * + * {@link https://api.slack.com/block-kit|Open Official Slack Block Kit Documentation} + * {@link https://www.blockbuilder.dev|Open Block Builder Documentation} + */ + + public providerIconUrl(providerIconUrl: Settable): this { + return this.set(providerIconUrl, Prop.ProviderIconUrl); + } +} + +export abstract class ProviderName extends Builder { + /** + * @description The originating application or domain of the video ex. YouTube or Vimeo. + * + * {@link https://api.slack.com/block-kit|Open Official Slack Block Kit Documentation} + * {@link https://www.blockbuilder.dev|Open Block Builder Documentation} + */ + + public providerName(providerName: Settable): this { + return this.set(providerName, Prop.ProviderName); + } +} + export abstract class Submit extends Builder { /** * @description Sets the text displayed on the button that submits the view. @@ -555,6 +598,23 @@ export abstract class Title extends Builder { } } +export abstract class TitleUrl extends Builder { + /** + * @description A hyperlink for the video's title text. + * + * **Slack Validation Rules and Tips:** + * * Must correspond to the non-embeddable URL for the video. + * * Must go to an HTTPS URL. + * + * {@link https://api.slack.com/block-kit|Open Official Slack Block Kit Documentation} + * {@link https://www.blockbuilder.dev|Open Block Builder Documentation} + */ + + public titleUrl(titleUrl: Settable): this { + return this.set(titleUrl, Prop.TitleUrl); + } +} + export abstract class ThreadTs extends Builder { /** * @description Instructs the Slack API to send the message to the thread of the message associated with the timestamp. @@ -568,6 +628,22 @@ export abstract class ThreadTs extends Builder { } } +export abstract class ThumbnailUrl extends Builder { + /** + * @description A URL that loads the thumbnail image of the video. + * + * **Slack Validation Rules and Tips:** + * * **Required property for Video blocks** ⚠ + * + * {@link https://api.slack.com/block-kit|Open Official Slack Block Kit Documentation} + * {@link https://www.blockbuilder.dev|Open Block Builder Documentation} + */ + + public thumbnailUrl(thumbnailUrl: Settable): this { + return this.set(thumbnailUrl, Prop.ThumbnailUrl); + } +} + export abstract class Ts extends Builder { /** * @description Instructs the Slack API to use the message to replaced an existing message. @@ -614,3 +690,21 @@ export abstract class Value extends Builder { return this.set(value, Prop.Value); } } + +export abstract class VideoUrl extends Builder { + /** + * @description The URL of the video to embed in the Video block. + * + * **Slack Validation Rules and Tips:** + * * **Required property for Video blocks** ⚠ + * * Must match any existing unfurl domains within the app. + * * Must point to an HTTPS URL. + * + * {@link https://api.slack.com/block-kit|Open Official Slack Block Kit Documentation} + * {@link https://www.blockbuilder.dev|Open Block Builder Documentation} + */ + + public videoUrl(videoUrl: Settable): this { + return this.set(videoUrl, Prop.VideoUrl); + } +} diff --git a/src/internal/types/index.ts b/src/internal/types/index.ts index 41d188e..78da52a 100644 --- a/src/internal/types/index.ts +++ b/src/internal/types/index.ts @@ -27,6 +27,7 @@ import type { ImageBuilder, InputBuilder, SectionBuilder, + VideoBuilder, } from '../../blocks'; export type ActionsElementBuilder = @@ -88,7 +89,8 @@ export type BlockBuilder = | HeaderBuilder | ImageBuilder | InputBuilder - | SectionBuilder; + | SectionBuilder + | ViewBlockBuilder; export type ViewBlockBuilder = ActionsBuilder @@ -97,7 +99,8 @@ export type ViewBlockBuilder = | HeaderBuilder | ImageBuilder | InputBuilder - | SectionBuilder; + | SectionBuilder + | VideoBuilder; export type FilterString = 'im' | 'mpim' | 'private' | 'public'; From b413d63b863d068da812cec1a5959882887df377 Mon Sep 17 00:00:00 2001 From: Ray East Date: Sat, 16 Jul 2022 17:56:28 +0100 Subject: [PATCH 02/14] :white_check_mark: Add tests for VideoBuilder --- tests/blocks/mocks/video.mock.ts | 14 +++++++++++++ tests/blocks/video.spec.ts | 32 ++++++++++++++++++++++++++++++ tests/methods/author-name.ts | 18 +++++++++++++++++ tests/methods/index.ts | 8 +++++++- tests/methods/provider-icon-url.ts | 18 +++++++++++++++++ tests/methods/provider-name.ts | 18 +++++++++++++++++ tests/methods/thumbnail-url.ts | 18 +++++++++++++++++ tests/methods/title-url.ts | 18 +++++++++++++++++ tests/methods/video-url.ts | 18 +++++++++++++++++ tests/mocks/method-arg-mocks.ts | 6 ++++++ 10 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 tests/blocks/mocks/video.mock.ts create mode 100644 tests/blocks/video.spec.ts create mode 100644 tests/methods/author-name.ts create mode 100644 tests/methods/provider-icon-url.ts create mode 100644 tests/methods/provider-name.ts create mode 100644 tests/methods/thumbnail-url.ts create mode 100644 tests/methods/title-url.ts create mode 100644 tests/methods/video-url.ts diff --git a/tests/blocks/mocks/video.mock.ts b/tests/blocks/mocks/video.mock.ts new file mode 100644 index 0000000..93e848a --- /dev/null +++ b/tests/blocks/mocks/video.mock.ts @@ -0,0 +1,14 @@ +import { VideoBuilder } from '../../../src/blocks/video'; + +export const params = { + blockId: 'blockId', + description: 'description', + providerIconUrl: 'providerIconUrl', + providerName: 'providerName', + thumbnailUrl: 'thumbnailUrl', + title: 'title', + titleUrl: 'titleUrl', + videoUrl: 'videoUrl', +}; + +export const mock = new VideoBuilder(params); diff --git a/tests/blocks/video.spec.ts b/tests/blocks/video.spec.ts new file mode 100644 index 0000000..9211944 --- /dev/null +++ b/tests/blocks/video.spec.ts @@ -0,0 +1,32 @@ +import { VideoBuilder as Class } from '../../src/blocks/video'; +import { SlackBlockDto as DtoClass } from '../../src/internal'; +import { params } from './mocks/video.mock'; +import * as methods from '../methods'; +import { testCompositeBuilderClass } from '../test-composite-builder-class'; + +const className = 'VideoBuilder'; +const category = 'Blocks'; + +const config = { + Class, + DtoClass, + params, + className, + category, + expectMarkdown: false, +}; + +const methodsConfig = [ + methods.altText, + methods.authorName, + methods.blockId, + methods.description, + methods.providerIconUrl, + methods.providerName, + methods.thumbnailUrl, + methods.title, + methods.titleUrl, + methods.videoUrl, +]; + +testCompositeBuilderClass({ config, methods: methodsConfig }); diff --git a/tests/methods/author-name.ts b/tests/methods/author-name.ts new file mode 100644 index 0000000..847f85b --- /dev/null +++ b/tests/methods/author-name.ts @@ -0,0 +1,18 @@ +import { CompositeBuilderClassConfig } from '../test-config-types'; +import { Prop } from '../../src/internal/constants'; +import { methodArgMocks } from '../mocks/method-arg-mocks'; +import { SlackDto } from '../../src/internal'; +import * as checks from '../checks'; + +export const authorName = (params: CompositeBuilderClassConfig): void => { + const config = { + ...params, + methodArgMock: methodArgMocks.authorName, + methodName: Prop.AuthorName, + propSetterPropName: Prop.AuthorName, + slackDtoParamName: SlackDto.mapParam(Prop.AuthorName), + }; + + checks.settableProperty(config); + checks.literalBuild(config); +}; diff --git a/tests/methods/index.ts b/tests/methods/index.ts index 94d2b59..4389d96 100644 --- a/tests/methods/index.ts +++ b/tests/methods/index.ts @@ -3,6 +3,7 @@ export * from './accessory'; export * from './action-id'; export * from './alt-text'; export * from './as-user'; +export * from './author-name'; export * from './attachments'; export * from './block-id'; export * from './blocks'; @@ -60,6 +61,8 @@ export * from './placeholder'; export * from './post-at'; export * from './primary'; export * from './private-meta-data'; +export * from './provider-icon-url'; +export * from './provider-name'; export * from './replace-original'; export * from './response-url-enabled'; export * from './submit'; @@ -67,7 +70,10 @@ export * from './submit-disabled'; export * from './text'; export * from './text-message'; export * from './thread-ts'; -export * from './ts'; +export * from './thumbnail-url'; export * from './title'; +export * from './title-url'; +export * from './ts'; export * from './url'; export * from './value'; +export * from './video-url'; diff --git a/tests/methods/provider-icon-url.ts b/tests/methods/provider-icon-url.ts new file mode 100644 index 0000000..96f107d --- /dev/null +++ b/tests/methods/provider-icon-url.ts @@ -0,0 +1,18 @@ +import { CompositeBuilderClassConfig } from '../test-config-types'; +import { Prop } from '../../src/internal/constants'; +import { methodArgMocks } from '../mocks/method-arg-mocks'; +import { SlackDto } from '../../src/internal'; +import * as checks from '../checks'; + +export const providerIconUrl = (params: CompositeBuilderClassConfig): void => { + const config = { + ...params, + methodArgMock: methodArgMocks.providerIconUrl, + methodName: Prop.ProviderIconUrl, + propSetterPropName: Prop.ProviderIconUrl, + slackDtoParamName: SlackDto.mapParam(Prop.ProviderIconUrl), + }; + + checks.settableProperty(config); + checks.literalBuild(config); +}; diff --git a/tests/methods/provider-name.ts b/tests/methods/provider-name.ts new file mode 100644 index 0000000..944368e --- /dev/null +++ b/tests/methods/provider-name.ts @@ -0,0 +1,18 @@ +import { CompositeBuilderClassConfig } from '../test-config-types'; +import { Prop } from '../../src/internal/constants'; +import { methodArgMocks } from '../mocks/method-arg-mocks'; +import { SlackDto } from '../../src/internal'; +import * as checks from '../checks'; + +export const providerName = (params: CompositeBuilderClassConfig): void => { + const config = { + ...params, + methodArgMock: methodArgMocks.providerName, + methodName: Prop.ProviderName, + propSetterPropName: Prop.ProviderName, + slackDtoParamName: SlackDto.mapParam(Prop.ProviderName), + }; + + checks.settableProperty(config); + checks.literalBuild(config); +}; diff --git a/tests/methods/thumbnail-url.ts b/tests/methods/thumbnail-url.ts new file mode 100644 index 0000000..9fa0270 --- /dev/null +++ b/tests/methods/thumbnail-url.ts @@ -0,0 +1,18 @@ +import { CompositeBuilderClassConfig } from '../test-config-types'; +import { Prop } from '../../src/internal/constants'; +import { methodArgMocks } from '../mocks/method-arg-mocks'; +import { SlackDto } from '../../src/internal'; +import * as checks from '../checks'; + +export const thumbnailUrl = (params: CompositeBuilderClassConfig): void => { + const config = { + ...params, + methodArgMock: methodArgMocks.thumbnailUrl, + methodName: Prop.ThumbnailUrl, + propSetterPropName: Prop.ThumbnailUrl, + slackDtoParamName: SlackDto.mapParam(Prop.ThumbnailUrl), + }; + + checks.settableProperty(config); + checks.literalBuild(config); +}; diff --git a/tests/methods/title-url.ts b/tests/methods/title-url.ts new file mode 100644 index 0000000..53a02a6 --- /dev/null +++ b/tests/methods/title-url.ts @@ -0,0 +1,18 @@ +import { CompositeBuilderClassConfig } from '../test-config-types'; +import { Prop } from '../../src/internal/constants'; +import { methodArgMocks } from '../mocks/method-arg-mocks'; +import { SlackDto } from '../../src/internal'; +import * as checks from '../checks'; + +export const titleUrl = (params: CompositeBuilderClassConfig): void => { + const config = { + ...params, + methodArgMock: methodArgMocks.titleUrl, + methodName: Prop.TitleUrl, + propSetterPropName: Prop.TitleUrl, + slackDtoParamName: SlackDto.mapParam(Prop.TitleUrl), + }; + + checks.settableProperty(config); + checks.literalBuild(config); +}; diff --git a/tests/methods/video-url.ts b/tests/methods/video-url.ts new file mode 100644 index 0000000..e731e77 --- /dev/null +++ b/tests/methods/video-url.ts @@ -0,0 +1,18 @@ +import { CompositeBuilderClassConfig } from '../test-config-types'; +import { Prop } from '../../src/internal/constants'; +import { methodArgMocks } from '../mocks/method-arg-mocks'; +import { SlackDto } from '../../src/internal'; +import * as checks from '../checks'; + +export const videoUrl = (params: CompositeBuilderClassConfig): void => { + const config = { + ...params, + methodArgMock: methodArgMocks.videoUrl, + methodName: Prop.VideoUrl, + propSetterPropName: Prop.VideoUrl, + slackDtoParamName: SlackDto.mapParam(Prop.VideoUrl), + }; + + checks.settableProperty(config); + checks.literalBuild(config); +}; diff --git a/tests/mocks/method-arg-mocks.ts b/tests/mocks/method-arg-mocks.ts index 6cff6d4..afebcc5 100644 --- a/tests/mocks/method-arg-mocks.ts +++ b/tests/mocks/method-arg-mocks.ts @@ -74,4 +74,10 @@ export const methodArgMocks = { submitDisabled: methodArgMocksByType.bool, focusOnLoad: methodArgMocksByType.bool, accessibilityLabel: methodArgMocksByType.string, + authorName: methodArgMocksByType.string, + providerIconUrl: methodArgMocksByType.string, + providerName: methodArgMocksByType.string, + titleUrl: methodArgMocksByType.string, + thumbnailUrl: methodArgMocksByType.string, + videoUrl: methodArgMocksByType.string, }; From 521ddd4f8b8fd6d2e94fa03d5bca73254123a9e6 Mon Sep 17 00:00:00 2001 From: Ray East Date: Sat, 16 Jul 2022 18:05:51 +0100 Subject: [PATCH 03/14] :bulb: JSDoc annotations for the Video block --- src/blocks/index.ts | 14 ++++++++++++++ src/internal/methods/set-methods.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/blocks/index.ts b/src/blocks/index.ts index 9ca4c8d..4648636 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -131,6 +131,20 @@ export function Section(params?: SectionParams): SectionBuilder { return new SectionBuilder(params); } +/** + * @param {Object} [params] Parameters passed to the constructor. + * @param {string} [params.blockId] Sets a string to be an identifier for the block, that will be available in interaction payloadsSets a string to be an identifier for any given block in a view or message. This is sent back to your app in interaction payloads and view submissions for your app to process. + * @param {string} [params.description] Sets a description for the video. + * @param {string} [params.providerIconUrl] Icon for the video provider - ex. YouTube or Vimeo icon. + * @param {string} [params.providerName] The originating application or domain of the video ex. YouTube or Vimeo. + * @param {string} [params.thumbnailUrl] A URL that loads the thumbnail image of the video. + * @param {string} [params.title] Sets the title displayed for the block, element, or confirmation dialog. + * @param {string} [params.titleUrl] A hyperlink for the video's title text. + * @param {string} [params.videoUrl] The URL of the video to embed in the Video block. + * + * {@link https://api.slack.com/reference/block-kit/blocks#section|View in Slack API Documentation} + */ + export function Video(params?: VideoParams): VideoBuilder { return new VideoBuilder(params); } diff --git a/src/internal/methods/set-methods.ts b/src/internal/methods/set-methods.ts index 9c2b3b5..e6c2960 100644 --- a/src/internal/methods/set-methods.ts +++ b/src/internal/methods/set-methods.ts @@ -203,7 +203,7 @@ export abstract class Deny extends Builder { export abstract class Description extends Builder { /** - * @description Sets the descriptive text displayed below the text field of the option. + * @description Sets the descriptive text displayed below the text field of the option or for a video, if creating a Video block. * * **Slack Validation Rules and Tips:** * * Maximum of 75 characters. From e87aa79cafe035a64e432127546d7002b8804d3c Mon Sep 17 00:00:00 2001 From: Ray East Date: Sat, 16 Jul 2022 18:07:23 +0100 Subject: [PATCH 04/14] :hammer: Refactor order of methods --- src/blocks/video.ts | 8 ++--- src/internal/methods/set-methods.ts | 50 ++++++++++++++--------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/blocks/video.ts b/src/blocks/video.ts index 1cd2fd7..4cfc0fa 100644 --- a/src/blocks/video.ts +++ b/src/blocks/video.ts @@ -10,9 +10,9 @@ import { End, ProviderIconUrl, ProviderName, + ThumbnailUrl, Title, TitleUrl, - ThumbnailUrl, VideoUrl, } from '../internal/methods'; @@ -21,9 +21,9 @@ export interface VideoParams { description?: string; providerIconUrl?: string; providerName?: string; + thumbnailUrl?: string; title?: string; titleUrl?: string; - thumbnailUrl?: string; videoUrl?: string; } @@ -34,9 +34,9 @@ export interface VideoBuilder extends AltText, End, ProviderIconUrl, ProviderName, + ThumbnailUrl, Title, TitleUrl, - ThumbnailUrl, VideoUrl { } @@ -65,8 +65,8 @@ applyMixins(VideoBuilder, [ End, ProviderIconUrl, ProviderName, + ThumbnailUrl, Title, TitleUrl, - ThumbnailUrl, VideoUrl, ]); diff --git a/src/internal/methods/set-methods.ts b/src/internal/methods/set-methods.ts index e6c2960..d9dacdf 100644 --- a/src/internal/methods/set-methods.ts +++ b/src/internal/methods/set-methods.ts @@ -579,68 +579,68 @@ export abstract class Text extends Builder { } } -export abstract class Title extends Builder { +export abstract class ThreadTs extends Builder { /** - * @description Sets the title displayed for the block, element, or confirmation dialog. - * - * **Slack Validation Rules and Tips:** - * * **Required for views and confirmation dialogs** ⚠ - * * For views, maximum of 24 characters. - * * For images, maximum of 2000 characters. - * * For confirmation dialogs, maximum of 100 characters. + * @description Instructs the Slack API to send the message to the thread of the message associated with the timestamp. * * {@link https://api.slack.com/block-kit|Open Official Slack Block Kit Documentation} * {@link https://www.blockbuilder.dev|Open Block Builder Documentation} */ - public title(title: Settable): this { - return this.set(title, Prop.Title); + public threadTs(threadTs: Settable): this { + return this.set(threadTs, Prop.ThreadTs); } } -export abstract class TitleUrl extends Builder { +export abstract class ThumbnailUrl extends Builder { /** - * @description A hyperlink for the video's title text. + * @description A URL that loads the thumbnail image of the video. * * **Slack Validation Rules and Tips:** - * * Must correspond to the non-embeddable URL for the video. - * * Must go to an HTTPS URL. + * * **Required property for Video blocks** ⚠ * * {@link https://api.slack.com/block-kit|Open Official Slack Block Kit Documentation} * {@link https://www.blockbuilder.dev|Open Block Builder Documentation} */ - public titleUrl(titleUrl: Settable): this { - return this.set(titleUrl, Prop.TitleUrl); + public thumbnailUrl(thumbnailUrl: Settable): this { + return this.set(thumbnailUrl, Prop.ThumbnailUrl); } } -export abstract class ThreadTs extends Builder { +export abstract class Title extends Builder { /** - * @description Instructs the Slack API to send the message to the thread of the message associated with the timestamp. + * @description Sets the title displayed for the block, element, or confirmation dialog. + * + * **Slack Validation Rules and Tips:** + * * **Required for views and confirmation dialogs** ⚠ + * * For views, maximum of 24 characters. + * * For images, maximum of 2000 characters. + * * For confirmation dialogs, maximum of 100 characters. * * {@link https://api.slack.com/block-kit|Open Official Slack Block Kit Documentation} * {@link https://www.blockbuilder.dev|Open Block Builder Documentation} */ - public threadTs(threadTs: Settable): this { - return this.set(threadTs, Prop.ThreadTs); + public title(title: Settable): this { + return this.set(title, Prop.Title); } } -export abstract class ThumbnailUrl extends Builder { +export abstract class TitleUrl extends Builder { /** - * @description A URL that loads the thumbnail image of the video. + * @description A hyperlink for the video's title text. * * **Slack Validation Rules and Tips:** - * * **Required property for Video blocks** ⚠ + * * Must correspond to the non-embeddable URL for the video. + * * Must go to an HTTPS URL. * * {@link https://api.slack.com/block-kit|Open Official Slack Block Kit Documentation} * {@link https://www.blockbuilder.dev|Open Block Builder Documentation} */ - public thumbnailUrl(thumbnailUrl: Settable): this { - return this.set(thumbnailUrl, Prop.ThumbnailUrl); + public titleUrl(titleUrl: Settable): this { + return this.set(titleUrl, Prop.TitleUrl); } } From 76d79d73d8ec4bdc320993d51ad6318a0782bbcb Mon Sep 17 00:00:00 2001 From: Ray East Date: Sat, 16 Jul 2022 18:09:27 +0100 Subject: [PATCH 05/14] :books: Add docs for Video and VideoBuilder --- docs/_sidebar.md | 1 + docs/bits/option.md | 2 +- docs/blocks/video.md | 108 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 docs/blocks/video.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index af34654..47c5b3b 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -32,6 +32,7 @@ * [Image](blocks/image.md "Block Builder – Image – Maintainable JavaScript Code for Slack Block Kit") * [Input](blocks/input.md "Block Builder – Input – Maintainable JavaScript Code for Slack Block Kit") * [Section](blocks/section.md "Block Builder – Section – Maintainable JavaScript Code for Slack Block Kit") + * [Video](blocks/video.md "Block Builder – Video – Maintainable JavaScript Code for Slack Block Kit") * **Element References** diff --git a/docs/bits/option.md b/docs/bits/option.md index ea8dc84..74be122 100644 --- a/docs/bits/option.md +++ b/docs/bits/option.md @@ -42,7 +42,7 @@ All setter methods return `this`, the instance of `OptionBuilder` on which it is OptionBuilder.description(string); ``` -Sets the descriptive text displayed below the text field of the option. +Sets the descriptive text displayed below the text field of the option or for a video, if creating a Video block. ```javascript OptionBuilder.text(string); ``` diff --git a/docs/blocks/video.md b/docs/blocks/video.md new file mode 100644 index 0000000..a5421fe --- /dev/null +++ b/docs/blocks/video.md @@ -0,0 +1,108 @@ +# Video + +?> **Note:** This document is a reference to the `VideoBuilder` object in **Block Builder**. For more information on how this carries over to the Slack API, view the [the Video docs](https://api.slack.com/reference/block-kit/blocks#video) on Slack's doc site. + +### Creating an Instance + +The function that creates a new instance of `VideoBuilder` is available as both a top-level import and as a member of its 'category', `Blocks`: + +```javascript +import { Video } from 'slack-block-builder'; + +const myObj = Video(params?); + +``` + +```javascript +import { Blocks } from 'slack-block-builder'; + +const myObj = Blocks.Video(params?); +``` + +### Params + +Each instance of the `VideoBuilder` object has chainable setter methods for the object's properties. However, properties with primitive values can optionally be passed to the instantiating function, should you prefer: + +`blockId` – *String* + +`description` – *String* + +`providerIconUrl` – *String* + +`providerName` – *String* + +`thumbnailUrl` – *String* + +`title` – *String* + +`titleUrl` – *String* + +`videoUrl` – *String* + + +?> **Note:** For an explanation of any one of the parameters, see its corresponding setter method below. + +### Setter Methods + +All setter methods return `this`, the instance of `VideoBuilder` on which it is called. + +```javascript +VideoBuilder.altText(string); +``` + +This a plain-text summary of the image element or block. +```javascript +VideoBuilder.authorName(string); +``` + +This a plain-text representation of the author of a video. +```javascript +VideoBuilder.blockId(string); +``` + +Sets a string to be an identifier for any given block in a view or message. This is sent back to your app in interaction payloads and view submissions for your app to process. +```javascript +VideoBuilder.description(string); +``` + +Sets the descriptive text displayed below the text field of the option or for a video, if creating a Video block. +```javascript +VideoBuilder.providerIconUrl(string); +``` + +Icon for the video provider - ex. YouTube or Vimeo icon. +```javascript +VideoBuilder.providerName(string); +``` + +The originating application or domain of the video ex. YouTube or Vimeo. +```javascript +VideoBuilder.thumbnailUrl(string); +``` + +A URL that loads the thumbnail image of the video. +```javascript +VideoBuilder.title(string); +``` + +Sets the title displayed for the block, element, or confirmation dialog. +```javascript +VideoBuilder.titleUrl(string); +``` + +A hyperlink for the video's title text. +```javascript +VideoBuilder.videoUrl(string); +``` + +The URL of the video to embed in the Video block. + +### Other Methods + +The `VideoBuilder` object also has other methods available: + +```javascript +VideoBuilder.end(); +``` + +Performs no alterations to the object on which it is called. It is meant to simulate a closing HTML tag for those who prefer to have an explicit end declared for an object. From c42531dfc155438bc981a5b000ad8151008edc5a Mon Sep 17 00:00:00 2001 From: Ray East Date: Sat, 16 Jul 2022 18:16:25 +0100 Subject: [PATCH 06/14] :books: Add Video to support sections of documentation --- README.md | 3 ++- docs/support.md | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 17c7a0b..2acaf0f 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,8 @@ Below is a list of supported objects and how to access them in **Block Builder** | Header | Block | :white_check_mark: | `Blocks.Header()` | Image | Block | :white_check_mark: | `Blocks.Image()` | Input | Block | :white_check_mark: | `Blocks.Input()` -| Section | Block | :white_check_mark: | `Blocks.Section()` +| Section | Block | :white_check_mark: | `Blocks.Section()` +| Video | Block | :white_check_mark: | `Blocks.Video()` | Button | Element | :white_check_mark:️ | `Elements.Button()` | Checkboxes | Element | :white_check_mark: | `Elements.Checkboxes()` | Date Picker | Element | :white_check_mark: | `Elements.DatePicker()` diff --git a/docs/support.md b/docs/support.md index 3344cd8..4c6fdb0 100644 --- a/docs/support.md +++ b/docs/support.md @@ -19,6 +19,7 @@ Below is a list of supported components and how to access them in **Block Builde | Image | Block | **Yes** | `Blocks.Image()` | [View Docs](/blocks/image.md) | Input | Block | **Yes** | `Blocks.Input()` | [View Docs](/blocks/input.md) | Section | Block | **Yes** | `Blocks.Section()` | [View Docs](/blocks/section.md) +| Video | Video | **Yes** | `Blocks.Video()` | [View Docs](/blocks/video.md) | Button | Element | **Yes**️ | `Elements.Button()` | [View Docs](/elements/button.md) | Checkboxes | Element | **Yes** | `Elements.Checkboxes()` | [View Docs](/elements/checkboxes.md) | Date Picker | Element | **Yes** | `Elements.DatePicker()` | [View Docs](/elements/datepicker.md) @@ -29,8 +30,8 @@ Below is a list of supported components and how to access them in **Block Builde | Time Picker | Element | **Yes** | `Elements.TimePicker()` | [View Docs](/elements/timepicker.md) | Select Menus | Element | **Yes** | `Elements.[Type]Select()` | | Multi-Select Menus | Element | **Yes** | `Elements.[Type]MultiSelect()` | -| Attachment | Legacy Feature | **Yes** | `Bits.Attachment()` | [View Docs](/bits/attachment.md) -| Confirmation Dialog | Composition Object | **Yes** | `Bits.ConfirmationDialog()` | [View Docs](/bits/confirmation-dialog.md) +| Attachment | Legacy Feature | **Yes** | `Bits.Attachment()` | [View Docs](/bits/attachment.md) +| Confirmation Dialog | Composition Object | **Yes** | `Bits.ConfirmationDialog()` | [View Docs](/bits/confirmation-dialog.md) | Option | Composition Object | **Yes** | `Bits.Option()` | [View Docs](/bits/option.md) | Option Group | Composition Object | **Yes** | `Bits.OptionGroup()` | [View Docs](/bits/option-group.md) From b7f8dab33ef4a803a0c00db9c3240913974d7281 Mon Sep 17 00:00:00 2001 From: Ray East Date: Sun, 17 Jul 2022 14:16:13 +0100 Subject: [PATCH 07/14] :bug: Fix TypeScript error when using conditionals in Accordion or Paginator build functions --- src/components/accordion-ui-component.ts | 8 +++++--- src/components/paginator-ui-component.ts | 10 ++++++---- src/internal/types/index.ts | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/accordion-ui-component.ts b/src/components/accordion-ui-component.ts index ce1b519..74bc90f 100644 --- a/src/components/accordion-ui-component.ts +++ b/src/components/accordion-ui-component.ts @@ -1,8 +1,8 @@ import { Blocks } from '../blocks'; import { Elements } from '../elements'; import { ComponentUIText } from '../internal/constants'; +import { Builder, AccordionStateManager, AccordionState } from '../internal/lib'; -import type { AccordionStateManager, AccordionState } from '../internal/lib'; import type { BlockBuilderReturnableFn, BlockBuilder, @@ -54,7 +54,7 @@ export class AccordionUIComponent { } public getBlocks(): BlockBuilder[] { - return this.items.map((item, index) => { + const unpruned = this.items.map((item, index) => { const isExpanded = this.paginator.checkItemIsExpandedByIndex(index); const blocks = [ @@ -65,10 +65,12 @@ export class AccordionUIComponent { expandedItems: this.paginator.getNextStateByItemIndex(index), }), })), - ...isExpanded ? this.builderFunction({ item }) : [], + ...isExpanded ? this.builderFunction({ item }).flat() : [], ]; return index === 0 ? blocks : [Blocks.Divider(), ...blocks]; }).flat(); + + return Builder.pruneUndefinedFromArray(unpruned); } } diff --git a/src/components/paginator-ui-component.ts b/src/components/paginator-ui-component.ts index 2b6a9b9..d0cce6f 100644 --- a/src/components/paginator-ui-component.ts +++ b/src/components/paginator-ui-component.ts @@ -1,8 +1,8 @@ import { Blocks } from '../blocks'; import { Elements } from '../elements'; import { ComponentUIText, PaginatorButtonId } from '../internal/constants'; +import { Builder, PaginatorStateManager, PaginatorState } from '../internal/lib'; -import type { PaginatorStateManager, PaginatorState } from '../internal/lib'; import type { BlockBuilder, BlockBuilderReturnableFn, @@ -11,7 +11,7 @@ import type { } from '../internal/types'; export type PaginatorActionIdFn = StringReturnableFn; + & { buttonId: PaginatorButtonId }>; export interface PageCountTextFnParams { page: number; @@ -62,10 +62,10 @@ export class PaginatorUIComponent { const blocksForEach = []; for (let i = 0; i < this.paginator.getTotalItems() && i < this.items.length; i += 1) { - blocksForEach.push(this.builderFunction({ item: this.items[i] })); + blocksForEach.push(this.builderFunction({ item: this.items[i] }).flat()); } - return this.paginator.getTotalPages() > 1 + const unpruned = this.paginator.getTotalPages() > 1 ? [ ...blocksForEach.flat(), Blocks.Context().elements(this.pageCountTextFunction({ @@ -92,5 +92,7 @@ export class PaginatorUIComponent { ), ] : blocksForEach.flat(); + + return Builder.pruneUndefinedFromArray(unpruned); } } diff --git a/src/internal/types/index.ts b/src/internal/types/index.ts index 78da52a..d575bb2 100644 --- a/src/internal/types/index.ts +++ b/src/internal/types/index.ts @@ -128,6 +128,6 @@ export type Appendable = UndefinableArray>; export type Fn = (arg: T) => R; -export type BlockBuilderReturnableFn = Fn; +export type BlockBuilderReturnableFn = Fn>; export type StringReturnableFn = Fn; From 57e2158e5e0f8c53933888ab8f0d593fa372d78a Mon Sep 17 00:00:00 2001 From: Ray East Date: Sun, 17 Jul 2022 14:16:40 +0100 Subject: [PATCH 08/14] :white_check_mark: Add tests for conditionals in Accordion and Paginator build functions --- tests/components/accordion.spec.ts | 393 ++++++++++++++++++++++++++++- tests/components/paginator.spec.ts | 212 +++++++++++++++- 2 files changed, 603 insertions(+), 2 deletions(-) diff --git a/tests/components/accordion.spec.ts b/tests/components/accordion.spec.ts index 9b597af..5b9024b 100644 --- a/tests/components/accordion.spec.ts +++ b/tests/components/accordion.spec.ts @@ -1,5 +1,9 @@ import { - Accordion, Modal, Blocks, BlockBuilder, + Accordion, + Modal, + Blocks, + BlockBuilder, + setIfTruthy, } from '../../src'; import { Human, humans } from './mock/data-set.mock'; @@ -2043,4 +2047,391 @@ describe('Testing Accordion:', () => { type: 'modal', })); }); + + test('Check that using conditionals in an accordion build method does not throw a type error', () => { + const result = Modal({ title: 'Testing' }) + .blocks( + Accordion({ + items: humans, + expandedItems: [], + titleText: ({ item }) => `${item.firstName} ${item.lastName}`, + actionId: (params) => JSON.stringify(params), + blocksForExpanded: ({ item: human }) => [ + Blocks.Section({ text: `${human.firstName} ${human.lastName}` }), + setIfTruthy(human, [ + Blocks.Section({ text: `${human.jobTitle}` }), + Blocks.Section({ text: `${human.department}` }), + ]), + Blocks.Section({ text: `${human.email}` }), + ], + }).getBlocks()) + .buildToJSON(); + + expect(result).toEqual(JSON.stringify({ + title: { + type: 'plain_text', + text: 'Testing', + }, + blocks: [ + { + text: { + type: 'mrkdwn', + text: 'Ray East', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[0]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Taras Neporozhniy', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[1]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Dima Tereshuk', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[2]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Lesha Power', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[3]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Yozhef Hisem', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[4]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Andrey Roland', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[5]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Vlad Filimonov', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[6]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Boris Boriska', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[7]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Vadim Grabovyy', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[8]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Alex Chernyshov', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[9]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Serega Grigoruk', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[10]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Igor Roik', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[11]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Dima Tretiakov', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[12]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Sasha Chernyavska', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[13]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Arthur Nick', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[14]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Dima Lutsik', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[15]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Dima Svirepchuk', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[16]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Dima Bilkun', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[17]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Pasha Akimenko', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[18]}', + type: 'button', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Karina Suprun', + }, + accessory: { + text: { + type: 'plain_text', + text: 'More', + }, + action_id: '{"expandedItems":[19]}', + type: 'button', + }, + type: 'section', + }, + ], + type: 'modal', + })); + }); }); diff --git a/tests/components/paginator.spec.ts b/tests/components/paginator.spec.ts index ad3efff..ab7e065 100644 --- a/tests/components/paginator.spec.ts +++ b/tests/components/paginator.spec.ts @@ -1,5 +1,9 @@ import { - Paginator, Modal, Blocks, BlockBuilder, + Paginator, + Modal, + Blocks, + BlockBuilder, + setIfTruthy, } from '../../src'; import { Human, humans } from './mock/data-set.mock'; @@ -602,4 +606,210 @@ describe('Testing Paginator:', () => { type: 'modal', })); }); + + test('Creating a paginator with conditionals in the builder function does not throw type errors.', () => { + const items = [humans[0], humans[1], humans[2], humans[3], humans[4]]; + const result = Modal({ title: 'Testing' }) + .blocks( + Paginator({ + items, + totalItems: 20, + page: 1, + perPage: 5, + actionId: (params) => JSON.stringify(params), + blocksForEach: ({ item: human }) => [ + Blocks.Section({ text: `${human.firstName} ${human.lastName}` }), + setIfTruthy(human, [ + Blocks.Section({ text: `${human.jobTitle}` }), + Blocks.Section({ text: `${human.department}` }), + ]), + Blocks.Section({ text: `${human.email}` }), + ], + }).getBlocks(), + ) + .buildToJSON(); + + expect(result).toEqual(JSON.stringify({ + title: { + type: 'plain_text', + text: 'Testing', + }, + blocks: [ + { + text: { + type: 'mrkdwn', + text: 'Ray East', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Lord of The Slack Apps', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Engineering', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'ray@ray.com', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Taras Neporozhniy', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Brave Cow', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Bad Ass MFs', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'taras@taras.com', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Dima Tereshuk', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Outlander', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Big Rollers', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'dima@dima.com', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Lesha Power', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Kalyanshyk', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'DDD Demons', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'lesha@lesha.com', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Yozhef Hisem', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Molnenosets', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'Zakarpatska', + }, + type: 'section', + }, + { + text: { + type: 'mrkdwn', + text: 'yozhik@yozhik.com', + }, + type: 'section', + }, + { + elements: [ + { + type: 'mrkdwn', + text: 'Page 1 of 4', + }, + ], + type: 'context', + }, + { + type: 'divider', + }, + { + elements: [ + { + text: { + type: 'plain_text', + text: 'Previous', + }, + action_id: '{"buttonId":"previous","totalItems":20,"perPage":5,"totalPages":4,"offset":15,"page":4}', + type: 'button', + }, + { + text: { + type: 'plain_text', + text: 'Next', + }, + action_id: '{"buttonId":"next","totalItems":20,"perPage":5,"totalPages":4,"offset":5,"page":2}', + type: 'button', + }, + ], + type: 'actions', + }, + ], + type: 'modal', + })); + }); }); From 6939574588b10f640b9042476afdbbda0fcacdf0 Mon Sep 17 00:00:00 2001 From: Ray East Date: Sun, 17 Jul 2022 14:21:02 +0100 Subject: [PATCH 09/14] :hammer: Add public keyword to method --- src/blocks/divider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/divider.ts b/src/blocks/divider.ts index 8c381f3..fcede8a 100644 --- a/src/blocks/divider.ts +++ b/src/blocks/divider.ts @@ -20,7 +20,7 @@ export interface DividerBuilder extends BlockId, export class DividerBuilder extends BlockBuilderBase { /** @internal */ - build(): Readonly { + public build(): Readonly { return this.getResult(SlackBlockDto, { type: BlockType.Divider, }); From 8b5e0e99cda51be40400b62a3e0ab7a66c7b9b8f Mon Sep 17 00:00:00 2001 From: Ray East Date: Sun, 17 Jul 2022 14:22:48 +0100 Subject: [PATCH 10/14] :shirt: Lint --- src/components/paginator-ui-component.ts | 2 +- tests/components/accordion.spec.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/paginator-ui-component.ts b/src/components/paginator-ui-component.ts index d0cce6f..446c85a 100644 --- a/src/components/paginator-ui-component.ts +++ b/src/components/paginator-ui-component.ts @@ -11,7 +11,7 @@ import type { } from '../internal/types'; export type PaginatorActionIdFn = StringReturnableFn; +& { buttonId: PaginatorButtonId }>; export interface PageCountTextFnParams { page: number; diff --git a/tests/components/accordion.spec.ts b/tests/components/accordion.spec.ts index 5b9024b..15bddc6 100644 --- a/tests/components/accordion.spec.ts +++ b/tests/components/accordion.spec.ts @@ -2064,7 +2064,8 @@ describe('Testing Accordion:', () => { ]), Blocks.Section({ text: `${human.email}` }), ], - }).getBlocks()) + }).getBlocks(), + ) .buildToJSON(); expect(result).toEqual(JSON.stringify({ From d7689aa3eb431d80459e6ef4fc48c8084753cb56 Mon Sep 17 00:00:00 2001 From: Ray East Date: Sun, 17 Jul 2022 14:36:07 +0100 Subject: [PATCH 11/14] :sparkles: Add getBlock() and getBlocks() utility functions --- src/utilities/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/utilities/index.ts b/src/utilities/index.ts index dab3f6f..17bcb6f 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -37,11 +37,21 @@ export function OptionGroupCollection(...optionGroups: Appendable>(...optionGroups); } +export function buildBlock(block: BlockBuilder): Readonly { + return block.build(); +} + +export function buildBlocks(...blocks: Appendable): Readonly[] { + return getBuiltCollection>(...blocks); +} + const utilities = { AttachmentCollection, BlockCollection, OptionCollection, OptionGroupCollection, + buildBlock, + buildBlocks, }; // Strange export. I know. For IDE highlighting purposes. From a52348035126a2a69a836fca07824e2d29f49077 Mon Sep 17 00:00:00 2001 From: Ray East Date: Sun, 17 Jul 2022 14:36:30 +0100 Subject: [PATCH 12/14] :white_check_mark: Add tests for getBlock and getBlocks() functions --- tests/utilities/index.spec.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/utilities/index.spec.ts b/tests/utilities/index.spec.ts index 393c7a1..d206ba5 100644 --- a/tests/utilities/index.spec.ts +++ b/tests/utilities/index.spec.ts @@ -3,6 +3,8 @@ import { AttachmentCollection, OptionCollection, OptionGroupCollection, + buildBlock, + buildBlocks, } from '../../src/utilities'; import { Blocks } from '../../src/blocks'; import { Bits } from '../../src/bits'; @@ -359,4 +361,25 @@ describe('Testing Utility Functions:', () => { expect(blocks).toEqual([]); }); + + test('Calling `buildBlock()` with a block returns the built block.', () => { + const block = Blocks.Section(); + const built = buildBlock(block); + + expect(built).toEqual(block.build()); + }); + + test('Calling `buildBlocks()` with blocks returns the built blocks.', () => { + const blocks = buildBlocks([ + Blocks.Section(), + Blocks.Section(), + Blocks.Section(), + ]); + + expect(blocks).toEqual([ + Blocks.Section().build(), + Blocks.Section().build(), + Blocks.Section().build(), + ]); + }); }); From 4ef1bf04595ee529ea81e6bedd03a30c609ec446 Mon Sep 17 00:00:00 2001 From: Ray East Date: Sun, 17 Jul 2022 14:38:35 +0100 Subject: [PATCH 13/14] :package: Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8f15bc..f12930d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "slack-block-builder", - "version": "2.5.0", + "version": "2.6.0", "description": "Maintainable code for interactive Slack messages, modals, home tabs, and workflow steps. A must-have for the Slack Block Kit framework.", "author": { "name": "Ray East", From eba0aad024ec0ee44170ee7092b292d796927298 Mon Sep 17 00:00:00 2001 From: Ray East Date: Sun, 17 Jul 2022 14:41:31 +0100 Subject: [PATCH 14/14] :bulb: Update JSDoc for buildBlock() and buildBlocks() functions --- src/utilities/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 17bcb6f..7659870 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -37,10 +37,18 @@ export function OptionGroupCollection(...optionGroups: Appendable>(...optionGroups); } +/** + * @description Returns the block passed into the function as a built block, an object that conforms to the Slack API. + */ + export function buildBlock(block: BlockBuilder): Readonly { return block.build(); } +/** + * @description Creates and returns an array of built blocks. Behaves in the same way as all appending methods, such as Surface.blocks(). + */ + export function buildBlocks(...blocks: Appendable): Readonly[] { return getBuiltCollection>(...blocks); }