expressa 1.1.3 → 1.2.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.
- package/README.md +4 -3
- package/controllers/collections.js +53 -32
- package/db/cached.js +3 -0
- package/db/file.js +3 -0
- package/db/memory.js +3 -0
- package/db/mongo.js +7 -8
- package/db/postgres.js +13 -0
- package/index.js +3 -1
- package/modules/admin/dist/index.html +1 -1
- package/modules/admin/dist/static/css/chunk-318c.ffb0d81e.css +1 -0
- package/modules/admin/dist/static/css/{chunk-d0cd.97323951.css → chunk-621e.97323951.css} +0 -0
- package/modules/admin/dist/static/css/{chunk-418a.5ee55031.css → chunk-6644.69ccf1a7.css} +1 -1
- package/modules/admin/dist/static/css/{chunk-787e.1d849865.css → chunk-e375.1d849865.css} +0 -0
- package/modules/admin/dist/static/js/{app.4c984719.js → app.03e8afa3.js} +1 -1
- package/modules/admin/dist/static/js/chunk-318c.03fa0032.js +1 -0
- package/modules/admin/dist/static/js/chunk-621e.a9ed86e2.js +1 -0
- package/modules/admin/dist/static/js/chunk-6644.9c6f5c7b.js +1 -0
- package/modules/admin/dist/static/js/chunk-e375.089d85cd.js +1 -0
- package/modules/admin/src/components/JSONEditor.vue +180 -181
- package/modules/admin/src/views/ListDocuments2.vue +309 -309
- package/package.json +1 -1
- package/test/collections.js +2 -3
- package/test/collections.querying.js +18 -4
- package/test/test.js +4 -0
- package/util.js +19 -0
- package/modules/admin/dist/static/css/chunk-1059.220d4af5.css +0 -1
- package/modules/admin/dist/static/js/chunk-1059.e6aa7b61.js +0 -1
- package/modules/admin/dist/static/js/chunk-418a.e0987b3c.js +0 -1
- package/modules/admin/dist/static/js/chunk-787e.3ee96fce.js +0 -1
- package/modules/admin/dist/static/js/chunk-d0cd.1781b9dd.js +0 -1
|
@@ -1,309 +1,309 @@
|
|
|
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-checkbox
|
|
11
|
-
v-model="isFiltersVisible"
|
|
12
|
-
class="mb-0 mr-4"
|
|
13
|
-
>
|
|
14
|
-
Show Filters
|
|
15
|
-
</el-checkbox>
|
|
16
|
-
|
|
17
|
-
<el-pagination
|
|
18
|
-
:page-size="pageSize"
|
|
19
|
-
:total="count"
|
|
20
|
-
:page-sizes="pageSizes"
|
|
21
|
-
:current-page.sync="page"
|
|
22
|
-
layout="sizes, prev, pager, next"
|
|
23
|
-
@current-change="update()"
|
|
24
|
-
@size-change="pageSize = $event, update()"
|
|
25
|
-
/>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<el-table
|
|
29
|
-
:data="tableRows"
|
|
30
|
-
element-loading-text="Loading"
|
|
31
|
-
border
|
|
32
|
-
fit
|
|
33
|
-
highlight-current-row
|
|
34
|
-
>
|
|
35
|
-
<el-table-column v-for="(name, i) in listedProperties" :key="name" :label="name" align="center">
|
|
36
|
-
<template slot-scope="scope">
|
|
37
|
-
<div v-if="scope.$index === 0 && isFiltersVisible" class="text-left">
|
|
38
|
-
<el-input
|
|
39
|
-
v-model="searchFilters[name]"
|
|
40
|
-
:placeholder="`Search by ${name}`"
|
|
41
|
-
:type="getFieldType(name) === 'number' ? 'number' : 'text'"
|
|
42
|
-
@change="update()"
|
|
43
|
-
/>
|
|
44
|
-
|
|
45
|
-
<el-checkbox
|
|
46
|
-
v-model="exactSearches[name]"
|
|
47
|
-
:disabled="getFieldType(name) === 'number'"
|
|
48
|
-
class="mt-3"
|
|
49
|
-
@change="update()"
|
|
50
|
-
>
|
|
51
|
-
Exact match
|
|
52
|
-
</el-checkbox>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<template v-else>
|
|
56
|
-
<router-link v-if="i === 0 && collectionName !== 'requestlog'" :to="'/edit/'+collectionName+'/'+scope.row._id">
|
|
57
|
-
{{ getPath(scope.row, name) }}
|
|
58
|
-
</router-link>
|
|
59
|
-
<router-link v-if="i === 0 && collectionName === 'requestlog'" :to="'/dev/viewrequest/'+scope.row._id">
|
|
60
|
-
{{ getPath(scope.row, name) }}
|
|
61
|
-
</router-link>
|
|
62
|
-
<span v-if="i !== 0">{{ getPath(scope.row, name) }}</span>
|
|
63
|
-
</template>
|
|
64
|
-
</template>
|
|
65
|
-
</el-table-column>
|
|
66
|
-
</el-table>
|
|
67
|
-
|
|
68
|
-
<div class="d-flex align-items-center my-3">
|
|
69
|
-
<router-link v-if="showAddButton" :to="'/edit/' + collectionName + '/create'" class="mr-2">
|
|
70
|
-
<button class="btn btn-primary">
|
|
71
|
-
Add
|
|
72
|
-
</button>
|
|
73
|
-
</router-link>
|
|
74
|
-
|
|
75
|
-
<button class="btn btn-secondary download-button mr-2" @click="downloadCSV()">
|
|
76
|
-
Download
|
|
77
|
-
</button>
|
|
78
|
-
|
|
79
|
-
<button class="btn btn-secondary download-button" @click="downloadAllCSV()">
|
|
80
|
-
Download All
|
|
81
|
-
</button>
|
|
82
|
-
|
|
83
|
-
<span class="mx-auto" />
|
|
84
|
-
|
|
85
|
-
<el-pagination
|
|
86
|
-
:page-size="pageSize"
|
|
87
|
-
:total="count"
|
|
88
|
-
:page-sizes="pageSizes"
|
|
89
|
-
:current-page.sync="page"
|
|
90
|
-
layout="sizes, prev, pager, next"
|
|
91
|
-
@current-change="update()"
|
|
92
|
-
@size-change="pageSize = $event, update()"
|
|
93
|
-
/>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
</template>
|
|
97
|
-
|
|
98
|
-
<script>
|
|
99
|
-
import request from '@/utils/request'
|
|
100
|
-
import saveAs from 'file-saver'
|
|
101
|
-
import objectPath from 'object-path'
|
|
102
|
-
|
|
103
|
-
const pageSizes = [25, 50, 100, 500]
|
|
104
|
-
|
|
105
|
-
export default {
|
|
106
|
-
name: 'ListDocuments',
|
|
107
|
-
props: {
|
|
108
|
-
collectionName: {
|
|
109
|
-
type: String,
|
|
110
|
-
default: 'collection'
|
|
111
|
-
},
|
|
112
|
-
filter: {
|
|
113
|
-
type: Object,
|
|
114
|
-
default: () => {}
|
|
115
|
-
},
|
|
116
|
-
columns: {
|
|
117
|
-
type: Array,
|
|
118
|
-
default: () => undefined
|
|
119
|
-
},
|
|
120
|
-
showAddButton: {
|
|
121
|
-
type: Boolean,
|
|
122
|
-
default: true
|
|
123
|
-
},
|
|
124
|
-
showPaginateOnBottom: {
|
|
125
|
-
type: Boolean,
|
|
126
|
-
default: true
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
data: () => ({
|
|
130
|
-
start: 0,
|
|
131
|
-
page: 1,
|
|
132
|
-
count: 0,
|
|
133
|
-
data: [],
|
|
134
|
-
schema: {},
|
|
135
|
-
exactSearches: {},
|
|
136
|
-
searchFilters: {},
|
|
137
|
-
listedProperties: [],
|
|
138
|
-
pageSizes: pageSizes,
|
|
139
|
-
pageSize: pageSizes[0],
|
|
140
|
-
isFiltersVisible: true,
|
|
141
|
-
}),
|
|
142
|
-
computed: {
|
|
143
|
-
tableRows() {
|
|
144
|
-
if (!this.isFiltersVisible) {
|
|
145
|
-
return this.data
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return [
|
|
149
|
-
// INFO: This empty row is used to display the search filters.
|
|
150
|
-
{},
|
|
151
|
-
...this.data
|
|
152
|
-
]
|
|
153
|
-
},
|
|
154
|
-
appliedSearchFilters() {
|
|
155
|
-
return Object.keys(this.searchFilters).reduce((filter, fieldName) => {
|
|
156
|
-
const searchKeyword = this.searchFilters[fieldName]
|
|
157
|
-
|
|
158
|
-
const fieldtype = this.getFieldType(fieldName)
|
|
159
|
-
|
|
160
|
-
if (!searchKeyword || fieldtype === 'custom') {
|
|
161
|
-
return filter
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const queryBuilder = this.getQueryBuilder(fieldtype)
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
...filter,
|
|
168
|
-
[fieldName]: queryBuilder({ fieldName, searchKeyword })
|
|
169
|
-
}
|
|
170
|
-
}, {})
|
|
171
|
-
},
|
|
172
|
-
allFilters() {
|
|
173
|
-
const params = {
|
|
174
|
-
page: this.page,
|
|
175
|
-
limit: this.pageSize,
|
|
176
|
-
orderby: '{"meta.created":-1}',
|
|
177
|
-
...this.filter,
|
|
178
|
-
query: { ...this.appliedSearchFilters }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const columns = this.columns || (this.collection.admin && this.collection.admin.columns)
|
|
182
|
-
|
|
183
|
-
if (columns) {
|
|
184
|
-
params.fields = columns.reduce((map, field) => {
|
|
185
|
-
map[field] = 1
|
|
186
|
-
return map
|
|
187
|
-
}, {})
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return params
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
watch: {
|
|
194
|
-
collectionName: {
|
|
195
|
-
handler: 'update'
|
|
196
|
-
},
|
|
197
|
-
'$route.params.collectionName': {
|
|
198
|
-
handler: 'resetFilters'
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
mounted() {
|
|
202
|
-
this.update()
|
|
203
|
-
},
|
|
204
|
-
methods: {
|
|
205
|
-
getPath(obj, path, defaultValue) {
|
|
206
|
-
const result = String.prototype.split.call(path, /[,[\].]+?/)
|
|
207
|
-
.filter(Boolean)
|
|
208
|
-
.reduce((res, key) => (res !== null && res !== undefined) ? res[key] : res, obj)
|
|
209
|
-
return (result === undefined || result === obj) ? defaultValue : result
|
|
210
|
-
},
|
|
211
|
-
async update() {
|
|
212
|
-
if (this.$route.params.collectionName) {
|
|
213
|
-
this.collectionName = this.$route.params.collectionName
|
|
214
|
-
}
|
|
215
|
-
// Avoid refetching schema info if name hasn't changed
|
|
216
|
-
if (!this.collection || this.collection._id !== this.collectionName) {
|
|
217
|
-
this.collection = (await request({ url: `/collection/${this.collectionName}` })).data
|
|
218
|
-
this.schema = this.collection.schema
|
|
219
|
-
}
|
|
220
|
-
const params = { ...this.allFilters }
|
|
221
|
-
const columns = this.columns || (this.collection.admin && this.collection.admin.columns)
|
|
222
|
-
const info = (await request({ url: `/${this.collectionName}/`, params })).data
|
|
223
|
-
this.count = info.itemsTotal
|
|
224
|
-
this.data = this.applyCustomColumnFilter(info.data)
|
|
225
|
-
this.listedProperties = columns || Object.keys(this.schema.properties)
|
|
226
|
-
},
|
|
227
|
-
applyCustomColumnFilter(tableData) {
|
|
228
|
-
const customColumnSearches = Object.keys(this.searchFilters)
|
|
229
|
-
.filter((fieldName) => this.getFieldType(fieldName) === 'custom')
|
|
230
|
-
|
|
231
|
-
return tableData.filter((row) => {
|
|
232
|
-
return customColumnSearches.every(fieldName => {
|
|
233
|
-
const searchKeyword = this.searchFilters[fieldName]
|
|
234
|
-
|
|
235
|
-
if (!searchKeyword) {
|
|
236
|
-
return true
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const stringified = row[fieldName].toString()
|
|
240
|
-
|
|
241
|
-
if (this.exactSearches[fieldName]) {
|
|
242
|
-
return stringified === searchKeyword
|
|
243
|
-
} else {
|
|
244
|
-
return stringified.toLowerCase().includes(searchKeyword.toLowerCase())
|
|
245
|
-
}
|
|
246
|
-
})
|
|
247
|
-
})
|
|
248
|
-
},
|
|
249
|
-
downloadCSV(data = this.data) {
|
|
250
|
-
const collection = this.collectionName
|
|
251
|
-
let text = this.listedProperties.map((name) => '"' + name + '"').join(',') + '\n'
|
|
252
|
-
if (data) {
|
|
253
|
-
text += data.map((row) =>
|
|
254
|
-
this.listedProperties.map((key) => '"' + String(objectPath.get(row, key) || '').replace(/"/g, '""') + '"')
|
|
255
|
-
).join('\n')
|
|
256
|
-
console.log(text)
|
|
257
|
-
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
|
|
258
|
-
saveAs(blob, collection + '.csv')
|
|
259
|
-
}
|
|
260
|
-
},
|
|
261
|
-
async downloadAllCSV() {
|
|
262
|
-
let rows = []
|
|
263
|
-
let page = 1
|
|
264
|
-
|
|
265
|
-
while (this.count > rows.length) {
|
|
266
|
-
const params = {
|
|
267
|
-
...this.allFilters,
|
|
268
|
-
page: page,
|
|
269
|
-
limit: 500,
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const response = (await request({ url: `/${this.collectionName}/`, params })).data
|
|
273
|
-
rows = [...response.data, ...rows]
|
|
274
|
-
page += 1
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
this.downloadCSV(this.applyCustomColumnFilter(rows))
|
|
278
|
-
},
|
|
279
|
-
getFieldType(fieldName) {
|
|
280
|
-
return (this.schema.properties[fieldName] &&
|
|
281
|
-
this.schema.properties[fieldName].type) || 'custom'
|
|
282
|
-
},
|
|
283
|
-
getQueryBuilder(fieldType) {
|
|
284
|
-
const queryBuilders = {
|
|
285
|
-
'string': this.getStringSearchQuery,
|
|
286
|
-
'number': (payload) => parseInt(payload.searchKeyword)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return queryBuilders[fieldType] || this.getStringSearchQuery
|
|
290
|
-
},
|
|
291
|
-
getStringSearchQuery({ fieldName, searchKeyword }) {
|
|
292
|
-
const regexQuery = { '$regex': searchKeyword, '$options': 'i' }
|
|
293
|
-
return this.exactSearches[fieldName] ? searchKeyword : regexQuery
|
|
294
|
-
},
|
|
295
|
-
resetFilters() {
|
|
296
|
-
this.exactSearches = {}
|
|
297
|
-
this.searchFilters = {}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
</script>
|
|
302
|
-
|
|
303
|
-
<style scoped>
|
|
304
|
-
.el-table >>> th {
|
|
305
|
-
background-color: #343a40 !important;
|
|
306
|
-
border-color: #454d55;
|
|
307
|
-
color: #fff;
|
|
308
|
-
}
|
|
309
|
-
</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-checkbox
|
|
11
|
+
v-model="isFiltersVisible"
|
|
12
|
+
class="mb-0 mr-4"
|
|
13
|
+
>
|
|
14
|
+
Show Filters
|
|
15
|
+
</el-checkbox>
|
|
16
|
+
|
|
17
|
+
<el-pagination
|
|
18
|
+
:page-size="pageSize"
|
|
19
|
+
:total="count"
|
|
20
|
+
:page-sizes="pageSizes"
|
|
21
|
+
:current-page.sync="page"
|
|
22
|
+
layout="sizes, prev, pager, next"
|
|
23
|
+
@current-change="update()"
|
|
24
|
+
@size-change="pageSize = $event, update()"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<el-table
|
|
29
|
+
:data="tableRows"
|
|
30
|
+
element-loading-text="Loading"
|
|
31
|
+
border
|
|
32
|
+
fit
|
|
33
|
+
highlight-current-row
|
|
34
|
+
>
|
|
35
|
+
<el-table-column v-for="(name, i) in listedProperties" :key="name" :label="name" align="center">
|
|
36
|
+
<template slot-scope="scope">
|
|
37
|
+
<div v-if="scope.$index === 0 && isFiltersVisible" class="text-left">
|
|
38
|
+
<el-input
|
|
39
|
+
v-model="searchFilters[name]"
|
|
40
|
+
:placeholder="`Search by ${name}`"
|
|
41
|
+
:type="getFieldType(name) === 'number' ? 'number' : 'text'"
|
|
42
|
+
@change="update()"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
<el-checkbox
|
|
46
|
+
v-model="exactSearches[name]"
|
|
47
|
+
:disabled="getFieldType(name) === 'number'"
|
|
48
|
+
class="mt-3"
|
|
49
|
+
@change="update()"
|
|
50
|
+
>
|
|
51
|
+
Exact match
|
|
52
|
+
</el-checkbox>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<template v-else>
|
|
56
|
+
<router-link v-if="i === 0 && collectionName !== 'requestlog'" :to="'/edit/'+collectionName+'/'+scope.row._id">
|
|
57
|
+
{{ getPath(scope.row, name) }}
|
|
58
|
+
</router-link>
|
|
59
|
+
<router-link v-if="i === 0 && collectionName === 'requestlog'" :to="'/dev/viewrequest/'+scope.row._id">
|
|
60
|
+
{{ getPath(scope.row, name) }}
|
|
61
|
+
</router-link>
|
|
62
|
+
<span v-if="i !== 0">{{ getPath(scope.row, name) }}</span>
|
|
63
|
+
</template>
|
|
64
|
+
</template>
|
|
65
|
+
</el-table-column>
|
|
66
|
+
</el-table>
|
|
67
|
+
|
|
68
|
+
<div class="d-flex align-items-center my-3">
|
|
69
|
+
<router-link v-if="showAddButton" :to="'/edit/' + collectionName + '/create'" class="mr-2">
|
|
70
|
+
<button class="btn btn-primary">
|
|
71
|
+
Add
|
|
72
|
+
</button>
|
|
73
|
+
</router-link>
|
|
74
|
+
|
|
75
|
+
<button class="btn btn-secondary download-button mr-2" @click="downloadCSV()">
|
|
76
|
+
Download
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
<button class="btn btn-secondary download-button" @click="downloadAllCSV()">
|
|
80
|
+
Download All
|
|
81
|
+
</button>
|
|
82
|
+
|
|
83
|
+
<span class="mx-auto" />
|
|
84
|
+
|
|
85
|
+
<el-pagination
|
|
86
|
+
:page-size="pageSize"
|
|
87
|
+
:total="count"
|
|
88
|
+
:page-sizes="pageSizes"
|
|
89
|
+
:current-page.sync="page"
|
|
90
|
+
layout="sizes, prev, pager, next"
|
|
91
|
+
@current-change="update()"
|
|
92
|
+
@size-change="pageSize = $event, update()"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<script>
|
|
99
|
+
import request from '@/utils/request'
|
|
100
|
+
import saveAs from 'file-saver'
|
|
101
|
+
import objectPath from 'object-path'
|
|
102
|
+
|
|
103
|
+
const pageSizes = [25, 50, 100, 500]
|
|
104
|
+
|
|
105
|
+
export default {
|
|
106
|
+
name: 'ListDocuments',
|
|
107
|
+
props: {
|
|
108
|
+
collectionName: {
|
|
109
|
+
type: String,
|
|
110
|
+
default: 'collection'
|
|
111
|
+
},
|
|
112
|
+
filter: {
|
|
113
|
+
type: Object,
|
|
114
|
+
default: () => {}
|
|
115
|
+
},
|
|
116
|
+
columns: {
|
|
117
|
+
type: Array,
|
|
118
|
+
default: () => undefined
|
|
119
|
+
},
|
|
120
|
+
showAddButton: {
|
|
121
|
+
type: Boolean,
|
|
122
|
+
default: true
|
|
123
|
+
},
|
|
124
|
+
showPaginateOnBottom: {
|
|
125
|
+
type: Boolean,
|
|
126
|
+
default: true
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
data: () => ({
|
|
130
|
+
start: 0,
|
|
131
|
+
page: 1,
|
|
132
|
+
count: 0,
|
|
133
|
+
data: [],
|
|
134
|
+
schema: {},
|
|
135
|
+
exactSearches: {},
|
|
136
|
+
searchFilters: {},
|
|
137
|
+
listedProperties: [],
|
|
138
|
+
pageSizes: pageSizes,
|
|
139
|
+
pageSize: pageSizes[0],
|
|
140
|
+
isFiltersVisible: true,
|
|
141
|
+
}),
|
|
142
|
+
computed: {
|
|
143
|
+
tableRows() {
|
|
144
|
+
if (!this.isFiltersVisible) {
|
|
145
|
+
return this.data
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [
|
|
149
|
+
// INFO: This empty row is used to display the search filters.
|
|
150
|
+
{},
|
|
151
|
+
...this.data
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
appliedSearchFilters() {
|
|
155
|
+
return Object.keys(this.searchFilters).reduce((filter, fieldName) => {
|
|
156
|
+
const searchKeyword = this.searchFilters[fieldName]
|
|
157
|
+
|
|
158
|
+
const fieldtype = this.getFieldType(fieldName)
|
|
159
|
+
|
|
160
|
+
if (!searchKeyword || fieldtype === 'custom') {
|
|
161
|
+
return filter
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const queryBuilder = this.getQueryBuilder(fieldtype)
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
...filter,
|
|
168
|
+
[fieldName]: queryBuilder({ fieldName, searchKeyword })
|
|
169
|
+
}
|
|
170
|
+
}, {})
|
|
171
|
+
},
|
|
172
|
+
allFilters() {
|
|
173
|
+
const params = {
|
|
174
|
+
page: this.page,
|
|
175
|
+
limit: this.pageSize,
|
|
176
|
+
orderby: '{"meta.created":-1}',
|
|
177
|
+
...this.filter,
|
|
178
|
+
query: { ...this.appliedSearchFilters }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const columns = this.columns || (this.collection.admin && this.collection.admin.columns)
|
|
182
|
+
|
|
183
|
+
if (columns) {
|
|
184
|
+
params.fields = columns.reduce((map, field) => {
|
|
185
|
+
map[field] = 1
|
|
186
|
+
return map
|
|
187
|
+
}, {})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return params
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
watch: {
|
|
194
|
+
collectionName: {
|
|
195
|
+
handler: 'update'
|
|
196
|
+
},
|
|
197
|
+
'$route.params.collectionName': {
|
|
198
|
+
handler: 'resetFilters'
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
mounted() {
|
|
202
|
+
this.update()
|
|
203
|
+
},
|
|
204
|
+
methods: {
|
|
205
|
+
getPath(obj, path, defaultValue) {
|
|
206
|
+
const result = String.prototype.split.call(path, /[,[\].]+?/)
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
.reduce((res, key) => (res !== null && res !== undefined) ? res[key] : res, obj)
|
|
209
|
+
return (result === undefined || result === obj) ? defaultValue : result
|
|
210
|
+
},
|
|
211
|
+
async update() {
|
|
212
|
+
if (this.$route.params.collectionName) {
|
|
213
|
+
this.collectionName = this.$route.params.collectionName
|
|
214
|
+
}
|
|
215
|
+
// Avoid refetching schema info if name hasn't changed
|
|
216
|
+
if (!this.collection || this.collection._id !== this.collectionName) {
|
|
217
|
+
this.collection = (await request({ url: `/collection/${this.collectionName}` })).data
|
|
218
|
+
this.schema = this.collection.schema
|
|
219
|
+
}
|
|
220
|
+
const params = { ...this.allFilters }
|
|
221
|
+
const columns = this.columns || (this.collection.admin && this.collection.admin.columns)
|
|
222
|
+
const info = (await request({ url: `/${this.collectionName}/`, params })).data
|
|
223
|
+
this.count = info.itemsTotal
|
|
224
|
+
this.data = this.applyCustomColumnFilter(info.data)
|
|
225
|
+
this.listedProperties = columns || Object.keys(this.schema.properties)
|
|
226
|
+
},
|
|
227
|
+
applyCustomColumnFilter(tableData) {
|
|
228
|
+
const customColumnSearches = Object.keys(this.searchFilters)
|
|
229
|
+
.filter((fieldName) => this.getFieldType(fieldName) === 'custom')
|
|
230
|
+
|
|
231
|
+
return tableData.filter((row) => {
|
|
232
|
+
return customColumnSearches.every(fieldName => {
|
|
233
|
+
const searchKeyword = this.searchFilters[fieldName]
|
|
234
|
+
|
|
235
|
+
if (!searchKeyword) {
|
|
236
|
+
return true
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const stringified = row[fieldName].toString()
|
|
240
|
+
|
|
241
|
+
if (this.exactSearches[fieldName]) {
|
|
242
|
+
return stringified === searchKeyword
|
|
243
|
+
} else {
|
|
244
|
+
return stringified.toLowerCase().includes(searchKeyword.toLowerCase())
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
},
|
|
249
|
+
downloadCSV(data = this.data) {
|
|
250
|
+
const collection = this.collectionName
|
|
251
|
+
let text = this.listedProperties.map((name) => '"' + name + '"').join(',') + '\n'
|
|
252
|
+
if (data) {
|
|
253
|
+
text += data.map((row) =>
|
|
254
|
+
this.listedProperties.map((key) => '"' + String(objectPath.get(row, key) || '').replace(/"/g, '""') + '"')
|
|
255
|
+
).join('\n')
|
|
256
|
+
console.log(text)
|
|
257
|
+
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
|
|
258
|
+
saveAs(blob, collection + '.csv')
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
async downloadAllCSV() {
|
|
262
|
+
let rows = []
|
|
263
|
+
let page = 1
|
|
264
|
+
|
|
265
|
+
while (this.count > rows.length) {
|
|
266
|
+
const params = {
|
|
267
|
+
...this.allFilters,
|
|
268
|
+
page: page,
|
|
269
|
+
limit: 500,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const response = (await request({ url: `/${this.collectionName}/`, params })).data
|
|
273
|
+
rows = [...response.data, ...rows]
|
|
274
|
+
page += 1
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.downloadCSV(this.applyCustomColumnFilter(rows))
|
|
278
|
+
},
|
|
279
|
+
getFieldType(fieldName) {
|
|
280
|
+
return (this.schema.properties[fieldName] &&
|
|
281
|
+
this.schema.properties[fieldName].type) || 'custom'
|
|
282
|
+
},
|
|
283
|
+
getQueryBuilder(fieldType) {
|
|
284
|
+
const queryBuilders = {
|
|
285
|
+
'string': this.getStringSearchQuery,
|
|
286
|
+
'number': (payload) => parseInt(payload.searchKeyword)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return queryBuilders[fieldType] || this.getStringSearchQuery
|
|
290
|
+
},
|
|
291
|
+
getStringSearchQuery({ fieldName, searchKeyword }) {
|
|
292
|
+
const regexQuery = { '$regex': searchKeyword, '$options': 'i' }
|
|
293
|
+
return this.exactSearches[fieldName] ? searchKeyword : regexQuery
|
|
294
|
+
},
|
|
295
|
+
resetFilters() {
|
|
296
|
+
this.exactSearches = {}
|
|
297
|
+
this.searchFilters = {}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
</script>
|
|
302
|
+
|
|
303
|
+
<style scoped>
|
|
304
|
+
.el-table >>> th {
|
|
305
|
+
background-color: #343a40 !important;
|
|
306
|
+
border-color: #454d55;
|
|
307
|
+
color: #fff;
|
|
308
|
+
}
|
|
309
|
+
</style>
|
package/package.json
CHANGED
package/test/collections.js
CHANGED
|
@@ -159,11 +159,10 @@ describe('basic collections', function () {
|
|
|
159
159
|
|
|
160
160
|
it('fail to read paged without permission', async function () {
|
|
161
161
|
const token = await testutils.getUserWithPermissions(api)
|
|
162
|
-
|
|
162
|
+
await request(app)
|
|
163
163
|
.get('/testdoc/?page=1')
|
|
164
164
|
.set('x-access-token', token)
|
|
165
|
-
.expect(
|
|
166
|
-
expect(res.body.data.length).to.equal(0)
|
|
165
|
+
.expect(401)
|
|
167
166
|
})
|
|
168
167
|
|
|
169
168
|
it('read a specific doc', async function () {
|
|
@@ -102,11 +102,10 @@ describe('querying collections', function () {
|
|
|
102
102
|
expect(res.body).to.have.lengthOf(1)
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
-
it('
|
|
106
|
-
|
|
105
|
+
it('errors without permission', async function () {
|
|
106
|
+
await request(app)
|
|
107
107
|
.get('/testdoc')
|
|
108
|
-
.expect(
|
|
109
|
-
expect(res.body).to.have.lengthOf(0)
|
|
108
|
+
.expect(401)
|
|
110
109
|
})
|
|
111
110
|
|
|
112
111
|
it('page 0 returns an error', async function () {
|
|
@@ -147,6 +146,21 @@ describe('querying collections', function () {
|
|
|
147
146
|
expect(res.body.data[0].title).to.equal('doc3')
|
|
148
147
|
})
|
|
149
148
|
|
|
149
|
+
it('pagemetadisable url parameter strips additional page detail', async function () {
|
|
150
|
+
const res = await request(app)
|
|
151
|
+
.get('/testdoc?limit=2&page=2&pagemetadisable=1')
|
|
152
|
+
.set('x-access-token', token)
|
|
153
|
+
.expect(200)
|
|
154
|
+
expect(res.body.itemsTotal).to.equal(undefined)
|
|
155
|
+
expect(res.body.itemsPerPage).to.equal(2)
|
|
156
|
+
expect(res.body.pages).to.equal(undefined)
|
|
157
|
+
expect(res.body.pagePrev).to.equal(1)
|
|
158
|
+
expect(res.body.pageNext).to.equal(undefined)
|
|
159
|
+
expect(res.body.data).to.have.lengthOf(1)
|
|
160
|
+
|
|
161
|
+
expect(res.body.data[0].title).to.equal('doc3')
|
|
162
|
+
})
|
|
163
|
+
|
|
150
164
|
it('page 3 is empty', async function () {
|
|
151
165
|
const res = await request(app)
|
|
152
166
|
.get('/testdoc?limit=2&page=3')
|