Skip to content

Commit 1347903

Browse files
authored
Merge pull request #20 from open-craft/agrendalath/bb-9902-remove-required-completion
feat: remove required completion scaling
2 parents 8505580 + cb01b4c commit 1347903

File tree

11 files changed

+87
-54
lines changed

11 files changed

+87
-54
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@open-craft/frontend-app-learning-paths",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "Frontend application template",
55
"repository": {
66
"type": "git",

src/learningpath/CourseCard.jsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,14 @@ export const CourseCard = ({
129129
<Card.Section className="pt-4 pt-md-1 pb-1"><h3>{name}</h3></Card.Section>
130130
<Card.Section className="pt-1 pb-1">
131131
{status.toLowerCase() === 'in progress' && !!statusVariant && (
132-
<ProgressBar
133-
now={progressBarPercent}
134-
label={`${progressBarPercent}%`}
135-
variant="primary"
136-
/>
132+
<>
133+
<ProgressBar
134+
now={progressBarPercent}
135+
label={`${progressBarPercent}%`}
136+
variant="primary"
137+
/>
138+
<div className="x-small text-right pt-1">content completed</div>
139+
</>
137140
)}
138141
</Card.Section>
139142
<Card.Footer orientation="horizontal" className="pt-3 pb-3 justify-content-between">

src/learningpath/CourseDetails.jsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
Close,
2222
ChevronLeft,
2323
} from '@openedx/paragon/icons';
24-
import { useCourseDetail, useOrganizations } from './data/queries';
24+
import { useCourseDetail, useOrganizations, useCredentialConfiguration } from './data/queries';
2525
import { buildAssetUrl, replaceStaticAssetReferences } from '../util/assetUrl';
2626
import { buildCourseHomeUrl } from './utils';
2727
import { useScreenSize } from '../hooks/useScreenSize';
@@ -66,6 +66,9 @@ const CourseDetailContent = ({
6666
logo: organizations[org]?.logo,
6767
}), [organizations, org]);
6868

69+
const { data: credentialConfig } = useCredentialConfiguration(activeCourseKey);
70+
const hasCertificate = credentialConfig?.hasCredentials;
71+
6972
return (
7073
<>
7174
<div className="hero">
@@ -126,13 +129,15 @@ const CourseDetailContent = ({
126129
</div>
127130
</div>
128131
)}
129-
<div className="d-flex align-items-center">
130-
<Icon src={Award} className="mr-4 mb-3.5" />
131-
<div>
132-
<p className="mb-0 font-weight-bold">Certificate</p>
133-
<p className="mb-0 text-muted">Earn a certificate</p>
132+
{hasCertificate && (
133+
<div className="d-flex align-items-center">
134+
<Icon src={Award} className="mr-4 mb-3.5" />
135+
<div>
136+
<p className="mb-0 font-weight-bold">Certificate</p>
137+
<p className="mb-0 text-muted">Earn a certificate</p>
138+
</div>
134139
</div>
135-
</div>
140+
)}
136141
{duration && (
137142
<div className="d-flex align-items-center">
138143
<Icon src={Calendar} className="mr-4 mb-3.5" />

src/learningpath/Dashboard.jsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ const Dashboard = () => {
143143

144144
// Get only the organizations that are present in the user's items.
145145
const availableOrganizations = useMemo(() => {
146-
if (!organizations || !items.length) { return {}; }
146+
if (!organizations || !items.length) { return []; }
147147

148148
const availableOrgKeys = new Set();
149149
items.forEach(item => {
@@ -152,14 +152,9 @@ const Dashboard = () => {
152152
}
153153
});
154154

155-
const filteredOrgs = {};
156-
availableOrgKeys.forEach(orgKey => {
157-
if (organizations[orgKey]) {
158-
filteredOrgs[orgKey] = organizations[orgKey];
159-
}
160-
});
161-
162-
return filteredOrgs;
155+
return Array.from(availableOrgKeys, orgKey => organizations[orgKey])
156+
.filter(Boolean)
157+
.sort((a, b) => (a.name || a.shortName).localeCompare(b.name || b.shortName));
163158
}, [organizations, items]);
164159

165160
const activeFiltersCount = useMemo(

src/learningpath/FilterPanel.jsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const FilterPanel = ({
104104
</div>
105105

106106
{/* Organization Checkboxes */}
107-
{organizations && Object.keys(organizations).length > 0 && (
107+
{organizations && organizations.length > 0 && (
108108
<div className="my-3">
109109
<Form.Group>
110110
<Form.Label className="h4 my-3">Program Type</Form.Label>
@@ -113,9 +113,9 @@ const FilterPanel = ({
113113
onChange={e => onChangeOrg(e.target.value, e.target.checked)}
114114
value={selectedOrgs}
115115
>
116-
{Object.entries(organizations).map(([shortName, org]) => (
117-
<Form.Checkbox key={shortName} value={shortName} className="font-weight-light">
118-
{org.name || shortName}
116+
{organizations.map(org => (
117+
<Form.Checkbox key={org.shortName} value={org.shortName} className="font-weight-light">
118+
{org.name || org.shortName}
119119
</Form.Checkbox>
120120
))}
121121
</Form.CheckboxSet>
@@ -142,10 +142,10 @@ FilterPanel.propTypes = {
142142
onChangeDateStatus: PropTypes.func.isRequired,
143143
selectedOrgs: PropTypes.arrayOf(PropTypes.string).isRequired,
144144
onChangeOrg: PropTypes.func.isRequired,
145-
organizations: PropTypes.objectOf(
145+
organizations: PropTypes.arrayOf(
146146
PropTypes.shape({
147-
name: PropTypes.string,
148-
shortName: PropTypes.string,
147+
name: PropTypes.string.isRequired,
148+
shortName: PropTypes.string.isRequired,
149149
}),
150150
).isRequired,
151151
onClose: PropTypes.func.isRequired,

src/learningpath/LearningPathCard.jsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const LearningPathCard = ({ learningPath, showFilters = false }) => {
9797
logo: organizations[org]?.logo,
9898
}), [organizations, org]);
9999

100-
const progressBarPercent = percent ? +percent.toFixed(1) : '0.0';
100+
const progressBarPercent = useMemo(() => +(percent * 100).toFixed(1), [percent]);
101101

102102
return (
103103
<Card orientation={orientation} className={`lp-card ${orientation}`} onMouseEnter={handleMouseEnter}>
@@ -117,11 +117,14 @@ const LearningPathCard = ({ learningPath, showFilters = false }) => {
117117
<Card.Section className="pt-1 pb-1 card-subtitle text-muted">{subtitleLine}</Card.Section>
118118
<Card.Section className="pt-1 pb-1">
119119
{status.toLowerCase() === 'in progress' && !!statusVariant && (
120-
<ProgressBar
121-
now={progressBarPercent}
122-
label={`${progressBarPercent}%`}
123-
variant="primary"
124-
/>
120+
<>
121+
<ProgressBar
122+
now={progressBarPercent}
123+
label={`${progressBarPercent}%`}
124+
variant="primary"
125+
/>
126+
<div className="x-small text-right pt-1">content completed</div>
127+
</>
125128
)}
126129
</Card.Section>
127130
<Card.Footer orientation="horizontal" className="pt-3 pb-3 justify-content-between">

src/learningpath/LearningPathDetails.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ const LearningPathDetailPage = () => {
210210
<Icon src={Award} className="mr-4 mb-3.5" />
211211
<div>
212212
<p className="mb-0 font-weight-bold">Certificate</p>
213-
<p className="mb-0 text-muted">Courses include certification</p>
213+
<p className="mb-0 text-muted">Awarded upon completing the learning path</p>
214214
</div>
215215
</div>
216216
<div className="d-flex">

src/learningpath/data/api.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,18 @@ export async function fetchOrganizations() {
183183
logo: org.logo,
184184
})));
185185
}
186+
187+
export async function fetchCredentialConfiguration(learningContextKey) {
188+
const client = getAuthenticatedHttpClient();
189+
try {
190+
const response = await client.get(
191+
`${getConfig().LMS_BASE_URL}/api/learning_credentials/v1/configured/${encodeURIComponent(learningContextKey)}/`,
192+
);
193+
return camelCaseObject(response.data);
194+
} catch (error) {
195+
return {
196+
hasCredentials: false,
197+
credentialCount: 0,
198+
};
199+
}
200+
}

src/learningpath/data/queries.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as api from './api';
44
import {
55
addCompletionStatus,
66
addLearningPaths,
7+
calculateCompletionStatus,
78
createCompletionsMap,
89
createCourseToLearningPathsMap,
910
} from './dataUtils';
@@ -19,6 +20,7 @@ export const QUERY_KEYS = {
1920
COURSE_COMPLETION: (courseId) => ['courseCompletion', courseId],
2021
COURSE_ENROLLMENT_STATUS: (courseId) => ['courseEnrollmentStatus', courseId],
2122
ORGANIZATIONS: ['organizations'],
23+
CREDENTIAL_CONFIGURATION: (learningContextKey) => ['credentialConfiguration', learningContextKey],
2224
};
2325

2426
// Stale time configurations
@@ -33,6 +35,7 @@ export const STALE_TIMES = {
3335
COMPLETIONS: 60 * 1000, // 1 minute
3436

3537
ORGANIZATIONS: 60 * 60 * 1000, // 1 hour
38+
CREDENTIALS: 5 * 60 * 1000, // 5 minutes
3639
};
3740

3841
// Learning Paths Queries
@@ -72,22 +75,8 @@ export const useLearningPaths = () => {
7275
return sum + (completion?.percent ?? 0);
7376
}, 0);
7477

75-
const progress = totalCompletion / totalCourses;
76-
const requiredCompletion = lp.requiredCompletion || 0;
77-
78-
let status = 'In progress';
79-
if (progress === 0) {
80-
status = 'Not started';
81-
} else if (progress >= requiredCompletion) {
82-
status = 'Completed';
83-
}
84-
85-
let percent = 0;
86-
if (requiredCompletion > 0) {
87-
percent = Math.round((progress / requiredCompletion) * 100);
88-
} else {
89-
percent = Math.round(progress * 100);
90-
}
78+
const percent = totalCompletion / totalCourses;
79+
const { status } = calculateCompletionStatus(percent);
9180

9281
let minDate = null;
9382
let maxDate = null;
@@ -292,6 +281,12 @@ export const useCourseDetail = (courseKey) => {
292281
queryFn: api.fetchAllCourseCompletions,
293282
});
294283

284+
queryClient.prefetchQuery({
285+
queryKey: QUERY_KEYS.CREDENTIAL_CONFIGURATION(courseKey),
286+
queryFn: () => api.fetchCredentialConfiguration(courseKey),
287+
staleTime: STALE_TIMES.CREDENTIALS,
288+
});
289+
295290
const completions = queryClient.getQueryData(QUERY_KEYS.COURSE_COMPLETIONS) || {};
296291
const completionsMap = createCompletionsMap(completions);
297292

@@ -324,6 +319,12 @@ export const usePrefetchCourseDetail = (courseId) => {
324319
queryFn: () => api.fetchCourseCompletion(courseId),
325320
staleTime: STALE_TIMES.COMPLETIONS,
326321
});
322+
323+
queryClient.prefetchQuery({
324+
queryKey: QUERY_KEYS.CREDENTIAL_CONFIGURATION(courseId),
325+
queryFn: () => api.fetchCredentialConfiguration(courseId),
326+
staleTime: STALE_TIMES.CREDENTIALS,
327+
});
327328
} catch (error) {
328329
// eslint-disable-next-line no-console
329330
console.error('Error prefetching course data:', error);
@@ -399,3 +400,10 @@ export const useOrganizations = () => useQuery({
399400
},
400401
staleTime: STALE_TIMES.ORGANIZATIONS,
401402
});
403+
404+
export const useCredentialConfiguration = (learningContextKey) => useQuery({
405+
queryKey: QUERY_KEYS.CREDENTIAL_CONFIGURATION(learningContextKey),
406+
queryFn: () => api.fetchCredentialConfiguration(learningContextKey),
407+
enabled: !!learningContextKey,
408+
staleTime: STALE_TIMES.CREDENTIALS,
409+
});

0 commit comments

Comments
 (0)