-
Notifications
You must be signed in to change notification settings - Fork 699
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
Lesson resources selection #12895
base: develop
Are you sure you want to change the base?
Lesson resources selection #12895
Changes from all commits
5e369ee
176fd3c
ed84a9a
27d94c3
2f36020
83c69e8
b05abc4
c4601a3
fb0ec8c
aef7e14
148c5b9
5a314b1
2f41854
51b3726
7da5057
43adff4
dd68dd1
4d1d90c
b34a28b
10365f2
e8f90f3
b7fa010
03301ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import { ref, computed } from 'vue'; | ||
|
||
/** | ||
* A composable for managing fetch operations with optional methods for additional data fetching. | ||
* | ||
* @param {Object} options **Required** Configuration options for the fetch operation. | ||
* @param {(...args) => Promise<any>} options.fetchMethod **Required** Function to fetch the | ||
* initial data. | ||
* * Should return a Promise resolving to the fetched data or a `{ results: any[], more: any }` | ||
* object. The "results" property should be the fetched data and the "more" property should be | ||
* the next "more" object to use in the fetchMore method. | ||
* | ||
* Example: | ||
* ```js | ||
* const { data, loading, error, fetchData } = useFetch({ | ||
* fetchMethod: () => ContentNodeResource.fetchBookmarks() | ||
* }) | ||
* ``` | ||
* | ||
* | ||
* @param {(more, ...args) => Promise<any>} [options.fetchMoreMethod] Function to fetch more data. | ||
* * This function receives a "moreParams" object as its first argument. This moreParams object is | ||
* from the "more" property of the response from the previous fetch to fetch more data. | ||
* * Should return a Promise resolving to { results: any[], more: any }. The "results" property | ||
* should be the fetched data and the "more" property should be the next "moreParams" object to | ||
* use in the next fetchMore method. | ||
* * FetchMore just works if the fetched data is an array | ||
* | ||
* Example: | ||
* | ||
* ```js | ||
* const { data, loading, error, fetchData } = useFetch({ | ||
* fetchMethod: () => ContentNodeResource.fetchBookmarks(), | ||
* fetchMoreMethod: moreParams => ContentNodeResource.fetchBookmarks(moreParams) | ||
* }) | ||
* ``` | ||
* | ||
* | ||
* @typedef {Object} FetchObject | ||
* @property {any} data The main fetched data. | ||
* @property {Object} error Error object if a fetch data failed. | ||
* @property {any} count The count of the fetched data. E.g., the total number of items. | ||
* @property {boolean} loading Data loading state. This loading doesnt reflect the loading when | ||
* fetching more data. refer to `loadingMore` for that. | ||
* @property {boolean} loadingMore Loading state when fetching more data. This is different from | ||
* `loading` which is for the main data fetch. | ||
* @property {boolean} hasMore A computed property to check if there is more data to fetch. | ||
* @property {(...args) => Promise<void>} fetchData A method to manually trigger the main fetch. | ||
* @property {(...args) => Promise<void>} fetchMore A method to manually trigger fetch more data. | ||
* | ||
* @returns {FetchObject} An object with properties and methods for managing the fetch process. | ||
*/ | ||
export default function useFetch(options) { | ||
const { fetchMethod, fetchMoreMethod } = options || {}; | ||
|
||
const loading = ref(false); | ||
const data = ref(null); | ||
const error = ref(null); | ||
const moreParams = ref(null); | ||
const count = ref(null); | ||
const loadingMore = ref(false); | ||
|
||
// useFetch metadata to manage synchronization of fetches | ||
const _fetchCount = ref(0); | ||
|
||
const hasMore = computed(() => moreParams.value != null); | ||
|
||
const _setData = (response, loadingMore) => { | ||
const responseData = fetchMoreMethod ? response.results : response; | ||
|
||
/** | ||
* For now, loading more just works if the data is an array. | ||
*/ | ||
if (loadingMore && Array.isArray(data.value) && Array.isArray(responseData)) { | ||
data.value = [...data.value, ...responseData]; | ||
} else if (!loadingMore) { | ||
data.value = responseData; | ||
} | ||
|
||
moreParams.value = response.more || null; | ||
count.value = response.count || null; | ||
}; | ||
|
||
const fetchData = async (...args) => { | ||
loading.value = true; | ||
loadingMore.value = false; // Reset loading more state | ||
error.value = null; | ||
_fetchCount.value += 1; | ||
const currentFetchCount = _fetchCount.value; | ||
|
||
// If the fetch count has changed, it means that a new fetch has been triggered | ||
// and this fetch is no longer relevant | ||
const newFetchHasStarted = () => currentFetchCount !== _fetchCount.value; | ||
|
||
try { | ||
const response = await fetchMethod(...args); | ||
if (newFetchHasStarted()) { | ||
return; | ||
} | ||
_setData(response); | ||
} catch (err) { | ||
if (newFetchHasStarted()) { | ||
return; | ||
} | ||
error.value = err; | ||
} | ||
|
||
loading.value = false; | ||
}; | ||
|
||
const fetchMore = async (...args) => { | ||
if (!moreParams.value || !fetchMoreMethod || loadingMore.value) { | ||
return; | ||
} | ||
|
||
loadingMore.value = true; | ||
error.value = null; | ||
const currentFetchCount = _fetchCount.value; | ||
|
||
// If the fetch count or fetch more count has changed, it means that a new fetch has been | ||
// triggered and this fetch is no longer relevant | ||
const newFetchHasStarted = () => currentFetchCount !== _fetchCount.value; | ||
|
||
try { | ||
const response = await fetchMoreMethod(moreParams.value, ...args); | ||
if (newFetchHasStarted()) { | ||
return; | ||
} | ||
_setData(response, true); | ||
} catch (err) { | ||
if (newFetchHasStarted()) { | ||
return; | ||
} | ||
error.value = err; | ||
} | ||
|
||
loadingMore.value = false; | ||
}; | ||
|
||
return { | ||
data, | ||
error, | ||
count, | ||
loading, | ||
hasMore, | ||
loadingMore, | ||
fetchData, | ||
fetchMore, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import uniqBy from 'lodash/uniqBy'; | ||
import { ref, computed, getCurrentInstance, watch } from 'vue'; | ||
import ContentNodeResource from 'kolibri-common/apiResources/ContentNodeResource'; | ||
import ChannelResource from 'kolibri-common/apiResources/ChannelResource'; | ||
import useFetch from './useFetch'; | ||
|
||
/** | ||
* @typedef {import('../../../../../../composables/useFetch').FetchObject} FetchObject | ||
*/ | ||
|
||
/** | ||
* Composable for managing the selection of resources within a topic tree. | ||
* This utility handles selection rules, manages fetch states for channels, bookmarks, | ||
* and topic trees, and offers methods to add, remove, or override selected resources. | ||
* | ||
* @typedef {Object} UseResourceSelectionResponse | ||
* @property {Object} topic Topic tree object, contains the information of the topic, | ||
* its ascendants and children. | ||
* Defined only if the `topicId` query in the route is set. | ||
* @property {boolean} loading Indicates whether the main topic tree, channels, and bookmarks | ||
* data are currently loading. This does not account for loading more data. For such cases, | ||
* use the fetch objects of each entity. | ||
* @property {FetchObject} channelsFetch Channels fetch object to manage the process of | ||
* fetching channels. We currently don't support fetching more channels. | ||
* @property {FetchObject} bookmarksFetch Bookmarks fetch object to manage the process of | ||
* fetching bookmarks. Fetching more bookmarks is supported. | ||
* @property {FetchObject} treeFetch Topic tree fetch object to manage the process of | ||
* fetching topic trees and their resources. Fetching more resources is supported. | ||
marcellamaki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @property {Array<(node: Object) => boolean>} selectionRules An array of functions that determine | ||
* whether a node can be selected. | ||
* @property {Array<Object>} selectedResources An array of currently selected resources. | ||
* @property {(resources: Array<Object>) => void} selectResources Adds the specified resources | ||
* to the `selectedResources` array. | ||
* @property {(resources: Array<Object>) => void} deselectResources Removes the specified resources | ||
* from the `selectedResources` array. | ||
* @property {(resources: Array<Object>) => void} setSelectedResources Replaces the current | ||
* `selectedResources` array with the provided resources array. | ||
* | ||
* @returns {UseResourceSelectionResponse} | ||
*/ | ||
export default function useResourceSelection() { | ||
const store = getCurrentInstance().proxy.$store; | ||
const route = computed(() => store.state.route); | ||
const topicId = computed(() => route.value.query.topicId); | ||
|
||
const selectionRules = ref([]); | ||
const selectedResources = ref([]); | ||
const topic = ref(null); | ||
|
||
const bookmarksFetch = useFetch({ | ||
fetchMethod: () => | ||
ContentNodeResource.fetchBookmarks({ | ||
params: { limit: 25, available: true }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think I've answered my own question and that you're talking about pagination There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From an API consumption perspective, is it confusing that the bookmarks fetching uses the Under the hood, this is a result of the Bookmarks contentnode API using Limit/Offset pagination, whereas all the others use a more cursor style pagination, I am just not sure this is helpful to the consumer :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think what's confusing is, at least to me, this isn't a criticism, @AlexVelezLl of your code -- I think you are in line with the existing usage. It's just something I have always sort of struggled with in terms of readability wherever we use this general pagination pattern. I just have to read and re-read things so many times to follow along. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the API perspective There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @marcellamaki Do you think that renaming this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, I think that would help - not required, but maybe a nice to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. super, just updated it to be called moreParams instead :) |
||
}), | ||
fetchMoreMethod: more => | ||
ContentNodeResource.fetchBookmarks({ | ||
params: more, | ||
}), | ||
}); | ||
|
||
const channelsFetch = useFetch({ | ||
fetchMethod: () => | ||
ChannelResource.fetchCollection({ | ||
getParams: { | ||
available: true, | ||
}, | ||
}), | ||
}); | ||
|
||
const fetchTree = async (params = {}) => { | ||
topic.value = await ContentNodeResource.fetchTree(params); | ||
return topic.value.children; | ||
}; | ||
|
||
const treeFetch = useFetch({ | ||
fetchMethod: () => fetchTree({ id: topicId.value, params: { include_coach_content: true } }), | ||
fetchMoreMethod: more => fetchTree(more), | ||
}); | ||
|
||
watch(topicId, () => { | ||
if (topicId.value) { | ||
treeFetch.fetchData(); | ||
} | ||
}); | ||
|
||
const loading = computed(() => { | ||
const sources = [bookmarksFetch, channelsFetch, treeFetch]; | ||
|
||
return sources.some(sourceFetch => sourceFetch.loading.value); | ||
}); | ||
|
||
const fetchInitialData = async () => { | ||
bookmarksFetch.fetchData(); | ||
channelsFetch.fetchData(); | ||
if (topicId.value) { | ||
treeFetch.fetchData(); | ||
} | ||
}; | ||
|
||
fetchInitialData(); | ||
|
||
const selectResources = (resources = []) => { | ||
if (!resources || !resources.length) { | ||
return; | ||
} | ||
if (resources.length === 1) { | ||
const [newResource] = resources; | ||
if (!selectedResources.value.find(res => res.id === newResource.id)) { | ||
selectedResources.value = [...selectedResources.value, newResource]; | ||
} | ||
} else { | ||
selectedResources.value = uniqBy([...selectedResources.value, ...resources], 'id'); | ||
} | ||
}; | ||
|
||
const deselectResources = (resources = []) => { | ||
if (!resources || !resources.length) { | ||
return; | ||
} | ||
selectedResources.value = selectedResources.value.filter(res => { | ||
return !resources.find(unselectedResource => unselectedResource.id === res.id); | ||
}); | ||
}; | ||
|
||
const setSelectedResources = (resources = []) => { | ||
selectedResources.value = resources; | ||
}; | ||
|
||
return { | ||
topic, | ||
loading, | ||
channelsFetch, | ||
bookmarksFetch, | ||
treeFetch, | ||
selectionRules, | ||
selectedResources, | ||
selectResources, | ||
deselectResources, | ||
setSelectedResources, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,8 +12,6 @@ | |
<slot></slot> | ||
</div> | ||
</AppBarPage> | ||
|
||
<router-view /> | ||
</NotificationsRoot> | ||
|
||
</template> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a guard here against loading twice, I think? If loading.value is already true, we shouldn't try to load again?