Skip to content

Commit 6db61a0

Browse files
committed
Simplifying boolean maps values
1 parent 6d1577b commit 6db61a0

9 files changed

Lines changed: 180 additions & 133 deletions

File tree

contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/EditBooleanMapModal.vue

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,6 @@
2828
:inputHandler="(value) => { selectedValues = value }"
2929
></slot>
3030

31-
<component
32-
:is="inputComponent"
33-
v-model="selectedValues"
34-
expanded
35-
hideLabel
36-
:nodeIds="nodeIds"
37-
/>
38-
3931
<span v-if="error" class="red--text">
4032
{{ error }}
4133
</span>
@@ -82,18 +74,6 @@
8274
type: Array,
8375
default: () => [],
8476
},
85-
/**
86-
* inputComponent is a component that will be used to render the input(s)
87-
* that will be used to edit the boolean map field.
88-
* It should be a component that accepts a v-model prop and a nodeIds prop.
89-
*
90-
* The v-model prop should support an object with the same structure as the
91-
* `this.selectedValues` data property.
92-
*/
93-
inputComponent: {
94-
type: Object,
95-
required: true,
96-
},
9777
},
9878
data() {
9979
return {
@@ -102,11 +82,9 @@
10282
/**
10383
* selectedValues is an object with the following structure:
10484
* {
105-
* [optionId]: true | [nodeId1, nodeId2, ...]
85+
* [optionId]: [nodeId1, nodeId2, ...]
10686
* }
107-
* If the value is true, it means that the option is selected for all nodes
108-
* If the value is an array of nodeIds, it means that the option is selected
109-
* just for those nodes
87+
* Where nodeIds is the id of the nodes that have the option selected
11088
*/
11189
selectedValues: {},
11290
};
@@ -144,13 +122,7 @@
144122
});
145123
});
146124
147-
Object.entries(optionsNodes).forEach(([key, nodeIds]) => {
148-
if (nodeIds.length === this.nodeIds.length) {
149-
this.selectedValues[key] = true;
150-
} else {
151-
this.selectedValues[key] = nodeIds;
152-
}
153-
});
125+
this.selectedValues = optionsNodes;
154126
},
155127
methods: {
156128
...mapActions('contentNode', ['updateContentNode', 'updateContentNodeDescendants']),
@@ -161,7 +133,8 @@
161133
if (this.validators && this.validators.length) {
162134
this.error = getInvalidText(
163135
this.validators,
164-
Object.keys(this.selectedValues).filter(key => this.selectedValues[key] === true)
136+
Object.keys(this.selectedValues)
137+
.filter(key => this.selectedValues[key].length === this.nodes.length)
165138
);
166139
} else {
167140
this.error = '';
@@ -177,7 +150,7 @@
177150
this.nodes.map(node => {
178151
const fieldValue = {};
179152
Object.entries(this.selectedValues).forEach(([key, value]) => {
180-
if (value === true || value.includes(node.id)) {
153+
if (value.includes(node.id)) {
181154
fieldValue[key] = true;
182155
}
183156
});

contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/EditLearningActivitiesModal.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
:title="$tr('editLearningActivitiesTitle')"
77
:nodeIds="nodeIds"
88
:validators="learningActivityValidators"
9-
:inputComponent="LearningActivityOptionsComponent"
109
:confirmationMessage="$tr('editedLearningActivities', { count: nodeIds.length })"
1110
@close="() => $emit('close')"
1211
>

contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditBooleanMapModal.spec.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ const options = Object.entries(Categories).map(([key, value]) => {
6262
const makeWrapper = ({
6363
nodeIds,
6464
field = 'categories',
65-
inputComponent = CategoryOptions,
6665
...restOptions
6766
}) => {
6867
return mount(EditBooleanMapModal, {
@@ -72,7 +71,6 @@ const makeWrapper = ({
7271
options,
7372
title: 'Edit Categories',
7473
field,
75-
inputComponent,
7674
autocompleteLabel: 'Select option',
7775
confirmationMessage: 'edited',
7876
...restOptions,

contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,23 @@
6565
ref="learning_activities"
6666
v-model="contentLearningActivities"
6767
:disabled="anyIsTopic"
68+
:nodeIds="nodeIds"
6869
@focus="trackClick('Learning activities')"
6970
/>
7071
<!-- Level -->
7172
<LevelsOptions
7273
v-if="oneSelected"
7374
ref="contentLevel"
7475
v-model="contentLevel"
76+
:nodeIds="nodeIds"
7577
@focus="trackClick('Levels dropdown')"
7678
/>
7779
<!-- What you will need -->
7880
<ResourcesNeededOptions
7981
v-if="oneSelected"
8082
ref="resourcesNeeded"
8183
v-model="resourcesNeeded"
84+
:nodeIds="nodeIds"
8285
@focus="trackClick('What you will need')"
8386
/>
8487
<!-- Tags -->
@@ -111,7 +114,12 @@
111114
</VFlex>
112115
</VLayout>
113116
<!-- Category -->
114-
<CategoryOptions v-if="oneSelected" ref="categories" v-model="categories" />
117+
<CategoryOptions
118+
v-if="oneSelected"
119+
ref="categories"
120+
v-model="categories"
121+
:nodeIds="nodeIds"
122+
/>
115123
</VFlex>
116124
</VLayout>
117125

@@ -434,14 +442,10 @@
434442
}
435443
436444
/**
437-
* This function is used to generate getter/setters for new metadata fields that are boolean maps:
438-
* - `grade_levels` (sometimes referred to as `content_levels`)
439-
* - `learner_needs` (resources needed)
445+
* This function is used to generate getter/setters having its value as
446+
* an array for metadata fields that are boolean maps:
440447
* - `accessibility_labels` (accessibility options)
441-
* - `learning_activities` (learning activities)
442-
* - `categories` (categories)
443448
*/
444-
445449
function generateNestedNodesGetterSetter(key) {
446450
return {
447451
get() {
@@ -475,6 +479,42 @@
475479
};
476480
}
477481
482+
/**
483+
* This function is used to generate getter/setters having its value as
484+
* an object for metadata fields that are boolean maps:
485+
* - `grade_levels` (sometimes referred to as `content_levels`)
486+
* - `learner_needs` (resources needed)
487+
* - `learning_activities` (learning activities)
488+
* - `categories` (categories)
489+
*/
490+
function generateNestedNodesGetterSetterObject(key) {
491+
return {
492+
get() {
493+
const value = {};
494+
for (const node of this.nodes) {
495+
const diffTrackerNode = this.diffTracker[node.id] || {};
496+
const currentValue = diffTrackerNode[key] || node[key] || {};
497+
Object.entries(currentValue).forEach(([option, optionValue]) => {
498+
if (optionValue) {
499+
value[option] = value[option] || [];
500+
value[option].push(node.id);
501+
}
502+
});
503+
}
504+
return value;
505+
},
506+
set(value) {
507+
const newMap = {};
508+
for (const option in value) {
509+
if (value[option].length === this.nodes.length) {
510+
newMap[option] = true;
511+
}
512+
}
513+
this.update({ [key]: newMap });
514+
},
515+
};
516+
}
517+
478518
export default {
479519
name: 'DetailsTabView',
480520
components: {
@@ -584,24 +624,31 @@
584624
role: generateGetterSetter('role_visibility'),
585625
language: generateGetterSetter('language'),
586626
accessibility: generateNestedNodesGetterSetter('accessibility_labels'),
587-
contentLevel: generateNestedNodesGetterSetter('grade_levels'),
588-
resourcesNeeded: generateNestedNodesGetterSetter('learner_needs'),
627+
contentLevel: generateNestedNodesGetterSetterObject('grade_levels'),
628+
resourcesNeeded: generateNestedNodesGetterSetterObject('learner_needs'),
589629
forBeginners: {
590630
get() {
591-
return this.resourcesNeeded.includes(ResourcesNeededTypes.FOR_BEGINNERS);
631+
const value = this.resourcesNeeded[ResourcesNeededTypes.FOR_BEGINNERS];
632+
return (
633+
value &&
634+
value.length === this.nodes.length
635+
);
592636
},
593637
set(value) {
594638
if (value) {
595-
this.resourcesNeeded = [...this.resourcesNeeded, ResourcesNeededTypes.FOR_BEGINNERS];
639+
this.resourcesNeeded = {
640+
...this.resourcesNeeded,
641+
[ResourcesNeededTypes.FOR_BEGINNERS]: this.nodeIds,
642+
};
596643
} else {
597-
this.resourcesNeeded = this.resourcesNeeded.filter(
598-
r => r !== ResourcesNeededTypes.FOR_BEGINNERS
599-
);
644+
const newMap = { ...this.resourcesNeeded };
645+
delete newMap[ResourcesNeededTypes.FOR_BEGINNERS];
646+
this.resourcesNeeded = newMap;
600647
}
601648
},
602649
},
603-
contentLearningActivities: generateNestedNodesGetterSetter('learning_activities'),
604-
categories: generateNestedNodesGetterSetter('categories'),
650+
contentLearningActivities: generateNestedNodesGetterSetterObject('learning_activities'),
651+
categories: generateNestedNodesGetterSetterObject('categories'),
605652
license() {
606653
return this.getValueFromNodes('license');
607654
},

contentcuration/contentcuration/frontend/shared/views/contentNodeFields/CategoryOptions.vue

Lines changed: 24 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,15 @@
110110
mixins: [constantsTranslationMixin, metadataTranslationMixin],
111111
props: {
112112
/**
113-
* It can receive a value as an array of strings of the selected categories, or
114-
* an object with the following structure:
113+
* This prop receives an object with the following structure:
115114
* {
116-
* [categoryId]: true | [nodeId1, nodeId2, ...]
115+
* [categoryId]: [nodeId1, nodeId2, ...]
117116
* }
118-
* If the value is true, it means that the option is selected for all nodes
119-
* If the value is an array of nodeIds, it means that the option is selected
120-
* just for those nodes
117+
* Where nodeId is the id of the node that has the category selected
121118
*/
122119
value: {
123-
type: [Array, Object],
124-
default: () => [],
120+
type: Object,
121+
required: true,
125122
},
126123
/**
127124
* An array of nodeIds that we are editing. If none, we will asume that we
@@ -160,7 +157,7 @@
160157
},
161158
autocompleteOptions() {
162159
const options = [...this.categoriesList];
163-
if (!Array.isArray(this.selected)) {
160+
if (this.expanded) {
164161
// Just boolean maps can have indeterminate values
165162
options.push({
166163
value: MIXED,
@@ -171,13 +168,14 @@
171168
return options;
172169
},
173170
autocompleteValues() {
174-
if (Array.isArray(this.selected)) {
175-
return this.selected;
176-
}
177171
const selectedValues = Object.entries(this.selected)
178-
.filter(entry => entry[1] === true) // no mixed values for boolean maps
172+
.filter(entry => entry[1].length === this.nodeIds.length)
179173
.map(([key]) => key);
180-
if (Object.values(this.selected).some(value => value !== true)) {
174+
if (
175+
this.expanded &&
176+
Object.values(this.selected)
177+
.some(value => value.length < this.nodeIds.length)
178+
) {
181179
selectedValues.push(MIXED);
182180
}
183181
return selectedValues;
@@ -201,17 +199,12 @@
201199
return this.nested ? { [rule]: `${item.level * 24}px` } : {};
202200
},
203201
add(value) {
204-
if (Array.isArray(this.selected)) {
205-
this.selected = [...this.selected, value];
206-
return;
207-
}
208-
this.selected = { ...this.selected, [value]: true };
202+
this.selected = {
203+
...this.selected,
204+
[value]: this.nodeIds,
205+
};
209206
},
210207
remove(value) {
211-
if (Array.isArray(this.selected)) {
212-
this.selected = this.selected.filter(i => !i.startsWith(value));
213-
return;
214-
}
215208
const newSelected = { ...this.selected };
216209
Object.keys(this.selected)
217210
.filter(selectedValue => selectedValue.startsWith(value))
@@ -221,10 +214,6 @@
221214
this.selected = newSelected;
222215
},
223216
removeAll() {
224-
if (Array.isArray(this.selected)) {
225-
this.selected = [];
226-
return;
227-
}
228217
this.selected = {};
229218
},
230219
tooltipText(optionId) {
@@ -253,15 +242,12 @@
253242
return val.split('.').length - 1;
254243
},
255244
isSelected(value) {
256-
if (Array.isArray(this.selected)) {
257-
return this.selected.some(v => v.startsWith(value));
258-
}
259-
// If not, this.selected is a boolean map
260-
261245
// If the value is truthy (true or an array of nodeIds) then
262246
// it is selected just if it is true (not an array)
263-
if (this.selected[value]) {
264-
return this.selected[value] === true;
247+
if (
248+
this.selected[value] &&
249+
this.selected[value].length === this.nodeIds.length) {
250+
return true;
265251
}
266252
267253
return this.isCheckboxSelectedByChildren(value);
@@ -274,9 +260,6 @@
274260
* child options, together they constitute the same array of selected contentNodes.
275261
*/
276262
isCheckboxSelectedByChildren(optionId) {
277-
if (!this.nodeIds || !this.nodeIds.length) {
278-
return false;
279-
}
280263
const childrenOptions = Object.keys(this.selected)
281264
.filter(selectedValue => selectedValue.startsWith(optionId))
282265
.map(selectedValue => this.selected[selectedValue]);
@@ -285,11 +268,11 @@
285268
return false; // No childen options
286269
} else if (childrenOptions.length === 1) {
287270
// just one child option, the value is deterrmined by if it is selected
288-
return childrenOptions[0] === true;
271+
return childrenOptions[0].length === this.nodeIds.length;
289272
}
290273
291274
// Here multiple children are selected or indeterminate
292-
if (childrenOptions.some(value => value === true)) {
275+
if (childrenOptions.some(value => value.length === this.nodeIds.length)) {
293276
// if some child value is selected for all nodes, then the parent option is selected
294277
return true;
295278
}
@@ -303,12 +286,8 @@
303286
return nodeIds.size === this.nodeIds.length;
304287
},
305288
isCheckboxIndeterminate(optionId) {
306-
// Just boolean maps can have indeterminate values
307-
if (Array.isArray(this.selected)) {
308-
return false;
309-
}
310-
if (this.selected[optionId]) {
311-
return this.selected[optionId] !== true;
289+
if (this.selected[optionId] && this.selected[optionId].length < this.nodeIds.length) {
290+
return true;
312291
}
313292
return (
314293
Object.keys(this.selected).some(selectedValue => selectedValue.startsWith(optionId)) &&

0 commit comments

Comments
 (0)