@windward/integrations 0.0.1

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 (54) hide show
  1. package/.editorconfig +13 -0
  2. package/.eslintrc.js +15 -0
  3. package/.prettierrc +4 -0
  4. package/README.md +19 -0
  5. package/babel.config.js +1 -0
  6. package/components/Integration/Driver/ManageAtutor.vue +143 -0
  7. package/components/Integration/Driver/ManageBase.vue +145 -0
  8. package/components/Integration/JobTable.vue +308 -0
  9. package/components/Integration/TestConnection.vue +45 -0
  10. package/config/integration.config.js +13 -0
  11. package/helpers/Driver/Atutor.ts +12 -0
  12. package/helpers/Driver/BaseDriver.ts +25 -0
  13. package/helpers/Driver/DriverInterface.ts +7 -0
  14. package/helpers/IntegrationHelper.ts +150 -0
  15. package/i18n/en-US/components/index.ts +7 -0
  16. package/i18n/en-US/components/integration/driver.ts +18 -0
  17. package/i18n/en-US/components/integration/index.ts +7 -0
  18. package/i18n/en-US/components/integration/job.ts +22 -0
  19. package/i18n/en-US/components/navigation/index.ts +5 -0
  20. package/i18n/en-US/components/navigation/integrations.ts +8 -0
  21. package/i18n/en-US/index.ts +16 -0
  22. package/i18n/en-US/modules/index.ts +5 -0
  23. package/i18n/en-US/pages/importContent.ts +3 -0
  24. package/i18n/en-US/pages/importCourse.ts +13 -0
  25. package/i18n/en-US/pages/index.ts +9 -0
  26. package/i18n/en-US/pages/vendor.ts +11 -0
  27. package/i18n/en-US/shared/error.ts +8 -0
  28. package/i18n/en-US/shared/index.ts +11 -0
  29. package/i18n/en-US/shared/menu.ts +3 -0
  30. package/i18n/en-US/shared/permission.ts +26 -0
  31. package/i18n/en-US/shared/settings.ts +1 -0
  32. package/jest.config.js +17 -0
  33. package/models/CourseSectionIntegration.ts +12 -0
  34. package/models/IntegrationJob.ts +12 -0
  35. package/models/Organization.ts +14 -0
  36. package/models/OrganizationIntegration.ts +17 -0
  37. package/models/RemoteContent.ts +12 -0
  38. package/models/RemoteCourse.ts +17 -0
  39. package/models/RemoteOrganization.ts +17 -0
  40. package/models/Vendor.ts +12 -0
  41. package/package.json +44 -0
  42. package/pages/admin/importCourse.vue +390 -0
  43. package/pages/admin/vendors.vue +241 -0
  44. package/pages/course/importContent.vue +25 -0
  45. package/plugin.js +110 -0
  46. package/test/Helpers/IntegrationHelper.spec.js +92 -0
  47. package/test/Pages/Admin/ImportCourse.spec.js +19 -0
  48. package/test/Pages/Admin/vendors.spec.js +19 -0
  49. package/test/Pages/Course/importContent.spec.js +19 -0
  50. package/test/__mocks__/lodashMock.js +31 -0
  51. package/test/__mocks__/modelMock.js +101 -0
  52. package/test/__mocks__/vuexMock.js +31 -0
  53. package/test/mocks.js +18 -0
  54. package/tsconfig.json +21 -0
