@windward/integrations 0.0.9 → 0.0.11

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 (34) 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 +370 -45
  8. package/components/Integration/JobTable.vue +18 -2
  9. package/components/Navigation/Items/CourseJobLog.vue +55 -0
  10. package/components/SecretField.vue +1 -0
  11. package/config/integration.config.js +2 -0
  12. package/helpers/Driver/Resourcespace.ts +15 -0
  13. package/i18n/en-US/components/file_import/index.ts +5 -0
  14. package/i18n/en-US/components/file_import/resourcespace.ts +4 -0
  15. package/i18n/en-US/components/index.ts +2 -0
  16. package/i18n/en-US/components/integration/driver.ts +7 -0
  17. package/i18n/en-US/components/integration/index.ts +2 -0
  18. package/i18n/en-US/components/integration/job.ts +0 -2
  19. package/i18n/en-US/components/integration/job_log.ts +24 -0
  20. package/i18n/en-US/shared/error.ts +1 -0
  21. package/i18n/en-US/shared/file.ts +5 -0
  22. package/i18n/en-US/shared/index.ts +2 -0
  23. package/models/OrganizationIntegration.ts +5 -0
  24. package/models/RemoteFile.ts +12 -0
  25. package/package.json +1 -1
  26. package/pages/admin/importCourse.vue +7 -1
  27. package/plugin.js +50 -0
  28. package/test/Components/FileImport/Dropbox.spec.js +24 -0
  29. package/test/Components/FileImport/GoogleDrive.spec.js +24 -0
  30. package/test/Components/FileImport/Resourcespace.spec.js +24 -0
  31. package/test/Components/Integration/Driver/ManageAtutor.spec.js +22 -0
  32. package/test/Components/Integration/Driver/ManageResourcespace.spec.js +22 -0
  33. package/test/__mocks__/componentsMock.js +12 -0
  34. 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>