Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<template>

<div id="app">
<VAutocomplete
:value="selected"
:items="categoriesList"
:searchInput.sync="categoryText"
:label="translateMetadataString('category')"
box
clearable
chips
deletableChips
multiple
item-value="value"
item-text="text"
@click:clear="$nextTick(() => removeAll())"
>
<template v-slot:selection="data">
<VTooltip bottom>
<template v-slot:activator="{ on, attrs }">
<VChip v-bind="attrs" close v-on="on" @input="remove(data.item.value)">
{{ data.item.text }}
</VChip>
</template>
<div>
<div>{{ tooltipHelper(data.item.value) }}</div>
</div>
</VTooltip>
</template>

<template v-slot:no-data>
<VListTile v-if="categoryText && categoryText.trim()">
<VListTileContent>
<VListTileTitle>
{{ $tr('noCategoryFoundText', { text: categoryText.trim() }) }}
</VListTileTitle>
</VListTileContent>
</VListTile>
</template>

<template v-slot:item="{ item }">
<div style="width: 100%; height: 100%" aria-hidden="true">
<VDivider v-if="!item.value.includes('.')" />
<KCheckbox
:checked="dropdownSelected[item.value]"
:label="item.text"
:value="item.value"
style="margin-top: 10px"
:style="treeItemStyle(item)"
:ripple="false"
@change="checked => checked ? add(item.value) : remove(item.value)"
/>
</div>
</template>
</VAutocomplete>
</div>

</template>

<script>

import { camelCase } from 'lodash';
import { constantsTranslationMixin, metadataTranslationMixin } from 'shared/mixins';
import { Categories, CategoriesLookup } from 'shared/constants';

const availablePaths = {};
const categoriesObj = {};
const dropdown = [];

Object.assign(availablePaths, CategoriesLookup);

/*
* This is used to create the categories object from which the dropdown list is generated
* and is the same as this to make sure the order in Kolibri and Studio are the same:
* https://github.com/learningequality/kolibri/blob/develop/kolibri/plugins/learn/assets/
* src/views/CategorySearchModal/CategorySearchOptions.vue#L73
*/
for (let subjectKey of Object.entries(Categories)
.sort((a, b) => a[1].length - b[1].length)
.map(a => a[0])) {
const ids = Categories[subjectKey].split('.');

let path = '';
let nested = categoriesObj;
for (let fragment of ids) {
path += fragment;
if (availablePaths[path]) {
const nestedKey = CategoriesLookup[path];
if (!nested[nestedKey]) {
nested[nestedKey] = {
value: path,
nested: {},
};
}
nested = nested[nestedKey].nested;
path += '.';
} else {
break;
}
}
}

const flattenCategories = obj => {
Object.keys(obj).forEach(key => {
if (key !== 'value' && key !== 'nested') {
dropdown.push({
text: key,
value: obj[key]['value'],
});
}
if (typeof obj[key] === 'object') {
flattenCategories(obj[key]);
}
});
return dropdown;
};

flattenCategories(categoriesObj);

export default {
name: 'CategoryOptions',
mixins: [constantsTranslationMixin, metadataTranslationMixin],
props: {
value: {
type: Array,
default: () => [],
},
},
data() {
return {
categoryText: null,
};
},
computed: {
categoriesList() {
return dropdown.map(category => {
return {
...category,
text: this.translateMetadataString(camelCase(category.text)),
level: this.findDepth(Categories[category.text]),
};
});
},
dropdownSelected() {
const obj = {};
for (let category of this.selected) {
const paths = category.split('.');
let cat = '';
for (let path of paths) {
cat += path;
obj[cat] = true;
cat += '.';
}
}
return obj;
},
selected: {
get() {
return this.value;
},
set(value) {
this.$emit('input', value);
},
},
nested() {
return !this.categoryText;
},
},
methods: {
treeItemStyle(item) {
return this.nested ? { paddingLeft: `${item.level * 24}px` } : {};
},
add(item) {
this.selected = [...this.selected, item];
},
remove(item) {
this.selected = this.selected.filter(i => !i.startsWith(item));
},
removeAll() {
this.selected = [];
},
tooltipHelper(id) {
return this.displayFamilyTree(dropdown, id)
.map(node => this.translateMetadataString(camelCase(node.text)))
.join(' - ');
},
findDepth(val) {
return val.split('.').length - 1;
},
displayFamilyTree(nodes, id) {
return nodes.filter(node => this.findFamilyTreeIds(id).includes(node.value));
},
/**
* @param {String} id The id of the selected category
* @returns {Array} An array of all ids of the family beginning with the selected category
*/
findFamilyTreeIds(id) {
const family = [];
let familyMember = id;
while (familyMember) {
family.push(familyMember);
familyMember = familyMember.includes('.')
? familyMember.substring(0, familyMember.lastIndexOf('.'))
: null;
}
return family;
},
},
$trs: {
noCategoryFoundText: 'Category not found',
},
};

