expressa 1.2.3 → 1.3.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.
Files changed (80) hide show
  1. package/.circleci/config.yml +2 -2
  2. package/controllers/users.js +43 -43
  3. package/index.js +1 -6
  4. package/listeners.js +64 -59
  5. package/middleware/permissions.js +53 -53
  6. package/modules/admin/dist/index.html +1 -1
  7. package/modules/admin/dist/index.html.gz +0 -0
  8. package/modules/admin/dist/static/css/{chunk-5cee.9685afb8.css → chunk-5061.ce0e4bdb.css} +0 -0
  9. package/modules/admin/dist/static/css/{chunk-5cee.9685afb8.css.gz → chunk-5061.ce0e4bdb.css.gz} +0 -0
  10. package/modules/admin/dist/static/css/chunk-5e50.f67eeaed.css +1 -0
  11. package/modules/admin/dist/static/css/chunk-5e50.f67eeaed.css.gz +0 -0
  12. package/modules/admin/dist/static/css/{chunk-621e.f3d0eab6.css → chunk-621e.b826be8d.css} +0 -0
  13. package/modules/admin/dist/static/css/{chunk-621e.f3d0eab6.css.gz → chunk-621e.b826be8d.css.gz} +0 -0
  14. package/modules/admin/dist/static/css/chunk-7109.2bd6f37e.css +1 -0
  15. package/modules/admin/dist/static/css/chunk-7109.2bd6f37e.css.gz +0 -0
  16. package/modules/admin/dist/static/css/chunk-7f4f.f67eeaed.css +1 -0
  17. package/modules/admin/dist/static/css/chunk-7f4f.f67eeaed.css.gz +0 -0
  18. package/modules/admin/dist/static/css/{chunk-bd5f.6ab011ba.css → chunk-bd5f.aefa49d7.css} +0 -0
  19. package/modules/admin/dist/static/css/{chunk-bd5f.6ab011ba.css.gz → chunk-bd5f.aefa49d7.css.gz} +0 -0
  20. package/modules/admin/dist/static/css/{chunk-e375.1d849865.css → chunk-e374.025d58bb.css} +0 -0
  21. package/modules/admin/dist/static/css/{chunk-e375.1d849865.css.gz → chunk-e374.025d58bb.css.gz} +0 -0
  22. package/modules/admin/dist/static/js/app.cdc00d38.js +1 -0
  23. package/modules/admin/dist/static/js/app.cdc00d38.js.gz +0 -0
  24. package/modules/admin/dist/static/js/chunk-3bc2.028748b7.js +1 -0
  25. package/modules/admin/dist/static/js/chunk-3bc2.028748b7.js.gz +0 -0
  26. package/modules/admin/dist/static/js/chunk-5061.83e6ccfa.js +1 -0
  27. package/modules/admin/dist/static/js/chunk-5061.83e6ccfa.js.gz +0 -0
  28. package/modules/admin/dist/static/js/chunk-5e17.a2b69611.js +1 -0
  29. package/modules/admin/dist/static/js/chunk-5e17.a2b69611.js.gz +0 -0
  30. package/modules/admin/dist/static/js/chunk-5e50.6820e69c.js +1 -0
  31. package/modules/admin/dist/static/js/chunk-5e50.6820e69c.js.gz +0 -0
  32. package/modules/admin/dist/static/js/chunk-621e.ccf42393.js +1 -0
  33. package/modules/admin/dist/static/js/chunk-621e.ccf42393.js.gz +0 -0
  34. package/modules/admin/dist/static/js/chunk-7109.10cb4860.js +1 -0
  35. package/modules/admin/dist/static/js/chunk-7109.10cb4860.js.gz +0 -0
  36. package/modules/admin/dist/static/js/chunk-7291.590524e9.js +1 -0
  37. package/modules/admin/dist/static/js/chunk-7291.590524e9.js.gz +0 -0
  38. package/modules/admin/dist/static/js/chunk-7f4f.ae4400a8.js +1 -0
  39. package/modules/admin/dist/static/js/chunk-7f4f.ae4400a8.js.gz +0 -0
  40. package/modules/admin/dist/static/js/{chunk-bd5f.af5f2197.js → chunk-bd5f.e9509f10.js} +0 -0
  41. package/modules/admin/dist/static/js/{chunk-bd5f.af5f2197.js.gz → chunk-bd5f.e9509f10.js.gz} +0 -0
  42. package/modules/admin/dist/static/js/chunk-e374.7c87a0dd.js +1 -0
  43. package/modules/admin/dist/static/js/chunk-e374.7c87a0dd.js.gz +0 -0
  44. package/modules/admin/dist/static/js/{chunk-e7c3.a8659352.js → chunk-e7c3.e5df8ceb.js} +1 -1
  45. package/modules/admin/dist/static/js/chunk-e7c3.e5df8ceb.js.gz +0 -0
  46. package/modules/admin/dist/static/js/chunk-f075.315ceec1.js +1 -0
  47. package/modules/admin/dist/static/js/chunk-f075.315ceec1.js.gz +0 -0
  48. package/modules/admin/dist/static/js/{chunk-libs.5637556a.js → chunk-libs.06c38828.js} +2 -2
  49. package/modules/admin/dist/static/js/{chunk-libs.5637556a.js.LICENSE.txt → chunk-libs.06c38828.js.LICENSE.txt} +0 -0
  50. package/modules/admin/dist/static/js/{chunk-libs.5637556a.js.LICENSE.txt.gz → chunk-libs.06c38828.js.LICENSE.txt.gz} +0 -0
  51. package/modules/admin/dist/static/js/chunk-libs.06c38828.js.gz +0 -0
  52. package/modules/admin/package.json +1 -0
  53. package/modules/admin/src/router/index.js +200 -185
  54. package/modules/admin/src/views/Endpoints.vue +250 -0
  55. package/modules/admin/src/views/ListDocuments2.vue +465 -407
  56. package/package.json +1 -1
  57. package/modules/admin/dist/static/css/chunk-1dff.39c7634b.css +0 -1
  58. package/modules/admin/dist/static/css/chunk-1dff.39c7634b.css.gz +0 -0
  59. package/modules/admin/dist/static/css/chunk-a99c.c4b0e273.css +0 -1
  60. package/modules/admin/dist/static/css/chunk-a99c.c4b0e273.css.gz +0 -0
  61. package/modules/admin/dist/static/js/app.947e8310.js +0 -1
  62. package/modules/admin/dist/static/js/app.947e8310.js.gz +0 -0
  63. package/modules/admin/dist/static/js/chunk-1dff.b5ee003b.js +0 -1
  64. package/modules/admin/dist/static/js/chunk-1dff.b5ee003b.js.gz +0 -0
  65. package/modules/admin/dist/static/js/chunk-3bc2.b0270c10.js +0 -1
  66. package/modules/admin/dist/static/js/chunk-3bc2.b0270c10.js.gz +0 -0
  67. package/modules/admin/dist/static/js/chunk-5cee.a6f0e769.js +0 -1
  68. package/modules/admin/dist/static/js/chunk-5cee.a6f0e769.js.gz +0 -0
  69. package/modules/admin/dist/static/js/chunk-621e.c63db96c.js +0 -1
  70. package/modules/admin/dist/static/js/chunk-621e.c63db96c.js.gz +0 -0
  71. package/modules/admin/dist/static/js/chunk-7291.f914fca7.js +0 -1
  72. package/modules/admin/dist/static/js/chunk-7291.f914fca7.js.gz +0 -0
  73. package/modules/admin/dist/static/js/chunk-a99c.3f48f127.js +0 -1
  74. package/modules/admin/dist/static/js/chunk-a99c.3f48f127.js.gz +0 -0
  75. package/modules/admin/dist/static/js/chunk-e375.7ba1b61f.js +0 -1
  76. package/modules/admin/dist/static/js/chunk-e375.7ba1b61f.js.gz +0 -0
  77. package/modules/admin/dist/static/js/chunk-e7c3.a8659352.js.gz +0 -0
  78. package/modules/admin/dist/static/js/chunk-f075.6b230660.js +0 -1
  79. package/modules/admin/dist/static/js/chunk-f075.6b230660.js.gz +0 -0
  80. package/modules/admin/dist/static/js/chunk-libs.5637556a.js.gz +0 -0
