@windward/integrations 0.0.4 → 0.0.6
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/.vscode/settings.json +3 -0
- package/components/Content/Blocks/ExternalIntegration/LtiConsumer.vue +206 -0
- package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumer.vue +282 -3
- package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumers.vue +198 -3
- package/components/ExternalIntegration/Driver/ManageLti1p1.vue +5 -5
- package/components/Settings/ExternalIntegration/LtiConsumerSettings.vue +108 -0
- package/i18n/en-US/components/content/blocks/external_integration/index.ts +5 -0
- package/i18n/en-US/components/content/blocks/external_integration/lti_consumer.ts +8 -0
- package/i18n/en-US/components/content/blocks/index.ts +5 -0
- package/i18n/en-US/components/content/index.ts +5 -0
- package/i18n/en-US/components/external_integration/driver/lti1p1.ts +10 -2
- package/i18n/en-US/components/index.ts +4 -0
- package/i18n/en-US/components/settings/external_integration/index.ts +5 -0
- package/i18n/en-US/components/settings/external_integration/lti_consumer.ts +7 -0
- package/i18n/en-US/components/settings/index.ts +5 -0
- package/i18n/en-US/shared/content_blocks.ts +8 -0
- package/i18n/en-US/shared/index.ts +2 -0
- package/i18n/en-US/shared/settings.ts +5 -1
- package/package.json +1 -1
- package/pages/admin/importCourse.vue +4 -4
- package/pages/admin/vendors.vue +2 -2
- package/pages/course/importContent.vue +1 -1
- package/plugin.js +26 -0
- package/test/Components/Content/Blocks/ExternalIntegration/LtiConsumer.spec.js +26 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div v-if="!missing && consumer">
|
|
4
|
+
<h1>{{ consumer.name }}</h1>
|
|
5
|
+
<TextViewer v-model="consumer.description"></TextViewer>
|
|
6
|
+
|
|
7
|
+
<v-btn
|
|
8
|
+
v-if="consumer.enabled"
|
|
9
|
+
color="primary"
|
|
10
|
+
block
|
|
11
|
+
@click="onLaunch"
|
|
12
|
+
>
|
|
13
|
+
<v-icon>mdi-launch</v-icon>
|
|
14
|
+
{{
|
|
15
|
+
$t(
|
|
16
|
+
'windward.integrations.components.content.blocks.external_integration.lti_consumer.launch',
|
|
17
|
+
[block.metadata.config.tool_id]
|
|
18
|
+
)
|
|
19
|
+
}}
|
|
20
|
+
</v-btn>
|
|
21
|
+
|
|
22
|
+
<v-alert v-if="!consumer.enabled" type="error">
|
|
23
|
+
{{
|
|
24
|
+
$t(
|
|
25
|
+
'windward.integrations.components.content.blocks.external_integration.lti_consumer.link_disabled'
|
|
26
|
+
)
|
|
27
|
+
}}
|
|
28
|
+
</v-alert>
|
|
29
|
+
|
|
30
|
+
<v-form
|
|
31
|
+
name="ltiForm"
|
|
32
|
+
ref="ltiForm"
|
|
33
|
+
:target="target"
|
|
34
|
+
:action="launchData.target || ''"
|
|
35
|
+
:method="launchData.method || 'POST'"
|
|
36
|
+
>
|
|
37
|
+
<input
|
|
38
|
+
type="hidden"
|
|
39
|
+
v-for="(value, key) of launchData.payload"
|
|
40
|
+
:key="key"
|
|
41
|
+
:name="key"
|
|
42
|
+
:value="value"
|
|
43
|
+
/>
|
|
44
|
+
</v-form>
|
|
45
|
+
<iframe
|
|
46
|
+
v-if="
|
|
47
|
+
block.metadata.config.launch_type === 'inline' &&
|
|
48
|
+
!!launchData.target
|
|
49
|
+
"
|
|
50
|
+
:name="frameId"
|
|
51
|
+
class="launch-frame"
|
|
52
|
+
></iframe>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<v-alert v-if="missing" type="error">
|
|
56
|
+
{{
|
|
57
|
+
$t(
|
|
58
|
+
'windward.integrations.components.content.blocks.external_integration.lti_consumer.missing_tool',
|
|
59
|
+
[block.metadata.config.tool_id]
|
|
60
|
+
)
|
|
61
|
+
}}
|
|
62
|
+
</v-alert>
|
|
63
|
+
<v-alert v-else-if="consumer === null" type="warning">
|
|
64
|
+
{{
|
|
65
|
+
$t(
|
|
66
|
+
'windward.integrations.components.content.blocks.external_integration.lti_consumer.configure_warning'
|
|
67
|
+
)
|
|
68
|
+
}}
|
|
69
|
+
</v-alert>
|
|
70
|
+
</div>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<script>
|
|
74
|
+
import _ from 'lodash'
|
|
75
|
+
import { mapGetters } from 'vuex'
|
|
76
|
+
import Crypto from '~/helpers/Crypto'
|
|
77
|
+
import TextViewer from '~/components/Text/TextViewer.vue'
|
|
78
|
+
import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
|
|
79
|
+
import Course from '~/models/Course'
|
|
80
|
+
import Organization from '~/models/Organization'
|
|
81
|
+
import Enrollment from '~/models/Enrollment'
|
|
82
|
+
import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consumer'
|
|
83
|
+
|
|
84
|
+
export default {
|
|
85
|
+
name: 'ContentBlockExternalIntegrationLti1p1Consumer',
|
|
86
|
+
extends: BaseContentBlock,
|
|
87
|
+
components: { TextViewer },
|
|
88
|
+
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
|
+
data() {
|
|
98
|
+
return {
|
|
99
|
+
consumer: null,
|
|
100
|
+
launchData: {},
|
|
101
|
+
saveState: false, // Override the base block to disable state saving
|
|
102
|
+
target: '_blank',
|
|
103
|
+
frameId: Crypto.id() + '_frame',
|
|
104
|
+
missing: false,
|
|
105
|
+
launched: false,
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
watch: {
|
|
109
|
+
'block.metadata.config.tool_id': {
|
|
110
|
+
deep: true,
|
|
111
|
+
handler(newVal, oldVal) {
|
|
112
|
+
// We changed the consumer tool id, reload
|
|
113
|
+
if (newVal !== oldVal) {
|
|
114
|
+
this.loadConsumer()
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
async fetch() {
|
|
120
|
+
this.target = this.frameId
|
|
121
|
+
this.loadConsumer()
|
|
122
|
+
},
|
|
123
|
+
computed: {
|
|
124
|
+
...mapGetters({
|
|
125
|
+
organization: 'organization/get',
|
|
126
|
+
course: 'course/get',
|
|
127
|
+
enrollment: 'enrollment/get',
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
mounted() {},
|
|
131
|
+
methods: {
|
|
132
|
+
async loadConsumer() {
|
|
133
|
+
try {
|
|
134
|
+
if (!_.isEmpty(this.block.metadata.config.tool_id)) {
|
|
135
|
+
this.consumer = await new Lti1p1Consumer()
|
|
136
|
+
.for(
|
|
137
|
+
new Organization({ id: this.organization.id }),
|
|
138
|
+
new Course({ id: this.course.id })
|
|
139
|
+
)
|
|
140
|
+
.find(this.block.metadata.config.tool_id)
|
|
141
|
+
} else {
|
|
142
|
+
this.consumer = null
|
|
143
|
+
}
|
|
144
|
+
this.missing = false
|
|
145
|
+
} catch (e) {
|
|
146
|
+
// Data conflict error
|
|
147
|
+
if (e.response.status === 404) {
|
|
148
|
+
this.missing = true
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
async onLaunch() {
|
|
153
|
+
try {
|
|
154
|
+
// Clear the launch data in-case we're in inline mode
|
|
155
|
+
// If the iframe isn't destroyed subsequent launches will be new windows instead of the target iframe
|
|
156
|
+
this.launchData = {}
|
|
157
|
+
|
|
158
|
+
this.launchData = await Lti1p1Consumer.custom(
|
|
159
|
+
new Enrollment({ id: this.enrollment.id }),
|
|
160
|
+
this.consumer,
|
|
161
|
+
'/launch'
|
|
162
|
+
).first()
|
|
163
|
+
|
|
164
|
+
switch (this.block.metadata.config.launch_type) {
|
|
165
|
+
case 'new_window':
|
|
166
|
+
this.target = '_blank'
|
|
167
|
+
break
|
|
168
|
+
case 'inline':
|
|
169
|
+
this.target = this.frameId
|
|
170
|
+
break
|
|
171
|
+
default:
|
|
172
|
+
this.target = '_blank'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Do a forceUpdate so the hidden fields are ready and available for the post below
|
|
176
|
+
await this.$forceUpdate()
|
|
177
|
+
|
|
178
|
+
if (!_.isEmpty(this.launchData)) {
|
|
179
|
+
this.$refs.ltiForm.$el.submit()
|
|
180
|
+
this.launched = true
|
|
181
|
+
} else {
|
|
182
|
+
this.$dialog.error(
|
|
183
|
+
this.$t(
|
|
184
|
+
'windward.integrations.components.content.blocks.external_integration.lti_consumer.unknown_error'
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error('LTI Link Launch Fail')
|
|
190
|
+
console.error(e)
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
onBeforeSave() {
|
|
194
|
+
// Set a generic body since we don't use this field
|
|
195
|
+
this.block.body = 'lti-' + this.consumer.version + '-consumer'
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<style scoped>
|
|
202
|
+
.launch-frame {
|
|
203
|
+
aspect-ratio: 16/9;
|
|
204
|
+
width: 100%;
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
@@ -1,13 +1,292 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
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-form v-model="formValid" @submit.prevent>
|
|
8
|
+
<v-row justify="center" align="center" class="mt-5">
|
|
9
|
+
<v-col cols="12">
|
|
10
|
+
<v-text-field
|
|
11
|
+
v-model="consumer.target"
|
|
12
|
+
:label="
|
|
13
|
+
$t(
|
|
14
|
+
'windward.integrations.components.external_integration.driver.lti1p1.target_url'
|
|
15
|
+
)
|
|
16
|
+
"
|
|
17
|
+
:hint="
|
|
18
|
+
$t(
|
|
19
|
+
'windward.integrations.components.external_integration.driver.lti1p1.target_url'
|
|
20
|
+
)
|
|
21
|
+
"
|
|
22
|
+
></v-text-field>
|
|
23
|
+
<v-text-field
|
|
24
|
+
v-model="consumer.name"
|
|
25
|
+
:placeholder="$t('shared.forms.name')"
|
|
26
|
+
:label="$t('shared.forms.name')"
|
|
27
|
+
:hint="$t('shared.forms.name')"
|
|
28
|
+
></v-text-field>
|
|
29
|
+
|
|
30
|
+
<label for="description">{{
|
|
31
|
+
$t('shared.forms.description')
|
|
32
|
+
}}</label>
|
|
33
|
+
<TextEditor
|
|
34
|
+
v-model="consumer.description"
|
|
35
|
+
id="description"
|
|
36
|
+
:height="200"
|
|
37
|
+
></TextEditor>
|
|
38
|
+
|
|
39
|
+
<v-switch
|
|
40
|
+
v-model="consumer.enabled"
|
|
41
|
+
:label="
|
|
42
|
+
$t(
|
|
43
|
+
'windward.integrations.components.external_integration.driver.lti1p1.enabled'
|
|
44
|
+
)
|
|
45
|
+
"
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
<v-text-field
|
|
49
|
+
v-model="consumer.metadata.key"
|
|
50
|
+
:placeholder="
|
|
51
|
+
$t(
|
|
52
|
+
'windward.integrations.components.external_integration.driver.lti1p1.' +
|
|
53
|
+
(consumer.id ? 'new_key' : 'key')
|
|
54
|
+
)
|
|
55
|
+
"
|
|
56
|
+
:label="
|
|
57
|
+
$t(
|
|
58
|
+
'windward.integrations.components.external_integration.driver.lti1p1.' +
|
|
59
|
+
(consumer.id ? 'change_key' : 'key')
|
|
60
|
+
)
|
|
61
|
+
"
|
|
62
|
+
></v-text-field>
|
|
63
|
+
|
|
64
|
+
<v-text-field
|
|
65
|
+
v-model="consumer.metadata.secret"
|
|
66
|
+
:placeholder="
|
|
67
|
+
$t(
|
|
68
|
+
'windward.integrations.components.external_integration.driver.lti1p1.' +
|
|
69
|
+
(consumer.id ? 'new_secret' : 'secret')
|
|
70
|
+
)
|
|
71
|
+
"
|
|
72
|
+
:label="
|
|
73
|
+
$t(
|
|
74
|
+
'windward.integrations.components.external_integration.driver.lti1p1.' +
|
|
75
|
+
(consumer.id
|
|
76
|
+
? 'change_secret'
|
|
77
|
+
: 'secret')
|
|
78
|
+
)
|
|
79
|
+
"
|
|
80
|
+
>
|
|
81
|
+
</v-text-field>
|
|
82
|
+
|
|
83
|
+
<v-data-table
|
|
84
|
+
:headers="customParameterHeaders"
|
|
85
|
+
:items="consumer.metadata.custom"
|
|
86
|
+
hide-default-footer
|
|
87
|
+
class="elevation-1"
|
|
88
|
+
>
|
|
89
|
+
<template #item.key="{ index }">
|
|
90
|
+
<v-text-field
|
|
91
|
+
v-model="
|
|
92
|
+
consumer.metadata.custom[index].key
|
|
93
|
+
"
|
|
94
|
+
:label="
|
|
95
|
+
$t(
|
|
96
|
+
'windward.integrations.components.external_integration.driver.lti1p1.parameter_name'
|
|
97
|
+
)
|
|
98
|
+
"
|
|
99
|
+
/>
|
|
100
|
+
</template>
|
|
101
|
+
<template #item.value="{ index }">
|
|
102
|
+
<v-text-field
|
|
103
|
+
v-model="
|
|
104
|
+
consumer.metadata.custom[index].value
|
|
105
|
+
"
|
|
106
|
+
:label="
|
|
107
|
+
$t(
|
|
108
|
+
'windward.integrations.components.external_integration.driver.lti1p1.value'
|
|
109
|
+
)
|
|
110
|
+
"
|
|
111
|
+
/>
|
|
112
|
+
</template>
|
|
113
|
+
<template #item.actions="{ index }">
|
|
114
|
+
<v-btn
|
|
115
|
+
text
|
|
116
|
+
color="primary"
|
|
117
|
+
@click="deleteCustomParameter(index)"
|
|
118
|
+
>
|
|
119
|
+
<v-icon small> mdi-delete </v-icon>
|
|
120
|
+
<span class="sr-only">{{
|
|
121
|
+
$t('shared.forms.delete')
|
|
122
|
+
}}</span>
|
|
123
|
+
</v-btn>
|
|
124
|
+
</template>
|
|
125
|
+
<template #footer>
|
|
126
|
+
<div class="text-center">
|
|
127
|
+
<v-btn
|
|
128
|
+
color="primary"
|
|
129
|
+
class="mb-3"
|
|
130
|
+
@click="addCustomParameter"
|
|
131
|
+
>{{ $t('shared.forms.add') }}</v-btn
|
|
132
|
+
>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
135
|
+
</v-data-table>
|
|
136
|
+
<br />
|
|
137
|
+
<v-select
|
|
138
|
+
v-model="consumer.metadata.security_level"
|
|
139
|
+
:items="securityLevels"
|
|
140
|
+
item-text="name"
|
|
141
|
+
item-value="value"
|
|
142
|
+
label="Security Level"
|
|
143
|
+
outlined
|
|
144
|
+
></v-select>
|
|
145
|
+
</v-col>
|
|
146
|
+
</v-row>
|
|
147
|
+
</v-form>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
3
150
|
</template>
|
|
4
151
|
|
|
5
152
|
<script>
|
|
153
|
+
import _ from 'lodash'
|
|
154
|
+
import { mapGetters } from 'vuex'
|
|
155
|
+
import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consumer'
|
|
156
|
+
import FormVue from '~/components/Form'
|
|
157
|
+
import Organization from '~/models/Organization'
|
|
158
|
+
import Course from '~/models/Course'
|
|
159
|
+
import TextEditor from '~/components/Text/TextEditor.vue'
|
|
160
|
+
|
|
6
161
|
export default {
|
|
7
162
|
name: 'ManageLti1p1ConsumerDriver',
|
|
163
|
+
components: { TextEditor },
|
|
164
|
+
extends: FormVue,
|
|
165
|
+
props: {
|
|
166
|
+
value: {
|
|
167
|
+
type: [Lti1p1Consumer, null],
|
|
168
|
+
required: false,
|
|
169
|
+
default: null,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
emits: ['update:consumer'],
|
|
173
|
+
meta: {
|
|
174
|
+
privilege: {
|
|
175
|
+
'': {
|
|
176
|
+
writable: true,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
8
180
|
data() {
|
|
9
|
-
return {
|
|
181
|
+
return {
|
|
182
|
+
render: false,
|
|
183
|
+
consumer: {
|
|
184
|
+
metadata: {
|
|
185
|
+
custom: [],
|
|
186
|
+
security_level: '',
|
|
187
|
+
security: [],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
customParameterHeaders: [
|
|
191
|
+
{
|
|
192
|
+
text: this.$t(
|
|
193
|
+
'windward.integrations.components.external_integration.driver.lti1p1.parameter_name'
|
|
194
|
+
),
|
|
195
|
+
value: 'key',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
text: this.$t(
|
|
199
|
+
'windward.integrations.components.external_integration.driver.lti1p1.value'
|
|
200
|
+
),
|
|
201
|
+
value: 'value',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
text: this.$t('shared.forms.actions'),
|
|
205
|
+
value: 'actions',
|
|
206
|
+
sortable: false,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
securityLevels: [
|
|
210
|
+
{ name: 'Full Access', value: 'full' },
|
|
211
|
+
{ name: 'Email Only', value: 'email' },
|
|
212
|
+
{ name: 'Name Only', value: 'name' },
|
|
213
|
+
{ name: 'Anonymous', value: 'anonymous' },
|
|
214
|
+
// { name: 'Custom', value: 'custom' }, // TODO: When this is selected provide direct access to check off LTI fields
|
|
215
|
+
],
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
computed: {
|
|
219
|
+
...mapGetters({
|
|
220
|
+
organization: 'organization/get',
|
|
221
|
+
course: 'course/get',
|
|
222
|
+
}),
|
|
223
|
+
},
|
|
224
|
+
created() {
|
|
225
|
+
if (_.isEmpty(this.value)) {
|
|
226
|
+
this.consumer = new Lti1p1Consumer(this.consumer)
|
|
227
|
+
} else {
|
|
228
|
+
this.consumer = new Lti1p1Consumer(_.cloneDeep(this.value))
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
mounted() {
|
|
232
|
+
if (
|
|
233
|
+
!this.$PermissionService.userHasAccessTo(
|
|
234
|
+
'plugin.windward.integrations.course.externalIntegration',
|
|
235
|
+
'writable'
|
|
236
|
+
)
|
|
237
|
+
) {
|
|
238
|
+
// Display an angry error that they can't view this driver
|
|
239
|
+
this.$dialog.error(this.$t('shared.error.description_401'), {
|
|
240
|
+
duration: null,
|
|
241
|
+
action: {
|
|
242
|
+
text: this.$t('shared.forms.close'),
|
|
243
|
+
onClick: (e, toastObject) => {
|
|
244
|
+
toastObject.goAway(0)
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
console.error('You do not have access to this consumer!')
|
|
249
|
+
|
|
250
|
+
// Return so we don't even attempt loading
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.render = true
|
|
255
|
+
},
|
|
256
|
+
methods: {
|
|
257
|
+
addCustomParameter() {
|
|
258
|
+
this.consumer.metadata.custom.push({ key: '', value: '' })
|
|
259
|
+
},
|
|
260
|
+
deleteCustomParameter(index) {
|
|
261
|
+
this.consumer.metadata.custom.splice(index, 1)
|
|
262
|
+
},
|
|
263
|
+
async save() {
|
|
264
|
+
let consumer = new Lti1p1Consumer(this.consumer).for(
|
|
265
|
+
new Organization({ id: this.organization.id }),
|
|
266
|
+
new Course({ id: this.course.id })
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
consumer = await consumer.save()
|
|
271
|
+
this.consumer = consumer
|
|
272
|
+
|
|
273
|
+
// Clone and delete the metadata since we don't need to share that around
|
|
274
|
+
// Also include the vendor going back for easier mapping
|
|
275
|
+
const consumerEvent = _.cloneDeep(consumer)
|
|
276
|
+
|
|
277
|
+
this.$dialog.success(this.$t('shared.forms.saved'))
|
|
278
|
+
this.$emit('update:consumer', consumerEvent)
|
|
279
|
+
} catch (e) {
|
|
280
|
+
this.$dialog.error(
|
|
281
|
+
this.$t('windward.integrations.shared.error.save_failed')
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
async onSave() {
|
|
286
|
+
if (this.formValid) {
|
|
287
|
+
await this.save()
|
|
288
|
+
}
|
|
289
|
+
},
|
|
10
290
|
},
|
|
11
|
-
methods: {},
|
|
12
291
|
}
|
|
13
292
|
</script>
|
|
@@ -1,13 +1,208 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div>
|
|
3
|
+
<Dialog
|
|
4
|
+
color="primary"
|
|
5
|
+
action-save
|
|
6
|
+
action-save-new
|
|
7
|
+
@click:save="onSaved"
|
|
8
|
+
>
|
|
9
|
+
<template #title>{{
|
|
10
|
+
$t(
|
|
11
|
+
'windward.integrations.components.external_integration.driver.lti1p1.new'
|
|
12
|
+
)
|
|
13
|
+
}}</template>
|
|
14
|
+
<template #trigger>{{ $t('shared.forms.new') }}</template>
|
|
15
|
+
<template #form="{ on, attrs }"
|
|
16
|
+
><ManageConsumer v-bind="attrs" v-on="on"></ManageConsumer
|
|
17
|
+
></template>
|
|
18
|
+
</Dialog>
|
|
19
|
+
|
|
20
|
+
<v-data-table
|
|
21
|
+
:headers="headers"
|
|
22
|
+
:items="consumers"
|
|
23
|
+
:items-per-page="10"
|
|
24
|
+
class="elevation-1"
|
|
25
|
+
>
|
|
26
|
+
<template #item.description="{ item }">
|
|
27
|
+
{{ item.description.replace(/(<([^>]+)>)/gi, '').trim() }}
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<template #item.enabled="{ item }">
|
|
31
|
+
<v-icon :color="item.enabled ? 'success' : 'error'"
|
|
32
|
+
>{{ item.enabled ? 'mdi-check' : 'mdi-close' }}
|
|
33
|
+
</v-icon>
|
|
34
|
+
<span v-if="!item.enabled" class="sr-only">{{
|
|
35
|
+
$t(
|
|
36
|
+
'windward.integrations.components.external_integration.driver.lti1p1.enabled'
|
|
37
|
+
)
|
|
38
|
+
}}</span>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<template #item.created_at="{ item }">
|
|
42
|
+
{{ $d(new Date(item.created_at), 'long') }}
|
|
43
|
+
</template>
|
|
44
|
+
<template #[`item.actions`]="{ index, item }">
|
|
45
|
+
<Dialog color="primary" action-save @click:save="onSaved">
|
|
46
|
+
<template #title>{{
|
|
47
|
+
$t(
|
|
48
|
+
'windward.integrations.components.external_integration.driver.lti1p1.edit'
|
|
49
|
+
)
|
|
50
|
+
}}</template>
|
|
51
|
+
<template #trigger>
|
|
52
|
+
<v-icon small>mdi-pencil</v-icon>
|
|
53
|
+
<span class="sr-only">{{
|
|
54
|
+
$t(
|
|
55
|
+
'windward.integrations.components.external_integration.driver.lti1p1.edit'
|
|
56
|
+
)
|
|
57
|
+
}}</span>
|
|
58
|
+
</template>
|
|
59
|
+
<template #form="{ on, attrs }"
|
|
60
|
+
><ManageConsumer
|
|
61
|
+
v-model="consumers[index]"
|
|
62
|
+
v-bind="attrs"
|
|
63
|
+
v-on="on"
|
|
64
|
+
></ManageConsumer
|
|
65
|
+
></template>
|
|
66
|
+
</Dialog>
|
|
67
|
+
|
|
68
|
+
<v-btn icon>
|
|
69
|
+
<v-icon @click="onConfirmDelete(item)"> mdi-delete </v-icon>
|
|
70
|
+
<span class="sr-only">{{ $t('shared.forms.delete') }}</span>
|
|
71
|
+
</v-btn>
|
|
72
|
+
</template>
|
|
73
|
+
</v-data-table>
|
|
74
|
+
</div>
|
|
3
75
|
</template>
|
|
4
76
|
|
|
5
77
|
<script>
|
|
78
|
+
import _ from 'lodash'
|
|
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
|
+
import Course from '~/models/Course'
|
|
84
|
+
import Organization from '~/models/Organization'
|
|
85
|
+
import Dialog from '~/components/Dialog.vue'
|
|
86
|
+
|
|
6
87
|
export default {
|
|
7
88
|
name: 'ManageLti1p1ConsumersDriver',
|
|
89
|
+
components: { SecretField, Dialog, ManageConsumer },
|
|
8
90
|
data() {
|
|
9
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
consumers: [],
|
|
93
|
+
headers: [
|
|
94
|
+
{
|
|
95
|
+
text: this.$t(
|
|
96
|
+
'windward.integrations.components.external_integration.driver.lti1p1.target_url'
|
|
97
|
+
),
|
|
98
|
+
value: 'target',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
text: this.$t(
|
|
102
|
+
'windward.integrations.components.external_integration.driver.lti1p1.name'
|
|
103
|
+
),
|
|
104
|
+
value: 'name',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
text: this.$t(
|
|
108
|
+
'windward.integrations.components.external_integration.driver.lti1p1.description'
|
|
109
|
+
),
|
|
110
|
+
value: 'description',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
text: this.$t(
|
|
114
|
+
'windward.integrations.components.external_integration.driver.lti1p1.enabled'
|
|
115
|
+
),
|
|
116
|
+
value: 'enabled',
|
|
117
|
+
},
|
|
118
|
+
{ text: this.$t('shared.forms.created'), value: 'created_at' },
|
|
119
|
+
{
|
|
120
|
+
text: this.$t('shared.forms.actions'),
|
|
121
|
+
value: 'actions',
|
|
122
|
+
sortable: false,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
async fetch() {
|
|
129
|
+
if (
|
|
130
|
+
!this.$PermissionService.userHasAccessTo(
|
|
131
|
+
'plugin.windward.integrations.course.externalIntegration',
|
|
132
|
+
'readable'
|
|
133
|
+
) ||
|
|
134
|
+
_.isEmpty(this.organization.id) ||
|
|
135
|
+
_.isEmpty(this.course.id)
|
|
136
|
+
) {
|
|
137
|
+
// Display an angry error that they can't view this driver
|
|
138
|
+
this.$dialog.error(this.$t('shared.error.description_401'), {
|
|
139
|
+
duration: null,
|
|
140
|
+
action: {
|
|
141
|
+
text: this.$t('shared.forms.close'),
|
|
142
|
+
onClick: (e, toastObject) => {
|
|
143
|
+
toastObject.goAway(0)
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
if (_.isEmpty(this.organization.id) || _.isEmpty(this.course.id)) {
|
|
148
|
+
console.error(
|
|
149
|
+
'Cannot load external integrations because organization or course is not set!'
|
|
150
|
+
)
|
|
151
|
+
} else {
|
|
152
|
+
console.error(
|
|
153
|
+
'You do not have access to this external integration!'
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Return so we don't even attempt loading
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await this.loadConsumers()
|
|
162
|
+
},
|
|
163
|
+
computed: {
|
|
164
|
+
...mapGetters({
|
|
165
|
+
organization: 'organization/get',
|
|
166
|
+
course: 'course/get',
|
|
167
|
+
}),
|
|
168
|
+
},
|
|
169
|
+
methods: {
|
|
170
|
+
onSaved(e) {
|
|
171
|
+
this.loadConsumers()
|
|
172
|
+
},
|
|
173
|
+
async loadConsumers() {
|
|
174
|
+
this.consumers = await new Lti1p1Consumer()
|
|
175
|
+
.for(
|
|
176
|
+
new Organization({ id: this.organization.id }),
|
|
177
|
+
new Course({ id: this.course.id })
|
|
178
|
+
)
|
|
179
|
+
.get()
|
|
180
|
+
},
|
|
181
|
+
onConfirmDelete(consumer) {
|
|
182
|
+
this.$dialog.show(this.$t('shared.forms.confirm_delete_text'), {
|
|
183
|
+
icon: 'mdi-help',
|
|
184
|
+
duration: null,
|
|
185
|
+
action: [
|
|
186
|
+
{
|
|
187
|
+
text: this.$t('shared.forms.cancel'),
|
|
188
|
+
onClick: (e, toastObject) => {
|
|
189
|
+
toastObject.goAway(0)
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
text: this.$t('shared.forms.confirm'),
|
|
194
|
+
// router navigation
|
|
195
|
+
onClick: (e, toastObject) => {
|
|
196
|
+
this.deleteConsumer(consumer)
|
|
197
|
+
toastObject.goAway(0)
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
})
|
|
202
|
+
},
|
|
203
|
+
deleteConsumer(consumer) {
|
|
204
|
+
consumer.delete()
|
|
205
|
+
},
|
|
10
206
|
},
|
|
11
|
-
methods: {},
|
|
12
207
|
}
|
|
13
208
|
</script>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
}}
|
|
11
11
|
</v-expansion-panel-header>
|
|
12
12
|
<v-expansion-panel-content>
|
|
13
|
-
<
|
|
13
|
+
<ManageProviders></ManageProviders>
|
|
14
14
|
</v-expansion-panel-content>
|
|
15
15
|
</v-expansion-panel>
|
|
16
16
|
<v-expansion-panel>
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
}}
|
|
23
23
|
</v-expansion-panel-header>
|
|
24
24
|
<v-expansion-panel-content>
|
|
25
|
-
<
|
|
25
|
+
<ManageConsumers></ManageConsumers>
|
|
26
26
|
</v-expansion-panel-content>
|
|
27
27
|
</v-expansion-panel>
|
|
28
28
|
</v-expansion-panels>
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
</template>
|
|
31
31
|
|
|
32
32
|
<script>
|
|
33
|
-
import
|
|
34
|
-
import
|
|
33
|
+
import ManageProviders from './Lti1p1/ManageProviders.vue'
|
|
34
|
+
import ManageConsumers from './Lti1p1/ManageConsumers.vue'
|
|
35
35
|
|
|
36
36
|
export default {
|
|
37
37
|
name: 'ManageLti1p1Driver',
|
|
38
|
-
components: {
|
|
38
|
+
components: { ManageProviders, ManageConsumers },
|
|
39
39
|
data() {
|
|
40
40
|
return {
|
|
41
41
|
panel: 0,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container>
|
|
3
|
+
<v-select
|
|
4
|
+
v-model="block.metadata.config.launch_type"
|
|
5
|
+
:label="
|
|
6
|
+
$t(
|
|
7
|
+
'windward.integrations.components.settings.external_integration.lti_consumer.launch_type'
|
|
8
|
+
)
|
|
9
|
+
"
|
|
10
|
+
:items="launchTypes"
|
|
11
|
+
item-text="name"
|
|
12
|
+
item-value="value"
|
|
13
|
+
></v-select>
|
|
14
|
+
{{
|
|
15
|
+
$t(
|
|
16
|
+
'windward.integrations.components.settings.external_integration.lti_consumer.link_select'
|
|
17
|
+
)
|
|
18
|
+
}}
|
|
19
|
+
<v-list>
|
|
20
|
+
<v-list-item
|
|
21
|
+
v-for="consumer in consumers"
|
|
22
|
+
:key="consumer.id"
|
|
23
|
+
two-line
|
|
24
|
+
:disabled="!consumer.enabled"
|
|
25
|
+
@click="onSelectLink(consumer)"
|
|
26
|
+
>
|
|
27
|
+
<v-list-item-avatar :color="!consumer.enabled ? 'error' : ''">
|
|
28
|
+
<v-icon> mdi-shield-link-variant-outline </v-icon>
|
|
29
|
+
</v-list-item-avatar>
|
|
30
|
+
<v-list-item-content>
|
|
31
|
+
<v-list-item-title>{{ consumer.name }}</v-list-item-title>
|
|
32
|
+
<v-list-item-subtitle v-if="!consumer.enabled">
|
|
33
|
+
<v-chip color="error">{{
|
|
34
|
+
$t(
|
|
35
|
+
'windward.integrations.components.settings.external_integration.lti_consumer.link_disabled'
|
|
36
|
+
)
|
|
37
|
+
}}</v-chip>
|
|
38
|
+
</v-list-item-subtitle>
|
|
39
|
+
<v-list-item-subtitle>{{
|
|
40
|
+
consumer.description.replace(/(<([^>]+)>)/gi, '').trim()
|
|
41
|
+
}}</v-list-item-subtitle>
|
|
42
|
+
</v-list-item-content>
|
|
43
|
+
</v-list-item>
|
|
44
|
+
</v-list>
|
|
45
|
+
</v-container>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script>
|
|
49
|
+
import _ from 'lodash'
|
|
50
|
+
import { mapGetters } from 'vuex'
|
|
51
|
+
import BaseContentSettings from '~/components/Content/Tool/BaseContentSettings.js'
|
|
52
|
+
import ManageConsumer from '../../ExternalIntegration/Driver/Lti1p1/ManageConsumer.vue'
|
|
53
|
+
import Course from '~/models/Course'
|
|
54
|
+
import Organization from '~/models/Organization'
|
|
55
|
+
import Lti1p1Consumer from '../../../models/ExternalIntegration/Lti1p1Consumer'
|
|
56
|
+
|
|
57
|
+
export default {
|
|
58
|
+
name: 'ContentBlockExternalIntegrationLti1p1ConsumerSettings',
|
|
59
|
+
extends: BaseContentSettings,
|
|
60
|
+
components: { ManageConsumer },
|
|
61
|
+
props: {
|
|
62
|
+
settings: { type: Object, required: false, default: null },
|
|
63
|
+
context: { type: String, required: false, default: 'block' },
|
|
64
|
+
},
|
|
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
|
+
data() {
|
|
82
|
+
return {
|
|
83
|
+
consumers: [],
|
|
84
|
+
launchTypes: [
|
|
85
|
+
{
|
|
86
|
+
name: this.$t(
|
|
87
|
+
'windward.integrations.components.settings.external_integration.lti_consumer.launch_type_new_window'
|
|
88
|
+
),
|
|
89
|
+
value: 'new_window',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: this.$t(
|
|
93
|
+
'windward.integrations.components.settings.external_integration.lti_consumer.launch_type_inline_frame'
|
|
94
|
+
),
|
|
95
|
+
value: 'inline',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
watch: {},
|
|
101
|
+
mounted() {},
|
|
102
|
+
methods: {
|
|
103
|
+
onSelectLink(consumer) {
|
|
104
|
+
this.block.metadata.config.tool_id = consumer.id
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
launch: 'Launch',
|
|
3
|
+
configure_warning:
|
|
4
|
+
'This block needs to be configured. Please select a LTI tool in the settings panel.',
|
|
5
|
+
missing_tool: 'The tool id {0} is missing',
|
|
6
|
+
unknown_error: 'Something went wrong! Could not launch this link!',
|
|
7
|
+
link_disabled: 'Link disabled',
|
|
8
|
+
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
export default {
|
|
2
|
+
name: 'Name',
|
|
3
|
+
description: 'Description',
|
|
2
4
|
key: 'Key',
|
|
3
5
|
secret: 'Secret',
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
parameter_name: 'Custom Parameter Name',
|
|
7
|
+
value: 'Value',
|
|
8
|
+
auto_key: 'Leave empty for autogenerated key',
|
|
9
|
+
auto_secret: 'Leave empty for autogenerated secret',
|
|
10
|
+
change_key: 'Change Key',
|
|
11
|
+
change_secret: 'Change Secret',
|
|
12
|
+
new_key: 'New Key',
|
|
13
|
+
new_secret: 'New Secret',
|
|
6
14
|
target_url: 'Target Url',
|
|
7
15
|
launch_url: 'Launch Url',
|
|
8
16
|
new: 'New LTI Link',
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import content from './content'
|
|
2
|
+
import settings from './settings'
|
|
1
3
|
import navigation from './navigation'
|
|
2
4
|
import integration from './integration'
|
|
3
5
|
import externalIntegration from './external_integration'
|
|
4
6
|
|
|
5
7
|
export default {
|
|
8
|
+
content,
|
|
9
|
+
settings,
|
|
6
10
|
navigation,
|
|
7
11
|
integration,
|
|
8
12
|
external_integration: externalIntegration,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import contentBlocks from './content_blocks'
|
|
1
2
|
import settings from './settings'
|
|
2
3
|
import menu from './menu'
|
|
3
4
|
import permission from './permission'
|
|
4
5
|
import error from './error'
|
|
5
6
|
|
|
6
7
|
export default {
|
|
8
|
+
content_blocks: contentBlocks,
|
|
7
9
|
settings,
|
|
8
10
|
menu,
|
|
9
11
|
permission,
|
package/package.json
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
</v-tabs>
|
|
18
18
|
<v-tabs-items v-model="tab">
|
|
19
19
|
<v-tab-item>
|
|
20
|
-
<
|
|
20
|
+
<h2>
|
|
21
21
|
{{ $t('windward.integrations.pages.import_course.title') }}
|
|
22
|
-
</
|
|
22
|
+
</h2>
|
|
23
23
|
|
|
24
24
|
<div v-if="!organizationIntegrations.length">
|
|
25
25
|
{{
|
|
@@ -144,13 +144,13 @@
|
|
|
144
144
|
</div>
|
|
145
145
|
</v-tab-item>
|
|
146
146
|
<v-tab-item>
|
|
147
|
-
<
|
|
147
|
+
<h2>
|
|
148
148
|
{{
|
|
149
149
|
$t(
|
|
150
150
|
'windward.integrations.components.integration.job.recent_jobs_title'
|
|
151
151
|
)
|
|
152
152
|
}}
|
|
153
|
-
</
|
|
153
|
+
</h2>
|
|
154
154
|
<IntegrationJobTable
|
|
155
155
|
ref="integrationJobTable"
|
|
156
156
|
:channel="'integrations.organizations.' + organization.id"
|
package/pages/admin/vendors.vue
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
<v-row justify="center" align="center">
|
|
4
4
|
<v-col cols="12">
|
|
5
5
|
<div class="d-flex mb-5">
|
|
6
|
-
<
|
|
6
|
+
<h2 class="mr-auto flex-grow-1">
|
|
7
7
|
{{
|
|
8
8
|
$t(
|
|
9
9
|
'windward.integrations.components.navigation.integrations.title'
|
|
10
10
|
)
|
|
11
11
|
}}
|
|
12
|
-
</
|
|
12
|
+
</h2>
|
|
13
13
|
</div>
|
|
14
14
|
</v-col>
|
|
15
15
|
</v-row>
|
package/plugin.js
CHANGED
|
@@ -6,6 +6,9 @@ import ExternalIntegrationIndexPage from './pages/course/externalIntegration/ind
|
|
|
6
6
|
import LoginLtiErrorPage from './pages/login/lti/error.vue'
|
|
7
7
|
import IntegrationHelper from './helpers/IntegrationHelper'
|
|
8
8
|
|
|
9
|
+
import LtiConsumerBlock from './components/Content/Blocks/ExternalIntegration/LtiConsumer'
|
|
10
|
+
import LtiConsumerBlockSettings from './components/Settings/ExternalIntegration/LtiConsumerSettings'
|
|
11
|
+
|
|
9
12
|
export default {
|
|
10
13
|
name: 'windward.integrations.name',
|
|
11
14
|
hooks: {
|
|
@@ -131,6 +134,29 @@ export default {
|
|
|
131
134
|
],
|
|
132
135
|
},
|
|
133
136
|
],
|
|
137
|
+
contentBlock: [
|
|
138
|
+
{
|
|
139
|
+
tag: 'windward-integrations-lti-consumer',
|
|
140
|
+
template: LtiConsumerBlock,
|
|
141
|
+
metadata: {
|
|
142
|
+
icon: 'mdi-shield-link-variant-outline',
|
|
143
|
+
name: 'windward.integrations.shared.content_blocks.title.lti_consumer',
|
|
144
|
+
grouping:
|
|
145
|
+
'windward.integrations.shared.content_blocks.grouping.integrations',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
settings: [
|
|
150
|
+
{
|
|
151
|
+
tag: 'windward-integrations-lti-consumer-settings',
|
|
152
|
+
template: LtiConsumerBlockSettings,
|
|
153
|
+
context: ['block.windward-integrations-lti-consumer'],
|
|
154
|
+
metadata: {
|
|
155
|
+
icon: 'mdi-cog',
|
|
156
|
+
name: 'windward.integrations.shared.settings.title.lti_consumer',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
],
|
|
134
160
|
},
|
|
135
161
|
services: {
|
|
136
162
|
Integration: IntegrationHelper,
|
|
@@ -0,0 +1,26 @@
|
|
|
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 LtiConsumer from '@/components/Content/Blocks/ExternalIntegration/LtiConsumer.vue'
|
|
8
|
+
|
|
9
|
+
Vue.use(Vuetify)
|
|
10
|
+
|
|
11
|
+
describe('LtiConsumer', () => {
|
|
12
|
+
test('LtiConsumer is a Vue instance', () => {
|
|
13
|
+
const wrapper = shallowMount(LtiConsumer, {
|
|
14
|
+
vuetify: new Vuetify(),
|
|
15
|
+
mocks: defaultMocks,
|
|
16
|
+
propsData: {
|
|
17
|
+
value: {
|
|
18
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
19
|
+
body: 'lti-integration',
|
|
20
|
+
metadata: {},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
expect(wrapper.vm).toBeTruthy()
|
|
25
|
+
})
|
|
26
|
+
})
|