diff --git a/README.md b/README.md index e553d41..58af3f9 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,8 @@ The `Accordion` component supports optional customizations, such as: `expandButtonText` – Used to pass in custom text for the button that expands an item, but has a default. `collapseButtonText` – Used to pass in custom text for the button that collapses an expanded item, but has a default. + +`isExpandable` – Used to display or not the expand/collapse button for a given item. ```javascript import { Modal, Blocks, Accordion } from 'slack-block-builder'; diff --git a/docs/components/accordion.md b/docs/components/accordion.md index 4eba0a1..68afad0 100644 --- a/docs/components/accordion.md +++ b/docs/components/accordion.md @@ -102,6 +102,10 @@ Used to customize the text for the expand button, which defaults to `'More'`. Used to customize the text for the collapse button, which defaults to `'Close'`. +`isExpandable` – *Function* `Optional` + +Used to show/hide expand/collapse button for a given item. + ### The `titleText` Function The `titleText` parameter accepts a function that takes an object that contains one of the items from the data set and returns a string to display as the title, next to the collapse/expand button. The object, at the moment, contains only one parameter, `item`, which is the item for which the title will be displayed. diff --git a/src/components/accordion-ui-component.ts b/src/components/accordion-ui-component.ts index 74bc90f..d839315 100644 --- a/src/components/accordion-ui-component.ts +++ b/src/components/accordion-ui-component.ts @@ -18,6 +18,7 @@ export type AccordionActionIdFn = StringReturnableFn; export type AccordionTitleTextFn = StringReturnableFn<{ item: T }>; export type AccordionBuilderFn = BlockBuilderReturnableFn<{ item: T }>; +export type AccordionIsExpandableFn = (item: T) => boolean; export interface AccordionUIComponentParams { items: T[]; paginator: AccordionStateManager; @@ -26,6 +27,7 @@ export interface AccordionUIComponentParams { titleTextFunction: AccordionTitleTextFn; actionIdFunction: AccordionActionIdFn; builderFunction: AccordionBuilderFn; + isExpandableFunction: AccordionIsExpandableFn; } export class AccordionUIComponent { @@ -43,6 +45,8 @@ export class AccordionUIComponent { private readonly builderFunction: AccordionBuilderFn; + private readonly isExpandableFunction: AccordionIsExpandableFn; + constructor(params: AccordionUIComponentParams) { this.items = params.items; this.paginator = params.paginator; @@ -51,20 +55,25 @@ export class AccordionUIComponent { this.titleTextFunction = params.titleTextFunction; this.actionIdFunction = params.actionIdFunction; this.builderFunction = params.builderFunction; + this.isExpandableFunction = params.isExpandableFunction; } public getBlocks(): BlockBuilder[] { const unpruned = this.items.map((item, index) => { const isExpanded = this.paginator.checkItemIsExpandedByIndex(index); + const section = Blocks.Section({ text: this.titleTextFunction({ item }) }); + + if (this.isExpandableFunction(item)) { + section.accessory(Elements.Button({ + text: isExpanded ? this.collapseButtonText : this.expandButtonText, + actionId: this.actionIdFunction({ + expandedItems: this.paginator.getNextStateByItemIndex(index), + }), + })); + } const blocks = [ - Blocks.Section({ text: this.titleTextFunction({ item }) }) - .accessory(Elements.Button({ - text: isExpanded ? this.collapseButtonText : this.expandButtonText, - actionId: this.actionIdFunction({ - expandedItems: this.paginator.getNextStateByItemIndex(index), - }), - })), + section, ...isExpanded ? this.builderFunction({ item }).flat() : [], ]; diff --git a/src/components/index.ts b/src/components/index.ts index 58d750b..8895497 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -11,6 +11,7 @@ import { AccordionTitleTextFn, AccordionActionIdFn, AccordionBuilderFn, + AccordionIsExpandableFn, } from './accordion-ui-component'; import { PaginatorStateManager, @@ -102,6 +103,7 @@ interface AccordionBaseParams { titleText: AccordionTitleTextFn; actionId: AccordionActionIdFn; blocksForExpanded: AccordionBuilderFn, + isExpandable?: AccordionIsExpandableFn, } export type AccordionParams = AccordionBaseParams & AccordionStateManagerParams; @@ -112,6 +114,7 @@ export type AccordionParams = AccordionBaseParams & AccordionStateManagerP * @param {AccordionTitleTextFn} [params.titleText] A function that receives an object with a single item and returns a string to be displayed next to the expand/collapse button. * @param {AccordionActionIdFn} [params.actionId] A function that receives the accordion state data and returns a string to set as the action IDs of the expand/collapse buttons. * @param {AccordionBuilderFn} [params.blocksForExpanded] A function that receives an object with a single item and returns the blocks to create for that item. + * @param {AccordionIsExpandableFn} [params.isExpandable] A function that receives an item and and returns a boolean that tells if the section should have an expand/collapse button. * @param {string} [params.expandButtonText] The text to display on the button that expands an item in the UI. * @param {string} [params.collapseButtonText] The text to display on the button that collapses an item in the UI. * @@ -129,6 +132,7 @@ export function Accordion(params: AccordionParams): AccordionUIComponent true), }); } diff --git a/tests/components/accordion.spec.ts b/tests/components/accordion.spec.ts index 15bddc6..d90c4fa 100644 --- a/tests/components/accordion.spec.ts +++ b/tests/components/accordion.spec.ts @@ -2435,4 +2435,241 @@ describe('Testing Accordion:', () => { type: 'modal', })); }); + + test('Check that only expandable items get a expand/collapse button', () => { + 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}` }), + ], + isExpandable: (item) => item.firstName === 'Taras', + }).getBlocks(), + ) + .buildToJSON(); + + expect(result).toEqual(JSON.stringify({ + title: { + type: 'plain_text', + text: 'Testing', + }, + blocks: [ + { + text: { + type: 'mrkdwn', + text: 'Ray East', + }, + 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', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Lesha Power', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Yozhef Hisem', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Andrey Roland', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Vlad Filimonov', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Boris Boriska', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Vadim Grabovyy', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Alex Chernyshov', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Serega Grigoruk', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Igor Roik', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Dima Tretiakov', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Sasha Chernyavska', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Arthur Nick', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Dima Lutsik', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Dima Svirepchuk', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Dima Bilkun', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Pasha Akimenko', + }, + type: 'section', + }, + { + type: 'divider', + }, + { + text: { + type: 'mrkdwn', + text: 'Karina Suprun', + }, + type: 'section', + }, + ], + type: 'modal', + })); + }); });