@@ -1,407 +1,465 @@
1
- <template>
2
- <div class="px-3">
3
- <div class="d-flex align-items-center py-2">
4
- <h3 class="mb-0 text-capitalize">
5
- {{ collectionName }}
6
- </h3>
7
-
8
- <span class="mx-auto" />
9
-
10
- <el-dropdown
11
- multiple
12
- class="mr-4"
13
- :hide-on-click="false"
14
- >
15
- <span class="el-dropdown-link">
16
- Select columns
17
- <i class="el-icon-arrow-down el-icon--right" />
18
- </span>
19
-
20
- <el-dropdown-menu slot="dropdown" style="max-height: 300px; overflow: auto">
21
- <el-checkbox-group v-model="selectedColumns" @input="update">
22
- <el-dropdown-item
23
- v-for="column in allPossibleColumns"
24
- :key="column"
25
- :label="column"
26
- :value="column"
27
- >
28
- <el-checkbox
29
- class="mb-0 mr-4"
30
- :label="column"
31
- :disabled="selectedColumns.length === 1 && selectedColumns[0] === column"
32
- />
33
- </el-dropdown-item>
34
- </el-checkbox-group>
35
- </el-dropdown-menu>
36
- </el-dropdown>
37
-
38
- <el-checkbox
39
- v-model="isFiltersVisible"
40
- class="mb-0 mr-4"
41
- >
42
- Show Filters
43
- </el-checkbox>
44
-
45
- <el-pagination
46
- :page-size="pageSize"
47
- :total="count"
48
- :page-sizes="pageSizes"
49
- :current-page.sync="page"
50
- layout="sizes, prev, pager, next"
51
- @current-change="update()"
52
- @size-change="pageSize = $event, update()"
53
- />
54
- </div>
55
-
56
- <el-table
57
- :data="tableRows"
58
- element-loading-text="Loading"
59
- border
60
- fit
61
- highlight-current-row
62
- @sort-change="handleSortChange"
63
- >
64
- <el-table-column
65
- v-for="(name, i) in selectedColumns"
66
- :key="name"
67
- :label="name"
68
- :prop="name"
69
- align="center"
70
- :sortable="sortableFieldTypes.includes(getFieldType(name)) ? 'custom': undefined"
71
- >
72
- <template slot-scope="scope">
73
- <div v-if="scope.$index === 0 && isFiltersVisible" class="text-left">
74
- <el-input
75
- v-model="searchFilters[name]"
76
- :placeholder="`Search by ${name}`"
77
- :type="getFieldType(name) === 'number' ? 'number' : 'text'"
78
- @change="update()"
79
- />
80
-
81
- <el-checkbox
82
- v-model="exactSearches[name]"
83
- :disabled="getFieldType(name) === 'number'"
84
- class="mt-3"
85
- @change="update()"
86
- >
87
- Exact match
88
- </el-checkbox>
89
- </div>
90
-
91
- <template v-else>
92
- <router-link v-if="i === 0 && collectionName !== 'requestlog'" :to="'/edit/'+collectionName+'/'+scope.row._id">
93
- {{ getPath(scope.row, name) }}
94
- </router-link>
95
- <router-link v-if="i === 0 && collectionName === 'requestlog'" :to="'/dev/viewrequest/'+scope.row._id">
96
- {{ getPath(scope.row, name) }}
97
- </router-link>
98
- <span v-if="i !== 0">{{ getPath(scope.row, name) }}</span>
99
- </template>
100
- </template>
101
- </el-table-column>
102
- </el-table>
103
-
104
- <div class="d-flex align-items-center my-3">
105
- <router-link v-if="showAddButton" :to="'/edit/' + collectionName + '/create'" class="mr-2">
106
- <button class="btn btn-primary">
107
- Add
108
- </button>
109
- </router-link>
110
-
111
- <button class="btn btn-secondary download-button mr-2" @click="downloadCSV()">
112
- Download
113
- </button>
114
-
115
- <button class="btn btn-secondary download-button" @click="downloadAllCSV()">
116
- Download All
117
- </button>
118
-
119
- <span class="mx-auto" />
120
-
121
- <el-pagination
122
- :page-size="pageSize"
123
- :total="count"
124
- :page-sizes="pageSizes"
125
- :current-page.sync="page"
126
- layout="sizes, prev, pager, next"
127
- @current-change="update()"
128
- @size-change="pageSize = $event, update()"
129
- />
130
- </div>
131
- </div>
132
- </template>
133
-
134
- <script>
135
- import request from '@/utils/request'
136
- import saveAs from 'file-saver'
137
- import objectPath from 'object-path'
138
-
139
- const pageSizes = [25, 50, 100, 500]
140
-
141
- export default {
142
- name: 'ListDocuments',
143
- props: {
144
- collectionName: {
145
- type: String,
146
- default: 'collection'
147
- },
148
- filter: {
149
- type: Object,
150
- default: () => {}
151
- },
152
- columns: {
153
- type: Array,
154
- default: () => undefined
155
- },
156
- showAddButton: {
157
- type: Boolean,
158
- default: true
159
- },
160
- showPaginateOnBottom: {
161
- type: Boolean,
162
- default: true
163
- },
164
- },
165
- data: () => ({
166
- page: 1,
167
- count: 0,
168
- data: [],
169
- schema: {},
170
- exactSearches: {},
171
- searchFilters: {},
172
- pageSizes: pageSizes,
173
- pageSize: pageSizes[0],
174
- isFiltersVisible: true,
175
- sortableFieldTypes: ['number', 'string', 'boolean'],
176
- orderBy: { order: null, prop: null },
177
- selectedColumns: [],
178
- allPossibleColumns: []
179
- }),
180
- computed: {
181
- tableRows() {
182
- if (!this.isFiltersVisible) {
183
- return this.data
184
- }
185
-
186
- return [
187
- // INFO: This empty row is used to display the search filters.
188
- {},
189
- ...this.data
190
- ]
191
- },
192
- appliedSearchFilters() {
193
- return Object.keys(this.searchFilters).reduce((filter, fieldName) => {
194
- const searchKeyword = this.searchFilters[fieldName]
195
-
196
- const fieldtype = this.getFieldType(fieldName)
197
-
198
- if (!searchKeyword || fieldtype === 'custom') {
199
- return filter
200
- }
201
-
202
- const queryBuilder = this.getQueryBuilder(fieldtype)
203
-
204
- return {
205
- ...filter,
206
- [fieldName]: queryBuilder({ fieldName, searchKeyword })
207
- }
208
- }, {})
209
- },
210
- allFilters() {
211
- let orderby = '{"meta.created":-1}'
212
-
213
- if (!!this.orderBy.prop && !!this.orderBy.order) {
214
- const propName = this.orderBy.prop
215
- const direction = this.orderBy.order === 'descending' ? '-1' : '1'
216
- orderby = `{"${propName}": ${direction}}`
217
- }
218
-
219
- const params = {
220
- orderby,
221
- page: this.page,
222
- limit: this.pageSize,
223
- ...this.filter,
224
- query: { ...this.appliedSearchFilters }
225
- }
226
-
227
- const columns = this.selectedColumns
228
-
229
- if (columns) {
230
- params.fields = columns.reduce((map, field) => {
231
- map[field] = 1
232
- return map
233
- }, {})
234
- }
235
-
236
- return params
237
- },
238
- },
239
- watch: {
240
- collectionName: {
241
- handler: 'update'
242
- },
243
- '$route.params.collectionName': {
244
- handler: 'resetTable'
245
- }
246
- },
247
- mounted() {
248
- this.update()
249
- },
250
- methods: {
251
- getPath(obj, path, defaultValue) {
252
- const result = String.prototype.split.call(path, /[,[\].]+?/)
253
- .filter(Boolean)
254
- .reduce((res, key) => (res !== null && res !== undefined) ? res[key] : res, obj)
255
- return (result === undefined || result === obj) ? defaultValue : result
256
- },
257
- handleSortChange(orderBy) {
258
- this.orderBy = orderBy
259
- this.update()
260
- },
261
- async update() {
262
- if (this.$route.params.collectionName) {
263
- this.collectionName = this.$route.params.collectionName
264
- }
265
-
266
- // Fetch and Set schema info if name hasn't changed
267
- if (!this.collection || this.collection._id !== this.collectionName) {
268
- this.collection = (await request({ url: `/collection/${this.collectionName}` })).data
269
- this.schema = this.collection.schema
270
- }
271
-
272
- // Set columns data if not set.
273
- if (!this.selectedColumns.length) {
274
- const columns = this.columns || (this.collection.admin && this.collection.admin.columns)
275
- this.allPossibleColumns = this.getAllPossibleColumns()
276
- this.selectedColumns = columns || Object.keys(this.schema.properties)
277
- }
278
-
279
- // Fetch and Set table data
280
- const params = { ...this.allFilters }
281
- const info = (await request({ url: `/${this.collectionName}/`, params })).data
282
- this.count = info.itemsTotal
283
- this.data = this.applyCustomColumnFilter(info.data)
284
- },
285
- getAllPossibleColumns() {
286
- const configuredCols = (this.collection.admin && this.collection.admin.columns) || []
287
- const allProperties = this.getSchemaProperties(this.schema)
288
- return Array.from(new Set(configuredCols.concat(allProperties).sort()))
289
- },
290
- getSchemaProperties(obj, parent) {
291
- let keys = []
292
- const properties = obj.properties
293
- Object.keys(properties).forEach(key => {
294
- const fullKey = `${parent ? parent + '.' : ''}${key}`
295
- if (properties[key].properties) {
296
- keys = keys.concat(this.getSchemaProperties(properties[key], fullKey))
297
- } else {
298
- keys.push(fullKey)
299
- }
300
- })
301
- return keys
302
- },
303
- getSchemaProperty(obj, path) {
304
- const parts = path.split('.')
305
- let partObj = obj
306
- for (const part of parts) {
307
- if (partObj.properties && partObj.properties[part]) {
308
- partObj = partObj.properties[part]
309
- } else {
310
- return
311
- }
312
- }
313
- return partObj
314
- },
315
- applyCustomColumnFilter(tableData) {
316
- const customColumnSearches = Object.keys(this.searchFilters)
317
- .filter((fieldName) => this.getFieldType(fieldName) === 'custom')
318
-
319
- return tableData.filter((row) => {
320
- return customColumnSearches.every(fieldName => {
321
- const searchKeyword = this.searchFilters[fieldName]
322
-
323
- if (!searchKeyword) {
324
- return true
325
- }
326
-
327
- const stringified = row[fieldName].toString()
328
-
329
- if (this.exactSearches[fieldName]) {
330
- return stringified === searchKeyword
331
- } else {
332
- return stringified.toLowerCase().includes(searchKeyword.toLowerCase())
333
- }
334
- })
335
- })
336
- },
337
- downloadCSV(data = this.data) {
338
- const collection = this.collectionName
339
- let text = this.selectedColumns.map((name) => '"' + name + '"').join(',') + '\n'
340
- if (data) {
341
- text += data.map((row) =>
342
- this.selectedColumns.map((key) => '"' + String(objectPath.get(row, key) || '').replace(/"/g, '""') + '"')
343
- ).join('\n')
344
- console.log(text)
345
- const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
346
- saveAs(blob, collection + '.csv')
347
- }
348
- },
349
- async downloadAllCSV() {
350
- let rows = []
351
- let page = 1
352
-
353
- while (this.count > rows.length) {
354
- const params = {
355
- ...this.allFilters,
356
- page: page,
357
- limit: 500,
358
- }
359
-
360
- const response = (await request({ url: `/${this.collectionName}/`, params })).data
361
- rows = [...response.data, ...rows]
362
- page += 1
363
- }
364
-
365
- this.downloadCSV(this.applyCustomColumnFilter(rows))
366
- },
367
- getFieldType(fieldName) {
368
- const property = this.getSchemaProperty(this.schema, fieldName)
369
- return (property && property.type) || 'custom'
370
- },
371
- getQueryBuilder(fieldType) {
372
- const queryBuilders = {
373
- 'string': this.getStringSearchQuery,
374
- 'number': (payload) => parseInt(payload.searchKeyword)
375
- }
376
-
377
- return queryBuilders[fieldType] || this.getStringSearchQuery
378
- },
379
- getStringSearchQuery({ fieldName, searchKeyword }) {
380
- const regexQuery = { '$regex': searchKeyword, '$options': 'i' }
381
- return this.exactSearches[fieldName] ? searchKeyword : regexQuery
382
- },
383
- resetTable() {
384
- // Reset Filters
385
- this.exactSearches = {}
386
- this.searchFilters = {}
387
- this.selectedColumns = []
388
-
389
- // Reset pagination
390
- this.page = 0
391
- this.pageSize = pageSizes[0]
392
-
393
- // Reset table data
394
- this.data = []
395
- this.count = 0
396
- }
397
- }
398
- }
399
- </script>
400
-
401
- <style scoped>
402
- .el-table >>> th {
403
- background-color: #343a40 !important;
404
- border-color: #454d55;
405
- color: #fff;
406
- }
407
- </style>
1
+ <template>
2
+ <div class="px-3">
3
+ <div class="d-flex align-items-center py-2">
4
+ <h3 class="mb-0 text-capitalize">
5
+ {{ collectionName }}
6
+ </h3>
7
+
8
+ <span class="mx-auto" />
9
+
10
+ <el-dropdown
11
+ multiple
12
+ class="mr-4"
13
+ :hide-on-click="false"
14
+ >
15
+ <span class="el-dropdown-link">
16
+ Select columns
17
+ <i class="el-icon-arrow-down el-icon--right" />
18
+ </span>
19
+
20
+ <el-dropdown-menu slot="dropdown" style="max-height: 300px; overflow: auto">
21
+ <el-checkbox-group v-model="selectedColumns" @input="update">
22
+ <el-dropdown-item
23
+ v-for="column in allPossibleColumns"
24
+ :key="column"
25
+ :label="column"
26
+ :value="column"
27
+ >
28
+ <el-checkbox
29
+ class="mb-0 mr-4"
30
+ :label="column"
31
+ :disabled="selectedColumns.length === 1 && selectedColumns[0] === column"
32
+ />
33
+ </el-dropdown-item>
34
+ </el-checkbox-group>
35
+ </el-dropdown-menu>
36
+ </el-dropdown>
37
+
38
+ <el-checkbox
39
+ v-model="isFiltersVisible"
40
+ class="mb-0 mr-4"
41
+ >
42
+ Show Filters
43
+ </el-checkbox>
44
+
45
+ <el-pagination
46
+ :page-size="pageSize"
47
+ :total="count"
48
+ :page-sizes="pageSizes"
49
+ :current-page.sync="page"
50
+ layout="sizes, prev, pager, next"
51
+ @current-change="update()"
52
+ @size-change="pageSize = $event, update()"
53
+ />
54
+ </div>
55
+
56
+ <div style="width: 50%" class="mb-2">
57
+ Params
58
+ <div v-for="(parameter, index) in params" :key="index" class="d-flex align-items-center mb-1">
59
+ <el-checkbox v-model="parameter.isEnabled" class="mr-2" />
60
+
61
+ <el-input v-model="parameter.key" placeholder="key" @input="handleParamKeyChange(index)" />
62
+
63
+ <el-input v-model="parameter.value" placeholder="value" class="ml-1" />
64
+
65
+ <el-button
66
+ v-if="params.length > 1"
67
+ icon="el-icon-close"
68
+ circle
69
+ type="text"
70
+ @click="removeParam(index)"
71
+ />
72
+ </div>
73
+
74
+ <el-button class="btn btn-primary mb-3" @click="update">
75
+ Search
76
+ </el-button>
77
+ </div>
78
+
79
+ <el-table
80
+ :data="tableRows"
81
+ element-loading-text="Loading"
82
+ border
83
+ fit
84
+ highlight-current-row
85
+ @sort-change="handleSortChange"
86
+ >
87
+ <el-table-column
88
+ v-for="(name, i) in selectedColumns"
89
+ :key="name"
90
+ :label="name"
91
+ :prop="name"
92
+ align="center"
93
+ :sortable="sortableFieldTypes.includes(getFieldType(name)) ? 'custom': undefined"
94
+ >
95
+ <template slot-scope="scope">
96
+ <div v-if="scope.$index === 0 && isFiltersVisible" class="text-left">
97
+ <el-input
98
+ v-model="searchFilters[name]"
99
+ :placeholder="`Search by ${name}`"
100
+ :type="getFieldType(name) === 'number' ? 'number' : 'text'"
101
+ @change="update()"
102
+ />
103
+
104
+ <el-checkbox
105
+ v-model="exactSearches[name]"
106
+ :disabled="getFieldType(name) === 'number'"
107
+ class="mt-3"
108
+ @change="update()"
109
+ >
110
+ Exact match
111
+ </el-checkbox>
112
+ </div>
113
+
114
+ <template v-else>
115
+ <router-link v-if="i === 0 && collectionName !== 'requestlog'" :to="'/edit/'+collectionName+'/'+scope.row._id">
116
+ {{ getPath(scope.row, name) }}
117
+ </router-link>
118
+ <router-link v-if="i === 0 && collectionName === 'requestlog'" :to="'/dev/viewrequest/'+scope.row._id">
119
+ {{ getPath(scope.row, name) }}
120
+ </router-link>
121
+ <span v-if="i !== 0">{{ getPath(scope.row, name) }}</span>
122
+ </template>
123
+ </template>
124
+ </el-table-column>
125
+ </el-table>
126
+
127
+ <div class="d-flex align-items-center my-3">
128
+ <router-link v-if="showAddButton" :to="'/edit/' + collectionName + '/create'" class="mr-2">
129
+ <button class="btn btn-primary">
130
+ Add
131
+ </button>
132
+ </router-link>
133
+
134
+ <button class="btn btn-secondary download-button mr-2" @click="downloadCSV()">
135
+ Download
136
+ </button>
137
+
138
+ <button class="btn btn-secondary download-button" @click="downloadAllCSV()">
139
+ Download All
140
+ </button>
141
+
142
+ <span class="mx-auto" />
143
+
144
+ <el-pagination
145
+ :page-size="pageSize"
146
+ :total="count"
147
+ :page-sizes="pageSizes"
148
+ :current-page.sync="page"
149
+ layout="sizes, prev, pager, next"
150
+ @current-change="update()"
151
+ @size-change="pageSize = $event, update()"
152
+ />
153
+ </div>
154
+ </div>
155
+ </template>
156
+
157
+ <script>
158
+ import request from '@/utils/request'
159
+ import saveAs from 'file-saver'
160
+ import objectPath from 'object-path'
161
+
162
+ const pageSizes = [25, 50, 100, 500]
163
+
164
+ export default {
165
+ name: 'ListDocuments',
166
+ props: {
167
+ collectionName: {
168
+ type: String,
169
+ default: 'collection'
170
+ },
171
+ filter: {
172
+ type: Object,
173
+ default: () => {}
174
+ },
175
+ columns: {
176
+ type: Array,
177
+ default: () => undefined
178
+ },
179
+ showAddButton: {
180
+ type: Boolean,
181
+ default: true
182
+ },
183
+ showPaginateOnBottom: {
184
+ type: Boolean,
185
+ default: true
186
+ },
187
+ },
188
+ data: () => ({
189
+ page: 1,
190
+ count: 0,
191
+ data: [],
192
+ schema: {},
193
+ exactSearches: {},
194
+ searchFilters: {},
195
+ pageSizes: pageSizes,
196
+ pageSize: pageSizes[0],
197
+ isFiltersVisible: true,
198
+ sortableFieldTypes: ['number', 'string', 'boolean'],
199
+ orderBy: { order: null, prop: null },
200
+ selectedColumns: [],
201
+ allPossibleColumns: [],
202
+ params: [
203
+ { key: '', value: '', isEnabled: true }
204
+ ]
205
+ }),
206
+ computed: {
207
+ tableRows() {
208
+ if (!this.isFiltersVisible) {
209
+ return this.data
210
+ }
211
+
212
+ return [
213
+ // INFO: This empty row is used to display the search filters.
214
+ {},
215
+ ...this.data
216
+ ]
217
+ },
218
+ appliedSearchFilters() {
219
+ return Object.keys(this.searchFilters).reduce((filter, fieldName) => {
220
+ const searchKeyword = this.searchFilters[fieldName]
221
+
222
+ const fieldtype = this.getFieldType(fieldName)
223
+
224
+ if (!searchKeyword || fieldtype === 'custom') {
225
+ return filter
226
+ }
227
+
228
+ const queryBuilder = this.getQueryBuilder(fieldtype)
229
+
230
+ return {
231
+ ...filter,
232
+ [fieldName]: queryBuilder({ fieldName, searchKeyword })
233
+ }
234
+ }, {})
235
+ },
236
+ allFilters() {
237
+ let orderby = '{"meta.created":-1}'
238
+
239
+ if (!!this.orderBy.prop && !!this.orderBy.order) {
240
+ const propName = this.orderBy.prop
241
+ const direction = this.orderBy.order === 'descending' ? '-1' : '1'
242
+ orderby = `{"${propName}": ${direction}}`
243
+ }
244
+
245
+ const params = {
246
+ orderby,
247
+ page: this.page,
248
+ limit: this.pageSize,
249
+ ...this.filter,
250
+ query: { ...this.appliedSearchFilters }
251
+ }
252
+
253
+ const columns = this.selectedColumns
254
+
255
+ if (columns) {
256
+ params.fields = columns.reduce((map, field) => {
257
+ map[field] = 1
258
+ return map
259
+ }, {})
260
+ }
261
+
262
+ return params
263
+ },
264
+ customParams() {
265
+ return this.params
266
+ .filter(param => param.isEnabled)
267
+ .map(param => {
268
+ if (param.key && param.value) {
269
+ return `${param.key}=${param.value}`
270
+ }
271
+
272
+ if (param.key) {
273
+ return param.key
274
+ }
275
+
276
+ if (param.value) {
277
+ return `=${param.value}`
278
+ }
279
+
280
+ return ''
281
+ }).join('&')
282
+ },
283
+ },
284
+ watch: {
285
+ collectionName: {
286
+ handler: 'update'
287
+ },
288
+ '$route.params.collectionName': {
289
+ handler: 'resetTable'
290
+ }
291
+ },
292
+ mounted() {
293
+ this.update()
294
+ },
295
+ methods: {
296
+ getPath(obj, path, defaultValue) {
297
+ const result = String.prototype.split.call(path, /[,[\].]+?/)
298
+ .filter(Boolean)
299
+ .reduce((res, key) => (res !== null && res !== undefined) ? res[key] : res, obj)
300
+ return (result === undefined || result === obj) ? defaultValue : result
301
+ },
302
+ handleSortChange(orderBy) {
303
+ this.orderBy = orderBy
304
+ this.update()
305
+ },
306
+ async update() {
307
+ if (this.$route.params.collectionName) {
308
+ this.collectionName = this.$route.params.collectionName
309
+ }
310
+
311
+ // Fetch and Set schema info if name hasn't changed
312
+ if (!this.collection || this.collection._id !== this.collectionName) {
313
+ this.collection = (await request({ url: `/collection/${this.collectionName}` })).data
314
+ this.schema = this.collection.schema
315
+ }
316
+
317
+ // Set columns data if not set.
318
+ if (!this.selectedColumns.length) {
319
+ const columns = this.columns || (this.collection.admin && this.collection.admin.columns)
320
+ this.allPossibleColumns = this.getAllPossibleColumns()
321
+ this.selectedColumns = columns || Object.keys(this.schema.properties)
322
+ }
323
+
324
+ // Fetch and Set table data
325
+ const params = { ...this.allFilters }
326
+ const info = (await request({ url: `/${this.collectionName}/?${this.customParams}`, params })).data
327
+ this.count = info.itemsTotal
328
+ this.data = this.applyCustomColumnFilter(info.data)
329
+ },
330
+ getAllPossibleColumns() {
331
+ const configuredCols = (this.collection.admin && this.collection.admin.columns) || []
332
+ const allProperties = this.getSchemaProperties(this.schema)
333
+ return Array.from(new Set(configuredCols.concat(allProperties).sort()))
334
+ },
335
+ getSchemaProperties(obj, parent) {
336
+ let keys = []
337
+ const properties = obj.properties
338
+ Object.keys(properties).forEach(key => {
339
+ const fullKey = `${parent ? parent + '.' : ''}${key}`
340
+ if (properties[key].properties && Object.keys(properties[key].properties).length) {
341
+ keys = keys.concat(this.getSchemaProperties(properties[key], fullKey))
342
+ } else {
343
+ keys.push(fullKey)
344
+ }
345
+ })
346
+ return keys
347
+ },
348
+ getSchemaProperty(obj, path) {
349
+ const parts = path.split('.')
350
+ let partObj = obj
351
+ for (const part of parts) {
352
+ if (partObj.properties && partObj.properties[part]) {
353
+ partObj = partObj.properties[part]
354
+ } else {
355
+ return
356
+ }
357
+ }
358
+ return partObj
359
+ },
360
+ applyCustomColumnFilter(tableData) {
361
+ const customColumnSearches = Object.keys(this.searchFilters)
362
+ .filter((fieldName) => this.getFieldType(fieldName) === 'custom')
363
+
364
+ return tableData.filter((row) => {
365
+ return customColumnSearches.every(fieldName => {
366
+ const searchKeyword = this.searchFilters[fieldName]
367
+
368
+ if (!searchKeyword) {
369
+ return true
370
+ }
371
+
372
+ const stringified = row[fieldName].toString()
373
+
374
+ if (this.exactSearches[fieldName]) {
375
+ return stringified === searchKeyword
376
+ } else {
377
+ return stringified.toLowerCase().includes(searchKeyword.toLowerCase())
378
+ }
379
+ })
380
+ })
381
+ },
382
+ downloadCSV(data = this.data) {
383
+ const collection = this.collectionName
384
+ let text = this.selectedColumns.map((name) => '"' + name + '"').join(',') + '\n'
385
+ if (data) {
386
+ text += data.map((row) =>
387
+ this.selectedColumns.map((key) => '"' + String(objectPath.get(row, key) || '').replace(/"/g, '""') + '"')
388
+ ).join('\n')
389
+ console.log(text)
390
+ const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
391
+ saveAs(blob, collection + '.csv')
392
+ }
393
+ },
394
+ async downloadAllCSV() {
395
+ let rows = []
396
+ let page = 1
397
+
398
+ while (this.count > rows.length) {
399
+ const params = {
400
+ ...this.allFilters,
401
+ page: page,
402
+ limit: 500,
403
+ }
404
+
405
+ const response = (await request({ url: `/${this.collectionName}/${this.customParams}`, params })).data
406
+ rows = [...response.data, ...rows]
407
+ page += 1
408
+ }
409
+
410
+ this.downloadCSV(this.applyCustomColumnFilter(rows))
411
+ },
412
+ getFieldType(fieldName) {
413
+ const property = this.getSchemaProperty(this.schema, fieldName)
414
+ return (property && property.type) || 'custom'
415
+ },
416
+ getQueryBuilder(fieldType) {
417
+ const queryBuilders = {
418
+ 'string': this.getStringSearchQuery,
419
+ 'number': (payload) => parseInt(payload.searchKeyword)
420
+ }
421
+
422
+ return queryBuilders[fieldType] || this.getStringSearchQuery
423
+ },
424
+ getStringSearchQuery({ fieldName, searchKeyword }) {
425
+ const regexQuery = { '$regex': searchKeyword, '$options': 'i' }
426
+ return this.exactSearches[fieldName] ? searchKeyword : regexQuery
427
+ },
428
+ resetTable() {
429
+ // Reset Filters
430
+ this.exactSearches = {}
431
+ this.searchFilters = {}
432
+ this.selectedColumns = []
433
+
434
+ // Reset pagination
435
+ this.page = 0
436
+ this.pageSize = pageSizes[0]
437
+
438
+ // Reset table data
439
+ this.data = []
440
+ this.count = 0
441
+ },
442
+ handleParamKeyChange(index) {
443
+ const key = this.params[index].key
444
+ const isLast = index === this.params.length - 1
445
+
446
+ if (!isLast || !key) {
447
+ return
448
+ }
449
+
450
+ this.params.push({ key: '', value: '', isEnabled: true })
451
+ },
452
+ removeParam(index) {
453
+ this.params.splice(index, 1)
454
+ },
455
+ }
456
+ }
457
+ </script>
458
+
459
+ <style scoped>
460
+ .el-table >>> th {
461
+ background-color: #343a40 !important;
462
+ border-color: #454d55;
463
+ color: #fff;
464
+ }
465
+ </style>