@windward/integrations 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/components/Content/Blocks/ExternalIntegration/LtiConsumer.vue +205 -0
  2. package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumer.vue +284 -3
  3. package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumers.vue +199 -3
  4. package/components/ExternalIntegration/Driver/Lti1p1/ManageProvider.vue +36 -6
  5. package/components/ExternalIntegration/Driver/Lti1p1/ManageProviders.vue +36 -17
  6. package/components/ExternalIntegration/Driver/ManageLti1p1.vue +5 -5
  7. package/components/ExternalIntegration/ProviderTargetPicker.vue +234 -0
  8. package/components/ExternalIntegration/ProviderTargetViewer.vue +50 -0
  9. package/components/Integration/Driver/ManageBase.vue +3 -1
  10. package/components/Integration/JobTable.vue +2 -1
  11. package/components/SecretField.vue +1 -1
  12. package/components/Settings/ExternalIntegration/LtiConsumerSettings.vue +105 -0
  13. package/i18n/en-US/components/content/blocks/external_integration/index.ts +5 -0
  14. package/i18n/en-US/components/content/blocks/external_integration/lti_consumer.ts +8 -0
  15. package/i18n/en-US/components/content/blocks/index.ts +5 -0
  16. package/i18n/en-US/components/content/index.ts +5 -0
  17. package/i18n/en-US/components/external_integration/driver/lti1p1.ts +11 -2
  18. package/i18n/en-US/components/external_integration/index.ts +2 -1
  19. package/i18n/en-US/components/external_integration/provider_target.ts +9 -0
  20. package/i18n/en-US/components/index.ts +4 -0
  21. package/i18n/en-US/components/settings/external_integration/index.ts +5 -0
  22. package/i18n/en-US/components/settings/external_integration/lti_consumer.ts +7 -0
  23. package/i18n/en-US/components/settings/index.ts +5 -0
  24. package/i18n/en-US/shared/content_blocks.ts +8 -0
  25. package/i18n/en-US/shared/index.ts +2 -0
  26. package/i18n/en-US/shared/settings.ts +5 -1
  27. package/package.json +1 -1
  28. package/pages/admin/importCourse.vue +3 -0
  29. package/pages/admin/vendors.vue +2 -1
  30. package/plugin.js +27 -1
  31. package/test/Components/Content/Blocks/ExternalIntegration/LtiConsumer.spec.js +26 -0
  32. package/test/Components/ExternalIntegration/ProviderTargetPicker.spec.js +22 -0
  33. package/test/Components/ExternalIntegration/ProviderTargetViewer.spec.js +22 -0
  34. package/test/__mocks__/componentsMock.js +12 -0
  35. package/test/__mocks__/modelMock.js +1 -1
  36. package/test/mocks.js +1 -0
