@windward/integrations 0.0.8 → 0.0.10

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 (49) hide show
  1. package/components/ExternalIntegration/Driver/Lti1p1/ManageProvider.vue +9 -6
  2. package/components/ExternalIntegration/Driver/Lti1p1/ManageProviders.vue +6 -5
  3. package/components/ExternalIntegration/Driver/Lti1p3/ManageProvider.vue +445 -0
  4. package/components/ExternalIntegration/Driver/Lti1p3/ManageProviders.vue +259 -0
  5. package/components/ExternalIntegration/Driver/ManageLti1p3.vue +45 -0
  6. package/components/FileImport/Dropbox.vue +9 -0
  7. package/components/FileImport/FileImportMenu.vue +111 -0
  8. package/components/FileImport/GoogleDrive.vue +9 -0
  9. package/components/FileImport/Resourcespace.vue +202 -0
  10. package/components/Integration/Driver/ManageAtutor.vue +35 -12
  11. package/components/Integration/Driver/ManageResourcespace.vue +137 -0
  12. package/components/Integration/JobLog.vue +281 -0
  13. package/components/Integration/JobTable.vue +15 -1
  14. package/components/SecretField.vue +1 -0
  15. package/config/integration.config.js +2 -0
  16. package/helpers/Driver/Resourcespace.ts +15 -0
  17. package/i18n/en-US/components/external_integration/driver/lti1p3.ts +13 -0
  18. package/i18n/en-US/components/external_integration/index.ts +2 -1
  19. package/i18n/en-US/components/file_import/index.ts +5 -0
  20. package/i18n/en-US/components/file_import/resourcespace.ts +4 -0
  21. package/i18n/en-US/components/index.ts +2 -0
  22. package/i18n/en-US/components/integration/driver.ts +8 -0
  23. package/i18n/en-US/components/integration/index.ts +2 -0
  24. package/i18n/en-US/components/integration/job.ts +1 -0
  25. package/i18n/en-US/components/integration/job_log.ts +8 -0
  26. package/i18n/en-US/shared/error.ts +1 -0
  27. package/i18n/en-US/shared/file.ts +5 -0
  28. package/i18n/en-US/shared/index.ts +2 -0
  29. package/models/ExternalIntegration/{Lti1p1Provider.ts → LtiProvider.ts} +2 -2
  30. package/models/OrganizationIntegration.ts +5 -0
  31. package/models/RemoteFile.ts +12 -0
  32. package/package.json +1 -1
  33. package/pages/admin/importCourse.vue +8 -2
  34. package/pages/admin/vendors.vue +3 -2
  35. package/pages/course/externalIntegration/index.vue +4 -3
  36. package/plugin.js +84 -1
  37. package/test/Components/ExternalIntegration/Lti1p3/ManageProvider.spec.js +19 -0
  38. package/test/Components/ExternalIntegration/Lti1p3/ManageProviders.spec.js +19 -0
  39. package/test/Components/ExternalIntegration/ManageLti1p3.spec.js +19 -0
  40. package/test/Components/FileImport/Dropbox.spec.js +24 -0
  41. package/test/Components/FileImport/GoogleDrive.spec.js +24 -0
  42. package/test/Components/FileImport/Resourcespace.spec.js +24 -0
  43. package/test/Components/Integration/Driver/ManageAtutor.spec.js +22 -0
  44. package/test/Components/Integration/Driver/ManageResourcespace.spec.js +22 -0
  45. package/test/Components/Integration/JobLog.spec.js +22 -0
  46. package/test/Components/Integration/JobTable.spec.js +23 -0
  47. package/test/__mocks__/componentsMock.js +24 -0
  48. package/test/__mocks__/modelMock.js +1 -0
  49. package/test/mocks.js +12 -0
