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
+