@@ -0,0 +1,205 @@
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
+ ref="ltiForm"
32
+ name="ltiForm"
33
+ :target="target"
34
+ :action="launchData.target || ''"
35
+ :method="launchData.method || 'POST'"
36
+ >
37
+ <input
38
+ v-for="(value, key) of launchData.payload"
39
+ :key="key"
40
+ type="hidden"
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
+ components: { TextViewer },
87
+ extends: BaseContentBlock,
88
+ data() {
89
+ return {
90
+ consumer: null,
91
+ launchData: {},
92
+ saveState: false, // Override the base block to disable state saving
93
+ target: '_blank',
94
+ frameId: Crypto.id() + '_frame',
95
+ missing: false,
96
+ launched: false,
97
+ }
98
+ },
99
+ async fetch() {
100
+ this.target = this.frameId
101
+ await this.loadConsumer()
102
+ },
103
+ computed: {
104
+ ...mapGetters({
105
+ organization: 'organization/get',
106
+ course: 'course/get',
107
+ enrollment: 'enrollment/get',
108
+ }),
109
+ },
110
+ watch: {
111
+ 'block.metadata.config.tool_id': {
112
+ deep: true,
113
+ handler(newVal, oldVal) {
114
+ // We changed the consumer tool id, reload
115
+ if (newVal !== oldVal) {
116
+ this.loadConsumer()
117
+ }
118
+ },
119
+ },
120
+ },
121
+ beforeMount() {
122
+ if (_.isEmpty(this.block.metadata.config)) {
123
+ this.block.metadata.config = {}
124
+ }
125
+ if (_.isEmpty(this.block.metadata.config.tool_id)) {
126
+ this.block.metadata.config.tool_id = null
127
+ }
128
+ },
129
+ mounted() {},
130
+ methods: {
131
+ async loadConsumer() {
132
+ try {
133
+ if (!_.isEmpty(this.block.metadata.config.tool_id)) {
134
+ this.consumer = await new Lti1p1Consumer()
135
+ .for(
136
+ new Organization({ id: this.organization.id }),
137
+ new Course({ id: this.course.id })
138
+ )
139
+ .find(this.block.metadata.config.tool_id)
140
+ } else {
141
+ this.consumer = null
142
+ }
143
+ this.missing = false
144
+ } catch (e) {
145
+ // Data conflict error
146
+ if (e.response.status === 404) {
147
+ this.missing = true
148
+ }
149
+ }
150
+ },
151
+ async onLaunch() {
152
+ try {
153
+ // Clear the launch data in-case we're in inline mode
154
+ // If the iframe isn't destroyed subsequent launches will be new windows instead of the target iframe
155
+ this.launchData = {}
156
+
157
+ this.launchData = await Lti1p1Consumer.custom(
158
+ new Enrollment({ id: this.enrollment.id }),
159
+ this.consumer,
160
+ '/launch'
161
+ ).first()
162
+
163
+ switch (this.block.metadata.config.launch_type) {
164
+ case 'new_window':
165
+ this.target = '_blank'
166
+ break
167
+ case 'inline':
168
+ this.target = this.frameId
169
+ break
170
+ default:
171
+ this.target = '_blank'
172
+ }
173
+
174
+ // Do a forceUpdate so the hidden fields are ready and available for the post below
175
+ await this.$forceUpdate()
176
+
177
+ if (!_.isEmpty(this.launchData)) {
178
+ this.$refs.ltiForm.$el.submit()
179
+ this.launched = true
180
+ } else {
181
+ this.$dialog.error(
182
+ this.$t(
183
+ 'windward.integrations.components.content.blocks.external_integration.lti_consumer.unknown_error'
184
+ )
185
+ )
186
+ }
187
+ } catch (e) {
188
+ // eslint-disable-next-line no-console
189
+ console.error('LTI Link Launch Fail', e)
190
+ }
191
+ },
192
+ onBeforeSave() {
193
+ // Set a generic body since we don't use this field
194
+ this.block.body = 'lti-' + this.consumer.version + '-consumer'
195
+ },
196
+ },
197
+ }
198
+ </script>
199
+
200
+ <style scoped>
201
+ .launch-frame {
202
+ aspect-ratio: 16/9;
203
+ width: 100%;
204
+ }
205
+ </style>
@@ -1,13 +1,294 @@
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
+ id="description"
35
+ v-model="consumer.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 Organization from '~/models/Organization'
156
+ import Course from '~/models/Course'
157
+ import TextEditor from '~/components/Text/TextEditor.vue'
158
+ import Lti1p1Consumer from '../../../../models/ExternalIntegration/Lti1p1Consumer'
159
+ import FormVue from '~/components/Form'
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
+
249
+ // eslint-disable-next-line no-console
250
+ console.error('You do not have access to this consumer!')
251
+
252
+ // Return so we don't even attempt loading
253
+ return false
254
+ }
255
+
256
+ this.render = true
257
+ },
258
+ methods: {
259
+ addCustomParameter() {
260
+ this.consumer.metadata.custom.push({ key: '', value: '' })
261
+ },
262
+ deleteCustomParameter(index) {
263
+ this.consumer.metadata.custom.splice(index, 1)
264
+ },
265
+ async save() {
266
+ let consumer = new Lti1p1Consumer(this.consumer).for(
267
+ new Organization({ id: this.organization.id }),
268
+ new Course({ id: this.course.id })
269
+ )
270
+
271
+ try {
272
+ consumer = await consumer.save()
273
+ this.consumer = consumer
274
+
275
+ // Clone and delete the metadata since we don't need to share that around
276
+ // Also include the vendor going back for easier mapping
277
+ const consumerEvent = _.cloneDeep(consumer)
278
+
279
+ this.$dialog.success(this.$t('shared.forms.saved'))
280
+ this.$emit('update:consumer', consumerEvent)
281
+ } catch (e) {
282
+ this.$dialog.error(
283
+ this.$t('windward.integrations.shared.error.save_failed')
284
+ )
285
+ }
286
+ },
287
+ async onSave() {
288
+ if (this.formValid) {
289
+ await this.save()
290
+ }
291
+ },
10
292
  },
11
- methods: {},
12
293
  }
13
294
  </script>