@windward/integrations 0.0.9 → 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 (33) hide show
  1. package/components/FileImport/Dropbox.vue +9 -0
  2. package/components/FileImport/FileImportMenu.vue +111 -0
  3. package/components/FileImport/GoogleDrive.vue +9 -0
  4. package/components/FileImport/Resourcespace.vue +202 -0
  5. package/components/Integration/Driver/ManageAtutor.vue +20 -12
  6. package/components/Integration/Driver/ManageResourcespace.vue +137 -0
  7. package/components/Integration/JobLog.vue +208 -25
  8. package/components/Integration/JobTable.vue +1 -1
  9. package/components/SecretField.vue +1 -0
  10. package/config/integration.config.js +2 -0
  11. package/helpers/Driver/Resourcespace.ts +15 -0
  12. package/i18n/en-US/components/file_import/index.ts +5 -0
  13. package/i18n/en-US/components/file_import/resourcespace.ts +4 -0
  14. package/i18n/en-US/components/index.ts +2 -0
  15. package/i18n/en-US/components/integration/driver.ts +7 -0
  16. package/i18n/en-US/components/integration/index.ts +2 -0
  17. package/i18n/en-US/components/integration/job.ts +0 -2
  18. package/i18n/en-US/components/integration/job_log.ts +8 -0
  19. package/i18n/en-US/shared/error.ts +1 -0
  20. package/i18n/en-US/shared/file.ts +5 -0
  21. package/i18n/en-US/shared/index.ts +2 -0
  22. package/models/OrganizationIntegration.ts +5 -0
  23. package/models/RemoteFile.ts +12 -0
  24. package/package.json +1 -1
  25. package/pages/admin/importCourse.vue +7 -1
  26. package/plugin.js +36 -0
  27. package/test/Components/FileImport/Dropbox.spec.js +24 -0
  28. package/test/Components/FileImport/GoogleDrive.spec.js +24 -0
  29. package/test/Components/FileImport/Resourcespace.spec.js +24 -0
  30. package/test/Components/Integration/Driver/ManageAtutor.spec.js +22 -0
  31. package/test/Components/Integration/Driver/ManageResourcespace.spec.js +22 -0
  32. package/test/__mocks__/componentsMock.js +12 -0
  33. package/test/__mocks__/modelMock.js +1 -0
@@ -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>
@@ -130,21 +130,29 @@ export default {
130
130
  },
