@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.
- package/components/FileImport/Dropbox.vue +9 -0
- package/components/FileImport/FileImportMenu.vue +111 -0
- package/components/FileImport/GoogleDrive.vue +9 -0
- package/components/FileImport/Resourcespace.vue +202 -0
- package/components/Integration/Driver/ManageAtutor.vue +20 -12
- package/components/Integration/Driver/ManageResourcespace.vue +137 -0
- package/components/Integration/JobLog.vue +370 -45
- package/components/Integration/JobTable.vue +18 -2
- package/components/Navigation/Items/CourseJobLog.vue +55 -0
- package/components/SecretField.vue +1 -0
- package/config/integration.config.js +2 -0
- package/helpers/Driver/Resourcespace.ts +15 -0
- package/i18n/en-US/components/file_import/index.ts +5 -0
- package/i18n/en-US/components/file_import/resourcespace.ts +4 -0
- package/i18n/en-US/components/index.ts +2 -0
- package/i18n/en-US/components/integration/driver.ts +7 -0
- package/i18n/en-US/components/integration/index.ts +2 -0
- package/i18n/en-US/components/integration/job.ts +0 -2
- package/i18n/en-US/components/integration/job_log.ts +24 -0
- package/i18n/en-US/shared/error.ts +1 -0
- package/i18n/en-US/shared/file.ts +5 -0
- package/i18n/en-US/shared/index.ts +2 -0
- package/models/OrganizationIntegration.ts +5 -0
- package/models/RemoteFile.ts +12 -0
- package/package.json +1 -1
- package/pages/admin/importCourse.vue +7 -1
- package/plugin.js +50 -0
- package/test/Components/FileImport/Dropbox.spec.js +24 -0
- package/test/Components/FileImport/GoogleDrive.spec.js +24 -0
- package/test/Components/FileImport/Resourcespace.spec.js +24 -0
- package/test/Components/Integration/Driver/ManageAtutor.spec.js +22 -0
- package/test/Components/Integration/Driver/ManageResourcespace.spec.js +22 -0
- package/test/__mocks__/componentsMock.js +12 -0
- package/test/__mocks__/modelMock.js +1 -0
|
@@ -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,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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
let response = { result: false }
|
|
134
|
+
try {
|
|
135
|
+
response = await this.testConnection(this.integration.metadata)
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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.
|
|
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>
|