Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds ability to preview non-practice resources from the sidepanel #13012

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ref } from 'vue';
import ContentNodeResource from 'kolibri-common/apiResources/ContentNodeResource';

export default function useFetchContentNode(contentId) {
AllanOXDi marked this conversation as resolved.
Show resolved Hide resolved
const contentNode = ref({});
const ancestors = ref([]);
const questions = ref([]);
const loading = ref(true);

const fetchContentNode = async () => {
ContentNodeResource.fetchModel({
id: contentId,
getParams: { no_available_filtering: true },
}).then(node => {
if (node != null) {
loading.value = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is null then will loading be true indefinitely?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I see! if node is null, theloading.valuewill remain true indefinitely because the condition to setloading.value = falseis only executed whennodeis not null. I can can see an infinite loading state here. Thanks for pointing that out

Copy link
Member

@nucleogenesis nucleogenesis Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure node can ever be null here - I don't think our api resource methods like fetchModel would return anything besides the data it gets from the API request or it throws an error.

So, in this case, I think you'd probably want to just treat your then() callback function as if it's exactly what you expected to get.

Then you can chain a catch method on it - but you'll need a way to tell the caller of useFetchContentNode to tell the Vue component it is used in that there were errors.

contentNode.value = node;

if (node.ancestors.length) {
ancestors.value = node.ancestors;
}

if (node.assessmentmetadata) {
questions.value = node.assessmentmetadata.assessment_item_ids;
}
} else {
loading.value = false;
}
});
};

fetchContentNode();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking note. You could skip making the function fetchContentNode altogether and just run the code within it directly since you're not returning the function from the composable and you call it within the comopsable.


return {
loading,
ancestors,
contentNode,
questions,
};
}
14 changes: 10 additions & 4 deletions kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
import LessonLearnerExercisePage from '../views/lessons/reports/LessonLearnerExercisePage.vue';
import QuestionLearnersPage from '../views/common/reports/QuestionLearnersPage.vue';
import EditLessonDetails from '../views/lessons/LessonSummaryPage/sidePanels/EditLessonDetails';
import PreviewSelectedResources from '../views/lessons/LessonSummaryPage/sidePanels/PreviewSelectedResources';
import PreviewSelectedResources from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/PreviewSelectedResources';
import LessonResourceSelection from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection';
import SelectionIndex from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectionIndex.vue';
import SelectFromBookmarks from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromBookmarks.vue';
Expand Down Expand Up @@ -157,9 +157,15 @@ export default [
component: SelectFromChannels,
},
{
name: PageNames.LESSON_PREVIEW_SELECTED_RESOURCES,
path: 'preview-resources',
component: ManageSelectedResources,
name: PageNames.LESSON_PREVIEW_RESOURCE,
path: 'preview',
component: PreviewSelectedResources,
props: toRoute => {
const contentId = toRoute.query.contentId;
return {
contentId,
};
},
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,18 @@
},
methods: {
contentLink(content) {
const { name, params, query } = this.$route;
const { params, query } = this.$route;
if (!content.is_leaf) {
return this.topicsLink(content.id);
}
// Just return the current route; router-link will handle the no-op from here
return { name, params, query };
return {
name: PageNames.LESSON_PREVIEW_RESOURCE,
params: params,
query: {
...query,
contentId: content.id,
},
};
},
topicsLink(topicId) {
const { name, params, query } = this.$route;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
<div v-if="loading">
<KCircularLoader />
</div>

<router-view
v-else
:setTitle="setTitle"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
<template>

<div>
<div class="channel-header">
<div>
<h6>
{{ coreString('selectFromChannels') }}
</h6>
</div>

<div>
<template v-if="isSelected">
<KIcon icon="onDevice" />
{{ addedIndicator$() }}
</template>

<KButton
v-if="isSelected"
:text="coreString('removeAction')"
:primary="true"
@click="removeResource()"
/>
<KButton
v-else
:text="addText$()"
:primary="false"
@click="addResource()"
/>
</div>
</div>

<ResourceSelectionBreadcrumbs
:ancestors="ancestors"
:channelsLink="channelsLink"
:topicsLink="topicsLink"
class="align-breadcrumbs"
/>

<div class="title-class">
<h5>
<KLabeledIcon :label="content.kind">
<template #icon>
<LearningActivityIcon :kind="content.learning_activities" />
</template>
<template>
{{ content.title }}
</template>
</KLabeledIcon>
</h5>
</div>

<ContentArea
:header="questionLabel(selectedQuestionIndex)"
:selectedQuestion="selectedQuestion"
:content="content"
:isExercise="isExercise"
/>

<SlotTruncator
v-if="description"
:maxHeight="75"
:showViewMore="true"
>
<!-- eslint-disable vue/no-v-html -->
<p
dir="auto"
v-html="description"
></p>
<!-- eslint-enable -->
</SlotTruncator>

<HeaderTable class="license-detail-style">
<HeaderTableRow :keyText="coreString('suggestedTime')">
<template #value>
{{ content.duration ? getTime(content.duration) : notAvailableLabel$() }}
</template>
</HeaderTableRow>

<HeaderTableRow :keyText="licenseDataHeader$()">
<template #value>
{{ licenseName }}
</template>
</HeaderTableRow>

<HeaderTableRow :keyText="copyrightHolderDataHeader$()">
<template #value>
{{ content.license_owner }}
</template>
</HeaderTableRow>
</HeaderTable>
</div>

</template>


<script>

import commonCoreStrings from 'kolibri/uiText/commonCoreStrings';
import { searchAndFilterStrings } from 'kolibri-common/strings/searchAndFilterStrings';
import { licenseLongName } from 'kolibri/uiText/licenses';
import markdownIt from 'markdown-it';
import { ContentNodeKinds } from 'kolibri/constants';
import LearningActivityIcon from 'kolibri-common/components/ResourceDisplayAndSearch/LearningActivityIcon.vue';
import SlotTruncator from 'kolibri-common/components/SlotTruncator';
import ContentArea from '../../../../../LessonSelectionContentPreviewPage/LessonContentPreview/ContentArea.vue';
import commonCoach from '../../../../../../common';
import { PageNames } from '../../../../../../../constants/index';
import ResourceSelectionBreadcrumbs from '../../../../../LessonResourceSelectionPage/SearchTools/ResourceSelectionBreadcrumbs.vue';

export default {
name: 'PreviewContent',
components: {
ContentArea,
SlotTruncator,
LearningActivityIcon,
ResourceSelectionBreadcrumbs,
},
mixins: [commonCoreStrings, commonCoach],
setup() {
const {
addText$,
copyrightHolderDataHeader$,
licenseDataHeader$,
addedIndicator$,
notAvailableLabel$,
} = searchAndFilterStrings;

return {
addText$,
licenseDataHeader$,
copyrightHolderDataHeader$,
addedIndicator$,
notAvailableLabel$,
};
},
props: {
currentContentNode: {
type: Object,
required: true,
},
ancestors: {
type: Array,
required: false,
default: () => [],
},
isSelected: {
type: Boolean,
required: true,
},
},
data() {
return {
selectedQuestionIndex: 0,
};
},
computed: {
channelsLink() {
return {
name: PageNames.LESSON_SELECT_RESOURCES_INDEX,
};
},
isExercise() {
return this.content.kind === ContentNodeKinds.EXERCISE;
},
selectedQuestion() {
if (this.isExercise) {
return this.questions[this.selectedQuestionIndex];
}
return '';
},
licenseName() {
return licenseLongName(this.content.license_name);
},
content() {
return this.currentContentNode;
},
description() {
if (this.content && this.content.description) {
const md = new markdownIt('zero', { breaks: true });
return md.render(this.content.description);
}

return undefined;
},
},
methods: {
topicsLink(topicId) {
const { params, query } = this.$route;

return {
name: PageNames.LESSON_SELECT_RESOURCES_TOPIC_TREE,
params: params,
query: {
...query,
topicId,
},
};
},
questionLabel(questionIndex) {
if (!this.isExercise) {
return '';
}
const questionNumber = questionIndex + 1;
return this.coreString('questionNumberLabel', { questionNumber });
},
addResource() {
this.$emit('addResource', this.content);
},
removeResource() {
this.$emit('removeResource', this.content);
},
getTime(seconds) {
return this.$tr('minutes', { value: Math.floor(seconds / 60) });
},
},
$trs: {
minutes: {
message: '{value, number, integer} {value, plural, one {minute} other {minutes}}',
context:
'Indicates time spent by learner on a specific activity. Only translate minute/minutes.',
},
},
};

</script>


<style lang="scss" scoped>

.license-detail-style {
margin: 30px 0 32px;
}

/deep/ .content-renderer {
position: relative;
top: -40px;
max-height: 500px;
}

.channel-header {
display: flex;
align-items: center;
justify-content: space-between;
}

.title-class {
position: relative;
top: -30px;
}

.align-breadcrumbs {
position: relative;
top: -35px;
}

</style>
Loading
Loading