131
131
  async onTestConnection() {
132
132
  this.testConnectionLoading = true
133
- const response = await this.testConnection(
134
- this.integration.metadata
135
- )
133
+ let response = { result: false }
134
+ try {
135
+ response = await this.testConnection(this.integration.metadata)
136
136
 
137
- if (response.result) {
138
- this.errorMessage = ''
139
- this.$dialog.success(
140
- this.$t(
141
- 'windward.integrations.shared.error.connect_success'
137
+ if (response.result) {
138
+ this.errorMessage = ''
139
+ this.$dialog.success(
140
+ this.$t(
141
+ 'windward.integrations.shared.error.connect_success'
142
+ )
142
143
  )
143
- )
144
- } else {
145
- this.errorMessage = response.message
144
+ } else {
145
+ this.errorMessage = response.message
146
+ this.$dialog.error(
147
+ this.$t(
148
+ 'windward.integrations.shared.error.connect_fail'
149
+ )
150
+ )
151
+ }
152
+ } catch (e) {
153
+ console.error(e)
146
154
  this.$dialog.error(
147
- this.$t('windward.integrations.shared.error.connect_fail')
155
+ this.$t('windward.integrations.shared.error.unknown')
148
156
  )
149
157
  }
150
158
 
@@ -0,0 +1,137 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="!render" class="integration-loading">
4
+ <v-progress-circular size="128" indeterminate />
5
+ </div>
6
+ <div v-if="render">
7
+ <v-row justify="center" align="center" class="mt-5">
8
+ <v-col cols="12">
9
+ <v-switch
10
+ v-model="integration.enabled"
11
+ :label="
12
+ $t(
13
+ 'windward.integrations.components.integration.driver.enabled'
14
+ )
15
+ "
16
+ />
17
+
18
+ <v-text-field
19
+ v-model="integration.metadata.config.url"
20
+ :label="
21
+ $t(
22
+ 'windward.integrations.components.integration.driver.resourcespace.url'
23
+ )
24
+ "
25
+ :hint="
26
+ $t(
27
+ 'windward.integrations.components.integration.driver.resourcespace.url_hint'
28
+ )
29
+ "
30
+ ></v-text-field>
31
+ <v-text-field
32
+ v-model="integration.metadata.config.username"
33
+ :label="
34
+ $t(
35
+ 'windward.integrations.components.integration.driver.resourcespace.username'
36
+ )
37
+ "
38
+ ></v-text-field>
39
+ <v-text-field
40
+ v-model="integration.metadata.config.key"
41
+ :label="
42
+ $t(
43
+ 'windward.integrations.components.integration.driver.resourcespace.key'
44
+ )
45
+ "
46
+ type="password"
47
+ ></v-text-field>
48
+
49
+ <v-switch
50
+ v-model="integration.metadata.config.ssl"
51
+ :label="
52
+ $t(
53
+ 'windward.integrations.components.integration.driver.ssl_enabled'
54
+ )
55
+ "
56
+ />
57
+
58
+ <TestConnection
59
+ :disabled="
60
+ !integration.metadata.config.url ||
61
+ !integration.metadata.config.username ||
62
+ !integration.metadata.config.key
63
+ "
64
+ :loading="testConnectionLoading"
65
+ :errors="errorMessage"
66
+ @click="onTestConnection"
67
+ ></TestConnection>
68
+ </v-col>
69
+ </v-row>
70
+ </div>
71
+ </div>
72
+ </template>
73
+
74
+ <script>
75
+ import TestConnection from '../TestConnection.vue'
76
+ import ManageBaseVue from './ManageBase.vue'
77
+
78
+ export default {
79
+ name: 'ManageResourcespaceDriver',
80
+ components: { TestConnection },
81
+ extends: ManageBaseVue,
82
+ data() {
83
+ return {
84
+ // formValid: true|false If this form is "complete" and passed validation on THIS component. Defined and watched in ManageBase.vue
85
+ // render: true|false If we should show the form aka when validation has passed. Defined and managed in ManageBase.vue
86
+ // integration: { metadata: {...} } The integration object to write to. Defined and loaded in ManageBase.vue
87
+ errorMessage: '',
88
+ testConnectionLoading: false,
89
+ }
90
+ },
91
+ methods: {
92
+ /**
93
+ * Lifecycle event called from ManageBase.vue when async fetch() completes.
94
+ * Once called this.integration will be available containing the integration model (or a new one)
95
+ */
96
+ onIntegrationLoaded() {
97
+ // Set SSL to enabled by default
98
+ if (_.get(this.integration.metadata.config, 'ssl', null) === null) {
99
+ this.integration.metadata.config.ssl = true
100
+ }
101
+ },
102
+ async onTestConnection() {
103
+ this.testConnectionLoading = true
104
+ let response = { result: false }
105
+ try {
106
+ response = await this.testConnection(this.integration.metadata)
107
+
108
+ if (response.result) {
109
+ this.errorMessage = ''
110
+ this.$dialog.success(
111
+ this.$t(
112
+ 'windward.integrations.shared.error.connect_success'
113
+ )
114
+ )
115
+ } else {
116
+ this.errorMessage = response.message
117
+ this.$dialog.error(
118
+ this.$t(
119
+ 'windward.integrations.shared.error.connect_fail'
120
+ )
121
+ )
122
+ }
123
+ } catch (e) {
124
+ console.error(e)
125
+ this.$dialog.error(
126
+ this.$t('windward.integrations.shared.error.unknown')
127
+ )
128
+ }
129
+
130
+ // We will indirectly validate the form via connection tests
131
+ // That way we can 100% confirm that the integration is valid
132
+ this.formValid = response.result
133
+ this.testConnectionLoading = false
134
+ },
135
+ },
136
+ }
137
+ </script>
@@ -5,28 +5,127 @@
5
5
  </v-btn>
6
6
  <Dialog v-model="logDialog" :trigger="false">
7
7
  <template #title>{{
8
- $t('windward.integrations.components.integration.job.view_log')
8
+ $t(
9
+ 'windward.integrations.components.integration.job_log.view_log'
10
+ )
9
11
  }}</template>
10
- <template #form>
11
- <v-progress-circular v-if="loading" size="128" indeterminate />
12
- <div v-if="!loading">
13
- <SearchField v-model="search" hide-filters></SearchField>
12
+ <template #form="{ on, attrs }">
13
+ <div v-bind="attrs" v-on="on">
14
+ <v-progress-circular
15
+ v-if="loading"
16
+ size="128"
17
+ indeterminate
18
+ />
19
+ <div v-if="!loading">
20
+ <SearchField
21
+ v-model="search"
22
+ hide-filters
23
+ ></SearchField>
14
24
 
15
- <v-alert
16
- v-for="logItem in filteredLog"
17
- :key="logItem.id"
18
- :type="logItem.level"
19
- >
20
- {{ logItem.message }}
21
- </v-alert>
25
+ <v-alert
26
+ v-for="logItem in filteredLog"
27
+ :key="logItem.id"
28
+ :type="logItem.level"
29
+ :icon="logItemIcon(logItem)"
30
+ class="log-item"
31
+ >
32
+ <p>{{ logItem.message }}</p>
33
+ <div v-if="showDetails(logItem)">
34
+ <v-divider class="mt-1 mb-1" />
35
+ <p
36
+ v-if="
37
+ logItem.context.remote_content &&
38
+ remoteCourse
39
+ "
40
+ >
41
+ {{
42
+ $t(
43
+ 'windward.integrations.components.integration.job_log.remote_course_id'
44
+ )
45
+ }}
46
+ {{ remoteCourse.id }}
47
+ </p>
48
+ <p v-if="logItem.context.remote_content">
49
+ {{
50
+ $t(
51
+ 'windward.integrations.components.integration.job_log.remote_content_id'
52
+ )
53
+ }}
54
+ {{ logItem.context.remote_content.id }}
55
+ </p>
56
+ <p v-if="logItem.context.remote_url">
57
+ {{
58
+ $t(
59
+ 'windward.integrations.components.integration.job_log.remote_url'
60
+ )
61
+ }}
62
+ <v-chip
63
+ :href="
64
+ isUrl(logItem.context.remote_url)
65
+ ? logItem.context.remote_url
66
+ : undefined
67
+ "
68
+ label
69
+ target="_blank"
70
+ >
71
+ <span
72
+ class="d-inline-block text-truncate chip-truncate"
73
+ >
74
+ {{ logItem.context.remote_url }}
75
+ </span>
22
76
 
23
- <p v-if="filteredLog.length === 0">
24
- {{
25
- $t(
26
- 'windward.integrations.components.integration.job.log_no_results'
27
- )
28
- }}
29
- </p>
77
+ <v-icon
78
+ v-if="
79
+ isUrl(
80
+ logItem.context.remote_url
81
+ )
82
+ "
83
+ class="ml-2"
84
+ >
85
+ mdi-launch
86
+ </v-icon>
87
+ </v-chip>
88
+ </p>
89
+ <div v-if="logItem.context.remote_code">
90
+ <p>
91
+ {{
92
+ $t(
93
+ 'windward.integrations.components.integration.job_log.remote_code'
94
+ )
95
+ }}
96
+ <v-btn
97
+ icon
98
+ @click="
99
+ onClickCopy(
100
+ logItem.context.remote_code
101
+ )
102
+ "
103
+ ><v-icon class="ml-2">
104
+ mdi-content-copy
105
+ </v-icon>
106
+ <span class="sr-only">{{
107
+ $t('shared.forms.copy')
108
+ }}</span>
109
+ </v-btn>
110
+ </p>
111
+ <div class="remote-code grey darken-3">
112
+ <code
113
+ class="grey darken-3"
114
+ v-text="logItem.context.remote_code"
115
+ ></code>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </v-alert>
120
+
121
+ <p v-if="filteredLog.length === 0">
122
+ {{
123
+ $t(
124
+ 'windward.integrations.components.integration.job_log.no_results'
125
+ )
126
+ }}
127
+ </p>
128
+ </div>
30
129
  </div>
31
130
  </template>
32
131
  </Dialog>
@@ -34,13 +133,14 @@
34
133
  </template>
35
134
 
36
135
  <script>
136
+ import _ from 'lodash'
37
137
  import { mapGetters } from 'vuex'
38
138
  import SearchField from '~/components/SearchField.vue'
39
139
  import Organization from '../../models/Organization'
40
140
 
41
141
  export default {
42
- components: { SearchField },
43
142
  name: 'IntegrationJobLog',
143
+ components: { SearchField },
44
144
  props: {
45
145
  id: { type: String, required: true },
46
146
  },
@@ -48,6 +148,7 @@ export default {
48
148
  return {
49
149
  search: {},
50
150
  loading: true,
151
+ job: {},
51
152
  log: [],
52
153
  logDialog: false,
53
154
  }
@@ -58,21 +159,87 @@ export default {
58
159
  }),
59
160
  filteredLog() {
60
161
  if (!this.search.term || this.search.term.length < 3) {
61
- return this.log
162
+ return this.job.log || []
62
163
  }
63
- const filtered = this.log.filter((item) => {
64
- return item.message
164
+ const filtered = this.job.log.filter((item) => {
165
+ let match = item.message
65
166
  .toLowerCase()
66
167
  .includes(this.search.term.toLowerCase())
168
+
169
+ // Search the remote course id
170
+ if (!match && _.get(item, 'context.remote_course.id', null)) {
171
+ match = _.get(item, 'context.remote_course.id', '')
172
+ .toString()
173
+ .toLowerCase()
174
+ .includes(this.search.term.toLowerCase())
175
+ }
176
+
177
+ // Search the remote assessment id
178
+ if (
179
+ !match &&
180
+ _.get(item, 'context.remote_assessment.id', null)
181
+ ) {
182
+ match = _.get(item, 'context.remote_assessment.id', '')
183
+ .toString()
184
+ .toLowerCase()
185
+ .includes(this.search.term.toLowerCase())
186
+ }
187
+
188
+ // Search the remote url
189
+ if (!match && item.context.remote_url) {
190
+ match = item.context.remote_url
191
+ .toLowerCase()
192
+ .includes(this.search.term.toLowerCase())
193
+ }
194
+
195
+ // Search the remote code
196
+ if (!match && item.context.remote_code) {
197
+ match = item.context.remote_code
198
+ .toLowerCase()
199
+ .includes(this.search.term.toLowerCase())
200
+ }
201
+
202
+ return match
67
203
  })
68
204
 
69
205
  return filtered
70
206
  },
207
+ remoteCourse() {
208
+ return _.get(this.job, 'metadata.import.remote_course', {})
209
+ },
210
+ localCourse() {
211
+ return _.get(this.job, 'metadata.import.local_course', {})
212
+ },
213
+ showDetails() {
214
+ return (item) => {
215
+ return (
216
+ (Array.isArray(item.context) && item.context.length > 0) ||
217
+ !_.isEmpty(item.context)
218
+ )
219
+ }
220
+ },
221
+ logItemIcon() {
222
+ return (logItem) => {
223
+ const type = _.get(logItem, 'context.type', null)
224
+ if (type === 'missing_file') {
225
+ return 'mdi-file-alert'
226
+ } else if (type === 'converted_file') {
227
+ return 'mdi-file-arrow-left-right'
228
+ }
229
+
230
+ return undefined
231
+ }
232
+ },
233
+ isUrl() {
234
+ return (url) => {
235
+ return /https?:\/\//.test(url)
236
+ }
237
+ },
71
238
  },
72
239
  methods: {
73
240
  async onViewLog() {
74
241
  this.logDialog = true
75
- this.log = []
242
+ this.job = {}
76
243
 
77
244
  const job = await new Organization({
78
245
  id: this.organization.id,
@@ -81,9 +248,13 @@ export default {
81
248
  .with('log')
82
249
  .find(this.id)
83
250
 
84
- this.log = job.log
251
+ this.job = job
85
252
  this.loading = false
86
253
  },
254
+ onClickCopy(data) {
255
+ navigator.clipboard.writeText(data)
256
+ this.$dialog.show(this.$t('shared.file.copied'))
257
+ },
87
258
  },
88
259
  }
89
260
  </script>
@@ -95,4 +266,16 @@ export default {
95
266
  .col-details {
96
267
  max-width: 100px;
97
268
  }
269
+ .chip-truncate {
270
+ max-width: 40vw;
271
+ direction: rtl;
272
+ }
273
+ .log-item p {
274
+ margin-bottom: 0px;
275
+ }
276
+ .remote-code {
277
+ background: inherit;
278
+ max-width: 40vw;
279
+ padding: 4px;
280
+ }
98
281
  </style>
@@ -49,7 +49,7 @@
49
49
  <th class="text-left">
50
50
  {{
51
51
  $t(
52
- 'windward.integrations.components.integration.job.view_log'
52
+ 'windward.integrations.components.integration.job_log.view_log'
53
53
  )
54
54
  }}
55
55
  </th>
@@ -6,6 +6,7 @@
6
6
  </v-btn>
7
7
  <v-btn v-if="copy" icon @click="copyText(value)">
8
8
  <v-icon>mdi-content-copy</v-icon>
9
+ <span class="sr-only">{{ $t('shared.forms.copy') }}</span>
9
10
  </v-btn>
10
11
  </template>
11
12
  </v-text-field>
@@ -4,6 +4,7 @@ import Canvas from '../helpers/Driver/Canvas'
4
4
  import Desire2Learn from '../helpers/Driver/Desire2Learn'
5
5
  import Moodle from '../helpers/Driver/Moodle'
6
6
  import GoogleClassroom from '../helpers/Driver/GoogleClassroom'
7
+ import Resourcespace from '../helpers/Driver/Resourcespace'
7
8
 
8
9
  export default {
9
10
  /**
@@ -20,5 +21,6 @@ export default {
20
21
  desire2learn: { driver: Desire2Learn },
21
22
  moodle: { driver: Moodle },
22
23
  google_classroom: { driver: GoogleClassroom },
24
+ resourcespace: { driver: Resourcespace },
23
25
  },
24
26
  }
@@ -0,0 +1,15 @@
1
+ // @ts-ignore
2
+ import Manage from '../../components/Integration/Driver/ManageResourcespace.vue'
3
+ import DriverInterface, { IntegrationComponents } from './DriverInterface'
4
+ import BaseDriver from './BaseDriver'
5
+
6
+ export default class Resourcespace
7
+ extends BaseDriver
8
+ implements DriverInterface
9
+ {
10
+ public components(): IntegrationComponents {
11
+ return {
12
+ Manage,
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ import resourcespace from './resourcespace'
2
+
3
+ export default {
4
+ resourcespace,
5
+ }
@@ -0,0 +1,4 @@
1
+ export default {
2
+ resource_id: 'Resource Id',
3
+ load_error: 'Failed to connect to Resourcespace',
4
+ }
@@ -3,6 +3,7 @@ import settings from './settings'
3
3
  import navigation from './navigation'
4
4
  import integration from './integration'
5
5
  import externalIntegration from './external_integration'
6
+ import fileImport from './file_import'
6
7
 
7
8
  export default {
8
9
  content,
@@ -10,4 +11,5 @@ export default {
10
11
  navigation,
11
12
  integration,
12
13
  external_integration: externalIntegration,
14
+ file_import: fileImport,
13
15
  }
@@ -27,6 +27,13 @@ export default {
27
27
  google_classroom: {
28
28
  manage_dialog_title: 'Manage Google Classroom Integration',
29
29
  },
30
+ resourcespace: {
31
+ manage_dialog_title: 'Manage Resourcespace Integration',
32
+ url: 'Resourcespace Url',
33
+ url_hint: 'Eg: https://resourcespace.mindedgeuniversity.com',
34
+ username: 'Username',
35
+ key: 'API Key',
36
+ },
30
37
  enabled: 'Integration Enabled',
31
38
  disabled: 'Integration Disabled',
32
39
  ssl_enabled: 'SSL Enabled (Should be enabled for production)',
@@ -1,7 +1,9 @@
1
1
  import driver from './driver'
2
2
  import job from './job'
3
+ import jobLog from './job_log'
3
4
 
4
5
  export default {
5
6
  driver,
6
7
  job,
8
+ job_log: jobLog,
7
9
  }
@@ -9,8 +9,6 @@ export default {
9
9
  created: 'Created',
10
10
  details: 'Details',
11
11
  started: 'Date started',
12
- view_log: 'View Log',
13
- log_no_results: 'No results found',
14
12
  job_details: {
15
13
  none: 'No details available',
16
14
  import_course: "Importing Course Id {0} '{1}'",
@@ -0,0 +1,8 @@
1
+ export default {
2
+ view_log: 'View Log',
3
+ no_results: 'No results found',
4
+ remote_course_id: 'Remote Course Id',
5
+ remote_content_id: 'Remtote Content Id',
6
+ remote_url: 'Remote Url',
7
+ remote_code: 'Remote Code',
8
+ }
@@ -5,4 +5,5 @@ export default {
5
5
  load_remote_content_failed: 'Could not load remote content',
6
6
  connect_success: 'Successfully connected',
7
7
  connect_fail: 'Failed to connect',
8
+ unknown: 'An unknown error occurred when trying to connect',
8
9
  }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ import_resourcespace: 'Import from Resourcespace',
3
+ import_google_drive: 'Import from Google Drive',
4
+ import_dropbox: 'Import from Dropbox',
5
+ }
@@ -2,6 +2,7 @@ import contentBlocks from './content_blocks'
2
2
  import settings from './settings'
3
3
  import menu from './menu'
4
4
  import permission from './permission'
5
+ import file from './file'
5
6
  import error from './error'
6
7
 
7
8
  export default {
@@ -9,5 +10,6 @@ export default {
9
10
  settings,
10
11
  menu,
11
12
  permission,
13
+ file,
12
14
  error,
13
15
  }
@@ -1,5 +1,6 @@
1
1
  import Model from '~/models/Model'
2
2
  import RemoteOrganization from './RemoteOrganization'
3
+ import RemoteFile from './RemoteFile'
3
4
 
4
5
  export default class OrganizationIntegration extends Model {
5
6
  get required(): string[] {
@@ -14,4 +15,8 @@ export default class OrganizationIntegration extends Model {
14
15
  remoteOrganizations() {
15
16
  return this.hasMany(RemoteOrganization)
16
17
  }
18
+
19
+ remoteFiles() {
20
+ return this.hasMany(RemoteFile)
21
+ }
17
22
  }
@@ -0,0 +1,12 @@
1
+ import Model from '~/models/Model'
2
+
3
+ export default class RemoteFile extends Model {
4
+ get required(): string[] {
5
+ return []
6
+ }
7
+
8
+ // Set the resource route of the model
9
+ resource() {
10
+ return 'remote-files'
11
+ }
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/integrations",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Windward UI Plugin Integrations for 3rd Party Systems",
5
5
  "main": "plugin.js",
6
6
  "scripts": {
@@ -210,13 +210,19 @@ export default {
210
210
  },
211
211
  async fetch() {
212
212
  this.loading.integration = true
213
- this.organizationIntegrations = await new Organization({
213
+ const organizationIntegrations = await new Organization({
214
214
  id: this.organization.id,
215
215
  })
216
216
  .integrations()
217
217
  .with(['vendor'])
218
218
  .where('enabled', true)
219
219
  .get()
220
+
221
+ // Filter out any vendors that don't support course imports
222
+ this.organizationIntegrations = organizationIntegrations.filter((o) => {
223
+ return _.get(o, 'vendor.driver.course_import', false)
224
+ })
225
+
220
226
  this.loading.integration = false
221
227
  },
222
228
  computed: {
package/plugin.js CHANGED
@@ -9,6 +9,11 @@ import IntegrationHelper from './helpers/IntegrationHelper'
9
9
  import LtiConsumerBlock from './components/Content/Blocks/ExternalIntegration/LtiConsumer'
10
10
  import LtiConsumerBlockSettings from './components/Settings/ExternalIntegration/LtiConsumerSettings'
11
11
 
12
+ import FileImportMenu from './components/FileImport/FileImportMenu.vue'
13
+ import FileImportResourcespace from './components/FileImport/Resourcespace.vue'
14
+ import FileImportGoogleDrive from './components/FileImport/GoogleDrive.vue'
15
+ import FileImportDropbox from './components/FileImport/Dropbox.vue'
16
+
12
17
  export default {
13
18
  name: 'windward.integrations.name',
14
19
  hooks: {
@@ -204,6 +209,37 @@ export default {
204
209
  },
205
210
  },
206
211
  ],
212
+ fileImport: [
213
+ {
214
+ tag: 'windward-integrations-file-import-menu',
215
+ template: FileImportMenu,
216
+ metadata: {
217
+ vendors: [
218
+ {
219
+ template: FileImportResourcespace,
220
+ i18n: 'windward.integrations.shared.file.import_resourcespace',
221
+ icon: 'mdi-cube',
222
+ product_code: 'resourcespace',
223
+ disabled: false,
224
+ },
225
+ {
226
+ template: FileImportGoogleDrive,
227
+ i18n: 'windward.integrations.shared.file.import_google_drive',
228
+ icon: 'mdi-google-drive',
229
+ product_code: 'google_drive',
230
+ disabled: true,
231
+ },
232
+ {
233
+ template: FileImportDropbox,
234
+ i18n: 'windward.integrations.shared.file.import_dropbox',
235
+ icon: 'mdi-dropbox',
236
+ product_code: 'dropbox',
237
+ disabled: true,
238
+ },
239
+ ],
240
+ },
241
+ },
242
+ ],
207
243
  },
208
244
  services: {
209
245
  Integration: IntegrationHelper,
@@ -0,0 +1,24 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import Dropbox from '@/components/FileImport/Dropbox.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('Dropbox', () => {
12
+ test('Dropbox is a Vue instance', () => {
13
+ const wrapper = shallowMount(Dropbox, {
14
+ vuetify: new Vuetify(),
15
+ propsData: {
16
+ organizationIntegration: {
17
+ id: '00000000-0000-0000-0000-000000000000',
18
+ },
19
+ },
20
+ mocks: defaultMocks,
21
+ })
22
+ expect(wrapper.vm).toBeTruthy()
23
+ })
24
+ })
@@ -0,0 +1,24 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import GoogleDrive from '@/components/FileImport/GoogleDrive.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('GoogleDrive', () => {
12
+ test('GoogleDrive is a Vue instance', () => {
13
+ const wrapper = shallowMount(GoogleDrive, {
14
+ vuetify: new Vuetify(),
15
+ propsData: {
16
+ organizationIntegration: {
17
+ id: '00000000-0000-0000-0000-000000000000',
18
+ },
19
+ },
20
+ mocks: defaultMocks,
21
+ })
22
+ expect(wrapper.vm).toBeTruthy()
23
+ })
24
+ })
@@ -0,0 +1,24 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import Resourcespace from '@/components/FileImport/Resourcespace.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('Resourcespace', () => {
12
+ test('Resourcespace is a Vue instance', () => {
13
+ const wrapper = shallowMount(Resourcespace, {
14
+ vuetify: new Vuetify(),
15
+ propsData: {
16
+ organizationIntegration: {
17
+ id: '00000000-0000-0000-0000-000000000000',
18
+ },
19
+ },
20
+ mocks: defaultMocks,
21
+ })
22
+ expect(wrapper.vm).toBeTruthy()
23
+ })
24
+ })
@@ -0,0 +1,22 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import ManageAtutor from '@/components/Integration/Driver/ManageAtutor.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('ManageAtutor', () => {
12
+ test('ManageAtutor is a Vue instance', () => {
13
+ const wrapper = shallowMount(ManageAtutor, {
14
+ vuetify: new Vuetify(),
15
+ mocks: defaultMocks,
16
+ propsData: {
17
+ vendor: {},
18
+ },
19
+ })
20
+ expect(wrapper.vm).toBeTruthy()
21
+ })
22
+ })
@@ -0,0 +1,22 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import ManageResourcespace from '@/components/Integration/Driver/ManageResourcespace.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('ManageResourcespace', () => {
12
+ test('ManageResourcespace is a Vue instance', () => {
13
+ const wrapper = shallowMount(ManageResourcespace, {
14
+ vuetify: new Vuetify(),
15
+ mocks: defaultMocks,
16
+ propsData: {
17
+ vendor: {},
18
+ },
19
+ })
20
+ expect(wrapper.vm).toBeTruthy()
21
+ })
22
+ })
@@ -11,6 +11,18 @@ jest.mock(
11
11
  { virtual: true }
12
12
  )
13
13
 
14
+ jest.mock(
15
+ '~/components/Breadcrumbs.vue',
16
+ () => {
17
+ return {
18
+ data() {
19
+ return { validation: {} }
20
+ },
21
+ }
22
+ },
23
+ { virtual: true }
24
+ )
25
+
14
26
  jest.mock(
15
27
  '~/components/SearchField.vue',
16
28
  () => {
@@ -30,6 +30,7 @@ const mockModels = [
30
30
  resource: 'remote-organizations',
31
31
  },
32
32
  { path: '../../models/Vendor', resource: 'vendors' },
33
+ { path: '../../models/RemoteFile', resource: 'remote-files' },
33
34
  ]
34
35
 
35
36
  // DO NOT ALTER THE BELOW CODE