@windward/integrations 0.0.6 → 0.0.7
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/Content/Blocks/ExternalIntegration/LtiConsumer.vue +22 -23
- package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumer.vue +9 -7
- package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumers.vue +12 -11
- package/components/ExternalIntegration/Driver/Lti1p1/ManageProvider.vue +36 -6
- package/components/ExternalIntegration/Driver/Lti1p1/ManageProviders.vue +36 -17
- package/components/ExternalIntegration/ProviderTargetPicker.vue +234 -0
- package/components/ExternalIntegration/ProviderTargetViewer.vue +50 -0
- package/components/Integration/Driver/ManageBase.vue +3 -1
- package/components/Integration/JobTable.vue +2 -1
- package/components/SecretField.vue +1 -1
- package/components/Settings/ExternalIntegration/LtiConsumerSettings.vue +16 -19
- package/i18n/en-US/components/external_integration/driver/lti1p1.ts +1 -0
- package/i18n/en-US/components/external_integration/index.ts +2 -1
- package/i18n/en-US/components/external_integration/provider_target.ts +9 -0
- package/package.json +1 -1
- package/pages/admin/importCourse.vue +3 -0
- package/pages/admin/vendors.vue +2 -1
- package/plugin.js +1 -1
- package/test/Components/ExternalIntegration/ProviderTargetPicker.spec.js +22 -0
- package/test/Components/ExternalIntegration/ProviderTargetViewer.spec.js +22 -0
- package/test/__mocks__/componentsMock.js +12 -0
- package/test/__mocks__/modelMock.js +1 -1
- package/test/mocks.js +1 -0
|
@@ -28,16 +28,16 @@
|
|
|
28
28
|
</v-alert>
|
|
29
29
|
|
|
30
30
|
<v-form
|
|
31
|
-
name="ltiForm"
|
|
32
31
|
ref="ltiForm"
|
|
32
|
+
name="ltiForm"
|
|
33
33
|
:target="target"
|
|
34
34
|
:action="launchData.target || ''"
|
|
35
35
|
:method="launchData.method || 'POST'"
|
|
36
36
|
>
|
|
37
37
|
<input
|
|
38
|
-
type="hidden"
|
|
39
38
|
v-for="(value, key) of launchData.payload"
|
|
40
39
|
:key="key"
|
|
40
|
+
type="hidden"
|
|
41
41
|
:name="key"
|
|
42
42
|
:value="value"
|
|
43
43
|
/>
|
|
@@ -83,17 +83,8 @@ import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consume
|
|
|
83
83
|
|
|
84
84
|
export default {
|
|
85
85
|
name: 'ContentBlockExternalIntegrationLti1p1Consumer',
|
|
86
|
-
extends: BaseContentBlock,
|
|
87
86
|
components: { TextViewer },
|
|
88
87
|
extends: BaseContentBlock,
|
|
89
|
-
beforeMount() {
|
|
90
|
-
if (_.isEmpty(this.block.metadata.config)) {
|
|
91
|
-
this.block.metadata.config = {}
|
|
92
|
-
}
|
|
93
|
-
if (_.isEmpty(this.block.metadata.config.tool_id)) {
|
|
94
|
-
this.block.metadata.config.tool_id = null
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
88
|
data() {
|
|
98
89
|
return {
|
|
99
90
|
consumer: null,
|
|
@@ -105,6 +96,17 @@ export default {
|
|
|
105
96
|
launched: false,
|
|
106
97
|
}
|
|
107
98
|
},
|
|
99
|
+
async fetch() {
|
|
100
|
+
this.target = this.frameId
|
|
101
|
+
await this.loadConsumer()
|
|
102
|
+
},
|
|
103
|
+
computed: {
|
|
104
|
+
...mapGetters({
|
|
105
|
+
organization: 'organization/get',
|
|
106
|
+
course: 'course/get',
|
|
107
|
+
enrollment: 'enrollment/get',
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
108
110
|
watch: {
|
|
109
111
|
'block.metadata.config.tool_id': {
|
|
110
112
|
deep: true,
|
|
@@ -116,16 +118,13 @@ export default {
|
|
|
116
118
|
},
|
|
117
119
|
},
|
|
118
120
|
},
|
|
119
|
-
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
course: 'course/get',
|
|
127
|
-
enrollment: 'enrollment/get',
|
|
128
|
-
}),
|
|
121
|
+
beforeMount() {
|
|
122
|
+
if (_.isEmpty(this.block.metadata.config)) {
|
|
123
|
+
this.block.metadata.config = {}
|
|
124
|
+
}
|
|
125
|
+
if (_.isEmpty(this.block.metadata.config.tool_id)) {
|
|
126
|
+
this.block.metadata.config.tool_id = null
|
|
127
|
+
}
|
|
129
128
|
},
|
|
130
129
|
mounted() {},
|
|
131
130
|
methods: {
|
|
@@ -186,8 +185,8 @@ export default {
|
|
|
186
185
|
)
|
|
187
186
|
}
|
|
188
187
|
} catch (e) {
|
|
189
|
-
|
|
190
|
-
console.error(e)
|
|
188
|
+
// eslint-disable-next-line no-console
|
|
189
|
+
console.error('LTI Link Launch Fail', e)
|
|
191
190
|
}
|
|
192
191
|
},
|
|
193
192
|
onBeforeSave() {
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
$t('shared.forms.description')
|
|
32
32
|
}}</label>
|
|
33
33
|
<TextEditor
|
|
34
|
-
v-model="consumer.description"
|
|
35
34
|
id="description"
|
|
35
|
+
v-model="consumer.description"
|
|
36
36
|
:height="200"
|
|
37
37
|
></TextEditor>
|
|
38
38
|
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
hide-default-footer
|
|
87
87
|
class="elevation-1"
|
|
88
88
|
>
|
|
89
|
-
<template #item.key="{ index }">
|
|
89
|
+
<template #[`item.key`]="{ index }">
|
|
90
90
|
<v-text-field
|
|
91
91
|
v-model="
|
|
92
92
|
consumer.metadata.custom[index].key
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"
|
|
99
99
|
/>
|
|
100
100
|
</template>
|
|
101
|
-
<template #item.value="{ index }">
|
|
101
|
+
<template #[`item.value`]="{ index }">
|
|
102
102
|
<v-text-field
|
|
103
103
|
v-model="
|
|
104
104
|
consumer.metadata.custom[index].value
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"
|
|
111
111
|
/>
|
|
112
112
|
</template>
|
|
113
|
-
<template #item.actions="{ index }">
|
|
113
|
+
<template #[`item.actions`]="{ index }">
|
|
114
114
|
<v-btn
|
|
115
115
|
text
|
|
116
116
|
color="primary"
|
|
@@ -152,11 +152,11 @@
|
|
|
152
152
|
<script>
|
|
153
153
|
import _ from 'lodash'
|
|
154
154
|
import { mapGetters } from 'vuex'
|
|
155
|
-
import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consumer'
|
|
156
|
-
import FormVue from '~/components/Form'
|
|
157
155
|
import Organization from '~/models/Organization'
|
|
158
156
|
import Course from '~/models/Course'
|
|
159
157
|
import TextEditor from '~/components/Text/TextEditor.vue'
|
|
158
|
+
import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consumer'
|
|
159
|
+
import FormVue from '~/components/Form'
|
|
160
160
|
|
|
161
161
|
export default {
|
|
162
162
|
name: 'ManageLti1p1ConsumerDriver',
|
|
@@ -240,11 +240,13 @@ export default {
|
|
|
240
240
|
duration: null,
|
|
241
241
|
action: {
|
|
242
242
|
text: this.$t('shared.forms.close'),
|
|
243
|
-
onClick: (
|
|
243
|
+
onClick: (_e, toastObject) => {
|
|
244
244
|
toastObject.goAway(0)
|
|
245
245
|
},
|
|
246
246
|
},
|
|
247
247
|
})
|
|
248
|
+
|
|
249
|
+
// eslint-disable-next-line no-console
|
|
248
250
|
console.error('You do not have access to this consumer!')
|
|
249
251
|
|
|
250
252
|
// Return so we don't even attempt loading
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
:items-per-page="10"
|
|
24
24
|
class="elevation-1"
|
|
25
25
|
>
|
|
26
|
-
<template #item.description="{ item }">
|
|
26
|
+
<template #[`item.description`]="{ item }">
|
|
27
27
|
{{ item.description.replace(/(<([^>]+)>)/gi, '').trim() }}
|
|
28
28
|
</template>
|
|
29
29
|
|
|
30
|
-
<template #item.enabled="{ item }">
|
|
30
|
+
<template #[`item.enabled`]="{ item }">
|
|
31
31
|
<v-icon :color="item.enabled ? 'success' : 'error'"
|
|
32
32
|
>{{ item.enabled ? 'mdi-check' : 'mdi-close' }}
|
|
33
33
|
</v-icon>
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
}}</span>
|
|
39
39
|
</template>
|
|
40
40
|
|
|
41
|
-
<template #item.created_at="{ item }">
|
|
41
|
+
<template #[`item.created_at`]="{ item }">
|
|
42
42
|
{{ $d(new Date(item.created_at), 'long') }}
|
|
43
43
|
</template>
|
|
44
44
|
<template #[`item.actions`]="{ index, item }">
|
|
@@ -77,16 +77,15 @@
|
|
|
77
77
|
<script>
|
|
78
78
|
import _ from 'lodash'
|
|
79
79
|
import { mapGetters } from 'vuex'
|
|
80
|
-
import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consumer'
|
|
81
|
-
import SecretField from '../../../SecretField.vue'
|
|
82
|
-
import ManageConsumer from './ManageConsumer.vue'
|
|
83
80
|
import Course from '~/models/Course'
|
|
84
81
|
import Organization from '~/models/Organization'
|
|
85
82
|
import Dialog from '~/components/Dialog.vue'
|
|
83
|
+
import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consumer'
|
|
84
|
+
import ManageConsumer from './ManageConsumer.vue'
|
|
86
85
|
|
|
87
86
|
export default {
|
|
88
87
|
name: 'ManageLti1p1ConsumersDriver',
|
|
89
|
-
components: {
|
|
88
|
+
components: { Dialog, ManageConsumer },
|
|
90
89
|
data() {
|
|
91
90
|
return {
|
|
92
91
|
consumers: [],
|
|
@@ -139,16 +138,18 @@ export default {
|
|
|
139
138
|
duration: null,
|
|
140
139
|
action: {
|
|
141
140
|
text: this.$t('shared.forms.close'),
|
|
142
|
-
onClick: (
|
|
141
|
+
onClick: (_e, toastObject) => {
|
|
143
142
|
toastObject.goAway(0)
|
|
144
143
|
},
|
|
145
144
|
},
|
|
146
145
|
})
|
|
147
146
|
if (_.isEmpty(this.organization.id) || _.isEmpty(this.course.id)) {
|
|
147
|
+
// eslint-disable-next-line no-console
|
|
148
148
|
console.error(
|
|
149
149
|
'Cannot load external integrations because organization or course is not set!'
|
|
150
150
|
)
|
|
151
151
|
} else {
|
|
152
|
+
// eslint-disable-next-line no-console
|
|
152
153
|
console.error(
|
|
153
154
|
'You do not have access to this external integration!'
|
|
154
155
|
)
|
|
@@ -167,7 +168,7 @@ export default {
|
|
|
167
168
|
}),
|
|
168
169
|
},
|
|
169
170
|
methods: {
|
|
170
|
-
onSaved(
|
|
171
|
+
onSaved() {
|
|
171
172
|
this.loadConsumers()
|
|
172
173
|
},
|
|
173
174
|
async loadConsumers() {
|
|
@@ -185,14 +186,14 @@ export default {
|
|
|
185
186
|
action: [
|
|
186
187
|
{
|
|
187
188
|
text: this.$t('shared.forms.cancel'),
|
|
188
|
-
onClick: (
|
|
189
|
+
onClick: (_e, toastObject) => {
|
|
189
190
|
toastObject.goAway(0)
|
|
190
191
|
},
|
|
191
192
|
},
|
|
192
193
|
{
|
|
193
194
|
text: this.$t('shared.forms.confirm'),
|
|
194
195
|
// router navigation
|
|
195
|
-
onClick: (
|
|
196
|
+
onClick: (_e, toastObject) => {
|
|
196
197
|
this.deleteConsumer(consumer)
|
|
197
198
|
toastObject.goAway(0)
|
|
198
199
|
},
|
|
@@ -4,11 +4,18 @@
|
|
|
4
4
|
<v-progress-circular size="128" indeterminate />
|
|
5
5
|
</div>
|
|
6
6
|
<div v-if="render">
|
|
7
|
-
<v-form v-model="formValid" @submit.prevent>
|
|
8
|
-
<v-row justify="center" align="center"
|
|
7
|
+
<v-form ref="form" v-model="formValid" @submit.prevent>
|
|
8
|
+
<v-row justify="center" align="center">
|
|
9
9
|
<v-col cols="12">
|
|
10
|
+
<ProviderTargetPicker
|
|
11
|
+
:target.sync="provider.target"
|
|
12
|
+
:metadata.sync="provider.metadata"
|
|
13
|
+
></ProviderTargetPicker>
|
|
14
|
+
|
|
10
15
|
<v-text-field
|
|
16
|
+
id="target-url"
|
|
11
17
|
v-model="provider.target"
|
|
18
|
+
disabled
|
|
12
19
|
:label="
|
|
13
20
|
$t(
|
|
14
21
|
'windward.integrations.components.external_integration.driver.lti1p1.target_url'
|
|
@@ -19,6 +26,7 @@
|
|
|
19
26
|
'windward.integrations.components.external_integration.driver.lti1p1.target_url'
|
|
20
27
|
)
|
|
21
28
|
"
|
|
29
|
+
:rules="validation.existsRules"
|
|
22
30
|
></v-text-field>
|
|
23
31
|
|
|
24
32
|
<v-switch
|
|
@@ -31,6 +39,7 @@
|
|
|
31
39
|
/>
|
|
32
40
|
|
|
33
41
|
<v-text-field
|
|
42
|
+
id="lti-key"
|
|
34
43
|
v-model="provider.metadata.key"
|
|
35
44
|
:placeholder="
|
|
36
45
|
$t(
|
|
@@ -50,6 +59,7 @@
|
|
|
50
59
|
></v-text-field>
|
|
51
60
|
|
|
52
61
|
<v-text-field
|
|
62
|
+
id="lti-secret"
|
|
53
63
|
v-model="provider.metadata.secret"
|
|
54
64
|
:placeholder="
|
|
55
65
|
$t(
|
|
@@ -240,15 +250,16 @@
|
|
|
240
250
|
<script>
|
|
241
251
|
import _ from 'lodash'
|
|
242
252
|
import { mapGetters } from 'vuex'
|
|
243
|
-
import
|
|
244
|
-
import FormVue from '~/components/Form'
|
|
253
|
+
import FormVue from '~/components/Form.vue'
|
|
245
254
|
import Role from '~/models/Role'
|
|
246
255
|
import Organization from '~/models/Organization'
|
|
247
256
|
import Course from '~/models/Course'
|
|
257
|
+
import ProviderTargetPicker from '../../ProviderTargetPicker.vue'
|
|
258
|
+
import Lti1p1Provider from '../../../../models/ExternalIntegration/Lti1p1Provider'
|
|
248
259
|
|
|
249
260
|
export default {
|
|
250
261
|
name: 'ManageLti1p1ProviderDriver',
|
|
251
|
-
components: {},
|
|
262
|
+
components: { ProviderTargetPicker },
|
|
252
263
|
extends: FormVue,
|
|
253
264
|
props: {
|
|
254
265
|
value: {
|
|
@@ -293,6 +304,7 @@ export default {
|
|
|
293
304
|
...mapGetters({
|
|
294
305
|
organization: 'organization/get',
|
|
295
306
|
course: 'course/get',
|
|
307
|
+
contentTree: 'content/getTree',
|
|
296
308
|
}),
|
|
297
309
|
},
|
|
298
310
|
created() {
|
|
@@ -317,11 +329,12 @@ export default {
|
|
|
317
329
|
duration: null,
|
|
318
330
|
action: {
|
|
319
331
|
text: this.$t('shared.forms.close'),
|
|
320
|
-
onClick: (
|
|
332
|
+
onClick: (_e, toastObject) => {
|
|
321
333
|
toastObject.goAway(0)
|
|
322
334
|
},
|
|
323
335
|
},
|
|
324
336
|
})
|
|
337
|
+
// eslint-disable-next-line no-console
|
|
325
338
|
console.error('You do not have access to this provider!')
|
|
326
339
|
|
|
327
340
|
// Return so we don't even attempt loading
|
|
@@ -358,13 +371,30 @@ export default {
|
|
|
358
371
|
|
|
359
372
|
this.$dialog.success(this.$t('shared.forms.saved'))
|
|
360
373
|
this.$emit('update:provider', providerEvent)
|
|
374
|
+
this.$emit('input', provider)
|
|
361
375
|
} catch (e) {
|
|
362
376
|
this.$dialog.error(
|
|
363
377
|
this.$t('windward.integrations.shared.error.save_failed')
|
|
364
378
|
)
|
|
365
379
|
}
|
|
380
|
+
|
|
381
|
+
// Create a new provider to clear the form if the original value is empty aka new
|
|
382
|
+
if (_.isEmpty(this.value)) {
|
|
383
|
+
this.provider = new Lti1p1Provider({
|
|
384
|
+
role_metadata: {},
|
|
385
|
+
metadata: {},
|
|
386
|
+
})
|
|
387
|
+
}
|
|
366
388
|
},
|
|
367
389
|
async onSave() {
|
|
390
|
+
this.$refs.form.validate()
|
|
391
|
+
if (this.formValid) {
|
|
392
|
+
await this.save()
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
async onSaveAndNew() {
|
|
397
|
+
this.$refs.form.validate()
|
|
368
398
|
if (this.formValid) {
|
|
369
399
|
await this.save()
|
|
370
400
|
}
|
|
@@ -23,17 +23,23 @@
|
|
|
23
23
|
:items-per-page="10"
|
|
24
24
|
class="elevation-1"
|
|
25
25
|
>
|
|
26
|
-
<template #item.
|
|
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.key`]="{ item }">
|
|
27
33
|
<SecretField v-model="item.metadata.key"></SecretField>
|
|
28
34
|
</template>
|
|
29
|
-
<template #item.metadata.secret="{ item }">
|
|
35
|
+
<template #[`item.metadata.secret`]="{ item }">
|
|
30
36
|
<SecretField v-model="item.metadata.secret"></SecretField>
|
|
31
37
|
</template>
|
|
32
|
-
<template #item.url="{ item }">
|
|
38
|
+
<template #[`item.url`]="{ item }">
|
|
33
39
|
<SecretField v-model="item.url" :hidden="false"></SecretField>
|
|
34
40
|
</template>
|
|
35
41
|
|
|
36
|
-
<template #item.enabled="{ item }">
|
|
42
|
+
<template #[`item.enabled`]="{ item }">
|
|
37
43
|
<v-icon :color="item.enabled ? 'success' : 'error'"
|
|
38
44
|
>{{ item.enabled ? 'mdi-check' : 'mdi-close' }}
|
|
39
45
|
</v-icon>
|
|
@@ -44,8 +50,8 @@
|
|
|
44
50
|
}}</span>
|
|
45
51
|
</template>
|
|
46
52
|
|
|
47
|
-
<template #item.created_at="{ item }">
|
|
48
|
-
{{ $d(new Date(item.created_at), '
|
|
53
|
+
<template #[`item.created_at`]="{ item }">
|
|
54
|
+
{{ $d(new Date(item.created_at), 'shorttime') }}
|
|
49
55
|
</template>
|
|
50
56
|
<template #[`item.actions`]="{ index, item }">
|
|
51
57
|
<Dialog color="primary" action-save @click:save="onSaved">
|
|
@@ -83,23 +89,24 @@
|
|
|
83
89
|
<script>
|
|
84
90
|
import _ from 'lodash'
|
|
85
91
|
import { mapGetters } from 'vuex'
|
|
86
|
-
import Lti1p1Provider from '../../../../models/ExternalIntegration/Lti1p1Provider'
|
|
87
|
-
import SecretField from '../../../SecretField.vue'
|
|
88
|
-
import ManageProvider from './ManageProvider.vue'
|
|
89
92
|
import Course from '~/models/Course'
|
|
90
93
|
import Organization from '~/models/Organization'
|
|
91
94
|
import Dialog from '~/components/Dialog.vue'
|
|
95
|
+
import Lti1p1Provider from '../../../../models/ExternalIntegration/Lti1p1Provider'
|
|
96
|
+
import SecretField from '../../../SecretField.vue'
|
|
97
|
+
import ProviderTargetViewer from '../../ProviderTargetViewer.vue'
|
|
98
|
+
import ManageProvider from './ManageProvider.vue'
|
|
92
99
|
|
|
93
100
|
export default {
|
|
94
101
|
name: 'ManageLti1p1ProvidersDriver',
|
|
95
|
-
components: { SecretField, Dialog, ManageProvider },
|
|
102
|
+
components: { SecretField, Dialog, ManageProvider, ProviderTargetViewer },
|
|
96
103
|
data() {
|
|
97
104
|
return {
|
|
98
105
|
providers: [],
|
|
99
106
|
headers: [
|
|
100
107
|
{
|
|
101
108
|
text: this.$t(
|
|
102
|
-
'windward.integrations.components.external_integration.driver.lti1p1.
|
|
109
|
+
'windward.integrations.components.external_integration.driver.lti1p1.target'
|
|
103
110
|
),
|
|
104
111
|
value: 'target',
|
|
105
112
|
},
|
|
@@ -151,16 +158,18 @@ export default {
|
|
|
151
158
|
duration: null,
|
|
152
159
|
action: {
|
|
153
160
|
text: this.$t('shared.forms.close'),
|
|
154
|
-
onClick: (
|
|
161
|
+
onClick: (_e, toastObject) => {
|
|
155
162
|
toastObject.goAway(0)
|
|
156
163
|
},
|
|
157
164
|
},
|
|
158
165
|
})
|
|
159
166
|
if (_.isEmpty(this.organization.id) || _.isEmpty(this.course.id)) {
|
|
167
|
+
// eslint-disable-next-line no-console
|
|
160
168
|
console.error(
|
|
161
169
|
'Cannot load external integrations because organization or course is not set!'
|
|
162
170
|
)
|
|
163
171
|
} else {
|
|
172
|
+
// eslint-disable-next-line no-console
|
|
164
173
|
console.error(
|
|
165
174
|
'You do not have access to this external integration!'
|
|
166
175
|
)
|
|
@@ -179,7 +188,7 @@ export default {
|
|
|
179
188
|
}),
|
|
180
189
|
},
|
|
181
190
|
methods: {
|
|
182
|
-
onSaved(
|
|
191
|
+
onSaved() {
|
|
183
192
|
this.loadProviders()
|
|
184
193
|
},
|
|
185
194
|
async loadProviders() {
|
|
@@ -197,14 +206,14 @@ export default {
|
|
|
197
206
|
action: [
|
|
198
207
|
{
|
|
199
208
|
text: this.$t('shared.forms.cancel'),
|
|
200
|
-
onClick: (
|
|
209
|
+
onClick: (_e, toastObject) => {
|
|
201
210
|
toastObject.goAway(0)
|
|
202
211
|
},
|
|
203
212
|
},
|
|
204
213
|
{
|
|
205
214
|
text: this.$t('shared.forms.confirm'),
|
|
206
215
|
// router navigation
|
|
207
|
-
onClick: (
|
|
216
|
+
onClick: (_e, toastObject) => {
|
|
208
217
|
this.deleteProvider(provider)
|
|
209
218
|
toastObject.goAway(0)
|
|
210
219
|
},
|
|
@@ -212,9 +221,19 @@ export default {
|
|
|
212
221
|
],
|
|
213
222
|
})
|
|
214
223
|
},
|
|
215
|
-
deleteProvider(provider) {
|
|
216
|
-
provider.delete()
|
|
224
|
+
async deleteProvider(provider) {
|
|
225
|
+
await provider.delete()
|
|
226
|
+
this.$dialog.success(this.$t('shared.response.deleted'))
|
|
227
|
+
// Reload providers now that we deleted one
|
|
228
|
+
this.loadProviders()
|
|
217
229
|
},
|
|
218
230
|
},
|
|
219
231
|
}
|
|
220
232
|
</script>
|
|
233
|
+
|
|
234
|
+
<style scoped>
|
|
235
|
+
.field--target {
|
|
236
|
+
max-width: 15em;
|
|
237
|
+
display: inline-block;
|
|
238
|
+
}
|
|
239
|
+
</style>
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-radio-group
|
|
4
|
+
v-model="localMetadata.target_type"
|
|
5
|
+
row
|
|
6
|
+
@change="onTargetTypeChange"
|
|
7
|
+
>
|
|
8
|
+
<template #label>
|
|
9
|
+
<div>
|
|
10
|
+
{{
|
|
11
|
+
$t(
|
|
12
|
+
'windward.integrations.components.external_integration.provider_target.link_to'
|
|
13
|
+
)
|
|
14
|
+
}}:
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
<v-radio
|
|
18
|
+
:label="
|
|
19
|
+
$t(
|
|
20
|
+
'windward.integrations.components.external_integration.provider_target.whole_course'
|
|
21
|
+
)
|
|
22
|
+
"
|
|
23
|
+
value="course"
|
|
24
|
+
></v-radio>
|
|
25
|
+
<v-radio
|
|
26
|
+
:label="
|
|
27
|
+
$t(
|
|
28
|
+
'windward.integrations.components.external_integration.provider_target.content_with_children'
|
|
29
|
+
)
|
|
30
|
+
"
|
|
31
|
+
value="locked_course_content_children"
|
|
32
|
+
></v-radio>
|
|
33
|
+
<v-radio
|
|
34
|
+
:label="
|
|
35
|
+
$t(
|
|
36
|
+
'windward.integrations.components.external_integration.provider_target.content_no_children'
|
|
37
|
+
)
|
|
38
|
+
"
|
|
39
|
+
value="locked_course_content"
|
|
40
|
+
></v-radio>
|
|
41
|
+
<v-radio
|
|
42
|
+
:label="
|
|
43
|
+
$t(
|
|
44
|
+
'windward.integrations.components.external_integration.provider_target.whole_course_page'
|
|
45
|
+
)
|
|
46
|
+
"
|
|
47
|
+
value="course_content"
|
|
48
|
+
></v-radio>
|
|
49
|
+
</v-radio-group>
|
|
50
|
+
|
|
51
|
+
<div
|
|
52
|
+
v-if="
|
|
53
|
+
localMetadata.target_type === 'course_content' ||
|
|
54
|
+
localMetadata.target_type ===
|
|
55
|
+
'locked_course_content_children' ||
|
|
56
|
+
localMetadata.target_type === 'locked_course_content'
|
|
57
|
+
"
|
|
58
|
+
class="content-tree secondary"
|
|
59
|
+
>
|
|
60
|
+
<v-treeview
|
|
61
|
+
v-model="selectedTreeviewContent"
|
|
62
|
+
selectable
|
|
63
|
+
selected-color="primary"
|
|
64
|
+
selection-type="independent"
|
|
65
|
+
return-object
|
|
66
|
+
open-all
|
|
67
|
+
:items="contentTree"
|
|
68
|
+
@input="onTreeChange"
|
|
69
|
+
>
|
|
70
|
+
<template #prepend="{ item }">
|
|
71
|
+
<v-chip v-if="isIncludedChild(item)" color="primary">
|
|
72
|
+
{{
|
|
73
|
+
$t(
|
|
74
|
+
'windward.integrations.components.external_integration.provider_target.included'
|
|
75
|
+
)
|
|
76
|
+
}}
|
|
77
|
+
</v-chip>
|
|
78
|
+
</template>
|
|
79
|
+
<template #label="{ item }">
|
|
80
|
+
{{ item.content.name_prefix }}
|
|
81
|
+
{{ item.content.name }}
|
|
82
|
+
</template>
|
|
83
|
+
</v-treeview>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
87
|
+
|
|
88
|
+
<script>
|
|
89
|
+
import _ from 'lodash'
|
|
90
|
+
import { mapGetters } from 'vuex'
|
|
91
|
+
export default {
|
|
92
|
+
name: 'ExternalIntegrationProviderTargetPicker',
|
|
93
|
+
props: {
|
|
94
|
+
target: { type: String, required: true, default: '' },
|
|
95
|
+
metadata: { type: Object, required: true, default: () => {} },
|
|
96
|
+
},
|
|
97
|
+
data() {
|
|
98
|
+
return {
|
|
99
|
+
selectedContent: null,
|
|
100
|
+
selectedTreeviewContent: [],
|
|
101
|
+
localTarget: '',
|
|
102
|
+
localMetadata: {},
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
computed: {
|
|
106
|
+
...mapGetters({
|
|
107
|
+
contentTree: 'content/getTree',
|
|
108
|
+
course: 'course/get',
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
watch: {
|
|
112
|
+
// Apply any changes coming up
|
|
113
|
+
target: {
|
|
114
|
+
handler(value) {
|
|
115
|
+
this.localTarget = _.cloneDeep(value)
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
metadata: {
|
|
119
|
+
deep: true,
|
|
120
|
+
handler(value) {
|
|
121
|
+
this.localMetadata = _.cloneDeep(value)
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
mounted() {
|
|
126
|
+
// Init our local vars
|
|
127
|
+
this.localTarget = _.cloneDeep(this.target)
|
|
128
|
+
this.localMetadata = _.cloneDeep(this.metadata)
|
|
129
|
+
|
|
130
|
+
// Set the target_type key if it doesn't exist
|
|
131
|
+
if (!this.localMetadata.target_type) {
|
|
132
|
+
this.localMetadata.target_type = ''
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Preset the selected content
|
|
136
|
+
if (this.localTarget) {
|
|
137
|
+
const match = this.localTarget.match(
|
|
138
|
+
/\/content\/(?<content_id>[a-zA-Z0-9-]{36})/i
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if (!_.isEmpty(match) && _.get(match, 'groups.content_id', null)) {
|
|
142
|
+
this.setSelectedContentById(
|
|
143
|
+
_.get(match, 'groups.content_id', null)
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.sync(this.localMetadata.target_type)
|
|
148
|
+
},
|
|
149
|
+
methods: {
|
|
150
|
+
setSelectedContentById(id) {
|
|
151
|
+
this.selectedContent = { id }
|
|
152
|
+
this.selectedTreeviewContent = [{ id }]
|
|
153
|
+
},
|
|
154
|
+
sync(targetType) {
|
|
155
|
+
let url = ''
|
|
156
|
+
|
|
157
|
+
if (
|
|
158
|
+
this.selectedContent &&
|
|
159
|
+
(targetType === 'course_content' ||
|
|
160
|
+
targetType === 'locked_course_content' ||
|
|
161
|
+
targetType === 'locked_course_content_children')
|
|
162
|
+
) {
|
|
163
|
+
url = `/course/${this.course.id}/content/${this.selectedContent.id}`
|
|
164
|
+
} else if (targetType === 'course') {
|
|
165
|
+
url = `/course/${this.course.id}/`
|
|
166
|
+
}
|
|
167
|
+
this.localTarget = url
|
|
168
|
+
// Sync up the changes to the parent component
|
|
169
|
+
this.$emit('update:target', this.localTarget)
|
|
170
|
+
this.$emit('update:metadata', this.localMetadata)
|
|
171
|
+
},
|
|
172
|
+
onTargetTypeChange(e) {
|
|
173
|
+
// Reset the selected content if you pick by course
|
|
174
|
+
if (e === 'course') {
|
|
175
|
+
this.selectedContent = null
|
|
176
|
+
this.selectedTreeviewContent = []
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.sync(e)
|
|
180
|
+
},
|
|
181
|
+
isIncludedChild(page, children = null) {
|
|
182
|
+
let isIncluded = false
|
|
183
|
+
// If we aren't including children then return hard false
|
|
184
|
+
if (
|
|
185
|
+
this.localMetadata.target_type !==
|
|
186
|
+
'locked_course_content_children'
|
|
187
|
+
) {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// First run, assign the children from the selected page
|
|
192
|
+
if (this.selectedContent && children === null) {
|
|
193
|
+
children = _.get(this.selectedContent, 'children', [])
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.selectedContent && page.parent_id) {
|
|
197
|
+
if (this.selectedContent.id === page.parent_id) {
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < children.length; i++) {
|
|
202
|
+
const child = children[i]
|
|
203
|
+
|
|
204
|
+
if (child.id === page.parent_id) {
|
|
205
|
+
return true
|
|
206
|
+
} else if (child.children && child.children.length) {
|
|
207
|
+
isIncluded = this.isIncludedChild(page, child.children)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return isIncluded
|
|
212
|
+
},
|
|
213
|
+
onTreeChange() {
|
|
214
|
+
// Enforce that only 1 item is selected at a time in the treeview
|
|
215
|
+
if (this.selectedTreeviewContent.length > 0) {
|
|
216
|
+
const last = this.selectedTreeviewContent.pop()
|
|
217
|
+
this.selectedTreeviewContent = [last]
|
|
218
|
+
this.selectedContent = last
|
|
219
|
+
} else {
|
|
220
|
+
this.selectedContent = null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.sync(this.localMetadata.target_type)
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
</script>
|
|
228
|
+
|
|
229
|
+
<style scoped>
|
|
230
|
+
.content-tree {
|
|
231
|
+
height: 230px;
|
|
232
|
+
overflow-y: auto;
|
|
233
|
+
}
|
|
234
|
+
</style>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span>{{ targetDisplayName }}</span>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
import _ from 'lodash'
|
|
7
|
+
import { mapGetters } from 'vuex'
|
|
8
|
+
export default {
|
|
9
|
+
name: 'ExternalIntegrationProviderTargetViewer',
|
|
10
|
+
props: {
|
|
11
|
+
target: { type: String, required: true, default: '' },
|
|
12
|
+
},
|
|
13
|
+
computed: {
|
|
14
|
+
...mapGetters({
|
|
15
|
+
organization: 'organization/get',
|
|
16
|
+
course: 'course/get',
|
|
17
|
+
contentTree: 'content/getTree',
|
|
18
|
+
}),
|
|
19
|
+
targetDisplayName() {
|
|
20
|
+
if (_.isEmpty(this.target)) {
|
|
21
|
+
return ''
|
|
22
|
+
}
|
|
23
|
+
const match = this.target.match(
|
|
24
|
+
/\/content\/(?<content_id>[a-zA-Z0-9-]{36})/i
|
|
25
|
+
)
|
|
26
|
+
if (!_.isEmpty(match) && _.get(match, 'groups.content_id', null)) {
|
|
27
|
+
const flatTree = this.$ContentService.flattenTree(
|
|
28
|
+
this.contentTree
|
|
29
|
+
)
|
|
30
|
+
const item = flatTree.find(
|
|
31
|
+
(c) => c.id === _.get(match, 'groups.content_id', null)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if (!_.isEmpty(item)) {
|
|
35
|
+
return (
|
|
36
|
+
_.get(item, 'content.name_prefix', '') +
|
|
37
|
+
' ' +
|
|
38
|
+
_.get(item, 'content.name', '')
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
return this.$t(
|
|
43
|
+
'windward.integrations.components.external_integration.provider_target.whole_course'
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return ''
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
@@ -38,11 +38,12 @@ export default {
|
|
|
38
38
|
duration: null,
|
|
39
39
|
action: {
|
|
40
40
|
text: this.$t('shared.forms.close'),
|
|
41
|
-
onClick: (
|
|
41
|
+
onClick: (_e, toastObject) => {
|
|
42
42
|
toastObject.goAway(0)
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
45
|
})
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
46
47
|
console.error('You do not have access to this integration!')
|
|
47
48
|
|
|
48
49
|
// Return so we don't even attempt loading
|
|
@@ -129,6 +130,7 @@ export default {
|
|
|
129
130
|
},
|
|
130
131
|
|
|
131
132
|
onIntegrationLoaded() {
|
|
133
|
+
// eslint-disable-next-line no-console
|
|
132
134
|
console.warn(
|
|
133
135
|
'Integration/Driver/ManageBase.vue onIntegrationLoaded called. Not extended!'
|
|
134
136
|
)
|
|
@@ -247,7 +247,7 @@ export default {
|
|
|
247
247
|
connect(channel, event) {
|
|
248
248
|
this.socket.connector.pusher.connection.bind(
|
|
249
249
|
'connected',
|
|
250
|
-
function (
|
|
250
|
+
function (_socket) {
|
|
251
251
|
// Connected to socket id: socket.socket_id
|
|
252
252
|
}
|
|
253
253
|
)
|
|
@@ -266,6 +266,7 @@ export default {
|
|
|
266
266
|
|
|
267
267
|
onSocketJob(data) {
|
|
268
268
|
if (_.isEmpty(data.integrationJob)) {
|
|
269
|
+
// eslint-disable-next-line no-console
|
|
269
270
|
console.error('onSocketJob Missing data', data)
|
|
270
271
|
return false
|
|
271
272
|
}
|
|
@@ -46,10 +46,8 @@
|
|
|
46
46
|
</template>
|
|
47
47
|
|
|
48
48
|
<script>
|
|
49
|
-
import _ from 'lodash'
|
|
50
49
|
import { mapGetters } from 'vuex'
|
|
51
50
|
import BaseContentSettings from '~/components/Content/Tool/BaseContentSettings.js'
|
|
52
|
-
import ManageConsumer from '../../ExternalIntegration/Driver/Lti1p1/ManageConsumer.vue'
|
|
53
51
|
import Course from '~/models/Course'
|
|
54
52
|
import Organization from '~/models/Organization'
|
|
55
53
|
import Lti1p1Consumer from '../../../models/ExternalIntegration/Lti1p1Consumer'
|
|
@@ -57,27 +55,10 @@ import Lti1p1Consumer from '../../../models/ExternalIntegration/Lti1p1Consumer'
|
|
|
57
55
|
export default {
|
|
58
56
|
name: 'ContentBlockExternalIntegrationLti1p1ConsumerSettings',
|
|
59
57
|
extends: BaseContentSettings,
|
|
60
|
-
components: { ManageConsumer },
|
|
61
58
|
props: {
|
|
62
59
|
settings: { type: Object, required: false, default: null },
|
|
63
60
|
context: { type: String, required: false, default: 'block' },
|
|
64
61
|
},
|
|
65
|
-
async fetch() {
|
|
66
|
-
this.consumers = await new Lti1p1Consumer()
|
|
67
|
-
.for(
|
|
68
|
-
new Organization({ id: this.organization.id }),
|
|
69
|
-
new Course({ id: this.course.id })
|
|
70
|
-
)
|
|
71
|
-
.orderBy('name')
|
|
72
|
-
.get()
|
|
73
|
-
},
|
|
74
|
-
computed: {
|
|
75
|
-
...mapGetters({
|
|
76
|
-
organization: 'organization/get',
|
|
77
|
-
course: 'course/get',
|
|
78
|
-
}),
|
|
79
|
-
},
|
|
80
|
-
beforeMount() {},
|
|
81
62
|
data() {
|
|
82
63
|
return {
|
|
83
64
|
consumers: [],
|
|
@@ -97,7 +78,23 @@ export default {
|
|
|
97
78
|
],
|
|
98
79
|
}
|
|
99
80
|
},
|
|
81
|
+
async fetch() {
|
|
82
|
+
this.consumers = await new Lti1p1Consumer()
|
|
83
|
+
.for(
|
|
84
|
+
new Organization({ id: this.organization.id }),
|
|
85
|
+
new Course({ id: this.course.id })
|
|
86
|
+
)
|
|
87
|
+
.orderBy('name')
|
|
88
|
+
.get()
|
|
89
|
+
},
|
|
90
|
+
computed: {
|
|
91
|
+
...mapGetters({
|
|
92
|
+
organization: 'organization/get',
|
|
93
|
+
course: 'course/get',
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
100
96
|
watch: {},
|
|
97
|
+
beforeMount() {},
|
|
101
98
|
mounted() {},
|
|
102
99
|
methods: {
|
|
103
100
|
onSelectLink(consumer) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
name: 'Target Name',
|
|
3
|
+
link_to: 'Link To',
|
|
4
|
+
included: 'Included',
|
|
5
|
+
whole_course: 'Whole Course',
|
|
6
|
+
whole_course_page: 'Whole Course Choose Landing Page',
|
|
7
|
+
content_with_children: 'Content with Child Pages',
|
|
8
|
+
content_no_children: 'Single Content Page',
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -287,6 +287,7 @@ export default {
|
|
|
287
287
|
.get()
|
|
288
288
|
} catch (e) {
|
|
289
289
|
if (e.response.status === 404) {
|
|
290
|
+
// eslint-disable-next-line no-console
|
|
290
291
|
console.error(e)
|
|
291
292
|
this.$dialog.error(
|
|
292
293
|
this.$t(
|
|
@@ -329,6 +330,7 @@ export default {
|
|
|
329
330
|
.get()
|
|
330
331
|
} catch (e) {
|
|
331
332
|
if (e.response.status === 404) {
|
|
333
|
+
// eslint-disable-next-line no-console
|
|
332
334
|
console.error(e)
|
|
333
335
|
this.$dialog.error(
|
|
334
336
|
this.$t(
|
|
@@ -374,6 +376,7 @@ export default {
|
|
|
374
376
|
}
|
|
375
377
|
} catch (e) {
|
|
376
378
|
if (e.response.status === 404) {
|
|
379
|
+
// eslint-disable-next-line no-console
|
|
377
380
|
console.error(e)
|
|
378
381
|
this.$dialog.error(
|
|
379
382
|
this.$t(
|
package/pages/admin/vendors.vue
CHANGED
|
@@ -151,9 +151,9 @@
|
|
|
151
151
|
import _ from 'lodash'
|
|
152
152
|
import { mapGetters } from 'vuex'
|
|
153
153
|
|
|
154
|
+
import Dialog from '~/components/Dialog.vue'
|
|
154
155
|
import Organization from '../../models/Organization'
|
|
155
156
|
import IntegrationJobTable from '../../components/Integration/JobTable.vue'
|
|
156
|
-
import Dialog from '~/components/Dialog.vue'
|
|
157
157
|
|
|
158
158
|
export default {
|
|
159
159
|
name: 'PluginIntegrationsAdminVendorsPage',
|
|
@@ -216,6 +216,7 @@ export default {
|
|
|
216
216
|
.integrations()
|
|
217
217
|
.get()
|
|
218
218
|
} catch (e) {
|
|
219
|
+
// eslint-disable-next-line no-console
|
|
219
220
|
console.error('Could not load organization integrations', e)
|
|
220
221
|
}
|
|
221
222
|
},
|
package/plugin.js
CHANGED
|
@@ -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 ProviderTargetPicker from '@/components/ExternalIntegration/ProviderTargetPicker.vue'
|
|
8
|
+
|
|
9
|
+
Vue.use(Vuetify)
|
|
10
|
+
|
|
11
|
+
describe('ProviderTargetPicker', () => {
|
|
12
|
+
test('ProviderTargetPicker is a Vue instance', () => {
|
|
13
|
+
const wrapper = shallowMount(ProviderTargetPicker, {
|
|
14
|
+
vuetify: new Vuetify(),
|
|
15
|
+
mocks: defaultMocks,
|
|
16
|
+
propsData: {
|
|
17
|
+
metadata: {},
|
|
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 ProviderTargetViewer from '@/components/ExternalIntegration/ProviderTargetViewer.vue'
|
|
8
|
+
|
|
9
|
+
Vue.use(Vuetify)
|
|
10
|
+
|
|
11
|
+
describe('ProviderTargetViewer', () => {
|
|
12
|
+
test('ProviderTargetViewer is a Vue instance', () => {
|
|
13
|
+
const wrapper = shallowMount(ProviderTargetViewer, {
|
|
14
|
+
vuetify: new Vuetify(),
|
|
15
|
+
mocks: defaultMocks,
|
|
16
|
+
propsData: {
|
|
17
|
+
target: '',
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
expect(wrapper.vm).toBeTruthy()
|
|
21
|
+
})
|
|
22
|
+
})
|