@windward/integrations 0.0.12 → 0.1.0

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.
@@ -1,23 +1,52 @@
1
1
  <template>
2
2
  <div>
3
3
  <div v-if="!missing && consumer">
4
- <h1>{{ consumer.name }}</h1>
5
- <TextViewer v-model="consumer.description"></TextViewer>
4
+ <h3>{{ block.metadata.config.title }}</h3>
6
5
 
7
- <v-btn
8
- v-if="consumer.enabled"
9
- color="primary"
10
- block
11
- @click="onLaunch"
6
+ <TextViewer
7
+ v-model="block.metadata.config.instructions"
8
+ ></TextViewer>
9
+
10
+ <v-row
11
+ v-if="
12
+ consumer.enabled &&
13
+ block.metadata.config.launch_type !== 'inline'
14
+ "
15
+ class="d-flex justify-center"
12
16
  >
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>
17
+ <v-col class="d-flex justify-center col-md-4">
18
+ <v-btn
19
+ v-if="
20
+ block.metadata.config.launch_type === 'new_window'
21
+ "
22
+ color="primary"
23
+ block
24
+ @click="onLaunch"
25
+ >
26
+ {{
27
+ $t(
28
+ 'windward.integrations.components.content.blocks.external_integration.lti_consumer.launch_in_' +
29
+ block.metadata.config.launch_type,
30
+ [consumer.name]
31
+ )
32
+ }}
33
+ </v-btn>
34
+ <v-btn
35
+ v-else
36
+ color="primary"
37
+ block
38
+ @click="setLaunchTimer(100)"
39
+ >
40
+ {{
41
+ $t(
42
+ 'windward.integrations.components.content.blocks.external_integration.lti_consumer.launch_in_' +
43
+ block.metadata.config.launch_type,
44
+ [consumer.name]
45
+ )
46
+ }}
47
+ </v-btn>
48
+ </v-col>
49
+ </v-row>
21
50
 
22
51
  <v-alert v-if="!consumer.enabled" type="error">
