@openedx/plugin-sample 3.4.0 → 3.6.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 +24 -559
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,347 +1,46 @@
1
- # Frontend Plugin Implementation Guide
1
+ # frontend-plugin-sample
2
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.
3
+ A React component that replaces the learner-dashboard's course list with one that supports archiving courses. Wired into the `org.openedx.frontend.learner_dashboard.course_list.v1` plugin slot. Reads each course's archive state from a filter-injected slot prop and writes back to the [`backend-plugin-sample`](../backend-plugin-sample/) REST API on toggle.
4
4
 
5
- ## Table of Contents
5
+ ## How to use it
6
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)
7
+ See the root [README](../README.md) for setup instructions. With Tutor, [`tutor-contrib-sample`](../tutor-contrib-sample/) installs the published npm package and wires it into the learner-dashboard slot. For local source development (with or without Tutor), the MFE-side files in [Local development setup](#local-development-setup) below are required.
16
8
 
17
- ## Overview
9
+ ## How it works
18
10
 
19
- This frontend plugin demonstrates **Open edX MFE customization** using the Frontend Plugin Framework to replace the course list component in the learner dashboard.
11
+ **The component.** [`src/plugin.jsx`](./src/plugin.jsx) exports `CourseList`, a Paragon-styled replacement for the learner-dashboard's default course list. It receives `courseListData` (`visibleList`, `filterOptions`, etc.) as a slot prop.
20
12
 
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
13
+ **Reading archive state without an extra API call.** The initial archive flag is read directly from each course run as `courseRun.isArchivedByLearner`. That field is injected into the learner-dashboard's `/init` response by the backend plugin's filter ([`pipeline.py`](../backend-plugin-sample/src/openedx_plugin_sample/pipeline.py)), which saves a round-trip on every dashboard load and keeps the archive state consistent with the rest of the course data from the same response. The REST API is still used for writes when the learner clicks archive/unarchive.
27
14
 
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)
15
+ **Authentication and config.** Writes go through `getAuthenticatedHttpClient()` from `@edx/frontend-platform/auth`, and the LMS origin comes from `getConfig().LMS_BASE_URL`. UI components are from [Paragon](https://paragon-openedx.netlify.app/).
32
16
 
33
- ## Frontend Plugin Framework
17
+ ## Local development setup
34
18
 
35
- ### What Are Plugin Slots?
19
+ Two files go in your MFE checkout root (e.g. `frontend-app-learner-dashboard/`), neither committed:
36
20
 
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.
21
+ **`module.config.js`** tells the MFE's webpack to resolve `@openedx/plugin-sample` to your local source tree instead of `node_modules`:
38
22
 
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 Data via the Filter Pipeline
94
-
95
- Rather than firing an extra GET to `course-archive-status/` on every dashboard
96
- load, the initial archive state is read directly off the slot props. The backend
97
- plugin uses an Open edX filter (see [`pipeline.py`](../backend-plugin-sample/src/openedx_plugin_sample/pipeline.py))
98
- to inject `isArchivedByLearner` into each courseRun in the Learner Home `/init`
99
- API response, so it arrives alongside the rest of the course data:
100
-
101
- ```jsx
102
- const [archivedCourses, setArchivedCourses] = useState(() => {
103
- const initial = new Set();
104
- (courseListData?.visibleList || []).forEach((courseData) => {
105
- if (courseData.courseRun?.isArchivedByLearner) {
106
- initial.add(courseData.courseRun.courseId);
107
- }
108
- });
109
- return initial;
110
- });
111
- ```
112
-
113
- **Why this pattern**: One fewer round-trip per dashboard load, and the archive
114
- state is consistent with the rest of the course data from the same response.
115
- The REST API is still used for writes (archive/unarchive) — see the toggle
116
- handler below.
117
-
118
- **Key Patterns:**
119
- - **Filter-injected data**: Read `courseRun.isArchivedByLearner` straight from slot props
120
- - **Authentication** (for writes): `getAuthenticatedHttpClient()` handles Open edX auth
121
- - **Configuration**: `getConfig().LMS_BASE_URL` gets platform URLs
122
-
123
- #### 3. Open edX UI Components
124
-
125
- The plugin uses **Paragon** (Open edX's design system):
126
-
127
- ```jsx
128
- import {
129
- Card,
130
- Container,
131
- Row,
132
- Col,
133
- Badge,
134
- Collapsible,
135
- Button,
136
- Spinner,
137
- Dropdown,
138
- IconButton,
139
- Icon,
140
- } from "@openedx/paragon";
141
- import { Archive, Unarchive, MoreVert } from "@openedx/paragon/icons";
142
- ```
143
-
144
- **Why Paragon**: Ensures consistent styling with the rest of Open edX interfaces.
145
-
146
- **Paragon Documentation**: [Paragon Design System](https://paragon-openedx.netlify.app/)
147
-
148
- ### State Management
149
-
150
- #### Archive Status Management
151
-
152
- ```jsx
153
- const [archivedCourses, setArchivedCourses] = useState(new Set());
154
- const [loadingStates, setLoadingStates] = useState(new Map());
155
-
156
- const handleArchiveToggle = async (courseId, isCurrentlyArchived) => {
157
- setLoadingStates((prev) => new Map(prev).set(courseId, true));
158
-
159
- try {
160
- // API calls to backend
161
- if (isCurrentlyArchived) {
162
- // Unarchive logic
163
- } else {
164
- // Archive logic
165
- }
166
-
167
- // Update local state
168
- setArchivedCourses((prev) => {
169
- const newSet = new Set(prev);
170
- isCurrentlyArchived ? newSet.delete(courseId) : newSet.add(courseId);
171
- return newSet;
172
- });
173
- } catch (error) {
174
- console.error("Archive operation failed:", error);
175
- } finally {
176
- setLoadingStates((prev) => {
177
- const newMap = new Map(prev);
178
- newMap.delete(courseId);
179
- return newMap;
180
- });
181
- }
182
- };
183
- ```
184
-
185
- **Patterns Used:**
186
- - **Optimistic Updates**: Update UI immediately, rollback on failure
187
- - **Loading States**: Track loading per course for better UX
188
- - **Immutable Updates**: Use functional setState for complex state
189
-
190
- ## Slot Integration Patterns
191
-
192
- ### CourseListSlot Integration
193
-
194
- **Target Slot**: `course_list_slot` in learner dashboard
195
-
196
- **Configuration Pattern** (for local development in `env.config.jsx`):
197
-
198
- ```javascript
199
- import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
200
- import { CourseList } from '@openedx/plugin-sample';
201
-
202
- const config = {
203
- pluginSlots: {
204
- course_list_slot: {
205
- keepDefault: false, // Hide original component
206
- plugins: [
207
- {
208
- op: PLUGIN_OPERATIONS.Insert,
209
- widget: {
210
- id: 'custom_course_list',
211
- type: DIRECT_PLUGIN,
212
- priority: 60,
213
- RenderWidget: CourseList // Your custom component
214
- },
215
- },
216
- ],
217
- },
218
- },
219
- }
220
- ```
221
-
222
- ### Plugin Configuration Options
223
-
224
- | Option | Purpose | Values |
225
- |--------|---------|--------|
226
- | **keepDefault** | Show/hide original component | `true`, `false` |
227
- | **op** | Plugin operation type | `Insert`, `Modify`, `Replace` |
228
- | **priority** | Loading order | Higher numbers load later |
229
- | **type** | Plugin implementation type | `DIRECT_PLUGIN`, `IFRAME_PLUGIN` |
230
- | **RenderWidget** | Your React component | Component reference |
231
-
232
- ### Slot Props and Data
233
-
234
- Each slot provides specific props. For CourseListSlot:
235
-
236
- ```jsx
237
- const CourseList = ({
238
- courseListData, // Course data from platform
239
- // Other props depend on the slot
240
- }) => {
241
- // courseListData.visibleList - Array of course objects
242
- // courseListData.course - Course metadata
243
- // courseListData.courseRun - Course run information
244
- };
245
- ```
246
-
247
- **Finding Slot Props**: Check the slot's README in the MFE repository, or examine the slot implementation in `/src/plugin-slots/`.
248
-
249
- ## API Integration
250
-
251
- ### Authentication Patterns
252
-
253
- **Open edX Authentication**:
254
- ```jsx
255
- import { getAuthenticatedHttpClient } from "@edx/frontend-platform/auth";
256
-
257
- const client = getAuthenticatedHttpClient();
258
- // Client automatically includes authentication headers
259
- ```
260
-
261
- **Configuration Access**:
262
- ```jsx
263
- import { getConfig } from "@edx/frontend-platform";
264
-
265
- const lmsBaseUrl = getConfig().LMS_BASE_URL;
266
- const apiUrl = `${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`;
267
- ```
268
-
269
- ### Error Handling Best Practices
270
-
271
- ```jsx
272
- try {
273
- const response = await client.post(url, data);
274
- // Success handling
275
- } catch (error) {
276
- console.error("API Error:", {
277
- status: error.response?.status,
278
- statusText: error.response?.statusText,
279
- data: error.response?.data,
280
- message: error.message,
281
- });
282
-
283
- // User feedback
284
- // Consider using toast notifications or error states
285
- }
286
- ```
287
-
288
- ### API Response Handling
289
-
290
- ```jsx
291
- // Handle paginated responses
292
- const response = await client.get(url);
293
- const items = response.data.results || []; // DRF pagination format
294
-
295
- // Handle different response formats
296
- if (response.data && Array.isArray(response.data)) {
297
- // Direct array response
298
- } else if (response.data.results) {
299
- // Paginated response
300
- } else {
301
- // Single object response
302
- }
303
- ```
304
-
305
- ## Development Workflow
306
-
307
- ### Prerequisites
308
-
309
- 1. **Tutor & Tutor-MFE Setup**: Tutor is installed and launched in `dev` mode.
310
- 2. **Backend Plugin**: Install the backend plugin (see [`../backend-plugin-sample/README.md`](../backend-plugin-sample/README.md))
311
- 3. **Node.js**: Version 16+ with npm or yarn
312
-
313
- ### Local Development Setup
314
-
315
- #### Step 1: Create module.config.js
316
-
317
- Create `module.config.js` in your MFE root, not committed to the repo.
318
- This tells the MFE to load/use the `@openedx/sample-plugin` package
319
- as a source (non-built) distribution .
320
-
321
- ```javascript
23
+ ```js
322
24
  module.exports = {
323
25
  localModules: [
324
26
  {
325
27
  moduleName: '@openedx/plugin-sample',
326
- dir: '/path/to/sample-plugin/frontend-plugin-sample',
327
- dist: 'src'
28
+ dir: '/absolute/path/to/sample-plugin/frontend-plugin-sample',
29
+ dist: 'src',
328
30
  },
329
31
  ],
330
32
  };
331
33
  ```
332
34
 
333
- #### Step 2: Create env.config.jsx
334
-
335
- Create `env.config.jsx` in your MFE root, not committed to the repo.
336
- This plugs the sample widget into the course list slot.
35
+ **`env.config.jsx`** — plugs the component into the slot:
337
36
 
338
- ```javascript
37
+ ```jsx
339
38
  import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
340
39
  import { CourseList } from '@openedx/plugin-sample';
341
40
 
342
41
  const config = {
343
42
  pluginSlots: {
344
- course_list_slot: {
43
+ 'org.openedx.frontend.learner_dashboard.course_list.v1': {
345
44
  keepDefault: false,
346
45
  plugins: [
347
46
  {
@@ -350,261 +49,27 @@ const config = {
350
49
  id: 'custom_course_list',
351
50
  type: DIRECT_PLUGIN,
352
51
  priority: 60,
353
- RenderWidget: CourseList
52
+ RenderWidget: CourseList,
354
53
  },
355
54
  },
356
55
  ],
357
56
  },
358
57
  },
359
- }
58
+ };
360
59
 
361
60
  export default config;
362
61
  ```
363
- **Purpose**: Webpack uses your local plugin code instead of the installed package.
364
-
365
- #### Step 3: Start Development
366
62
 
367
- Now, from the MFE repository root, install requirements and run the dev server.
63
+ Then, from the MFE checkout:
368
64
 
369
65
  ```bash
370
- # Install requirements
371
66
  npm ci
372
67
 
373
- # If running Tutor:
374
- tutor mounts add . # Instruct tutor-mfe to redict requests to this local MFE devserver
375
- tutor dev reboot -d mfe
68
+ # With Tutor — point tutor-mfe at your local MFE devserver:
69
+ tutor mounts add .
70
+ tutor dev reboot -d mfe
376
71
  npm run dev
377
72
 
378
- # If not running Tutor:
73
+ # Without Tutor:
379
74
  npm start
380
75
  ```
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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openedx/plugin-sample",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "dist"