@windward/integrations 0.0.5 → 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.
@@ -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>LTI 1.1 Consumers Not Implemented yet</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>LTI 1.1 Consumers Not Implemented yet</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
- <ListProviders></ListProviders>
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
- <ListConsumers></ListConsumers>
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 ListProviders from './Lti1p1/ManageProviders.vue'
34
- import ListConsumers from './Lti1p1/ManageConsumers.vue'
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: { ListProviders, ListConsumers },
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,5 @@
1
+ import ltiConsumer from './lti_consumer'
2
+
3
+ export default {
4
+ lti_consumer: ltiConsumer,
5
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import externalIntegration from './external_integration'
2
+
3
+ export default {
4
+ external_integration: externalIntegration,
5
+ }
@@ -0,0 +1,5 @@
1
+ import blocks from './blocks'
2
+
3
+ export default {
4
+ blocks,
5
+ }
@@ -1,8 +1,16 @@
1
1
  export default {
2
+ name: 'Name',
3
+ description: 'Description',
2
4
  key: 'Key',
3
5
  secret: 'Secret',
4
- auto_key: 'Autogenerated Key',
5
- auto_secret: 'Autogenerated Secret',
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,
@@ -0,0 +1,5 @@
1
+ import ltiConsumer from './lti_consumer'
2
+
3
+ export default {
4
+ lti_consumer: ltiConsumer,
5
+ }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ launch_type: 'Launch Type',
3
+ launch_type_new_window: 'New Window',
4
+ launch_type_inline_frame: 'Inline Frame',
5
+ link_select: 'Select a Link:',
6
+ link_disabled: 'Link Disabled',
7
+ }
@@ -0,0 +1,5 @@
1
+ import externalIntegration from './external_integration'
2
+
3
+ export default {
4
+ external_integration: externalIntegration,
5
+ }
@@ -0,0 +1,8 @@
1
+ export default {
2
+ title: {
3
+ lti_consumer: 'LTI Link',
4
+ },
5
+ grouping: {
6
+ integrations: 'Integrations',
7
+ },
8
+ }
@@ -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,
@@ -1 +1,5 @@
1
- export default {}
1
+ export default {
2
+ title: {
3
+ lti_consumer: 'LTI Consumer',
4
+ },
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/integrations",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Windward UI Plugin Integrations for 3rd Party Systems",
5
5
  "main": "plugin.js",
6
6
  "scripts": {
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
+ })