|
| 1 | +<!-- |
| 2 | +Copyright 2017 Super Adventure Developers |
| 3 | +See the NOTICE file at the top-level directory of this distribution and at |
| 4 | +https://github.com/nafundi/super-adventure/blob/master/NOTICE. |
| 5 | + |
| 6 | +This file is part of Super Adventure. It is subject to the license terms in |
| 7 | +the LICENSE file found in the top-level directory of this distribution and at |
| 8 | +https://www.apache.org/licenses/LICENSE-2.0. No part of Super Adventure, |
| 9 | +including this file, may be copied, modified, propagated, or distributed |
| 10 | +except according to the terms contained in the LICENSE file. |
| 11 | +--> |
| 12 | +<template> |
| 13 | + <div> |
| 14 | + <alert v-bind="alert" @close="alert.state = false"/> |
| 15 | + <float-row> |
| 16 | + <button type="button" class="btn btn-primary" @click="newFieldKey.state = true"> |
| 17 | + <span class="icon-plus-circle"></span> Create Field Key |
| 18 | + </button> |
| 19 | + </float-row> |
| 20 | + <loading v-if="fieldKeys == null" :state="awaitingResponse"/> |
| 21 | + <p v-else-if="fieldKeys.length === 0"> |
| 22 | + There are no field keys yet. You will need to create some to download |
| 23 | + forms and submit data from your device. |
| 24 | + </p> |
| 25 | + <table v-else id="field-key-list-table" class="table table-hover"> |
| 26 | + <thead> |
| 27 | + <tr> |
| 28 | + <th>Nickname</th> |
| 29 | + <th>Created</th> |
| 30 | + <th>Last Used</th> |
| 31 | + <th>Auto-Configure</th> |
| 32 | + </tr> |
| 33 | + </thead> |
| 34 | + <tbody> |
| 35 | + <tr v-for="(fieldKey, index) in fieldKeys" :key="fieldKey.key" |
| 36 | + :class="highlight(fieldKey, 'id')" :data-index="index"> |
| 37 | + <td>{{ fieldKey.displayName }}</td> |
| 38 | + <td>{{ fieldKey.created }}</td> |
| 39 | + <td>{{ fieldKey.lastUsed }}</td> |
| 40 | + <td> |
| 41 | + <a class="field-key-list-popover-link" role="button">See code</a> |
| 42 | + </td> |
| 43 | + </tr> |
| 44 | + </tbody> |
| 45 | + </table> |
| 46 | + |
| 47 | + <field-key-new v-bind="newFieldKey" @hide="newFieldKey.state = false" |
| 48 | + @success="afterCreate"/> |
| 49 | + </div> |
| 50 | +</template> |
| 51 | + |
| 52 | +<script> |
| 53 | +import Vue from 'vue'; |
| 54 | +import moment from 'moment'; |
| 55 | +import qrcode from 'qrcode-generator'; |
| 56 | +import { deflate } from 'pako/lib/deflate'; |
| 57 | + |
| 58 | +import FieldKeyNew from './new.vue'; |
| 59 | +import alert from '../../mixins/alert'; |
| 60 | +import highlight from '../../mixins/highlight'; |
| 61 | +import request from '../../mixins/request'; |
| 62 | + |
| 63 | +const QR_CODE_TYPE_NUMBER = 0; |
| 64 | +// This is the level used in Collect. |
| 65 | +const QR_CODE_ERROR_CORRECTION_LEVEL = 'L'; |
| 66 | +const QR_CODE_CELL_SIZE = 3; |
| 67 | +const QR_CODE_MARGIN = 0; |
| 68 | + |
| 69 | +class FieldKeyPresenter { |
| 70 | + constructor(fieldKey) { |
| 71 | + this._fieldKey = fieldKey; |
| 72 | + } |
| 73 | + |
| 74 | + get id() { return this._fieldKey.id; } |
| 75 | + get displayName() { return this._fieldKey.displayName; } |
| 76 | + |
| 77 | + get key() { |
| 78 | + if (this._key != null) return this._key; |
| 79 | + this._key = Vue.prototype.$uniqueId(); |
| 80 | + return this._key; |
| 81 | + } |
| 82 | + |
| 83 | + get created() { |
| 84 | + const createdAt = moment(this._fieldKey.createdAt).fromNow(); |
| 85 | + const createdBy = this._fieldKey.createdBy.displayName; |
| 86 | + return `${createdAt} by ${createdBy}`; |
| 87 | + } |
| 88 | + |
| 89 | + get lastUsed() { |
| 90 | + const { lastUsed } = this._fieldKey; |
| 91 | + return lastUsed != null ? moment(lastUsed).fromNow() : ''; |
| 92 | + } |
| 93 | + |
| 94 | + get url() { |
| 95 | + return `${window.location.origin}/api/v1/key/${this._fieldKey.token}`; |
| 96 | + } |
| 97 | + |
| 98 | + get qrCodeImgHtml() { |
| 99 | + if (this._qrCodeImgHtml != null) return this._qrCodeImgHtml; |
| 100 | + const code = qrcode(QR_CODE_TYPE_NUMBER, QR_CODE_ERROR_CORRECTION_LEVEL); |
| 101 | + // Collect requires the JSON to have 'general' and 'admin' keys, even if the |
| 102 | + // associated values are empty objects. |
| 103 | + const settings = { general: { server_url: this.url }, admin: {} }; |
| 104 | + const deflated = deflate(JSON.stringify(settings), { to: 'string' }); |
| 105 | + code.addData(btoa(deflated)); |
| 106 | + code.make(); |
| 107 | + this._qrCodeImgHtml = code.createImgTag(QR_CODE_CELL_SIZE, QR_CODE_MARGIN); |
| 108 | + return this._qrCodeImgHtml; |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +const POPOVER_CONTENT_TEMPLATE = ` |
| 113 | + <div id="field-key-list-popover-content"> |
| 114 | + <div class="field-key-list-img-container"></div> |
| 115 | + <div> |
| 116 | + <a href="https://docs.opendatakit.org/collect-import-export/" target="_blank"> |
| 117 | + What’s this? |
| 118 | + </a> |
| 119 | + </div> |
| 120 | + </div> |
| 121 | +`; |
| 122 | + |
| 123 | +export default { |
| 124 | + name: 'FieldKeyList', |
| 125 | + components: { FieldKeyNew }, |
| 126 | + mixins: [alert(), request(), highlight()], |
| 127 | + data() { |
| 128 | + return { |
| 129 | + alert: alert.blank(), |
| 130 | + requestId: null, |
| 131 | + fieldKeys: null, |
| 132 | + highlighted: null, |
| 133 | + enabledPopoverLinks: new Set(), |
| 134 | + // The <a> element whose popover is currently shown, as a jQuery object. |
| 135 | + popoverLink: null, |
| 136 | + newFieldKey: { |
| 137 | + state: false |
| 138 | + } |
| 139 | + }; |
| 140 | + }, |
| 141 | + watch: { |
| 142 | + alert() { |
| 143 | + this.$emit('alert'); |
| 144 | + } |
| 145 | + }, |
| 146 | + created() { |
| 147 | + this.fetchData(); |
| 148 | + }, |
| 149 | + mounted() { |
| 150 | + $('body').click(this.toggleFieldKeyListPopovers); |
| 151 | + }, |
| 152 | + beforeDestroy() { |
| 153 | + $('body').off('click', this.toggleFieldKeyListPopovers); |
| 154 | + }, |
| 155 | + methods: { |
| 156 | + fetchData() { |
| 157 | + this.fieldKeys = null; |
| 158 | + this.enabledPopoverLinks = new Set(); |
| 159 | + const headers = { 'X-Extended-Metadata': 'true' }; |
| 160 | + this |
| 161 | + .get('/field-keys', { headers }) |
| 162 | + .then(fieldKeys => { |
| 163 | + this.fieldKeys = fieldKeys.map(fieldKey => new FieldKeyPresenter(fieldKey)); |
| 164 | + }) |
| 165 | + .catch(() => {}); |
| 166 | + }, |
| 167 | + popoverContent(fieldKey) { |
| 168 | + const content = $(POPOVER_CONTENT_TEMPLATE); |
| 169 | + content.find('.field-key-list-img-container').append(fieldKey.qrCodeImgHtml); |
| 170 | + return content[0].outerHTML; |
| 171 | + }, |
| 172 | + enablePopover(popoverLink) { |
| 173 | + const index = popoverLink.closest('tr').data('index'); |
| 174 | + if (this.enabledPopoverLinks.has(index)) return; |
| 175 | + popoverLink.popover({ |
| 176 | + container: 'body', |
| 177 | + trigger: 'manual', |
| 178 | + placement: 'left', |
| 179 | + content: this.popoverContent(this.fieldKeys[index]), |
| 180 | + html: true |
| 181 | + }); |
| 182 | + this.enabledPopoverLinks.add(index); |
| 183 | + }, |
| 184 | + showPopover(popoverLink) { |
| 185 | + this.enablePopover(popoverLink); |
| 186 | + popoverLink.popover('show'); |
| 187 | + this.popoverLink = popoverLink; |
| 188 | + }, |
| 189 | + hidePopover() { |
| 190 | + if (this.popoverLink == null) return; |
| 191 | + this.popoverLink.popover('hide'); |
| 192 | + this.popoverLink = null; |
| 193 | + }, |
| 194 | + popoverContainsElement(element) { |
| 195 | + if (this.popoverLink == null) return false; |
| 196 | + const popover = $('#field-key-list-popover-content').closest('.popover'); |
| 197 | + return element[0] === popover[0] || $.contains(popover[0], element[0]); |
| 198 | + }, |
| 199 | + // This method's name should be unique, because jQuery off() uses the name |
| 200 | + // of the function passed to it. |
| 201 | + toggleFieldKeyListPopovers(event) { |
| 202 | + const target = $(event.target); |
| 203 | + if (target.hasClass('field-key-list-popover-link')) { |
| 204 | + // true if the user clicked on the link whose popover is currently shown |
| 205 | + // and false if not. |
| 206 | + const samePopover = this.popoverLink != null && |
| 207 | + event.target === this.popoverLink[0]; |
| 208 | + if (!samePopover) { |
| 209 | + this.hidePopover(); |
| 210 | + this.showPopover(target); |
| 211 | + } |
| 212 | + } else if (this.popoverLink != null && !this.popoverContainsElement(target)) { |
| 213 | + this.hidePopover(); |
| 214 | + } |
| 215 | + }, |
| 216 | + afterCreate(fieldKey) { |
| 217 | + this.fetchData(); |
| 218 | + this.alert = alert.success(`The field key “${fieldKey.displayName}” was created successfully.`); |
| 219 | + this.highlighted = fieldKey.id; |
| 220 | + } |
| 221 | + } |
| 222 | +}; |
| 223 | +</script> |
| 224 | + |
| 225 | +<style lang="sass"> |
| 226 | +@import '../../../assets/scss/variables'; |
| 227 | + |
| 228 | +#field-key-list-table tbody td { |
| 229 | + vertical-align: middle; |
| 230 | +} |
| 231 | + |
| 232 | +#field-key-list-popover-content { |
| 233 | + padding: 3px; |
| 234 | + |
| 235 | + .field-key-list-img-container { |
| 236 | + border: 3px solid $color-subpanel-border; |
| 237 | + margin-bottom: 3px; |
| 238 | + } |
| 239 | + |
| 240 | + a { |
| 241 | + color: white; |
| 242 | + } |
| 243 | +} |
| 244 | +</style> |
0 commit comments