diff --git a/docs/userGuide/syntax/cardstacks.md b/docs/userGuide/syntax/cardstacks.md index 7c5107c050..c672f1c6eb 100644 --- a/docs/userGuide/syntax/cardstacks.md +++ b/docs/userGuide/syntax/cardstacks.md @@ -142,6 +142,7 @@ Name | Type | Default | Description --- | --- | --- | --- blocks | `String` | `2` | Number of `card` columns per row.
Supports: `1`, `2`, `3`, `4`, `6` searchable | `Boolean` | `false` | Whether the card stack is searchable. +show-select-all | `Boolean` | `true` | Whether the select all tag button appears. (`false` by default if total tags ≤ 3) `card`: Name | Type | Default | Description @@ -149,7 +150,7 @@ Name | Type | Default | Description tag | `String` | `null` | Tags of each card component.
Each unique tag should be seperated by a `,`.
Tags are added to the search field. header | `String` | `null` | Header of each card component.
Supports the use of inline markdown elements. keywords | `String` | `null` | Keywords of each card component.
Each unique keyword should be seperated by a `,`.
Keywords are added to the search field. -disable | `Boolean` | `false` | Disable card.
This removes visibility of the card and makes it unsearchable. +disabled | `Boolean` | `false` | Disable card.
This removes visibility of the card and makes it unsearchable. diff --git a/packages/core/src/html/MdAttributeRenderer.ts b/packages/core/src/html/MdAttributeRenderer.ts index 442689d883..cfd41c9bfc 100644 --- a/packages/core/src/html/MdAttributeRenderer.ts +++ b/packages/core/src/html/MdAttributeRenderer.ts @@ -1,4 +1,5 @@ import has from 'lodash/has'; +import isNil from 'lodash/isNil'; import { getVslotShorthandName } from './vueSlotSyntaxProcessor'; import type { MarkdownProcessor } from './MarkdownProcessor'; import * as logger from '../utils/logger'; @@ -7,6 +8,7 @@ import { MbNode, NodeOrText, parseHTML } from '../utils/node'; const _ = { has, + isNil, }; /** @@ -160,6 +162,26 @@ export class MdAttributeRenderer { this.processSlotAttribute(node, 'header', false); } + // eslint-disable-next-line class-methods-use-this + processCardStackAttributes(node: MbNode) { + if (!node.children || !node.attribs) { + return; + } + + const showSelectAllRaw = node.attribs['show-select-all']; + // Handles the 'show-select-all' attribute on the cardstack itself + if (!_.isNil(showSelectAllRaw)) { + // Check if they have specified 'false' explicitly + const showSelectAll = showSelectAllRaw.toLowerCase(); + if (showSelectAll === 'false') { + node.attribs['show-select-all'] = 'false'; + } else { + // Default option or if user specifies any other value it is treated as true. + node.attribs['show-select-all'] = 'true'; + } + } + } + /* * Card Stack */ diff --git a/packages/core/src/html/NodeProcessor.ts b/packages/core/src/html/NodeProcessor.ts index 46e40faaba..08a8aaa319 100644 --- a/packages/core/src/html/NodeProcessor.ts +++ b/packages/core/src/html/NodeProcessor.ts @@ -212,6 +212,9 @@ export class NodeProcessor { case 'tooltip': this.mdAttributeRenderer.processTooltip(node); break; + case 'cardstack': + this.mdAttributeRenderer.processCardStackAttributes(node); + break; case 'card': this.mdAttributeRenderer.processCardAttributes(node); break; diff --git a/packages/core/test/unit/html/NodeProcessor.test.ts b/packages/core/test/unit/html/NodeProcessor.test.ts index f42f4a2779..2c3b4b085b 100644 --- a/packages/core/test/unit/html/NodeProcessor.test.ts +++ b/packages/core/test/unit/html/NodeProcessor.test.ts @@ -452,3 +452,22 @@ test('slot nodes which have tag names other than "template" are shifted one leve expect(cheerio.html(testNode)).toEqual(expected); }); + +test('processNode standardizes cardstack show-select-all attribute', () => { + const nodeProcessor = getNewDefaultNodeProcessor(); + + const templateFalse = ''; + const nodeFalse = parseHTML(templateFalse)[0] as MbNode; + nodeProcessor.processNode(nodeFalse, new Context(path.resolve(''), [], {}, {})); + expect(nodeFalse.attribs['show-select-all']).toBe('false'); + + const templateTrue = ''; + const nodeTrue = parseHTML(templateTrue)[0] as MbNode; + nodeProcessor.processNode(nodeTrue, new Context(path.resolve(''), [], {}, {})); + expect(nodeTrue.attribs['show-select-all']).toBe('true'); + + const templateNone = ''; + const nodeNone = parseHTML(templateNone)[0] as MbNode; + nodeProcessor.processNode(nodeNone, new Context(path.resolve(''), [], {}, {})); + expect(nodeNone.attribs['show-select-all']).toBeUndefined(); +}); diff --git a/packages/vue-components/src/__tests__/CardStack.spec.js b/packages/vue-components/src/__tests__/CardStack.spec.js index 8c04b1486f..783fc6b607 100644 --- a/packages/vue-components/src/__tests__/CardStack.spec.js +++ b/packages/vue-components/src/__tests__/CardStack.spec.js @@ -10,6 +10,13 @@ const DEFAULT_GLOBAL_MOUNT_OPTIONS = { stubs: DEFAULT_STUBS, }; +const CARDS_FOR_SELECT_ALL = ` + + + + +`; + const CARDS_CUSTOMISATION = ` Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor @@ -91,4 +98,135 @@ describe('CardStack', () => { await wrapper.vm.$nextTick(); expect(wrapper.element).toMatchSnapshot(); }); + + test('should have all tags checked by default on load', async () => { + const wrapper = mount(CardStack, { + slots: { default: CARDS_CUSTOMISATION }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + const allTags = wrapper.vm.cardStackRef.tagMapping.map(key => key[0]); + expect(wrapper.vm.selectedTags).toEqual(expect.arrayContaining(allTags)); + expect(wrapper.vm.allSelected).toBe(true); + }); + + test('toggleAllTags should unselect everything and then select everything', async () => { + const wrapper = mount(CardStack, { + slots: { default: CARDS_FOR_SELECT_ALL }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + // selected all initially + expect(wrapper.vm.allSelected).toBe(true); + + // deselect everything + const selectAllBadge = wrapper.find('.select-all-toggle'); + await selectAllBadge.trigger('click'); + expect(wrapper.vm.selectedTags.length).toBe(0); + expect(wrapper.vm.allSelected).toBe(false); + + // all cards should be hidden + const cards = wrapper.findAllComponents(Card); + cards.forEach((card) => { + if (card.props('tag') === 'Short') { + expect(card.vm.disableTag).toBe(true); + } + }); + + // select all again -> everything should be selected back + await selectAllBadge.trigger('click'); + expect(wrapper.vm.allSelected).toBe(true); + expect(wrapper.vm.selectedTags.length).toBeGreaterThan(0); + }); + + test('Select All checkbox should sync with individual tag clicks', async () => { + const wrapper = mount(CardStack, { + slots: { default: CARDS_FOR_SELECT_ALL }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + // uncheck first tag + const firstTagBadge = wrapper.findAll('.tag-badge').at(1); + await firstTagBadge.trigger('click'); + + // select all should no longer be checked + expect(wrapper.vm.allSelected).toBe(false); + const selectAllIndicator = wrapper.find('.select-all-toggle .tag-indicator'); + expect(selectAllIndicator.text()).not.toContain('✓'); + + // Check first tag -> select all should be checked again + await firstTagBadge.trigger('click'); + expect(wrapper.vm.allSelected).toBe(true); + }); + + test('should show Select All badge by default', async () => { + const wrapper = mount(CardStack, { + slots: { default: CARDS_FOR_SELECT_ALL }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + // The first badge with .bg-dark is the "Select All" badge + const selectAllBadge = wrapper.find('.select-all-toggle'); + expect(selectAllBadge.exists()).toBe(true); + expect(selectAllBadge.text()).toContain('Select All'); + }); + + test('should hide Select All badge when showSelectAll is false', async () => { + const wrapper = mount(CardStack, { + propsData: { + showSelectAll: false, // Testing boolean false + }, + slots: { default: CARDS_FOR_SELECT_ALL }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const selectAllBadge = wrapper.find('.select-all-toggle'); + expect(selectAllBadge.exists()).toBe(false); + }); + + test('should hide Select All badge when showSelectAll is "false" string (case-insensitive)', async () => { + // This simulates the parser passing show-select-all="fAlse" + const wrapper = mount(CardStack, { + propsData: { + searchable: true, + showSelectAll: 'fAlse', + }, + slots: { default: CARDS_FOR_SELECT_ALL }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const selectAllBadge = wrapper.find('.select-all-toggle'); + expect(selectAllBadge.exists()).toBe(false); + }); + + test('should show Select All badge when showSelectAll is "true" string', async () => { + const wrapper = mount(CardStack, { + propsData: { + searchable: true, + showSelectAll: 'true', + }, + slots: { default: CARDS_FOR_SELECT_ALL }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const selectAllBadge = wrapper.find('.select-all-toggle'); + expect(selectAllBadge.exists()).toBe(true); + }); + + test('should hide Select All badge when below threshold (<=3 tags)', async () => { + const wrapper = mount(CardStack, { + slots: { default: CARDS_CUSTOMISATION }, // Only 2 tags + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const selectAllBadge = wrapper.find('.select-all-toggle'); + expect(selectAllBadge.exists()).toBe(false); + }); }); diff --git a/packages/vue-components/src/__tests__/__snapshots__/CardStack.spec.js.snap b/packages/vue-components/src/__tests__/__snapshots__/CardStack.spec.js.snap index 7524c4a9b6..b7fc0a0994 100644 --- a/packages/vue-components/src/__tests__/__snapshots__/CardStack.spec.js.snap +++ b/packages/vue-components/src/__tests__/__snapshots__/CardStack.spec.js.snap @@ -16,6 +16,7 @@ exports[`CardStack markdown in header, content 1`] = ` type="text" /> + @@ -96,6 +97,7 @@ exports[`CardStack should not hide cards when no filter is provided 1`] = ` type="text" /> + -     + ✓ @@ -218,6 +220,7 @@ exports[`CardStack should not hide cards when no filter is provided 2`] = ` type="text" /> + diff --git a/packages/vue-components/src/cardstack/CardStack.vue b/packages/vue-components/src/cardstack/CardStack.vue index a962d27f0f..bc39b6e0ed 100644 --- a/packages/vue-components/src/cardstack/CardStack.vue +++ b/packages/vue-components/src/cardstack/CardStack.vue @@ -12,6 +12,17 @@ /> + + + +     + + Select All +