23
52
  {{
@@ -33,6 +62,7 @@
33
62
  :target="target"
34
63
  :action="launchData.target || ''"
35
64
  :method="launchData.method || 'POST'"
65
+ @submit.prevent
36
66
  >
37
67
  <input
38
68
  v-for="(value, key) of launchData.payload"
@@ -50,6 +80,35 @@
50
80
  :name="frameId"
51
81
  class="launch-frame"
52
82
  ></iframe>
83
+ <Dialog
84
+ v-if="block.metadata.config.launch_type === 'modal'"
85
+ v-model="openModal"
86
+ :trigger="false"
87
+ persistent
88
+ max-width="1350px"
89
+ @click:close="closeModal"
90
+ >
91
+ <template #title>{{ block.metadata.config.title }}</template>
92
+ <template #trigger>
93
+ <v-icon small>mdi-pencil</v-icon>
94
+ <span class="sr-only">{{
95
+ $t(
96
+ 'windward.integrations.components.external_integration.driver.lti1p1.edit'
97
+ )
98
+ }}</span>
99
+ </template>
100
+ <template #form="{ on, attrs }">
101
+ <iframe
102
+ v-if="launched"
103
+ v-bind="attrs"
104
+ :name="frameId"
105
+ class="launch-frame"
106
+ v-on="on"
107
+ ></iframe>
108
+
109
+ <div v-else></div>
110
+ </template>
111
+ </Dialog>
53
112
  </div>
54
113
 
55
114
  <v-alert v-if="missing" type="error">
@@ -88,17 +147,18 @@
88
147
  <script>
89
148
  import _ from 'lodash'
90
149
  import { mapGetters } from 'vuex'
91
- import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consumer'
150
+ import LtiConsumer from '../../../../models/ExternalIntegration/LtiConsumer'
92
151
  import Crypto from '~/helpers/Crypto'
93
152
  import TextViewer from '~/components/Text/TextViewer.vue'
94
153
  import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
95
154
  import Course from '~/models/Course'
96
155
  import Organization from '~/models/Organization'
97
156
  import Enrollment from '~/models/Enrollment'
157
+ import Dialog from '~/components/Dialog.vue'
98
158
 
99
159
  export default {
100
160
  name: 'ContentBlockExternalIntegrationLti1p1Consumer',
101
- components: { TextViewer },
161
+ components: { TextViewer, Dialog },
102
162
  extends: BaseContentBlock,
103
163
  data() {
104
164
  return {
@@ -109,6 +169,8 @@ export default {
109
169
  frameId: Crypto.id() + '_frame',
110
170
  missing: false,
111
171
  launched: false,
172
+ launchTimeout: {},
173
+ openModal: false,
112
174
  }
113
175
  },
114
176
  async fetch() {
@@ -123,12 +185,27 @@ export default {
123
185
  }),
124
186
  },
125
187
  watch: {
188
+ 'block.metadata.config.launch_type': {
189
+ deep: true,
190
+ handler(newVal, oldVal) {
191
+ // We changed the consumer tool id, reload
192
+ if (
193
+ newVal === 'inline' &&
194
+ this.block.metadata.config.tool_id !== null
195
+ ) {
196
+ this.setLaunchTimer()
197
+ }
198
+ },
199
+ },
126
200
  'block.metadata.config.tool_id': {
127
201
  deep: true,
128
202
  handler(newVal, oldVal) {
129
203
  // We changed the consumer tool id, reload
130
204
  if (newVal !== oldVal) {
131
205
  this.loadConsumer()
206
+ if (this.block.metadata.config.launch_type === 'inline') {
207
+ this.setLaunchTimer()
208
+ }
132
209
  }
133
210
  },
134
211
  },
@@ -140,13 +217,45 @@ export default {
140
217
  if (_.isEmpty(this.block.metadata.config.tool_id)) {
141
218
  this.block.metadata.config.tool_id = null
142
219
  }
220
+ if (_.isEmpty(this.block.metadata.config.title)) {
221
+ this.block.metadata.config.title = this.$t(
222
+ 'windward.integrations.components.content.blocks.external_integration.lti_consumer.title'
223
+ )
224
+ }
225
+ if (_.isEmpty(this.block.metadata.config.instructions)) {
226
+ this.block.metadata.config.instructions = this.$t(
227
+ 'windward.integrations.components.content.blocks.external_integration.lti_consumer.instructions'
228
+ )
229
+ }
230
+ },
231
+ mounted() {
232
+ if (
233
+ this.block.metadata.config.tool_id !== null &&
234
+ this.block.metadata.config.launch_type === 'inline'
235
+ ) {
236
+ this.setLaunchTimer()
237
+ }
238
+ },
239
+
240
+ destroyed() {
241
+ clearTimeout(this.launchTimeout)
143
242
  },
144
- mounted() {},
145
243
  methods: {
244
+ setLaunchTimer(timeout = 1000) {
245
+ clearTimeout(this.launchTimeout)
246
+ this.launchTimeout = setTimeout(this.onLaunch, timeout)
247
+ this.launched = true
248
+ },
249
+ closeModal() {
250
+ this.launchData = {}
251
+ this.target = ''
252
+ this.openModal = false
253
+ this.launched = false
254
+ },
146
255
  async loadConsumer() {
147
256
  try {
148
257
  if (!_.isEmpty(this.block.metadata.config.tool_id)) {
149
- this.consumer = await new Lti1p1Consumer()
258
+ this.consumer = await new LtiConsumer()
150
259
  .for(
151
260
  new Organization({ id: this.organization.id }),
152
261
  new Course({ id: this.course.id })
@@ -168,8 +277,11 @@ export default {
168
277
  // Clear the launch data in-case we're in inline mode
169
278
  // If the iframe isn't destroyed subsequent launches will be new windows instead of the target iframe
170
279
  this.launchData = {}
280
+ if (this.block.metadata.config.launch_type === 'modal') {
281
+ this.openModal = true
282
+ }
171
283
 
172
- this.launchData = await Lti1p1Consumer.custom(
284
+ this.launchData = await LtiConsumer.custom(
173
285
  new Enrollment({ id: this.enrollment.id }),
174
286
  this.consumer,
175
287
  '/launch'
@@ -182,6 +294,9 @@ export default {
182
294
  case 'inline':
183
295
  this.target = this.frameId
184
296
  break
297
+ case 'modal':
298
+ this.target = this.frameId
299
+ break
185
300
  default:
186
301
  this.target = '_blank'
187
302
  }
@@ -216,5 +331,6 @@ export default {
216
331
  .launch-frame {
217
332
  aspect-ratio: 16/9;
218
333
  width: 100%;
334
+ border-style: none;
219
335
  }
220
336
  </style>
@@ -33,6 +33,8 @@
33
33
  <TextEditor
34
34
  id="description"
35
35
  v-model="consumer.description"
36
+ menubar="edit"
37
+ toolbar="undo redo | bold italic underline strikethrough "
36
38
  :height="200"
37
39
  ></TextEditor>
38
40
 
@@ -148,7 +150,7 @@
148
150
  <script>
149
151
  import _ from 'lodash'
150
152
  import { mapGetters } from 'vuex'
151
- import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consumer'
153
+ import LtiConsumer from '../../../../models/ExternalIntegration/LtiConsumer'
152
154
  import Organization from '~/models/Organization'
153
155
  import Course from '~/models/Course'
154
156
  import TextEditor from '~/components/Text/TextEditor.vue'
@@ -160,7 +162,7 @@ export default {
160
162
  extends: FormVue,
161
163
  props: {
162
164
  value: {
163
- type: [Lti1p1Consumer, null],
165
+ type: [LtiConsumer, null],
164
166
  required: false,
165
167
  default: null,
166
168
  },
@@ -220,9 +222,9 @@ export default {
220
222
  },
221
223
  created() {
222
224
  if (_.isEmpty(this.value)) {
223
- this.consumer = new Lti1p1Consumer(this.consumer)
225
+ this.consumer = new LtiConsumer(this.consumer)
224
226
  } else {
225
- this.consumer = new Lti1p1Consumer(_.cloneDeep(this.value))
227
+ this.consumer = new LtiConsumer(_.cloneDeep(this.value))
226
228
  }
227
229
  },
228
230
  mounted() {
@@ -260,7 +262,7 @@ export default {
260
262
  this.consumer.metadata.custom.splice(index, 1)
261
263
  },
262
264
  async save() {
263
- let consumer = new Lti1p1Consumer(this.consumer).for(
265
+ let consumer = new LtiConsumer(this.consumer).for(
264
266
  new Organization({ id: this.organization.id }),
265
267
  new Course({ id: this.course.id })
266
268
  )
@@ -79,7 +79,7 @@
79
79
  <script>
80
80
  import _ from 'lodash'
81
81
  import { mapGetters } from 'vuex'
82
- import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consumer'
82
+ import LtiConsumer from '../../../../models/ExternalIntegration/LtiConsumer'
83
83
  import ManageConsumer from './ManageConsumer.vue'
84
84
  import Course from '~/models/Course'
85
85
  import Organization from '~/models/Organization'
@@ -168,11 +168,12 @@ export default {
168
168
  this.loadConsumers()
169
169
  },
170
170
  async loadConsumers() {
171
- this.consumers = await new Lti1p1Consumer()
171
+ this.consumers = await new LtiConsumer()
172
172
  .for(
173
173
  new Organization({ id: this.organization.id }),
174
174
  new Course({ id: this.course.id })
175
175
  )
176
+ .where('version', '1.1')
176
177
  .get()
177
178
  },
178
179
  onConfirmDelete(consumer) {
@@ -0,0 +1,284 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="!render" class="integration-loading">
4
+ <v-progress-circular size="128" indeterminate />
5
+ </div>
6
+ <div v-if="render">
7
+ <v-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.target_url'
15
+ )
16
+ "
17
+ :hint="
18
+ $t(
19
+ 'windward.integrations.components.external_integration.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
+ id="description"
35
+ v-model="consumer.description"
36
+ menubar="edit"
37
+ toolbar="undo redo | bold italic underline strikethrough "
38
+ :height="200"
39
+ ></TextEditor>
40
+
41
+ <v-switch
42
+ v-model="consumer.enabled"
43
+ :label="$t('shared.forms.enabled')"
44
+ />
45
+ <v-text-field
46
+ v-model="consumer.metadata.tool_oidc_auth_endpoint"
47
+ :placeholder="
48
+ $t(
49
+ 'windward.integrations.components.external_integration.driver.lti1p3.tool_oidc_auth_endpoint'
50
+ )
51
+ "
52
+ :label="
53
+ $t(
54
+ 'windward.integrations.components.external_integration.driver.lti1p3.tool_oidc_auth_endpoint'
55
+ )
56
+ "
57
+ ></v-text-field>
58
+ <v-text-field
59
+ v-model="consumer.metadata.tool_public_keyset_url"
60
+ :placeholder="
61
+ $t(
62
+ 'windward.integrations.components.external_integration.driver.lti1p3.tool_public_keyset_url'
63
+ )
64
+ "
65
+ :label="
66
+ $t(
67
+ 'windward.integrations.components.external_integration.driver.lti1p3.tool_public_keyset_url'
68
+ )
69
+ "
70
+ ></v-text-field>
71
+
72
+ <v-data-table
73
+ :headers="customParameterHeaders"
74
+ :items="consumer.metadata.custom"
75
+ hide-default-footer
76
+ class="elevation-1"
77
+ >
78
+ <template #[`item.key`]="{ index }">
79
+ <v-text-field
80
+ v-model="
81
+ consumer.metadata.custom[index].key
82
+ "
83
+ :label="
84
+ $t(
85
+ 'windward.integrations.components.external_integration.driver.lti1p1.parameter_name'
86
+ )
87
+ "
88
+ />
89
+ </template>
90
+ <template #[`item.value`]="{ index }">
91
+ <v-text-field
92
+ v-model="
93
+ consumer.metadata.custom[index].value
94
+ "
95
+ :label="
96
+ $t(
97
+ 'windward.integrations.components.external_integration.driver.lti1p1.value'
98
+ )
99
+ "
100
+ />
101
+ </template>
102
+ <template #[`item.actions`]="{ index }">
103
+ <v-btn
104
+ text
105
+ color="primary"
106
+ @click="deleteCustomParameter(index)"
107
+ >
108
+ <v-icon small> mdi-delete </v-icon>
109
+ <span class="sr-only">{{
110
+ $t('shared.forms.delete')
111
+ }}</span>
112
+ </v-btn>
113
+ </template>
114
+ <template #footer>
115
+ <div class="text-center">
116
+ <v-btn
117
+ color="primary"
118
+ class="mb-3"
119
+ @click="addCustomParameter"
120
+ >{{ $t('shared.forms.add') }}</v-btn
121
+ >
122
+ </div>
123
+ </template>
124
+ </v-data-table>
125
+ <br />
126
+ <v-select
127
+ v-model="consumer.metadata.security_level"
128
+ :items="securityLevels"
129
+ item-text="name"
130
+ item-value="value"
131
+ label="Security Level"
132
+ outlined
133
+ ></v-select>
134
+ </v-col>
135
+ </v-row>
136
+ </v-form>
137
+ </div>
138
+ </div>
139
+ </template>
140
+
141
+ <script>
142
+ import _ from 'lodash'
143
+ import { mapGetters } from 'vuex'
144
+ import LtiConsumer from '../../../../models/ExternalIntegration/LtiConsumer'
145
+ import Organization from '~/models/Organization'
146
+ import Course from '~/models/Course'
147
+ import TextEditor from '~/components/Text/TextEditor.vue'
148
+ import FormVue from '~/components/Form'
149
+
150
+ export default {
151
+ name: 'ManageLti1p3ConsumerDriver',
152
+ components: { TextEditor },
153
+ extends: FormVue,
154
+ props: {
155
+ value: {
156
+ type: [LtiConsumer, null],
157
+ required: false,
158
+ default: null,
159
+ },
160
+ },
161
+ emits: ['update:consumer'],
162
+ meta: {
163
+ privilege: {
164
+ '': {
165
+ writable: true,
166
+ },
167
+ },
168
+ },
169
+ data() {
170
+ return {
171
+ render: false,
172
+ consumer: {
173
+ version: '1.3',
174
+ metadata: {
175
+ custom: [],
176
+ security_level: '',
177
+ security: [],
178
+ },
179
+ },
180
+ customParameterHeaders: [
181
+ {
182
+ text: this.$t(
183
+ 'windward.integrations.components.external_integration.driver.lti1p1.parameter_name'
184
+ ),
185
+ value: 'key',
186
+ },
187
+ {
188
+ text: this.$t(
189
+ 'windward.integrations.components.external_integration.driver.lti1p1.value'
190
+ ),
191
+ value: 'value',
192
+ },
193
+ {
194
+ text: this.$t('shared.forms.actions'),
195
+ value: 'actions',
196
+ sortable: false,
197
+ },
198
+ ],
199
+ securityLevels: [
200
+ { name: 'Full Access', value: 'full' },
201
+ { name: 'Email Only', value: 'email' },
202
+ { name: 'Name Only', value: 'name' },
203
+ { name: 'Anonymous', value: 'anonymous' },
204
+ // { name: 'Custom', value: 'custom' }, // TODO: When this is selected provide direct access to check off LTI fields
205
+ ],
206
+ }
207
+ },
208
+ computed: {
209
+ ...mapGetters({
210
+ organization: 'organization/get',
211
+ course: 'course/get',
212
+ }),
213
+ },
214
+ created() {
215
+ if (_.isEmpty(this.value)) {
216
+ this.consumer = new LtiConsumer(this.consumer)
217
+ } else {
218
+ this.consumer = new LtiConsumer(_.cloneDeep(this.value))
219
+ }
220
+ },
221
+ mounted() {
222
+ if (
223
+ !this.$PermissionService.userHasAccessTo(
224
+ 'plugin.windward.integrations.course.externalIntegration',
225
+ 'writable'
226
+ )
227
+ ) {
228
+ // Display an angry error that they can't view this driver
229
+ this.$dialog.error(this.$t('shared.error.description_401'), {
230
+ duration: null,
231
+ action: {
232
+ text: this.$t('shared.forms.close'),
233
+ onClick: (_e, toastObject) => {
234
+ toastObject.goAway(0)
235
+ },
236
+ },
237
+ })
238
+
239
+ // eslint-disable-next-line no-console
240
+ console.error('You do not have access to this consumer!')
241
+
242
+ // Return so we don't even attempt loading
243
+ return false
244
+ }
245
+
246
+ this.render = true
247
+ },
248
+ methods: {
249
+ addCustomParameter() {
250
+ this.consumer.metadata.custom.push({ key: '', value: '' })
251
+ },
252
+ deleteCustomParameter(index) {
253
+ this.consumer.metadata.custom.splice(index, 1)
254
+ },
255
+ async save() {
256
+ let consumer = new LtiConsumer(this.consumer).for(
257
+ new Organization({ id: this.organization.id }),
258
+ new Course({ id: this.course.id })
259
+ )
260
+
261
+ try {
262
+ consumer = await consumer.save()
263
+ this.consumer = consumer
264
+
265
+ // Clone and delete the metadata since we don't need to share that around
266
+ // Also include the vendor going back for easier mapping
267
+ const consumerEvent = _.cloneDeep(consumer)
268
+
269
+ this.$dialog.success(this.$t('shared.forms.saved'))
270
+ this.$emit('update:consumer', consumerEvent)
271
+ } catch (e) {
272
+ this.$dialog.error(
273
+ this.$t('windward.integrations.shared.error.save_failed')
274
+ )
275
+ }
276
+ },
277
+ async onSave() {
278
+ if (this.formValid) {
279
+ await this.save()
280
+ }
281
+ },
282
+ },
283
+ }
284
+ </script>
@@ -0,0 +1,229 @@
1
+ <template>
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>{{
15
+ $t(
16
+ 'windward.integrations.components.external_integration.driver.lti1p1.new'
17
+ )
18
+ }}</template>
19
+ <template #form="{ on, attrs }"
20
+ ><ManageConsumer v-bind="attrs" v-on="on"></ManageConsumer
21
+ ></template>
22
+ </Dialog>
23
+
24
+ <v-data-table
25
+ :headers="headers"
26
+ :items="consumers"
27
+ :items-per-page="10"
28
+ class="elevation-1"
29
+ >
30
+ <template #[`item.description`]="{ item }">
31
+ {{ item.description.replace(/(<([^>]+)>)/gi, '').trim() }}
32
+ </template>
33
+
34
+ <template #[`item.enabled`]="{ item }">
35
+ <v-icon :color="item.enabled ? 'success' : 'error'"
36
+ >{{ item.enabled ? 'mdi-check' : 'mdi-close' }}
37
+ </v-icon>
38
+ <span v-if="!item.enabled" class="sr-only">{{
39
+ $t('shared.forms.enabled')
40
+ }}</span>
41
+ </template>
42
+
43
+ <template #[`item.created_at`]="{ item }">
44
+ {{ $d(new Date(item.created_at), 'long') }}
45
+ </template>
46
+ <template #[`item.actions`]="{ index, item }">
47
+ <Dialog color="primary">
48
+ <template #title>{{
49
+ $t(
50
+ 'windward.integrations.components.external_integration.driver.lti1p3.view'
51
+ )
52
+ }}</template>
53
+ <template #trigger>
54
+ <v-icon small>mdi-eye-outline</v-icon>
55
+ <span class="sr-only">{{
56
+ $t(
57
+ 'windward.integrations.components.external_integration.driver.lti1p3.view'
58
+ )
59
+ }}</span>
60
+ </template>
61
+ <template #form="{ on, attrs }"
62
+ ><ViewConsumer
63
+ v-model="consumers[index]"
64
+ v-bind="attrs"
65
+ v-on="on"
66
+ ></ViewConsumer
67
+ ></template>
68
+ </Dialog>
69
+ <Dialog color="primary" action-save @click:save="onSaved">
70
+ <template #title>{{
71
+ $t(
72
+ 'windward.integrations.components.external_integration.driver.lti1p3.edit'
73
+ )
74
+ }}</template>
75
+ <template #trigger>
76
+ <v-icon small>mdi-pencil</v-icon>
77
+ <span class="sr-only">{{
78
+ $t(
79
+ 'windward.integrations.components.external_integration.driver.lti1p3.edit'
80
+ )
81
+ }}</span>
82
+ </template>
83
+ <template #form="{ on, attrs }"
84
+ ><ManageConsumer
85
+ v-model="consumers[index]"
86
+ v-bind="attrs"
87
+ v-on="on"
88
+ ></ManageConsumer
89
+ ></template>
90
+ </Dialog>
91
+
92
+ <v-btn icon>
93
+ <v-icon @click="onConfirmDelete(item)"> mdi-delete </v-icon>
94
+ <span class="sr-only">{{ $t('shared.forms.delete') }}</span>
95
+ </v-btn>
96
+ </template>
97
+ </v-data-table>
98
+ </div>
99
+ </template>
100
+
101
+ <script>
102
+ import _ from 'lodash'
103
+ import { mapGetters } from 'vuex'
104
+ import LtiConsumer from '../../../../models/ExternalIntegration/LtiConsumer'
105
+ import ManageConsumer from './ManageConsumer.vue'
106
+ import ViewConsumer from './ViewConsumer.vue'
107
+ import Course from '~/models/Course'
108
+ import Organization from '~/models/Organization'
109
+ import Dialog from '~/components/Dialog.vue'
110
+
111
+ export default {
112
+ name: 'ManageLti1p1ConsumersDriver',
113
+ components: { Dialog, ManageConsumer, ViewConsumer },
114
+ data() {
115
+ return {
116
+ consumers: [],
117
+ headers: [
118
+ {
119
+ text: this.$t(
120
+ 'windward.integrations.components.external_integration.target_url'
121
+ ),
122
+ value: 'target',
123
+ },
124
+ {
125
+ text: this.$t('shared.forms.name'),
126
+ value: 'name',
127
+ },
128
+ {
129
+ text: this.$t('shared.forms.description'),
130
+ value: 'description',
131
+ },
132
+ {
133
+ text: this.$t('shared.forms.enabled'),
134
+ value: 'enabled',
135
+ },
136
+ { text: this.$t('shared.forms.created'), value: 'created_at' },
137
+ {
138
+ text: this.$t('shared.forms.actions'),
139
+ value: 'actions',
140
+ sortable: false,
141
+ },
142
+ ],
143
+ }
144
+ },
145
+
146
+ async fetch() {
147
+ if (
148
+ !this.$PermissionService.userHasAccessTo(
149
+ 'plugin.windward.integrations.course.externalIntegration',
150
+ 'readable'
151
+ ) ||
152
+ _.isEmpty(this.organization.id) ||
153
+ _.isEmpty(this.course.id)
154
+ ) {
155
+ // Display an angry error that they can't view this driver
156
+ this.$dialog.error(this.$t('shared.error.description_401'), {
157
+ duration: null,
158
+ action: {
159
+ text: this.$t('shared.forms.close'),
160
+ onClick: (_e, toastObject) => {
161
+ toastObject.goAway(0)
162
+ },
163
+ },
164
+ })
165
+ if (_.isEmpty(this.organization.id) || _.isEmpty(this.course.id)) {
166
+ // eslint-disable-next-line no-console
167
+ console.error(
168
+ 'Cannot load external integrations because organization or course is not set!'
169
+ )
170
+ } else {
171
+ // eslint-disable-next-line no-console
172
+ console.error(
173
+ 'You do not have access to this external integration!'
174
+ )
175
+ }
176
+
177
+ // Return so we don't even attempt loading
178
+ return false
179
+ }
180
+
181
+ await this.loadConsumers()
182
+ },
183
+ computed: {
184
+ ...mapGetters({
185
+ organization: 'organization/get',
186
+ course: 'course/get',
187
+ }),
188
+ },
189
+ methods: {
190
+ onSaved() {
191
+ this.loadConsumers()
192
+ },
193
+ async loadConsumers() {
194
+ this.consumers = await new LtiConsumer()
195
+ .for(
196
+ new Organization({ id: this.organization.id }),
197
+ new Course({ id: this.course.id })
198
+ )
199
+ .where('version', '1.3')
200
+ .get()
201
+ },
202
+ onConfirmDelete(consumer) {
203
+ this.$dialog.show(this.$t('shared.forms.confirm_delete_text'), {
204
+ icon: 'mdi-help',
205
+ duration: null,
206
+ action: [
207
+ {
208
+ text: this.$t('shared.forms.cancel'),
209
+ onClick: (_e, toastObject) => {
210
+ toastObject.goAway(0)
211
+ },
212
+ },
213
+ {
214
+ text: this.$t('shared.forms.confirm'),
215
+ // router navigation
216
+ onClick: (_e, toastObject) => {
217
+ this.deleteConsumer(consumer)
218
+ toastObject.goAway(0)
219
+ },
220
+ },
221
+ ],
222
+ })
223
+ },
224
+ deleteConsumer(consumer) {
225
+ consumer.delete()
226
+ },
227
+ },
228
+ }
229
+ </script>
@@ -127,6 +127,20 @@
127
127
  )
128
128
  "
129
129
  ></v-text-field>
130
+ <v-text-field
131
+ id="lti-oauth2-aud"
132
+ v-model="provider.metadata.platform_oauth2_audience"
133
+ :label="
134
+ $t(
135
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_oauth2_audience'
136
+ )
137
+ "
138
+ :hint="
139
+ $t(
140
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_oauth2_audience'
141
+ )
142
+ "
143
+ ></v-text-field>
130
144
  <v-text-field
131
145
  id="lti-client-id"
132
146
  v-model="provider.metadata.tool_client_id"
@@ -0,0 +1,224 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="!render" class="integration-loading">
4
+ <v-progress-circular size="128" indeterminate />
5
+ </div>
6
+ <div v-if="render">
7
+ <v-form v-model="formValid" @submit.prevent>
8
+ <v-row justify="center" align="center" class="mt-5">
9
+ <v-col cols="12">
10
+ <SecretField
11
+ v-model="consumer.metadata.platform_id"
12
+ :hidden="false"
13
+ :placeholder="
14
+ $t(
15
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_id'
16
+ )
17
+ "
18
+ :label="
19
+ $t(
20
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_id'
21
+ )
22
+ "
23
+ ></SecretField>
24
+ <SecretField
25
+ v-model="consumer.metadata.client_id"
26
+ :hidden="false"
27
+ :placeholder="
28
+ $t(
29
+ 'windward.integrations.components.external_integration.driver.lti1p3.tool_client_id'
30
+ )
31
+ "
32
+ :label="
33
+ $t(
34
+ 'windward.integrations.components.external_integration.driver.lti1p3.tool_client_id'
35
+ )
36
+ "
37
+ ></SecretField>
38
+ <SecretField
39
+ v-model="consumer.metadata.deployment_id"
40
+ :hidden="false"
41
+ :placeholder="
42
+ $t(
43
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_deployment_id'
44
+ )
45
+ "
46
+ :label="
47
+ $t(
48
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_deployment_id'
49
+ )
50
+ "
51
+ ></SecretField>
52
+ <SecretField
53
+ v-model="
54
+ consumer.metadata.platform_oidc_auth_endpoint
55
+ "
56
+ :hidden="false"
57
+ :placeholder="
58
+ $t(
59
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_oidc_auth_endpoint'
60
+ )
61
+ "
62
+ :label="
63
+ $t(
64
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_oidc_auth_endpoint'
65
+ )
66
+ "
67
+ ></SecretField>
68
+ <SecretField
69
+ v-model="
70
+ consumer.metadata.platform_public_keyset_url
71
+ "
72
+ :hidden="false"
73
+ :placeholder="
74
+ $t(
75
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_public_keyset_url'
76
+ )
77
+ "
78
+ :label="
79
+ $t(
80
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_public_keyset_url'
81
+ )
82
+ "
83
+ ></SecretField>
84
+ <SecretField
85
+ v-model="
86
+ consumer.metadata
87
+ .platform_oauth2_access_token_url
88
+ "
89
+ :hidden="false"
90
+ :placeholder="
91
+ $t(
92
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_oauth2_access_token_url'
93
+ )
94
+ "
95
+ :label="
96
+ $t(
97
+ 'windward.integrations.components.external_integration.driver.lti1p3.platform_oauth2_access_token_url'
98
+ )
99
+ "
100
+ ></SecretField>
101
+ </v-col>
102
+ </v-row>
103
+ </v-form>
104
+ </div>
105
+ </div>
106
+ </template>
107
+
108
+ <script>
109
+ import _ from 'lodash'
110
+ import { mapGetters } from 'vuex'
111
+ import LtiConsumer from '../../../../models/ExternalIntegration/LtiConsumer'
112
+ import SecretField from '../../../SecretField.vue'
113
+ import Organization from '~/models/Organization'
114
+ import Course from '~/models/Course'
115
+ import FormVue from '~/components/Form'
116
+ export default {
117
+ name: 'ViewLti1p3Consumer',
118
+ components: { SecretField },
119
+ extends: FormVue,
120
+ props: {
121
+ value: {
122
+ type: [LtiConsumer, null],
123
+ required: false,
124
+ default: null,
125
+ },
126
+ },
127
+ emits: ['view:consumer'],
128
+ meta: {
129
+ privilege: {
130
+ '': {
131
+ writable: true,
132
+ },
133
+ },
134
+ },
135
+ data() {
136
+ return {
137
+ render: false,
138
+ consumer: {
139
+ version: '1.3',
140
+ metadata: {
141
+ custom: [],
142
+ security_level: '',
143
+ security: [],
144
+ },
145
+ },
146
+ }
147
+ },
148
+ computed: {
149
+ ...mapGetters({
150
+ organization: 'organization/get',
151
+ course: 'course/get',
152
+ }),
153
+ },
154
+ created() {
155
+ if (_.isEmpty(this.value)) {
156
+ this.consumer = new LtiConsumer(this.consumer)
157
+ } else {
158
+ this.consumer = new LtiConsumer(_.cloneDeep(this.value))
159
+ }
160
+ },
161
+ mounted() {
162
+ if (
163
+ !this.$PermissionService.userHasAccessTo(
164
+ 'plugin.windward.integrations.course.externalIntegration',
165
+ 'writable'
166
+ )
167
+ ) {
168
+ // Display an angry error that they can't view this driver
169
+ this.$dialog.error(this.$t('shared.error.description_401'), {
170
+ duration: null,
171
+ action: {
172
+ text: this.$t('shared.forms.close'),
173
+ onClick: (_e, toastObject) => {
174
+ toastObject.goAway(0)
175
+ },
176
+ },
177
+ })
178
+
179
+ // eslint-disable-next-line no-console
180
+ console.error('You do not have access to this consumer!')
181
+
182
+ // Return so we don't even attempt loading
183
+ return false
184
+ }
185
+
186
+ this.render = true
187
+ },
188
+ methods: {
189
+ addCustomParameter() {
190
+ this.consumer.metadata.custom.push({ key: '', value: '' })
191
+ },
192
+ deleteCustomParameter(index) {
193
+ this.consumer.metadata.custom.splice(index, 1)
194
+ },
195
+ async save() {
196
+ let consumer = new LtiConsumer(this.consumer).for(
197
+ new Organization({ id: this.organization.id }),
198
+ new Course({ id: this.course.id })
199
+ )
200
+
201
+ try {
202
+ consumer = await consumer.save()
203
+ this.consumer = consumer
204
+
205
+ // Clone and delete the metadata since we don't need to share that around
206
+ // Also include the vendor going back for easier mapping
207
+ const consumerEvent = _.cloneDeep(consumer)
208
+
209
+ this.$dialog.success(this.$t('shared.forms.saved'))
210
+ this.$emit('update:consumer', consumerEvent)
211
+ } catch (e) {
212
+ this.$dialog.error(
213
+ this.$t('windward.integrations.shared.error.save_failed')
214
+ )
215
+ }
216
+ },
217
+ async onSave() {
218
+ if (this.formValid) {
219
+ await this.save()
220
+ }
221
+ },
222
+ },
223
+ }
224
+ </script>
@@ -22,7 +22,7 @@
22
22
  }}
23
23
  </v-expansion-panel-header>
24
24
  <v-expansion-panel-content>
25
- <!-- <ManageConsumers></ManageConsumers>-->
25
+ <ManageConsumers></ManageConsumers>
26
26
  </v-expansion-panel-content>
27
27
  </v-expansion-panel>
28
28
  </v-expansion-panels>
@@ -31,11 +31,11 @@
31
31
 
32
32
  <script>
33
33
  import ManageProviders from './Lti1p3/ManageProviders.vue'
34
- /* import ManageConsumers from './Lti1p3/ManageConsumers.vue' */
34
+ import ManageConsumers from './Lti1p3/ManageConsumers.vue'
35
35
 
36
36
  export default {
37
37
  name: 'ManageLti1p3Driver',
38
- components: { ManageProviders },
38
+ components: { ManageProviders, ManageConsumers },
39
39
  data() {
40
40
  return {
41
41
  panel: 0,
@@ -1,5 +1,12 @@
1
1
  <template>
2
- <v-text-field id="secret-field" :value="value" readonly :type="fieldType">
2
+ <v-text-field
3
+ id="secret-field"
4
+ :value="value"
5
+ readonly
6
+ :type="fieldType"
7
+ :placeholder="placeholder"
8
+ :label="label"
9
+ >
3
10
  <template #append>
4
11
  <v-btn icon @click="toggleClear">
5
12
  <v-icon>{{ showClear ? 'mdi-eye-off' : 'mdi-eye' }}</v-icon>
@@ -19,6 +26,14 @@ export default {
19
26
  value: { type: String, required: false, default: '' },
20
27
  hidden: { type: Boolean, required: false, default: true },
21
28
  copy: { type: Boolean, required: false, default: true },
29
+ placeholder: {
30
+ type: String,
31
+ required: false,
32
+ },
33
+ label: {
34
+ type: String,
35
+ required: false,
36
+ },
22
37
  },
23
38
  data() {
24
39
  return {
@@ -1,5 +1,30 @@
1
1
  <template>
2
2
  <v-container>
3
+ <v-text-field
4
+ v-model="block.metadata.config.title"
5
+ outlined
6
+ :counter="50"
7
+ maxlength="50"
8
+ :label="
9
+ $t(
10
+ 'windward.integrations.components.settings.external_integration.lti_consumer.title'
11
+ )
12
+ "
13
+ ></v-text-field>
14
+ <v-textarea
15
+ v-model="block.metadata.config.instructions"
16
+ outlined
17
+ :counter="255"
18
+ maxlength="255"
19
+ :label="
20
+ $t(
21
+ 'windward.integrations.components.settings.external_integration.lti_consumer.instructions'
22
+ )
23
+ "
24
+ ></v-textarea>
25
+ <br />
26
+ <v-divider class="primary"></v-divider>
27
+ <br />
3
28
  <v-select
4
29
  v-model="block.metadata.config.launch_type"
5
30
  :label="
@@ -17,40 +42,55 @@
17
42
  )
18
43
  }}
19
44
  <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)"
45
+ <v-list-item-group
46
+ v-model="block.metadata.config.tool_id"
47
+ color="primary"
26
48
  >
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>
49
+ <v-list-item
50
+ v-for="consumer in consumers"
51
+ :key="consumer.id"
52
+ two-line
53
+ :disabled="!consumer.enabled"
54
+ :value="consumer.id"
55
+ >
56
+ <v-list-item-avatar
57
+ :color="!consumer.enabled ? 'error' : ''"
58
+ >
59
+ <v-icon> mdi-shield-link-variant-outline</v-icon>
60
+ </v-list-item-avatar>
61
+ <v-list-item-content color="secondary">
62
+ <v-list-item-title>{{
63
+ consumer.name
64
+ }}</v-list-item-title>
65
+ <v-list-item-subtitle v-if="!consumer.enabled">
66
+ <v-chip color="error"
67
+ >{{
68
+ $t(
69
+ 'windward.integrations.components.settings.external_integration.lti_consumer.link_disabled'
70
+ )
71
+ }}
72
+ </v-chip>
73
+ </v-list-item-subtitle>
74
+ <v-list-item-subtitle
75
+ >{{
76
+ consumer.description
77
+ .replace(/(<([^>]+)>)/gi, '')
78
+ .trim()
79
+ }}
80
+ </v-list-item-subtitle>
81
+ </v-list-item-content>
82
+ </v-list-item>
83
+ </v-list-item-group>
44
84
  </v-list>
45
85
  </v-container>
46
86
  </template>
47
87
 
48
88
  <script>
49
89
  import { mapGetters } from 'vuex'
90
+ import LtiConsumer from '../../../models/ExternalIntegration/LtiConsumer'
50
91
  import BaseContentSettings from '~/components/Content/Tool/BaseContentSettings.js'
51
92
  import Course from '~/models/Course'
52
93
  import Organization from '~/models/Organization'
53
- import Lti1p1Consumer from '../../../models/ExternalIntegration/Lti1p1Consumer'
54
94
 
55
95
  export default {
56
96
  name: 'ContentBlockExternalIntegrationLti1p1ConsumerSettings',
@@ -75,11 +115,17 @@ export default {
75
115
  ),
76
116
  value: 'inline',
77
117
  },
118
+ {
119
+ name: this.$t(
120
+ 'windward.integrations.components.settings.external_integration.lti_consumer.launch_type_modal'
121
+ ),
122
+ value: 'modal',
123
+ },
78
124
  ],
79
125
  }
80
126
  },
81
127
  async fetch() {
82
- this.consumers = await new Lti1p1Consumer()
128
+ this.consumers = await new LtiConsumer()
83
129
  .for(
84
130
  new Organization({ id: this.organization.id }),
85
131
  new Course({ id: this.course.id })
@@ -96,10 +142,6 @@ export default {
96
142
  watch: {},
97
143
  beforeMount() {},
98
144
  mounted() {},
99
- methods: {
100
- onSelectLink(consumer) {
101
- this.block.metadata.config.tool_id = consumer.id
102
- },
103
- },
145
+ methods: {},
104
146
  }
105
147
  </script>
@@ -1,5 +1,12 @@
1
1
  export default {
2
+ title: 'LTI Consumer Block Title',
3
+ instructions:
4
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam fringilla ipsum eget ante tempus blandit. Maecenas vel massa\n' +
5
+ 'nec tellus vestibulum porttitor non a enim.',
2
6
  launch: 'Launch',
7
+ goodbye: 'goodbye',
8
+ launch_in_new_window: 'launch {0} in new tab',
9
+ launch_in_modal: 'launch {0} in modal',
3
10
  configure_warning:
4
11
  'This block needs to be configured. Please select a LTI tool in the settings panel.',
5
12
  no_access:
@@ -1,12 +1,15 @@
1
1
  export default {
2
2
  key: 'Key',
3
+ issuer: 'Issuer',
3
4
  tool_public_keyset_url: 'Tool Keyset Url',
4
5
  tool_oidc_auth_endpoint: 'Tool OpenID Connect Endpoint',
6
+ login_hint: 'login hint',
5
7
  platform_id: 'Platform Identifier',
6
8
  platform_name: 'Platform Name',
7
9
  platform_public_keyset_url: 'Platform Keyset Url',
8
10
  platform_oidc_auth_endpoint: 'Platform OpenID Connect Endpoint',
9
11
  platform_oauth2_access_token_url: 'Platform OAuth2 Access Token URL',
12
+ platform_oauth2_audience: 'Platform Oauth2 audience',
10
13
  tool_client_id: 'Tool Client ID',
11
14
  platform_deployment_id: 'Deployment ID',
12
15
  enable_button_feedback:
@@ -15,4 +18,5 @@ export default {
15
18
  value: 'Value',
16
19
  new: 'New LTI Link',
17
20
  edit: 'Edit LTI Link',
21
+ view: 'View LTI link details',
18
22
  }
@@ -1,7 +1,10 @@
1
1
  export default {
2
+ title: 'title',
3
+ instructions: 'instructions',
2
4
  launch_type: 'Launch Type',
3
5
  launch_type_new_window: 'New Window',
4
6
  launch_type_inline_frame: 'Inline Frame',
7
+ launch_type_modal: 'Modal',
5
8
  link_select: 'Select a Link:',
6
9
  link_disabled: 'Link Disabled',
7
10
  }
@@ -1,7 +1,7 @@
1
1
  // @ts-ignore
2
2
  import Model from '~/models/Model'
3
3
 
4
- export default class Lti1p1Consumer extends Model {
4
+ export default class LtiConsumer extends Model {
5
5
  get required(): string[] {
6
6
  return []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/integrations",
3
- "version": "0.0.12",
3
+ "version": "0.1.0",
4
4
  "description": "Windward UI Plugin Integrations for 3rd Party Systems",
5
5
  "main": "plugin.js",
6
6
  "scripts": {