@@ -0,0 +1,390 @@
1
+ <template>
2
+ <div>
3
+ <v-tabs v-model="tab" class="mb-5">
4
+ <v-tabs-slider></v-tabs-slider>
5
+
6
+ <v-tab>
7
+ {{ $t('windward.integrations.pages.import_course.title') }}
8
+ </v-tab>
9
+
10
+ <v-tab>
11
+ {{
12
+ $t(
13
+ 'windward.integrations.components.integration.job.recent_jobs_title'
14
+ )
15
+ }}
16
+ </v-tab>
17
+ </v-tabs>
18
+ <v-tabs-items v-model="tab">
19
+ <v-tab-item>
20
+ <h1>
21
+ {{ $t('windward.integrations.pages.import_course.title') }}
22
+ </h1>
23
+
24
+ <div v-if="!organizationIntegrations.length">
25
+ {{
26
+ $t(
27
+ 'windward.integrations.pages.import_course.no_vendors'
28
+ )
29
+ }}
30
+ </div>
31
+ <div v-if="organizationIntegrations.length">
32
+ <v-select
33
+ v-model="selected.integrationId"
34
+ :items="organizationIntegrations"
35
+ :loading="loading.integration"
36
+ item-text="vendor.name"
37
+ item-value="id"
38
+ :label="
39
+ $t(
40
+ 'windward.integrations.pages.import_course.select_vendor'
41
+ )
42
+ "
43
+ @change="onSelectIntegration"
44
+ ></v-select>
45
+
46
+ <v-autocomplete
47
+ v-model="selected.remoteOrgId"
48
+ :items="remoteOrganizations"
49
+ :loading="loading.org"
50
+ item-value="id"
51
+ item-text="name"
52
+ :label="
53
+ $t(
54
+ 'windward.integrations.pages.import_course.select_remote_organization'
55
+ )
56
+ "
57
+ :disabled="!selected.integrationId"
58
+ @change="onSelectRemoteOrg"
59
+ >
60
+ <template #selection="{ item }">
61
+ {{ item.name }} - ({{ item.base_url }})
62
+ </template>
63
+ <template #item="{ item }">
64
+ {{ item.name }} - ({{ item.base_url }})
65
+ </template>
66
+ </v-autocomplete>
67
+
68
+ <v-autocomplete
69
+ v-model="selected.remoteCourseId"
70
+ :items="remoteCourses"
71
+ :loading="loading.course"
72
+ item-text="name"
73
+ item-value="id"
74
+ :label="
75
+ $t(
76
+ 'windward.integrations.pages.import_course.select_remote_course'
77
+ )
78
+ "
79
+ :disabled="!selected.remoteOrgId"
80
+ @change="onSelectRemoteCourse"
81
+ >
82
+ <template #selection="{ item }">
83
+ {{ item.name }} - ({{
84
+ $t(
85
+ 'windward.integrations.components.integration.driver.id'
86
+ )
87
+ }}: {{ item.id }})
88
+ </template>
89
+ <template #item="{ item }">
90
+ {{ item.name }} - ({{
91
+ $t(
92
+ 'windward.integrations.components.integration.driver.id'
93
+ )
94
+ }}: {{ item.id }})
95
+ </template>
96
+ </v-autocomplete>
97
+
98
+ <v-switch
99
+ v-model="allowSelectContent"
100
+ :label="
101
+ $t(
102
+ 'windward.integrations.pages.import_course.select_remote_content'
103
+ )
104
+ "
105
+ :disabled="!selected.remoteCourseId"
106
+ @change="onToggleRemoteContent"
107
+ ></v-switch>
108
+
109
+ <v-switch
110
+ v-model="sendEmail"
111
+ :label="
112
+ $t(
113
+ 'windward.integrations.pages.import_course.notify'
114
+ )
115
+ "
116
+ :disabled="!selected.remoteCourseId"
117
+ ></v-switch>
118
+
119
+ <v-btn color="primary" @click="onImport">
120
+ {{
121
+ $t(
122
+ 'windward.integrations.pages.import_course.start_import'
123
+ )
124
+ }}
125
+ </v-btn>
126
+
127
+ <v-divider v-if="allowSelectContent" class="mt-5 mb-5" />
128
+
129
+ <v-progress-circular
130
+ v-if="allowSelectContent && loading.content"
131
+ size="64"
132
+ indeterminate
133
+ >
134
+ </v-progress-circular>
135
+ <v-treeview
136
+ v-if="allowSelectContent && !loading.content"
137
+ v-model="selected.remoteContentIds"
138
+ selectable
139
+ selected-color="primary"
140
+ selection-type="independent"
141
+ :items="remoteContent"
142
+ >
143
+ </v-treeview>
144
+ </div>
145
+ </v-tab-item>
146
+ <v-tab-item>
147
+ <h1>
148
+ {{
149
+ $t(
150
+ 'windward.integrations.components.integration.job.recent_jobs_title'
151
+ )
152
+ }}
153
+ </h1>
154
+ <IntegrationJobTable
155
+ ref="integrationJobTable"
156
+ :channel="'integrations.organizations.' + organization.id"
157
+ :event="'.Windward\\Integrations\\App\\Events\\RemoteCourseImportProgressEvent'"
158
+ ></IntegrationJobTable>
159
+ </v-tab-item>
160
+ </v-tabs-items>
161
+ </div>
162
+ </template>
163
+
164
+ <script>
165
+ import _ from 'lodash'
166
+ import { mapGetters } from 'vuex'
167
+ import Course from '~/models/Course'
168
+ import Organization from '../../models/Organization'
169
+ import OrganizationIntegration from '../../models/OrganizationIntegration'
170
+ import RemoteOrganization from '../../models/RemoteOrganization'
171
+ import RemoteCourse from '../../models/RemoteCourse'
172
+ import RemoteContent from '../../models/RemoteContent'
173
+ import IntegrationJobTable from '../../components/Integration/JobTable.vue'
174
+
175
+ export default {
176
+ name: 'PluginIntegrationsAdminImportCoursePage',
177
+ components: { IntegrationJobTable },
178
+ layout: 'authenticated',
179
+ meta: {
180
+ privilege: {
181
+ 'plugin.windward.integrations.course': {
182
+ writable: true,
183
+ },
184
+ },
185
+ },
186
+ data() {
187
+ return {
188
+ tab: 0,
189
+ selected: {
190
+ integrationId: null,
191
+ remoteOrgId: null,
192
+ remoteCourseId: null,
193
+ remoteContentIds: [],
194
+ },
195
+ loading: {
196
+ integration: false,
197
+ org: false,
198
+ course: false,
199
+ content: false,
200
+ },
201
+ allowSelectContent: false,
202
+ organizationIntegrations: [],
203
+ remoteOrganizations: [],
204
+ remoteCourses: [],
205
+ remoteContent: [],
206
+ // vendorCache used to store remote datasets so rapid switching the <v-select> doesn't poll the api every time
207
+ vendorCache: {},
208
+ sendEmail: false,
209
+ }
210
+ },
211
+ async fetch() {
212
+ this.loading.integration = true
213
+ this.organizationIntegrations = await new Organization({
214
+ id: this.organization.id,
215
+ })
216
+ .integrations()
217
+ .with(['vendor'])
218
+ .where('enabled', true)
219
+ .get()
220
+ this.loading.integration = false
221
+ },
222
+ computed: {
223
+ ...mapGetters({
224
+ organization: 'organization/get',
225
+ }),
226
+ },
227
+ methods: {
228
+ async onImport() {
229
+ // Check to see if we're import individual content
230
+ if (this.selected.remoteContentIds.length) {
231
+ //
232
+ }
233
+
234
+ this.$dialog.success(
235
+ this.$t(
236
+ 'windward.integrations.pages.import_course.import_started'
237
+ )
238
+ )
239
+
240
+ await new Course({
241
+ // Name required by model but not actually used on the api
242
+ name: this.selected.remoteCourseId,
243
+ remote_content_ids: this.selected.remoteContentIds,
244
+ options: {
245
+ send_email: this.sendEmail,
246
+ },
247
+ })
248
+ .for(
249
+ new OrganizationIntegration({
250
+ id: this.selected.integrationId,
251
+ }),
252
+ new RemoteOrganization({
253
+ id: this.selected.remoteOrgId,
254
+ }),
255
+ new RemoteCourse({
256
+ id: this.selected.remoteCourseId,
257
+ })
258
+ )
259
+ .save()
260
+
261
+ // If the jobs table is already loaded, force a re-load to catch the new job we just kicked off
262
+ // This way a users new job will appear instantly so they can see it worked even if it says "not started"
263
+ if (!_.isEmpty(this.$refs.integrationJobTable)) {
264
+ this.$refs.integrationJobTable.loadJobs()
265
+ }
266
+
267
+ // Switch to the jobs tab
268
+ this.tab = 1
269
+ },
270
+ async onToggleRemoteContent() {
271
+ this.loading.content = true
272
+ if (this.allowSelectContent) {
273
+ try {
274
+ this.remoteContent = await new RemoteContent()
275
+ .custom(
276
+ new OrganizationIntegration({
277
+ id: this.selected.integrationId,
278
+ }),
279
+ new RemoteOrganization({
280
+ id: this.selected.remoteOrgId,
281
+ }),
282
+ new RemoteCourse({
283
+ id: this.selected.remoteCourseId,
284
+ }),
285
+ new RemoteContent()
286
+ )
287
+ .get()
288
+ } catch (e) {
289
+ if (e.response.status === 404) {
290
+ console.error(e)
291
+ this.$dialog.error(
292
+ this.$t(
293
+ 'windward.integrations.shared.error.load_remote_content_failed'
294
+ )
295
+ )
296
+ }
297
+ }
298
+ } else {
299
+ this.remoteContent = []
300
+ this.selected.remoteContentIds = []
301
+ }
302
+ this.loading.content = false
303
+ },
304
+ onSelectRemoteCourse() {
305
+ // Reset the remote content if we change the course
306
+ this.allowSelectContent = false
307
+ this.remoteContent = []
308
+ this.selected.remoteContentIds = []
309
+ },
310
+ async onSelectRemoteOrg() {
311
+ // Clear data since we're changing org
312
+ this.remoteCourses = []
313
+ this.remoteContent = []
314
+ this.selected.remoteCourseId = null
315
+ this.selected.remoteContentIds = []
316
+
317
+ this.loading.course = true
318
+ try {
319
+ this.remoteCourses = await new RemoteCourse()
320
+ .custom(
321
+ new OrganizationIntegration({
322
+ id: this.selected.integrationId,
323
+ }),
324
+ new RemoteOrganization({
325
+ id: this.selected.remoteOrgId,
326
+ }),
327
+ new RemoteCourse()
328
+ )
329
+ .get()
330
+ } catch (e) {
331
+ if (e.response.status === 404) {
332
+ console.error(e)
333
+ this.$dialog.error(
334
+ this.$t(
335
+ 'windward.integrations.shared.error.load_remote_course_failed'
336
+ )
337
+ )
338
+ }
339
+ }
340
+ this.loading.course = false
341
+ },
342
+ async onSelectIntegration() {
343
+ this.loading.org = true
344
+ // Clear out the remote data since we changed vendors
345
+ this.remoteOrganizations = []
346
+ this.remoteCourses = []
347
+ this.remoteContent = []
348
+
349
+ this.selected.remoteOrgId = null
350
+ this.selected.remoteCourseId = null
351
+ this.selected.remoteContentIds = []
352
+
353
+ // Check the vendor cache
354
+ const cachedOrgs = _.get(
355
+ this.vendorCache,
356
+ `${this.selected.integrationId}.remoteOrganizations`,
357
+ null
358
+ )
359
+
360
+ // Check to see if we already cached the org list for this vendor
361
+ if (cachedOrgs) {
362
+ this.remoteOrganizations = cachedOrgs
363
+ } else {
364
+ try {
365
+ this.remoteOrganizations =
366
+ await new OrganizationIntegration({
367
+ id: this.selected.integrationId,
368
+ })
369
+ .remoteOrganizations()
370
+ .get()
371
+
372
+ this.vendorCache[this.selected.integrationId] = {
373
+ remoteOrganizations: this.remoteOrganizations,
374
+ }
375
+ } catch (e) {
376
+ if (e.response.status === 404) {
377
+ console.error(e)
378
+ this.$dialog.error(
379
+ this.$t(
380
+ 'windward.integrations.shared.error.load_remote_organization_failed'
381
+ )
382
+ )
383
+ }
384
+ }
385
+ }
386
+ this.loading.org = false
387
+ },
388
+ },
389
+ }
390
+ </script>
@@ -0,0 +1,241 @@
1
+ <template>
2
+ <div>
3
+ <v-row justify="center" align="center">
4
+ <v-col cols="12">
5
+ <div class="d-flex mb-5">
6
+ <h1 class="mr-auto flex-grow-1">
7
+ {{
8
+ $t(
9
+ 'windward.integrations.components.navigation.integrations.title'
10
+ )
11
+ }}
12
+ </h1>
13
+ </div>
14
+ </v-col>
15
+ </v-row>
16
+ <v-row>
17
+ <v-col cols="12">
18
+ <v-simple-table>
19
+ <template #default>
20
+ <thead>
21
+ <tr>
22
+ <th class="text-left">
23
+ {{
24
+ $t(
25
+ 'windward.integrations.pages.vendor.vendor_name'
26
+ )
27
+ }}
28
+ </th>
29
+ <th class="text-left">
30
+ {{
31
+ $t(
32
+ 'windward.integrations.pages.vendor.configured'
33
+ )
34
+ }}
35
+ </th>
36
+ <th class="text-left">
37
+ {{
38
+ $t(
39
+ 'windward.integrations.pages.vendor.enabled'
40
+ )
41
+ }}
42
+ </th>
43
+ <th class="text-left">
44
+ {{ $t('shared.forms.actions') }}
45
+ </th>
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ <tr
50
+ v-for="item in filteredIntegrations"
51
+ :key="item.id"
52
+ >
53
+ <td>{{ item.name }}</td>
54
+ <td>
55
+ <v-icon
56
+ :color="
57
+ item.organizationIntegration
58
+ ? 'success'
59
+ : 'error'
60
+ "
61
+ >{{
62
+ item.organizationIntegration
63
+ ? 'mdi-check'
64
+ : 'mdi-close'
65
+ }}
66
+ </v-icon>
67
+ <span
68
+ v-if="!item.organizationIntegration"
69
+ class="sr-only"
70
+ >{{
71
+ $t(
72
+ 'windward.integrations.pages.vendor.vendor_disabled'
73
+ )
74
+ }}</span
75
+ >
76
+ </td>
77
+ <td>
78
+ <v-icon
79
+ :color="
80
+ item.organizationIntegration &&
81
+ item.organizationIntegration.enabled
82
+ ? 'success'
83
+ : 'error'
84
+ "
85
+ >{{
86
+ item.organizationIntegration &&
87
+ item.organizationIntegration.enabled
88
+ ? 'mdi-check'
89
+ : 'mdi-close'
90
+ }}
91
+ </v-icon>
92
+ <span
93
+ v-if="
94
+ !item.organizationIntegration ||
95
+ !item.organizationIntegration
96
+ .enabled
97
+ "
98
+ class="sr-only"
99
+ >{{
100
+ $t(
101
+ 'windward.integrations.pages.vendor.organization_integration_disabled'
102
+ )
103
+ }}</span
104
+ >
105
+ </td>
106
+ <td>
107
+ <v-btn
108
+ color="primary"
109
+ outlined
110
+ @click="onManageIntegration(item)"
111
+ >
112
+ <v-icon>mdi-pencil</v-icon>
113
+ <span class="sr-only">
114
+ {{
115
+ $t(
116
+ 'windward.integrations.pages.vendor.manage'
117
+ )
118
+ }}
119
+ </span>
120
+ </v-btn>
121
+ </td>
122
+ </tr>
123
+ </tbody>
124
+ </template>
125
+ </v-simple-table>
126
+
127
+ <Dialog v-model="manageDialog" :trigger="false" action-save>
128
+ <template #title>{{
129
+ $t(
130
+ 'windward.integrations.components.integration.driver.' +
131
+ activeVendor.product_code +
132
+ '.manage_dialog_title'
133
+ )
134
+ }}</template>
135
+ <template #form="{ on, attrs }">
136
+ <component
137
+ :is="activeManageDriver"
138
+ :vendor="activeVendor"
139
+ v-bind="attrs"
140
+ v-on="on"
141
+ @update:integration="onUpdateIntegration"
142
+ ></component>
143
+ </template>
144
+ </Dialog>
145
+ </v-col>
146
+ </v-row>
147
+ </div>
148
+ </template>
149
+
150
+ <script>
151
+ import _ from 'lodash'
152
+ import { mapGetters } from 'vuex'
153
+
154
+ import Dialog from '~/components/Dialog.vue'
155
+ import Organization from '../../models/Organization'
156
+ import IntegrationJobTable from '../../components/Integration/JobTable.vue'
157
+
158
+ export default {
159
+ name: 'PluginIntegrationsAdminVendorsPage',
160
+ components: { Dialog, IntegrationJobTable },
161
+ layout: 'authenticated',
162
+ meta: {
163
+ privilege: {
164
+ 'plugin.windward.integrations.organization.integration': {
165
+ readable: true,
166
+ },
167
+ },
168
+ },
169
+ data() {
170
+ return {
171
+ Pusher: null,
172
+ Echo: null,
173
+ vendors: [],
174
+ organizationIntegrations: [],
175
+ manageDialog: false,
176
+ activeVendor: null,
177
+ activeManageDriver: null,
178
+ }
179
+ },
180
+ async fetch() {
181
+ await this.$Integration.load()
182
+ this.vendors = this.$Integration.getVendors()
183
+ },
184
+ computed: {
185
+ ...mapGetters({
186
+ organization: 'organization/get',
187
+ }),
188
+ filteredIntegrations() {
189
+ const integrations = []
190
+
191
+ for (const vendor of this.vendors) {
192
+ const vendorItem = _.cloneDeep(vendor)
193
+ vendorItem.organizationIntegration = null
194
+
195
+ for (const org of this.organizationIntegrations) {
196
+ if (org.integration_vendor_id === vendor.id) {
197
+ vendorItem.organizationIntegration = org
198
+ }
199
+ }
200
+
201
+ integrations.push(vendorItem)
202
+ }
203
+
204
+ return integrations
205
+ },
206
+ },
207
+ mounted() {
208
+ this.load()
209
+ },
210
+ methods: {
211
+ async load() {
212
+ try {
213
+ this.organizationIntegrations = await new Organization({
214
+ id: this.organization.id,
215
+ })
216
+ .integrations()
217
+ .get()
218
+ } catch (e) {
219
+ console.error('Could not load organization integrations', e)
220
+ }
221
+ },
222
+ onUpdateIntegration(newIntegration) {
223
+ const integration = this.organizationIntegrations.find(
224
+ (i) => i.id === newIntegration.id
225
+ )
226
+
227
+ if (_.isEmpty(integration)) {
228
+ this.organizationIntegrations.push(newIntegration)
229
+ } else {
230
+ integration.enabled = newIntegration.enabled
231
+ }
232
+ },
233
+ onManageIntegration(vendor) {
234
+ this.activeManageDriver =
235
+ this.$Integration.getVendorManageComponent(vendor.id)
236
+ this.activeVendor = vendor
237
+ this.manageDialog = true
238
+ },
239
+ },
240
+ }
241
+ </script>
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <div>
3
+ <h1>Import Content</h1>
4
+ <p>Not Implemented</p>
5
+ </div>
6
+ </template>
7
+
8
+ <script>
9
+ export default {
10
+ name: 'PluginIntegrationsImportContentPage',
11
+ layout: 'authenticated',
12
+ meta: {
13
+ privilege: {
14
+ 'plugin.windward.integrations.organization.integration': {
15
+ readable: true,
16
+ },
17
+ },
18
+ },
19
+ data() {
20
+ return {}
21
+ },
22
+ async fetch() {},
23
+ computed: {},
24
+ }
25
+ </script>