</script>
<style lang="less" scoped>

/deep/ .v-list__tile {
flex-wrap: wrap;
}

</style>
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,21 @@
>
<!-- Learning activity -->
<LearningActivityOptions
id="learning_activities"
ref="learning_activities"
v-model="contentLearningActivities"
@focus="trackClick('Learning activities')"
/>
<!-- Level -->
<LevelsOptions
id="levels"
ref="contentLevel"
v-model="contentLevel"
@focus="trackClick('Levels dropdown')"
/>
<!-- What you will need -->
<ResourcesNeededOptions
id="resources_needed"
ref="resourcesNeeded"
v-model="resourcesNeeded"
@focus="trackClick('What you will need')"
Expand Down Expand Up @@ -106,6 +109,8 @@
</VCombobox>
</VFlex>
</VLayout>
<!-- Category -->
<CategoryOptions ref="categories" v-model="categories" />
</VFlex>
</VLayout>

Expand Down Expand Up @@ -176,6 +181,7 @@
</h1>
<!-- Language -->
<LanguageDropdown
id="language"
ref="language"
v-model="language"
class="mb-2"
Expand All @@ -189,6 +195,7 @@
<!-- Visibility -->
<VisibilityDropdown
v-if="allResources"
id="role_visibility"
ref="role_visibility"
v-model="role"
:placeholder="getPlaceholder('role')"
Expand Down Expand Up @@ -299,6 +306,7 @@

<!-- License -->
<LicenseDropdown
id="license"
ref="license"
v-model="licenseItem"
:required="isUnique(license) && isUnique(license_description) && !disableAuthEdits"
Expand Down Expand Up @@ -366,6 +374,7 @@
import LevelsOptions from './LevelsOptions.vue';
import ResourcesNeededOptions from './ResourcesNeededOptions.vue';
import LearningActivityOptions from './LearningActivityOptions.vue';
import CategoryOptions from './CategoryOptions.vue';

import {
getTitleValidators,
Expand Down Expand Up @@ -459,6 +468,7 @@
LevelsOptions,
ResourcesNeededOptions,
LearningActivityOptions,
CategoryOptions,
},
mixins: [constantsTranslationMixin, metadataTranslationMixin],
props: {
Expand Down Expand Up @@ -541,7 +551,7 @@
contentLevel: generateNestedNodesGetterSetter('grade_levels'),
resourcesNeeded: generateNestedNodesGetterSetter('learner_needs'),
contentLearningActivities: generateNestedNodesGetterSetter('learning_activities'),

categories: generateNestedNodesGetterSetter('categories'),
mastery_model() {
return this.getExtraFieldsValueFromNodes('mastery_model');
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
multiple
deletableChips
:rules="learningActivityRules"
attach="learning_activities"
attach="#learning_activities"
/>

</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
:label="translateMetadataString('level')"
multiple
deletableChips
attach="contentLevel"
attach="#levels"
/>

</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
multiple
deletableChips
clearable
attach="resourcesNeeded"
attach="#resources_needed"
/>

</template>
Expand Down
Loading