@openedx/plugin-sample 0.0.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.
Files changed (2) hide show
  1. package/README.md +610 -0
  2. package/package.json +20 -0
package/README.md ADDED
@@ -0,0 +1,610 @@
1
+ # Frontend Plugin Implementation Guide
2
+
3
+ This directory contains a React component that demonstrates how to customize Open edX micro-frontends (MFEs) using the Frontend Plugin Framework. The plugin replaces the default course list in the learner dashboard with a custom implementation that includes course archiving functionality.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Frontend Plugin Framework](#frontend-plugin-framework)
9
+ - [CourseList Component Example](#courselist-component-example)
10
+ - [Slot Integration Patterns](#slot-integration-patterns)
11
+ - [API Integration](#api-integration)
12
+ - [Development Workflow](#development-workflow)
13
+ - [Deployment Considerations](#deployment-considerations)
14
+ - [Customizing This Example](#customizing-this-example)
15
+ - [Troubleshooting](#troubleshooting)
16
+
17
+ ## Overview
18
+
19
+ This frontend plugin demonstrates **Open edX MFE customization** using the Frontend Plugin Framework to replace the course list component in the learner dashboard.
20
+
21
+ **What this plugin provides:**
22
+ - **Custom CourseList Component**: Enhanced course display with archive functionality
23
+ - **Backend API Integration**: Connects to the sample backend plugin APIs
24
+ - **Slot Replacement Pattern**: Shows how to replace existing MFE components
25
+ - **State Management**: React patterns for plugin development
26
+ - **Authentication Integration**: Uses Open edX authentication system
27
+
28
+ **Official Documentation:**
29
+ - [Frontend Plugin Slots](https://docs.openedx.org/en/latest/site_ops/how-tos/use-frontend-plugin-slots.html)
30
+ - [Available Plugin Slots Reference](https://docs.openedx.org/en/latest/site_ops/references/frontend-plugin-slots.html)
31
+ - [OEP-65: Frontend Composability](https://docs.openedx.org/projects/openedx-proposals/en/latest/architectural-decisions/oep-0065-arch-frontend-composability.html)
32
+
33
+ ## Frontend Plugin Framework
34
+
35
+ ### What Are Plugin Slots?
36
+
37
+ A "frontend plugin slot" is an area of a web page that can be customized with different visual elements without forking the codebase. This allows site operators to customize MFEs using configuration files.
38
+
39
+ **Key Concepts:**
40
+ - **Slot**: A predefined customization point in an MFE
41
+ - **Plugin**: Custom code that fills or modifies a slot
42
+ - **Operations**: Actions you can take on slots (Insert, Modify, Replace)
43
+
44
+ ### Plugin Operations
45
+
46
+ | Operation | What It Does | When To Use |
47
+ |-----------|--------------|-------------|
48
+ | **Insert** | Add new components before/after existing ones | Adding new features alongside existing ones |
49
+ | **Modify** | Change properties of existing components | Tweaking existing functionality |
50
+ | **Replace** | Completely replace existing components | Major customization (like this example) |
51
+
52
+ ### Discovering Available Slots
53
+
54
+ **Slot Documentation**: [Available Frontend Plugin Slots](https://docs.openedx.org/en/latest/site_ops/references/frontend-plugin-slots.html)
55
+
56
+ **MFE-Specific Slots**: Each MFE documents its slots in `/src/plugin-slots/` directory:
57
+ - [Learner Dashboard Slots](https://github.com/openedx/frontend-app-learner-dashboard/tree/master/src/plugin-slots)
58
+ - [Course Authoring Slots](https://github.com/openedx/frontend-app-course-authoring/tree/master/src/plugin-slots)
59
+ - [Gradebook Slots](https://github.com/openedx/frontend-app-gradebook/tree/master/src/plugin-slots)
60
+
61
+ ## CourseList Component Example
62
+
63
+ **File**: [`src/plugin.jsx`](./src/plugin.jsx)
64
+
65
+ ### Component Structure
66
+
67
+ ```jsx
68
+ const CourseList = ({ courseListData }) => {
69
+ const [archivedCourses, setArchivedCourses] = useState(new Set());
70
+ const [loadingStates, setLoadingStates] = useState(new Map());
71
+
72
+ // Component implementation...
73
+ };
74
+ ```
75
+
76
+ ### Key Features
77
+
78
+ #### 1. Slot Data Integration
79
+
80
+ The component receives `courseListData` from the learner dashboard slot:
81
+
82
+ ```jsx
83
+ // Safety check for slot data
84
+ if (!courseListData || !courseListData.visibleList) {
85
+ return <div>Loading courses...</div>;
86
+ }
87
+
88
+ const courses = courseListData.visibleList;
89
+ ```
90
+
91
+ **Slot Props**: Each slot provides specific data. For CourseListSlot, see the [slot documentation](https://github.com/openedx/frontend-app-learner-dashboard/tree/master/src/plugin-slots/CourseListSlot#plugin-props).
92
+
93
+ #### 2. Backend API Integration
94
+
95
+ ```jsx
96
+ useEffect(() => {
97
+ const fetchArchivedCourses = async () => {
98
+ const client = getAuthenticatedHttpClient();
99
+ const lmsBaseUrl = getConfig().LMS_BASE_URL;
100
+
101
+ const response = await client.get(
102
+ `${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`,
103
+ { params: { is_archived: true } }
104
+ );
105
+
106
+ const archivedCourseIds = new Set(
107
+ response.data.results.map((item) => item.course_id)
108
+ );
109
+ setArchivedCourses(archivedCourseIds);
110
+ };
111
+
112
+ fetchArchivedCourses();
113
+ }, []);
114
+ ```
115
+
116
+ **Key Patterns:**
117
+ - **Authentication**: `getAuthenticatedHttpClient()` handles Open edX auth
118
+ - **Configuration**: `getConfig().LMS_BASE_URL` gets platform URLs
119
+ - **Error Handling**: Try/catch blocks for API failures
120
+
121
+ #### 3. Open edX UI Components
122
+
123
+ The plugin uses **Paragon** (Open edX's design system):
124
+
125
+ ```jsx
126
+ import {
127
+ Card,
128
+ Container,
129
+ Row,
130
+ Col,
131
+ Badge,
132
+ Collapsible,
133
+ Button,
134
+ Spinner,
135
+ Dropdown,
136
+ IconButton,
137
+ Icon,
138
+ } from "@openedx/paragon";
139
+ import { Archive, Unarchive, MoreVert } from "@openedx/paragon/icons";
140
+ ```
141
+
142
+ **Why Paragon**: Ensures consistent styling with the rest of Open edX interfaces.
143
+
144
+ **Paragon Documentation**: [Paragon Design System](https://paragon-openedx.netlify.app/)
145
+
146
+ ### State Management
147
+
148
+ #### Archive Status Management
149
+
150
+ ```jsx
151
+ const [archivedCourses, setArchivedCourses] = useState(new Set());
152
+ const [loadingStates, setLoadingStates] = useState(new Map());
153
+
154
+ const handleArchiveToggle = async (courseId, isCurrentlyArchived) => {
155
+ setLoadingStates((prev) => new Map(prev).set(courseId, true));
156
+
157
+ try {
158
+ // API calls to backend
159
+ if (isCurrentlyArchived) {
160
+ // Unarchive logic
161
+ } else {
162
+ // Archive logic
163
+ }
164
+
165
+ // Update local state
166
+ setArchivedCourses((prev) => {
167
+ const newSet = new Set(prev);
168
+ isCurrentlyArchived ? newSet.delete(courseId) : newSet.add(courseId);
169
+ return newSet;
170
+ });
171
+ } catch (error) {
172
+ console.error("Archive operation failed:", error);
173
+ } finally {
174
+ setLoadingStates((prev) => {
175
+ const newMap = new Map(prev);
176
+ newMap.delete(courseId);
177
+ return newMap;
178
+ });
179
+ }
180
+ };
181
+ ```
182
+
183
+ **Patterns Used:**
184
+ - **Optimistic Updates**: Update UI immediately, rollback on failure
185
+ - **Loading States**: Track loading per course for better UX
186
+ - **Immutable Updates**: Use functional setState for complex state
187
+
188
+ ## Slot Integration Patterns
189
+
190
+ ### CourseListSlot Integration
191
+
192
+ **Target Slot**: `course_list_slot` in learner dashboard
193
+
194
+ **Configuration Pattern** (for local development in `env.config.jsx`):
195
+
196
+ ```javascript
197
+ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
198
+ import { CourseList } from '@openedx/plugin-sample';
199
+
200
+ const config = {
201
+ pluginSlots: {
202
+ course_list_slot: {
203
+ keepDefault: false, // Hide original component
204
+ plugins: [
205
+ {
206
+ op: PLUGIN_OPERATIONS.Insert,
207
+ widget: {
208
+ id: 'custom_course_list',
209
+ type: DIRECT_PLUGIN,
210
+ priority: 60,
211
+ RenderWidget: CourseList // Your custom component
212
+ },
213
+ },
214
+ ],
215
+ },
216
+ },
217
+ }
218
+ ```
219
+
220
+ ### Plugin Configuration Options
221
+
222
+ | Option | Purpose | Values |
223
+ |--------|---------|--------|
224
+ | **keepDefault** | Show/hide original component | `true`, `false` |
225
+ | **op** | Plugin operation type | `Insert`, `Modify`, `Replace` |
226
+ | **priority** | Loading order | Higher numbers load later |
227
+ | **type** | Plugin implementation type | `DIRECT_PLUGIN`, `IFRAME_PLUGIN` |
228
+ | **RenderWidget** | Your React component | Component reference |
229
+
230
+ ### Slot Props and Data
231
+
232
+ Each slot provides specific props. For CourseListSlot:
233
+
234
+ ```jsx
235
+ const CourseList = ({
236
+ courseListData, // Course data from platform
237
+ // Other props depend on the slot
238
+ }) => {
239
+ // courseListData.visibleList - Array of course objects
240
+ // courseListData.course - Course metadata
241
+ // courseListData.courseRun - Course run information
242
+ };
243
+ ```
244
+
245
+ **Finding Slot Props**: Check the slot's README in the MFE repository, or examine the slot implementation in `/src/plugin-slots/`.
246
+
247
+ ## API Integration
248
+
249
+ ### Authentication Patterns
250
+
251
+ **Open edX Authentication**:
252
+ ```jsx
253
+ import { getAuthenticatedHttpClient } from "@edx/frontend-platform/auth";
254
+
255
+ const client = getAuthenticatedHttpClient();
256
+ // Client automatically includes authentication headers
257
+ ```
258
+
259
+ **Configuration Access**:
260
+ ```jsx
261
+ import { getConfig } from "@edx/frontend-platform";
262
+
263
+ const lmsBaseUrl = getConfig().LMS_BASE_URL;
264
+ const apiUrl = `${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`;
265
+ ```
266
+
267
+ ### Error Handling Best Practices
268
+
269
+ ```jsx
270
+ try {
271
+ const response = await client.post(url, data);
272
+ // Success handling
273
+ } catch (error) {
274
+ console.error("API Error:", {
275
+ status: error.response?.status,
276
+ statusText: error.response?.statusText,
277
+ data: error.response?.data,
278
+ message: error.message,
279
+ });
280
+
281
+ // User feedback
282
+ // Consider using toast notifications or error states
283
+ }
284
+ ```
285
+
286
+ ### API Response Handling
287
+
288
+ ```jsx
289
+ // Handle paginated responses
290
+ const response = await client.get(url);
291
+ const items = response.data.results || []; // DRF pagination format
292
+
293
+ // Handle different response formats
294
+ if (response.data && Array.isArray(response.data)) {
295
+ // Direct array response
296
+ } else if (response.data.results) {
297
+ // Paginated response
298
+ } else {
299
+ // Single object response
300
+ }
301
+ ```
302
+
303
+ ## Development Workflow
304
+
305
+ ### Prerequisites
306
+
307
+ 1. **MFE Setup**: Have a learner dashboard MFE running locally
308
+ 2. **Backend Plugin**: Install the backend plugin (see [`../backend-plugin-sample/README.md`](../backend-plugin-sample/README.md))
309
+ 3. **Node.js**: Version 16+ with npm or yarn
310
+
311
+ ### Local Development Setup
312
+
313
+ #### Step 0: Build Plugin
314
+
315
+ ```bash
316
+ cd /path/to/sample-plugin/frontend-plugin-sample
317
+ npm run build
318
+ ```
319
+
320
+ #### Step 1: Install Plugin Package
321
+
322
+ ```bash
323
+ # In your MFE directory (e.g., frontend-app-learner-dashboard)
324
+ npm install /path/to/sample-plugin/frontend-plugin-sample
325
+ ```
326
+
327
+ #### Step 2: Create env.config.jsx
328
+
329
+ Create `env.config.jsx` in your MFE root (not committed to repo):
330
+
331
+ ```javascript
332
+ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
333
+ import { CourseList } from '@openedx/plugin-sample';
334
+
335
+ const config = {
336
+ pluginSlots: {
337
+ course_list_slot: {
338
+ keepDefault: false,
339
+ plugins: [
340
+ {
341
+ op: PLUGIN_OPERATIONS.Insert,
342
+ widget: {
343
+ id: 'custom_course_list',
344
+ type: DIRECT_PLUGIN,
345
+ priority: 60,
346
+ RenderWidget: CourseList
347
+ },
348
+ },
349
+ ],
350
+ },
351
+ },
352
+ }
353
+
354
+ export default config;
355
+ ```
356
+
357
+ #### Step 3: Create module.config.js
358
+
359
+ Create `module.config.js` for local development:
360
+
361
+ ```javascript
362
+ module.exports = {
363
+ localModules: [
364
+ {
365
+ moduleName: '@openedx/plugin-sample',
366
+ dir: '/path/to/sample-plugin/frontend-plugin-sample'
367
+ },
368
+ ],
369
+ };
370
+ ```
371
+
372
+ **Purpose**: Webpack uses your local plugin code instead of the installed package.
373
+
374
+ #### Step 4: Start Development
375
+
376
+ ```bash
377
+ # In your MFE directory
378
+ npm ci
379
+ npm start
380
+ ```
381
+
382
+ ### Development vs Production Configuration
383
+
384
+ **Local Development**:
385
+ - Uses `env.config.jsx` for slot configuration
386
+ - Uses `module.config.js` for local code loading
387
+ - Hot reload for faster development
388
+
389
+ **Production Deployment**:
390
+ - Configuration via Tutor plugins
391
+ - Plugin installed as npm package
392
+ - Optimized builds and caching
393
+
394
+ ### Testing Frontend Plugins
395
+
396
+ #### Unit Testing
397
+
398
+ ```javascript
399
+ // Example test structure
400
+ import { render, screen } from '@testing-library/react';
401
+ import { CourseList } from './plugin';
402
+
403
+ describe('CourseList Plugin', () => {
404
+ test('renders course list with archive functionality', () => {
405
+ const mockCourseData = {
406
+ visibleList: [/* mock course data */]
407
+ };
408
+
409
+ render(<CourseList courseListData={mockCourseData} />);
410
+
411
+ expect(screen.getByText('Archive')).toBeInTheDocument();
412
+ });
413
+ });
414
+ ```
415
+
416
+ #### Integration Testing
417
+
418
+ Test within the actual MFE environment:
419
+
420
+ 1. Set up MFE with plugin installed
421
+ 2. Create test courses in platform
422
+ 3. Verify plugin functionality
423
+ 4. Test API integration
424
+ 5. Check error handling
425
+
426
+ ## Deployment Considerations
427
+
428
+ ### Production Deployment with Tutor
429
+
430
+ **Tutor Plugin Configuration** (see [`../tutor-contrib-sample/README.md`](../tutor-contrib-sample/README.md)):
431
+
432
+ ```python
433
+ # In tutor plugin
434
+ PLUGIN_SLOTS.add_items([
435
+ (
436
+ "learner-dashboard",
437
+ "custom_course_list",
438
+ """
439
+ {
440
+ op: PLUGIN_OPERATIONS.Insert,
441
+ type: DIRECT_PLUGIN,
442
+ priority: 50,
443
+ RenderWidget: CourseList
444
+ }"""
445
+ ),
446
+ ])
447
+ ```
448
+
449
+ ### Performance Considerations
450
+
451
+ **Bundle Size**:
452
+ - Frontend plugins are included in MFE bundles
453
+ - Minimize dependencies and use tree shaking
454
+ - Consider lazy loading for large plugins
455
+
456
+ **API Performance**:
457
+ - Implement proper caching strategies
458
+ - Use pagination for large datasets
459
+ - Optimize backend API response times
460
+
461
+ **User Experience**:
462
+ - Show loading states during API calls
463
+ - Handle errors gracefully
464
+ - Provide offline fallback behavior
465
+
466
+ ### Browser Compatibility
467
+
468
+ - Follow MFE browser support requirements
469
+ - Test across different browsers
470
+ - Use polyfills if needed for newer JS features
471
+
472
+ ## Customizing This Example
473
+
474
+ ### For Different Slots
475
+
476
+ 1. **Identify Target Slot**: Check [available slots](https://docs.openedx.org/en/latest/site_ops/references/frontend-plugin-slots.html)
477
+ 2. **Study Slot Props**: Examine slot documentation for available data
478
+ 3. **Adapt Component**: Modify component to work with slot-specific data
479
+ 4. **Update Configuration**: Change slot name in plugin configuration
480
+
481
+ **Example - Adapting for Header Slot**:
482
+
483
+ ```jsx
484
+ // Original CourseList component
485
+ const CourseList = ({ courseListData }) => { /* ... */ };
486
+
487
+ // Adapted for header slot
488
+ const CustomHeader = ({ logo, mainMenu, userMenu }) => {
489
+ // Use header-specific props
490
+ return (
491
+ <Header logo={logo} mainMenu={mainMenu}>
492
+ {/* Your customizations */}
493
+ </Header>
494
+ );
495
+ };
496
+ ```
497
+
498
+ ### Adding New Features
499
+
500
+ **Common Extension Patterns**:
501
+
502
+ ```jsx
503
+ // Add new state
504
+ const [newFeatureData, setNewFeatureData] = useState([]);
505
+
506
+ // Add new API calls
507
+ useEffect(() => {
508
+ const fetchNewFeatureData = async () => {
509
+ // Your API integration
510
+ };
511
+ }, []);
512
+
513
+ // Add new UI elements
514
+ return (
515
+ <Container>
516
+ {/* Existing course list */}
517
+ {/* Your new feature */}
518
+ <YourNewComponent data={newFeatureData} />
519
+ </Container>
520
+ );
521
+ ```
522
+
523
+ ### Component Composition
524
+
525
+ **Reusable Components**:
526
+ ```jsx
527
+ // Create reusable sub-components
528
+ const ArchiveButton = ({ courseId, isArchived, onToggle }) => (
529
+ <Button onClick={() => onToggle(courseId, isArchived)}>
530
+ {isArchived ? 'Unarchive' : 'Archive'}
531
+ </Button>
532
+ );
533
+
534
+ // Use in main component
535
+ const CourseList = ({ courseListData }) => (
536
+ <div>
537
+ {courses.map(course => (
538
+ <Card key={course.id}>
539
+ {/* Course info */}
540
+ <ArchiveButton
541
+ courseId={course.id}
542
+ isArchived={isArchived(course.id)}
543
+ onToggle={handleArchiveToggle}
544
+ />
545
+ </Card>
546
+ ))}
547
+ </div>
548
+ );
549
+ ```
550
+
551
+ ## Troubleshooting
552
+
553
+ ### Common Issues
554
+
555
+ **Plugin Not Loading**:
556
+ - Check `env.config.jsx` slot name matches target slot
557
+ - Verify plugin is installed (`npm list @openedx/plugin-sample`)
558
+ - Ensure MFE supports the plugin framework version
559
+ - Check browser console for JavaScript errors
560
+
561
+ **Slot Data Issues**:
562
+ - Console.log slot props to understand data structure
563
+ - Check if slot provides expected data (some slots may not provide certain props)
564
+ - Verify slot exists in the MFE version you're using
565
+
566
+ **API Integration Problems**:
567
+ - Verify backend plugin is installed and running
568
+ - Check API URLs match backend configuration
569
+ - Ensure CORS settings allow frontend-backend communication
570
+ - Test API endpoints directly in browser/Postman
571
+
572
+ **Styling Issues**:
573
+ - Use Paragon components for consistent styling
574
+ - Check CSS specificity conflicts
575
+ - Verify theme variables are available
576
+ - Test across different screen sizes
577
+
578
+ **Development Setup Issues**:
579
+ - Ensure `module.config.js` path is correct
580
+ - Check that both `env.config.jsx` and `module.config.js` are in MFE root
581
+ - Verify file permissions and syntax
582
+
583
+ ### Debugging Techniques
584
+
585
+ **Console Debugging**:
586
+ ```jsx
587
+ // Add debug logging
588
+ console.log("DEBUG: CourseList props:", { courseListData });
589
+ console.log("DEBUG: API response:", response.data);
590
+ console.log("DEBUG: Archive states:", Array.from(archivedCourses));
591
+ ```
592
+
593
+ **React Developer Tools**:
594
+ - Use React DevTools to inspect component state
595
+ - Check component hierarchy and props
596
+ - Monitor state changes during interactions
597
+
598
+ **Network Debugging**:
599
+ - Use browser DevTools Network tab
600
+ - Check API request/response details
601
+ - Verify authentication headers are present
602
+
603
+ ### Getting Help
604
+
605
+ 1. **Documentation**: Start with [official frontend plugin documentation](https://docs.openedx.org/en/latest/site_ops/how-tos/use-frontend-plugin-slots.html)
606
+ 2. **MFE-Specific Help**: Check individual MFE repositories for slot documentation
607
+ 3. **Community**: [Open edX Slack #frontend-platform channel](https://openedx.org/slack)
608
+ 4. **Issues**: Report bugs in relevant MFE repositories or this sample repository
609
+
610
+ This frontend plugin demonstrates the power and flexibility of the Open edX Frontend Plugin Framework. By following these patterns, you can create rich customizations that integrate seamlessly with the Open edX ecosystem.
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@openedx/plugin-sample",
3
+ "version": "0.0.0",
4
+ "main": "dist/index.js",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "repository": "https://github.com/openedx/sample-plugin",
9
+ "scripts": {
10
+ "build": "fedx-scripts babel src --out-dir dist --source-maps --ignore **/*.test.jsx,**/*.test.js"
11
+ },
12
+ "peerDependencies": {
13
+ "@edx/frontend-platform": "*",
14
+ "@openedx/paragon": "*",
15
+ "react": "*"
16
+ },
17
+ "devDependencies": {
18
+ "@openedx/frontend-build": "*"
19
+ }
20
+ }