@iblai/data-layer 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +412 -2
- package/dist/index.esm.js +194 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +205 -1
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/src/features/courses/__tests__/completion.test.d.ts +1 -0
- package/dist/src/features/courses/__tests__/outline-fixture.d.ts +10 -0
- package/dist/src/features/courses/api-slice.d.ts +270 -0
- package/dist/src/features/courses/completion.d.ts +47 -0
- package/dist/src/features/courses/types.d.ts +94 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43735,6 +43735,187 @@ const perLearnerApiSlice = react.createApi({
|
|
|
43735
43735
|
});
|
|
43736
43736
|
const { useGetPerLearnerActivityQuery, useLazyGetPerLearnerActivityQuery, useGetPerLearnerInfoQuery, useLazyGetPerLearnerInfoQuery, useGetOverTimeActivityQuery, useLazyGetOverTimeActivityQuery, } = perLearnerApiSlice;
|
|
43737
43737
|
|
|
43738
|
+
const LEARNER_COMPLETION_PATH = '/api/catalog/enrollment/courses/completion/';
|
|
43739
|
+
const LEARNER_IDENTIFIER_KEYS = ['username', 'user_id', 'email'];
|
|
43740
|
+
const isPresent = (value) => value !== undefined && value !== null && String(value).trim() !== '';
|
|
43741
|
+
/**
|
|
43742
|
+
* Checks the args carry exactly one learner identifier plus `course_id` and
|
|
43743
|
+
* `platform_key`. Returns a reason string when they don't.
|
|
43744
|
+
*/
|
|
43745
|
+
function validateLearnerCompletionArgs(args) {
|
|
43746
|
+
const identifiers = LEARNER_IDENTIFIER_KEYS.filter((key) => isPresent(args[key]));
|
|
43747
|
+
if (identifiers.length !== 1) {
|
|
43748
|
+
return 'Exactly one of username, user_id or email is required.';
|
|
43749
|
+
}
|
|
43750
|
+
if (!isPresent(args.course_id))
|
|
43751
|
+
return 'course_id is required.';
|
|
43752
|
+
if (!isPresent(args.platform_key))
|
|
43753
|
+
return 'platform_key is required.';
|
|
43754
|
+
return null;
|
|
43755
|
+
}
|
|
43756
|
+
/**
|
|
43757
|
+
* Builds the completion URL for one `endpoint` selector. Only the learner
|
|
43758
|
+
* identifier, `course_id` and `platform_key` are sent; the DM forwards
|
|
43759
|
+
* nothing else to edX.
|
|
43760
|
+
*/
|
|
43761
|
+
function buildLearnerCompletionUrl(args, endpoint) {
|
|
43762
|
+
const params = new URLSearchParams();
|
|
43763
|
+
const record = args;
|
|
43764
|
+
for (const key of [...LEARNER_IDENTIFIER_KEYS, 'platform_key', 'course_id']) {
|
|
43765
|
+
if (isPresent(record[key]))
|
|
43766
|
+
params.set(key, String(record[key]));
|
|
43767
|
+
}
|
|
43768
|
+
params.set('endpoint', endpoint);
|
|
43769
|
+
return `${LEARNER_COMPLETION_PATH}?${params.toString()}`;
|
|
43770
|
+
}
|
|
43771
|
+
// 404 means "course not on tenant / learner not enrolled"; retrying won't
|
|
43772
|
+
// change that, and the outline call is heavy. 5xx keeps the default retries.
|
|
43773
|
+
const LEARNER_COMPLETION_MAX_RETRIES = 3;
|
|
43774
|
+
const learnerCompletionExtraOptions = {
|
|
43775
|
+
retryCondition: (error, _args, { attempt }) => attempt <= LEARNER_COMPLETION_MAX_RETRIES && (error === null || error === void 0 ? void 0 : error.status) !== 404,
|
|
43776
|
+
};
|
|
43777
|
+
/**
|
|
43778
|
+
* One completion request through `iblFetchBaseQuery` (DM token, token
|
|
43779
|
+
* refresh, registered error handlers). `iblFetchBaseQuery` throws on HTTP
|
|
43780
|
+
* errors, which RTK would turn into a SerializedError without the status;
|
|
43781
|
+
* catching here keeps `{ status, data }` so the UI can tell 403/404/502 apart.
|
|
43782
|
+
*/
|
|
43783
|
+
async function fetchLearnerCompletion(args, endpoint, api, extraOptions = {}) {
|
|
43784
|
+
const invalid = validateLearnerCompletionArgs(args);
|
|
43785
|
+
if (invalid) {
|
|
43786
|
+
return {
|
|
43787
|
+
error: {
|
|
43788
|
+
status: 'CUSTOM_ERROR',
|
|
43789
|
+
error: invalid,
|
|
43790
|
+
data: { kind: 'invalid_request', error: invalid },
|
|
43791
|
+
},
|
|
43792
|
+
};
|
|
43793
|
+
}
|
|
43794
|
+
try {
|
|
43795
|
+
const result = await iblFetchBaseQuery({
|
|
43796
|
+
url: buildLearnerCompletionUrl(args, endpoint),
|
|
43797
|
+
service: exports.SERVICES.DM,
|
|
43798
|
+
method: 'GET',
|
|
43799
|
+
}, api, { ...learnerCompletionExtraOptions, ...extraOptions });
|
|
43800
|
+
if ('error' in result && result.error) {
|
|
43801
|
+
return { error: result.error };
|
|
43802
|
+
}
|
|
43803
|
+
return { data: result.data };
|
|
43804
|
+
}
|
|
43805
|
+
catch (error) {
|
|
43806
|
+
return { error: error };
|
|
43807
|
+
}
|
|
43808
|
+
}
|
|
43809
|
+
const completionError = (kind, status) => ({ kind, status, retryable: kind === 'unavailable' });
|
|
43810
|
+
/**
|
|
43811
|
+
* Maps a failed completion query onto the states the UI shows. 403 and 404
|
|
43812
|
+
* are deliberately coarse on the backend (one body for every hidden case),
|
|
43813
|
+
* so don't try to read more into them.
|
|
43814
|
+
*/
|
|
43815
|
+
function normalizeLearnerCompletionError(error) {
|
|
43816
|
+
if (error === undefined || error === null)
|
|
43817
|
+
return null;
|
|
43818
|
+
if (typeof error !== 'object')
|
|
43819
|
+
return completionError('unknown');
|
|
43820
|
+
const { status, data } = error;
|
|
43821
|
+
if (status === 'CUSTOM_ERROR') {
|
|
43822
|
+
const kind = data === null || data === void 0 ? void 0 : data.kind;
|
|
43823
|
+
return completionError(kind === 'invalid_request' ? 'invalid_request' : 'unknown', status);
|
|
43824
|
+
}
|
|
43825
|
+
if (status === 'FETCH_ERROR' || status === 'TIMEOUT_ERROR') {
|
|
43826
|
+
return completionError('unavailable', status);
|
|
43827
|
+
}
|
|
43828
|
+
if (typeof status === 'number') {
|
|
43829
|
+
if (status === 400)
|
|
43830
|
+
return completionError('invalid_request', status);
|
|
43831
|
+
if (status === 401)
|
|
43832
|
+
return completionError('unauthorized', status);
|
|
43833
|
+
if (status === 403)
|
|
43834
|
+
return completionError('forbidden', status);
|
|
43835
|
+
if (status === 404)
|
|
43836
|
+
return completionError('not_enrolled', status);
|
|
43837
|
+
if (status === 502 || status === 503 || status === 504) {
|
|
43838
|
+
return completionError('unavailable', status);
|
|
43839
|
+
}
|
|
43840
|
+
return completionError('unknown', status);
|
|
43841
|
+
}
|
|
43842
|
+
return completionError('unknown');
|
|
43843
|
+
}
|
|
43844
|
+
/* ------------------------------------------------------------------------ */
|
|
43845
|
+
/* Outline helpers */
|
|
43846
|
+
/* ------------------------------------------------------------------------ */
|
|
43847
|
+
/** Fills the fields edX omits: `children ?? []`, `complete`/`resume_block ?? false`. */
|
|
43848
|
+
function normalizeOutline(block) {
|
|
43849
|
+
var _a, _b, _c;
|
|
43850
|
+
return {
|
|
43851
|
+
...block,
|
|
43852
|
+
children: ((_a = block.children) !== null && _a !== void 0 ? _a : []).map(normalizeOutline),
|
|
43853
|
+
complete: (_b = block.complete) !== null && _b !== void 0 ? _b : false,
|
|
43854
|
+
resume_block: (_c = block.resume_block) !== null && _c !== void 0 ? _c : false,
|
|
43855
|
+
};
|
|
43856
|
+
}
|
|
43857
|
+
/** Depth-first, course order. The root has depth 0 and `parentId` null. */
|
|
43858
|
+
function flattenOutline(root) {
|
|
43859
|
+
const entries = [];
|
|
43860
|
+
const visit = (block, depth, parentId, parentPath) => {
|
|
43861
|
+
var _a;
|
|
43862
|
+
const path = [...parentPath, block.display_name];
|
|
43863
|
+
entries.push({ block, depth, parentId, path });
|
|
43864
|
+
for (const child of (_a = block.children) !== null && _a !== void 0 ? _a : [])
|
|
43865
|
+
visit(child, depth + 1, block.id, path);
|
|
43866
|
+
};
|
|
43867
|
+
visit(root, 0, null, []);
|
|
43868
|
+
return entries;
|
|
43869
|
+
}
|
|
43870
|
+
/**
|
|
43871
|
+
* Follows `resume_block: true` from the root to the deepest marked node (the
|
|
43872
|
+
* last-visited leaf). Returns null when the learner hasn't visited anything.
|
|
43873
|
+
*/
|
|
43874
|
+
function findResumeBlock(root) {
|
|
43875
|
+
var _a, _b;
|
|
43876
|
+
// The root may or may not carry the flag itself; either way descend from it.
|
|
43877
|
+
let current = root.resume_block ? root : null;
|
|
43878
|
+
let candidates = (_a = root.children) !== null && _a !== void 0 ? _a : [];
|
|
43879
|
+
for (;;) {
|
|
43880
|
+
const next = candidates.find((block) => block.resume_block);
|
|
43881
|
+
if (!next)
|
|
43882
|
+
return current;
|
|
43883
|
+
current = next;
|
|
43884
|
+
candidates = (_b = next.children) !== null && _b !== void 0 ? _b : [];
|
|
43885
|
+
}
|
|
43886
|
+
}
|
|
43887
|
+
/** Counts leaves (blocks without children) by `type`. */
|
|
43888
|
+
function countByType(root) {
|
|
43889
|
+
var _a;
|
|
43890
|
+
const counts = {};
|
|
43891
|
+
for (const { block, depth } of flattenOutline(root)) {
|
|
43892
|
+
if (depth === 0 || block.children !== undefined)
|
|
43893
|
+
continue;
|
|
43894
|
+
// Childless containers (an empty vertical) aren't content.
|
|
43895
|
+
if (block.type === 'chapter' || block.type === 'sequential' || block.type === 'vertical') {
|
|
43896
|
+
continue;
|
|
43897
|
+
}
|
|
43898
|
+
counts[block.type] = ((_a = counts[block.type]) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
43899
|
+
}
|
|
43900
|
+
return counts;
|
|
43901
|
+
}
|
|
43902
|
+
/** Top-level sections (chapters) for a summary view, in course order. */
|
|
43903
|
+
function getSections(root) {
|
|
43904
|
+
var _a;
|
|
43905
|
+
return ((_a = root.children) !== null && _a !== void 0 ? _a : [])
|
|
43906
|
+
.filter((block) => block.type === 'chapter')
|
|
43907
|
+
.map((block) => {
|
|
43908
|
+
var _a;
|
|
43909
|
+
return ({
|
|
43910
|
+
id: block.id,
|
|
43911
|
+
display_name: block.display_name,
|
|
43912
|
+
completion_percentage: block.completion_percentage,
|
|
43913
|
+
complete: (_a = block.complete) !== null && _a !== void 0 ? _a : false,
|
|
43914
|
+
block,
|
|
43915
|
+
});
|
|
43916
|
+
});
|
|
43917
|
+
}
|
|
43918
|
+
|
|
43738
43919
|
const coursesApiSlice = react.createApi({
|
|
43739
43920
|
reducerPath: 'coursesApiSlice',
|
|
43740
43921
|
baseQuery: iblFetchBaseQuery,
|
|
@@ -43776,9 +43957,20 @@ const coursesApiSlice = react.createApi({
|
|
|
43776
43957
|
},
|
|
43777
43958
|
providesTags: ['user-assigned-courses'],
|
|
43778
43959
|
}),
|
|
43960
|
+
/**
|
|
43961
|
+
* Per-learner course outline with block-level completion. Name the learner
|
|
43962
|
+
* with exactly one of `username` / `user_id` / `email`, even for one's own
|
|
43963
|
+
* data. Errors keep their HTTP status; map them with
|
|
43964
|
+
* `normalizeLearnerCompletionError`.
|
|
43965
|
+
*/
|
|
43966
|
+
getLearnerCourseOutline: builder.query({
|
|
43967
|
+
queryFn: (args, api, extraOptions) => fetchLearnerCompletion(args, 'outline', api, extraOptions),
|
|
43968
|
+
// The outline is heavy on large courses; keep it around briefly.
|
|
43969
|
+
keepUnusedDataFor: 120,
|
|
43970
|
+
}),
|
|
43779
43971
|
}),
|
|
43780
43972
|
});
|
|
43781
|
-
const { useGetUserEnrolledCoursesQuery, useLazyGetUserEnrolledCoursesQuery, useGetUserAssignedCoursesQuery, useLazyGetUserAssignedCoursesQuery, } = coursesApiSlice;
|
|
43973
|
+
const { useGetUserEnrolledCoursesQuery, useLazyGetUserEnrolledCoursesQuery, useGetUserAssignedCoursesQuery, useLazyGetUserAssignedCoursesQuery, useGetLearnerCourseOutlineQuery, useLazyGetLearnerCourseOutlineQuery, } = coursesApiSlice;
|
|
43782
43974
|
|
|
43783
43975
|
const courseMetadataApiSlice = react.createApi({
|
|
43784
43976
|
reducerPath: 'courseMetadataApiSlice',
|
|
@@ -49211,6 +49403,7 @@ exports.HOSTING_ENDPOINTS = HOSTING_ENDPOINTS;
|
|
|
49211
49403
|
exports.HOSTING_QUERY_KEYS = HOSTING_QUERY_KEYS;
|
|
49212
49404
|
exports.HOSTING_REDUCER_PATH = HOSTING_REDUCER_PATH;
|
|
49213
49405
|
exports.IblDataLayer = IblDataLayer;
|
|
49406
|
+
exports.LEARNER_COMPLETION_PATH = LEARNER_COMPLETION_PATH;
|
|
49214
49407
|
exports.LOGO_ENDPOINTS = LOGO_ENDPOINTS;
|
|
49215
49408
|
exports.MCP_ENDPOINTS = MCP_ENDPOINTS;
|
|
49216
49409
|
exports.MCP_REDUCER_PATH = MCP_REDUCER_PATH;
|
|
@@ -49293,6 +49486,7 @@ exports.authApiSlice = authApiSlice;
|
|
|
49293
49486
|
exports.billingApiSlice = billingApiSlice;
|
|
49294
49487
|
exports.billingCustomSlice = billingCustomSlice;
|
|
49295
49488
|
exports.buildGlobalSearchUrl = buildGlobalSearchUrl;
|
|
49489
|
+
exports.buildLearnerCompletionUrl = buildLearnerCompletionUrl;
|
|
49296
49490
|
exports.buildRecentMessagesUrl = buildRecentMessagesUrl;
|
|
49297
49491
|
exports.buildSessionRetrieveUrl = buildSessionRetrieveUrl;
|
|
49298
49492
|
exports.buildSharedSessionRetrieveUrl = buildSharedSessionRetrieveUrl;
|
|
@@ -49309,6 +49503,7 @@ exports.clawApiSlice = clawApiSlice;
|
|
|
49309
49503
|
exports.coreApiSlice = coreApiSlice;
|
|
49310
49504
|
exports.coreCustomApiSlice = coreCustomApiSlice;
|
|
49311
49505
|
exports.coreFakeCustomPublicImageAssetApiSlice = coreFakeCustomPublicImageAssetApiSlice;
|
|
49506
|
+
exports.countByType = countByType;
|
|
49312
49507
|
exports.courseMetadataApiSlice = courseMetadataApiSlice;
|
|
49313
49508
|
exports.coursesApiSlice = coursesApiSlice;
|
|
49314
49509
|
exports.credentialsApiSlice = credentialsApiSlice;
|
|
@@ -49323,12 +49518,16 @@ exports.evalTotalPages = evalTotalPages;
|
|
|
49323
49518
|
exports.evaluationsApiSlice = evaluationsApiSlice;
|
|
49324
49519
|
exports.fakeBaseQueryWithLogging = fakeBaseQueryWithLogging;
|
|
49325
49520
|
exports.featureTags = featureTags;
|
|
49521
|
+
exports.fetchLearnerCompletion = fetchLearnerCompletion;
|
|
49326
49522
|
exports.fetchUserLlmUsage = fetchUserLlmUsage;
|
|
49327
49523
|
exports.filterCourseUserRoles = filterCourseUserRoles;
|
|
49328
49524
|
exports.filterGlobalUserRoles = filterGlobalUserRoles;
|
|
49329
49525
|
exports.filterOrgUserRoles = filterOrgUserRoles;
|
|
49330
49526
|
exports.filterSlashSkills = filterSlashSkills;
|
|
49527
|
+
exports.findResumeBlock = findResumeBlock;
|
|
49528
|
+
exports.flattenOutline = flattenOutline;
|
|
49331
49529
|
exports.getNextRecentMessagesPageParam = getNextRecentMessagesPageParam;
|
|
49530
|
+
exports.getSections = getSections;
|
|
49332
49531
|
exports.getSpendCapExceededError = getSpendCapExceededError;
|
|
49333
49532
|
exports.getUserRoleScope = getUserRoleScope;
|
|
49334
49533
|
exports.gradingApiSlice = gradingApiSlice;
|
|
@@ -49352,6 +49551,8 @@ exports.mentorReducer = mentorReducer;
|
|
|
49352
49551
|
exports.moderationLogsApiReducer = moderationLogsApiReducer;
|
|
49353
49552
|
exports.moderationLogsApiSlice = moderationLogsApiSlice;
|
|
49354
49553
|
exports.monetizationApiSlice = monetizationApiSlice;
|
|
49554
|
+
exports.normalizeLearnerCompletionError = normalizeLearnerCompletionError;
|
|
49555
|
+
exports.normalizeOutline = normalizeOutline;
|
|
49355
49556
|
exports.normalizeUserLlmUsageError = normalizeUserLlmUsageError;
|
|
49356
49557
|
exports.notificationsApiSlice = notificationsApiSlice;
|
|
49357
49558
|
exports.notificationsCustomApiSlice = notificationsCustomApiSlice;
|
|
@@ -49646,6 +49847,7 @@ exports.useGetIntegrationCredentialsSchemaQuery = useGetIntegrationCredentialsSc
|
|
|
49646
49847
|
exports.useGetIssuersQuery = useGetIssuersQuery;
|
|
49647
49848
|
exports.useGetItemSubscriptionQuery = useGetItemSubscriptionQuery;
|
|
49648
49849
|
exports.useGetLLMCredentialsQuery = useGetLLMCredentialsQuery;
|
|
49850
|
+
exports.useGetLearnerCourseOutlineQuery = useGetLearnerCourseOutlineQuery;
|
|
49649
49851
|
exports.useGetLearnerDetailsQuery = useGetLearnerDetailsQuery;
|
|
49650
49852
|
exports.useGetLlmUsageMetricsQuery = useGetLlmUsageMetricsQuery;
|
|
49651
49853
|
exports.useGetLlmUsageObservationsQuery = useGetLlmUsageObservationsQuery;
|
|
@@ -49911,6 +50113,7 @@ exports.useLazyGetIntegrationCredentialsSchemaQuery = useLazyGetIntegrationCrede
|
|
|
49911
50113
|
exports.useLazyGetIssuersQuery = useLazyGetIssuersQuery;
|
|
49912
50114
|
exports.useLazyGetItemSubscriptionQuery = useLazyGetItemSubscriptionQuery;
|
|
49913
50115
|
exports.useLazyGetLLMCredentialsQuery = useLazyGetLLMCredentialsQuery;
|
|
50116
|
+
exports.useLazyGetLearnerCourseOutlineQuery = useLazyGetLearnerCourseOutlineQuery;
|
|
49914
50117
|
exports.useLazyGetLlmsQuery = useLazyGetLlmsQuery;
|
|
49915
50118
|
exports.useLazyGetLtiKeyDetailQuery = useLazyGetLtiKeyDetailQuery;
|
|
49916
50119
|
exports.useLazyGetMCPServerQuery = useLazyGetMCPServerQuery;
|
|
@@ -50234,6 +50437,7 @@ exports.userApiSlice = userApiSlice;
|
|
|
50234
50437
|
exports.userChatHistoryApiSlice = userChatHistoryApiSlice;
|
|
50235
50438
|
exports.userInvitationsApiSlice = userInvitationsApiSlice;
|
|
50236
50439
|
exports.userLlmUsageApiSlice = userLlmUsageApiSlice;
|
|
50440
|
+
exports.validateLearnerCompletionArgs = validateLearnerCompletionArgs;
|
|
50237
50441
|
exports.validateUserLlmUsageParams = validateUserLlmUsageParams;
|
|
50238
50442
|
exports.watchedGroupsApiSlice = watchedGroupsApiSlice;
|
|
50239
50443
|
exports.workflowsApiSlice = workflowsApiSlice;
|