@@ -0,0 +1,259 @@
1
+ <template>
2
+ <div>
3
+ <Dialog
4
+ color="primary"
5
+ action-save
6
+ action-save-new
7
+ @click:save="onSaved"
8
+ >
9
+ <template #title>{{
10
+ $t(
11
+ 'windward.integrations.components.external_integration.driver.lti1p1.new'
12
+ )
13
+ }}</template>
14
+ <template #trigger>{{ $t('shared.forms.new') }}</template>
15
+ <template #form="{ on, attrs }"
16
+ ><ManageProvider v-bind="attrs" v-on="on"></ManageProvider
17
+ ></template>
18
+ </Dialog>
19
+
20
+ <v-data-table
21
+ :headers="headers"
22
+ :items="providers"
23
+ :items-per-page="10"
24
+ class="elevation-1"
25
+ >
26
+ <template #[`item.target`]="{ item }">
27
+ <ProviderTargetViewer
28
+ class="field--target text-truncate"
29
+ :target="item.target"
30
+ ></ProviderTargetViewer>
31
+ </template>
32
+ <template #[`item.metadata.tool_public_keyset_url`]="{ item }">
33
+ <SecretField
34
+ v-model="item.metadata.tool_public_keyset_url"
35
+ :hidden="false"
36
+ ></SecretField>
37
+ </template>
38
+ <template #[`item.metadata.tool_oidc_auth_endpoint`]="{ item }">
39
+ <SecretField
40
+ v-model="item.metadata.tool_oidc_auth_endpoint"
41
+ :hidden="false"
42
+ ></SecretField>
43
+ </template>
44
+ <template #[`item.url`]="{ item }">
45
+ <SecretField v-model="item.url" :hidden="false"></SecretField>
46
+ </template>
47
+
48
+ <template #[`item.enabled`]="{ item }">
49
+ <v-icon :color="item.enabled ? 'success' : 'error'"
50
+ >{{ item.enabled ? 'mdi-check' : 'mdi-close' }}
51
+ </v-icon>
52
+ <span v-if="!item.enabled" class="sr-only">{{
53
+ $t('shared.forms.enabled')
54
+ }}</span>
55
+ </template>
56
+
57
+ <template #[`item.grade_sync`]="{ item }">
58
+ <v-icon :color="item.grade_sync ? 'success' : 'error'"
59
+ >{{ item.grade_sync ? 'mdi-check' : 'mdi-close' }}
60
+ </v-icon>
61
+ <span v-if="!item.grade_sync" class="sr-only">{{
62
+ $t(
63
+ 'windward.integrations.components.external_integration.grade_sync'
64
+ )
65
+ }}</span>
66
+ </template>
67
+
68
+ <template #[`item.created_at`]="{ item }">
69
+ {{ $d(new Date(item.created_at), 'short') }}
70
+ </template>
71
+ <template #[`item.actions`]="{ index, item }">
72
+ <Dialog color="primary" action-save @click:save="onSaved">
73
+ <template #title>{{
74
+ $t(
75
+ 'windward.integrations.components.external_integration.driver.lti1p3.edit'
76
+ )
77
+ }}</template>
78
+ <template #trigger>
79
+ <v-icon small>mdi-pencil</v-icon>
80
+ <span class="sr-only">{{
81
+ $t(
82
+ 'windward.integrations.components.external_integration.driver.lti1p3.edit'
83
+ )
84
+ }}</span>
85
+ </template>
86
+ <template #form="{ on, attrs }"
87
+ ><ManageProvider
88
+ v-model="providers[index]"
89
+ v-bind="attrs"
90
+ v-on="on"
91
+ ></ManageProvider
92
+ ></template>
93
+ </Dialog>
94
+
95
+ <v-btn icon>
96
+ <v-icon @click="onConfirmDelete(item)"> mdi-delete </v-icon>
97
+ <span class="sr-only">{{ $t('shared.forms.delete') }}</span>
98
+ </v-btn>
99
+ </template>
100
+ </v-data-table>
101
+ </div>
102
+ </template>
103
+
104
+ <script>
105
+ import _ from 'lodash'
106
+ import { mapGetters } from 'vuex'
107
+ import LtiProvider from '../../../../models/ExternalIntegration/LtiProvider'
108
+ import SecretField from '../../../SecretField.vue'
109
+ import ProviderTargetViewer from '../../ProviderTargetViewer.vue'
110
+ import ManageProvider from './ManageProvider.vue'
111
+ import Dialog from '~/components/Dialog.vue'
112
+ import Organization from '~/models/Organization'
113
+ import Course from '~/models/Course'
114
+
115
+ export default {
116
+ name: 'ManageLti1p1ProvidersDriver',
117
+ components: { SecretField, Dialog, ManageProvider, ProviderTargetViewer },
118
+ data() {
119
+ return {
120
+ providers: [],
121
+ headers: [
122
+ {
123
+ text: this.$t(
124
+ 'windward.integrations.components.external_integration.target'
125
+ ),
126
+ value: 'target',
127
+ },
128
+ {
129
+ text: this.$t(
130
+ 'windward.integrations.components.external_integration.driver.lti1p3.tool_public_keyset_url'
131
+ ),
132
+ value: 'metadata.tool_public_keyset_url',
133
+ },
134
+ {
135
+ text: this.$t(
136
+ 'windward.integrations.components.external_integration.driver.lti1p3.tool_oidc_auth_endpoint'
137
+ ),
138
+ value: 'metadata.tool_oidc_auth_endpoint',
139
+ },
140
+ {
141
+ text: this.$t(
142
+ 'windward.integrations.components.external_integration.launch_url'
143
+ ),
144
+ value: 'url',
145
+ },
146
+ {
147
+ text: this.$t('shared.forms.enabled'),
148
+ value: 'enabled',
149
+ },
150
+ {
151
+ text: this.$t(
152
+ 'windward.integrations.components.external_integration.grade_sync'
153
+ ),
154
+ value: 'grade_sync',
155
+ },
156
+ { text: this.$t('shared.forms.created'), value: 'created_at' },
157
+ {
158
+ text: this.$t('shared.forms.actions'),
159
+ value: 'actions',
160
+ sortable: false,
161
+ },
162
+ ],
163
+ }
164
+ },
165
+
166
+ async fetch() {
167
+ if (
168
+ !this.$PermissionService.userHasAccessTo(
169
+ 'plugin.windward.integrations.course.externalIntegration',
170
+ 'readable'
171
+ ) ||
172
+ _.isEmpty(this.organization.id) ||
173
+ _.isEmpty(this.course.id)
174
+ ) {
175
+ // Display an angry error that they can't view this driver
176
+ this.$dialog.error(this.$t('shared.error.description_401'), {
177
+ duration: null,
178
+ action: {
179
+ text: this.$t('shared.forms.close'),
180
+ onClick: (_e, toastObject) => {
181
+ toastObject.goAway(0)
182
+ },
183
+ },
184
+ })
185
+ if (_.isEmpty(this.organization.id) || _.isEmpty(this.course.id)) {
186
+ // eslint-disable-next-line no-console
187
+ console.error(
188
+ 'Cannot load external integrations because organization or course is not set!'
189
+ )
190
+ } else {
191
+ // eslint-disable-next-line no-console
192
+ console.error(
193
+ 'You do not have access to this external integration!'
194
+ )
195
+ }
196
+
197
+ // Return so we don't even attempt loading
198
+ return false
199
+ }
200
+
201
+ await this.loadProviders()
202
+ },
203
+ computed: {
204
+ ...mapGetters({
205
+ organization: 'organization/get',
206
+ course: 'course/get',
207
+ }),
208
+ },
209
+ methods: {
210
+ onSaved() {
211
+ this.loadProviders()
212
+ },
213
+ async loadProviders() {
214
+ this.providers = await new LtiProvider()
215
+ .for(
216
+ new Organization({ id: this.organization.id }),
217
+ new Course({ id: this.course.id })
218
+ )
219
+ .where('version', '1.3')
220
+ .get()
221
+ },
222
+ onConfirmDelete(provider) {
223
+ this.$dialog.show(this.$t('shared.forms.confirm_delete_text'), {
224
+ icon: 'mdi-help',
225
+ duration: null,
226
+ action: [
227
+ {
228
+ text: this.$t('shared.forms.cancel'),
229
+ onClick: (_e, toastObject) => {
230
+ toastObject.goAway(0)
231
+ },
232
+ },
233
+ {
234
+ text: this.$t('shared.forms.confirm'),
235
+ // router navigation
236
+ onClick: (_e, toastObject) => {
237
+ this.deleteProvider(provider)
238
+ toastObject.goAway(0)
239
+ },
240
+ },
241
+ ],
242
+ })
243
+ },
244
+ async deleteProvider(provider) {
245
+ await provider.delete()
246
+ this.$dialog.success(this.$t('shared.response.deleted'))
247
+ // Reload providers now that we deleted one
248
+ this.loadProviders()
249
+ },
250
+ },
251
+ }
252
+ </script>
253
+
254
+ <style scoped>
255
+ .field--target {
256
+ max-width: 15em;
257
+ display: inline-block;
258
+ }
259
+ </style>
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <div>
3
+ <v-expansion-panels v-model="panel">
4
+ <v-expansion-panel>
5
+ <v-expansion-panel-header>
6
+ {{
7
+ $t(
8
+ 'windward.integrations.components.external_integration.provider_panel_title'
9
+ )
10
+ }}
11
+ </v-expansion-panel-header>
12
+ <v-expansion-panel-content>
13
+ <ManageProviders></ManageProviders>
14
+ </v-expansion-panel-content>
15
+ </v-expansion-panel>
16
+ <v-expansion-panel>
17
+ <v-expansion-panel-header>
18
+ {{
19
+ $t(
20
+ 'windward.integrations.components.external_integration.consumer_panel_title'
21
+ )
22
+ }}
23
+ </v-expansion-panel-header>
24
+ <v-expansion-panel-content>
25
+ <!-- <ManageConsumers></ManageConsumers>-->
26
+ </v-expansion-panel-content>
27
+ </v-expansion-panel>
28
+ </v-expansion-panels>
29
+ </div>
30
+ </template>
31
+
32
+ <script>
33
+ import ManageProviders from './Lti1p3/ManageProviders.vue'
34
+ /* import ManageConsumers from './Lti1p3/ManageConsumers.vue' */
35
+
36
+ export default {
37
+ name: 'ManageLti1p3Driver',
38
+ components: { ManageProviders },
39
+ data() {
40
+ return {
41
+ panel: 0,
42
+ }
43
+ },
44
+ }
45
+ </script>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <div>Dropbox Import Disabled</div>
3
+ </template>
4
+
5
+ <script>
6
+ export default {
7
+ name: 'FileImportDropbox',
8
+ }
9
+ </script>
@@ -0,0 +1,111 @@
1
+ <template>
2
+ <div>
3
+ <v-list-item
4
+ v-for="importVendor in enabledFileImports"
5
+ :key="importVendor.product_code"
6
+ @click="onClickImport(importVendor)"
7
+ >
8
+ <v-list-item-icon>
9
+ <v-icon>{{ importVendor.icon }}</v-icon>
10
+ </v-list-item-icon>
11
+ <v-list-item-title>{{ $t(importVendor.i18n) }}</v-list-item-title>
12
+ </v-list-item>
13
+
14
+ <Dialog
15
+ v-model="showDialog"
16
+ :trigger="false"
17
+ persistent
18
+ @click:close="$emit('click:close')"
19
+ >
20
+ <template #title>{{ $t(activeVendor.i18n) }}</template>
21
+
22
+ <template #form="{ on, attrs }">
23
+ <component
24
+ :is="activeVendor.template"
25
+ v-if="activeVendor"
26
+ :organization-integration="
27
+ activeVendor.organization_integration
28
+ "
29
+ v-bind="attrs"
30
+ v-on="on"
31
+ @uploaded="$emit('uploaded', $event)"
32
+ ></component>
33
+ </template>
34
+ </Dialog>
35
+ </div>
36
+ </template>
37
+
38
+ <script>
39
+ import _ from 'lodash'
40
+ import { mapGetters } from 'vuex'
41
+ import Organization from '../../models/Organization'
42
+ import Dialog from '~/components/Dialog.vue'
43
+
44
+ export default {
45
+ name: 'FileImportMenu',
46
+ components: { Dialog },
47
+ props: {
48
+ metadata: {
49
+ type: Object,
50
+ required: false,
51
+ default() {
52
+ return {}
53
+ },
54
+ },
55
+ },
56
+ data() {
57
+ return {
58
+ organizationIntegrations: [],
59
+ activeVendor: null,
60
+ showDialog: false,
61
+ }
62
+ },
63
+ async fetch() {
64
+ const organizationIntegrations = await new Organization({
65
+ id: this.organization.id,
66
+ })
67
+ .integrations()
68
+ .with(['vendor'])
69
+ .where('enabled', true)
70
+ .where('vendor.enabled', true)
71
+ .get()
72
+ if (!_.isEmpty(organizationIntegrations)) {
73
+ this.organizationIntegrations = organizationIntegrations
74
+ }
75
+ },
76
+ computed: {
77
+ ...mapGetters({
78
+ organization: 'organization/get',
79
+ }),
80
+ enabledFileImports() {
81
+ const enabled = []
82
+ // Get only the vendors that are defined to be displayed as a fileImport vendor from plugin.js
83
+ const available = _.get(this.metadata, 'vendors', [])
84
+
85
+ // Loop over all the orgIntegrations that we've confirmed are enabled
86
+ this.organizationIntegrations.forEach((orgInt) => {
87
+ const productCode = _.get(orgInt, 'vendor.product_code')
88
+ const found = available.find((c) => {
89
+ return c.product_code === productCode && !!c.template
90
+ })
91
+
92
+ if (found) {
93
+ // Set the organization integration so we can reference the api later
94
+ found.organization_integration = orgInt
95
+ enabled.push(found)
96
+ }
97
+ })
98
+
99
+ return enabled
100
+ },
101
+ },
102
+ mounted() {},
103
+ methods: {
104
+ onClickImport(importVendor) {
105
+ this.$emit('click', importVendor)
106
+ this.activeVendor = importVendor
107
+ this.showDialog = true
108
+ },
109
+ },
110
+ }
111
+ </script>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <div>Google Drive Import Disabled</div>
3
+ </template>
4
+
5
+ <script>
6
+ export default {
7
+ name: 'FileImportGoogleDrive',
8
+ }
9
+ </script>
@@ -0,0 +1,202 @@
1
+ <template>
2
+ <div>
3
+ <SearchField
4
+ v-model="search"
5
+ :num-results="numFiles"
6
+ @click:search="onLoadAssets($event)"
7
+ @click:clear="onClickClear"
8
+ ></SearchField>
9
+
10
+ <v-btn
11
+ color="primary"
12
+ block
13
+ :disabled="selectedFiles.length === 0"
14
+ :loading="loading"
15
+ @click="onConfirmImportFiles"
16
+ >
17
+ <v-icon>mdi-content-duplicate</v-icon>
18
+ {{
19
+ $t('shared.file.import_selected_count', [selectedFiles.length])
20
+ }}
21
+ </v-btn>
22
+
23
+ <div v-if="loaded && !loading && numFiles === 0" class="text-center">
24
+ <p>{{ $t('shared.file.none_found') }}</p>
25
+ </div>
26
+
27
+ <v-data-table
28
+ v-if="numFiles"
29
+ v-model="selectedFiles"
30
+ class="mt-5"
31
+ show-select
32
+ item-key="id"
33
+ :headers="headers"
34
+ :items="files"
35
+ :items-per-page="25"
36
+ :loading="loading"
37
+ >
38
+ <template #[`item.preview`]="{ item }">
39
+ <v-img :src="item.preview" width="150px" :aspect-ratio="16 / 9">
40
+ <template #placeholder>
41
+ <v-skeleton-loader
42
+ class="mx-auto"
43
+ max-width="300"
44
+ type="image"
45
+ ></v-skeleton-loader>
46
+ </template>
47
+ </v-img>
48
+ </template>
49
+ <template #[`item.created_at`]="{ item }">
50
+ {{ $d(new Date(item.created_at), 'shorttime') }}
51
+ </template>
52
+ <template #[`item.updated_at`]="{ item }">
53
+ {{ $d(new Date(item.updated_at), 'shorttime') }}
54
+ </template>
55
+ </v-data-table>
56
+ </div>
57
+ </template>
58
+
59
+ <script>
60
+ import { mapGetters } from 'vuex'
61
+ import OrganizationIntegration from '../../models/OrganizationIntegration'
62
+ import RemoteFile from '../../models/RemoteFile'
63
+ import Course from '~/models/Course'
64
+ import SearchField from '~/components/SearchField.vue'
65
+
66
+ export default {
67
+ name: 'FileImportResourcespace',
68
+ components: { SearchField },
69
+ props: {
70
+ organizationIntegration: { type: Object, required: true },
71
+ },
72
+ data() {
73
+ return {
74
+ loading: false,
75
+ loaded: false,
76
+ search: {},
77
+ files: [],
78
+ selectedFiles: [],
79
+ headers: [
80
+ {
81
+ text: this.$t('shared.file.preview'),
82
+ align: 'start',
83
+ sortable: false,
84
+ value: 'preview',
85
+ },
86
+ {
87
+ text: this.$t('shared.file.name'),
88
+ value: 'filename',
89
+ },
90
+ {
91
+ text: this.$t(
92
+ 'windward.integrations.components.file_import.resourcespace.resource_id'
93
+ ),
94
+ value: 'id',
95
+ },
96
+ {
97
+ text: this.$t('shared.forms.created'),
98
+ value: 'created_at',
99
+ },
100
+ {
101
+ text: this.$t('shared.forms.updated'),
102
+ value: 'updated_at',
103
+ },
104
+ ],
105
+ }
106
+ },
107
+ async fetch() {},
108
+ computed: {
109
+ ...mapGetters({
110
+ organization: 'organization/get',
111
+ course: 'course/get',
112
+ }),
113
+ numFiles() {
114
+ return this.files.length
115
+ },
116
+ },
117
+ mounted() {},
118
+ methods: {
119
+ async onLoadAssets(search = {}) {
120
+ this.loading = true
121
+ this.loaded = true
122
+ try {
123
+ this.files = await new OrganizationIntegration(
124
+ this.organizationIntegration
125
+ )
126
+ .remoteFiles()
127
+ .where('term', search.term)
128
+ .get()
129
+ } catch (e) {
130
+ this.$dialog.error(
131
+ this.$t(
132
+ 'windward.integrations.components.file_import.resourcespace.load_error'
133
+ )
134
+ )
135
+ console.error(e)
136
+ }
137
+
138
+ this.loading = false
139
+ },
140
+ onClickClear() {
141
+ this.selectedFiles = []
142
+ },
143
+ onConfirmImportFiles() {
144
+ const self = this
145
+ this.$dialog.show(
146
+ this.$t('shared.file.confirm_import', [
147
+ this.selectedFiles.length,
148
+ ]),
149
+ {
150
+ icon: 'mdi-question',
151
+ duration: null,
152
+ action: [
153
+ {
154
+ text: this.$t('shared.forms.cancel'),
155
+ onClick: (_e, toastObject) => {
156
+ toastObject.goAway(0)
157
+ },
158
+ },
159
+ {
160
+ text: this.$t('shared.forms.confirm'),
161
+ // router navigation
162
+ onClick: (_e, toastObject) => {
163
+ self.importFiles()
164
+ toastObject.goAway(0)
165
+ },
166
+ },
167
+ ],
168
+ }
169
+ )
170
+ },
171
+ async importFiles() {
172
+ this.loading = true
173
+ try {
174
+ const uploaded = await new RemoteFile(this.selectedFiles)
175
+ .for(
176
+ new OrganizationIntegration(
177
+ this.organizationIntegration
178
+ ),
179
+ new Course({ id: this.course.id })
180
+ )
181
+ .save()
182
+
183
+ this.$dialog.success(
184
+ this.$t('shared.file.import_success', [
185
+ this.selectedFiles.length,
186
+ ])
187
+ )
188
+ this.$emit('uploaded', uploaded)
189
+ } catch (e) {
190
+ console.log('Failed to import!', this.selectedFiles)
191
+ console.error(e)
192
+ this.$dialog.error(
193
+ this.$t('shared.file.import_failed', [
194
+ this.selectedFiles.length,
195
+ ])
196
+ )
197
+ }
198
+ this.loading = false
199
+ },
200
+ },
201
+ }
202
+ </script>