waibu-db 1.0.1

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/bajo/.alias +1 -0
  4. package/bajo/config.json +17 -0
  5. package/bajo/method/admin-menu.js +46 -0
  6. package/bajo/method/get-params.js +18 -0
  7. package/bajo/method/get-schema-ext.js +133 -0
  8. package/bajo/method/method-map.js +9 -0
  9. package/bajo/method/record/create.js +12 -0
  10. package/bajo/method/record/find-one.js +20 -0
  11. package/bajo/method/record/find.js +26 -0
  12. package/bajo/method/record/get.js +14 -0
  13. package/bajo/method/record/remove.js +10 -0
  14. package/bajo/method/record/update.js +12 -0
  15. package/bajo/method/stat/aggregate.js +13 -0
  16. package/bajo/method/stat/histogram.js +13 -0
  17. package/bajoI18N/resource/en-US.json +18 -0
  18. package/bajoI18N/resource/id.json +40 -0
  19. package/lib/crud/add-handler.js +39 -0
  20. package/lib/crud/all-handler.js +31 -0
  21. package/lib/crud/delete-handler.js +37 -0
  22. package/lib/crud/details-handler.js +18 -0
  23. package/lib/crud/edit-handler.js +33 -0
  24. package/lib/crud/export-handler.js +15 -0
  25. package/lib/crud/helper/add-ons-handler.js +46 -0
  26. package/lib/crud/helper/build-params.js +14 -0
  27. package/lib/crud/list-handler.js +27 -0
  28. package/lib/prep-crud.js +17 -0
  29. package/package.json +30 -0
  30. package/waibuBootstrap/theme/component/btn-add.js +15 -0
  31. package/waibuBootstrap/theme/component/btn-back.js +13 -0
  32. package/waibuBootstrap/theme/component/btn-clone.js +30 -0
  33. package/waibuBootstrap/theme/component/btn-columns.js +45 -0
  34. package/waibuBootstrap/theme/component/btn-delete.js +43 -0
  35. package/waibuBootstrap/theme/component/btn-details.js +32 -0
  36. package/waibuBootstrap/theme/component/btn-edit.js +56 -0
  37. package/waibuBootstrap/theme/component/btn-export.js +129 -0
  38. package/waibuBootstrap/theme/component/echarts.js +58 -0
  39. package/waibuBootstrap/theme/component/pagination.js +60 -0
  40. package/waibuBootstrap/theme/component/query.js +137 -0
  41. package/waibuBootstrap/theme/component/recs-info.js +50 -0
  42. package/waibuBootstrap/theme/component/table.js +161 -0
  43. package/waibuMpa/extend/waibuAdmin/route/@model/@action.js +14 -0
  44. package/waibuMpa/partial/crud/_addons.html +5 -0
  45. package/waibuMpa/partial/crud/_form.html +12 -0
  46. package/waibuMpa/partial/crud/add-handler.html +19 -0
  47. package/waibuMpa/partial/crud/details-handler.html +21 -0
  48. package/waibuMpa/partial/crud/echarts-window.html +9 -0
  49. package/waibuMpa/partial/crud/edit-handler.html +19 -0
  50. package/waibuMpa/partial/crud/list-handler.html +26 -0
  51. package/waibuMpa/sumba/route/export/@action.js +14 -0
  52. package/waibuMpa/template/crud/add.html +2 -0
  53. package/waibuMpa/template/crud/details.html +2 -0
  54. package/waibuMpa/template/crud/edit.html +2 -0
  55. package/waibuMpa/template/crud/list.html +2 -0
  56. package/waibuMpa/template/disabled.html +8 -0
@@ -0,0 +1,137 @@
1
+ async function query (params = {}) {
2
+ const { generateId } = this.plugin.app.bajo
3
+ const { jsonStringify } = this.plugin.app.waibuMpa
4
+ const { find, get, without, isEmpty, filter, upperFirst } = this.plugin.app.bajo.lib._
5
+ const qsKey = this.plugin.app.waibu.config.qsKey
6
+ const schema = get(this, 'locals.schema', {})
7
+ if (schema.view.disabled.includes('find')) {
8
+ params.html = ''
9
+ return
10
+ }
11
+ let fields = without(get(this, `locals._meta.query.${qsKey.fields}`, '').split(','), '')
12
+ if (isEmpty(fields)) fields = schema.view.fields
13
+ fields = filter(fields, f => schema.sortables.includes(f))
14
+ const id = generateId('alpha')
15
+ const columns = []
16
+ const models = []
17
+ const selects = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'contains', 'starts', 'ends', '!in', '!contains', '!starts', '!ends']
18
+ for (const f of schema.view.fields) {
19
+ if (!fields.includes(f)) continue
20
+ const prop = find(schema.properties, { name: f })
21
+ const ops = []
22
+ if (['float', 'double', 'integer', 'smallint'].includes(prop.type)) ops.push('eq', 'neq', 'gt', 'gte', 'lt', 'lte')
23
+ else if (['datetime', 'date', 'time'].includes(prop.type)) ops.push('eq', 'neq', 'gt', 'gte', 'lt', 'lte')
24
+ else if (['boolean'].includes(prop.type)) ops.push('eq', 'neq')
25
+ else ops.push(...selects)
26
+ if (ops.length === 0) continue
27
+ const sels = ops.map(o => `<c:option>${o}</c:option>`)
28
+ models.push(`${f}Op: 'eq'`, `${f}Val: ''`)
29
+ columns.push(`
30
+ <c:grid-col col="4-md" flex="align-items:center">
31
+ <c:form-check x-model="selected" t:label="field.${f}" value="${f}" />
32
+ </c:grid-col>
33
+ <c:grid-col col="3-md">
34
+ <c:form-select x-model="${f}Op">
35
+ ${sels.join('\n')}
36
+ </c:form-select>
37
+ </c:grid-col>
38
+ <c:grid-col col="5-md">
39
+ <c:form-input x-model="${f}Val" />
40
+ </c:grid-col>
41
+ `)
42
+ }
43
+ params.noTag = true
44
+ const container = params.attr.modal ? 'modal' : 'drawer'
45
+ params.html = await this.buildSentence(`
46
+ <c:form-input type="search" t:placeholder="Query" id="${id}" x-data="{ query: '' }" x-init="
47
+ const url = new URL(window.location.href)
48
+ query = url.searchParams.get('${qsKey.query}') ?? ''
49
+ " x-model="query" @on-query.window="query = $event.detail ?? ''" @keyup.enter="$dispatch('on-submit')">
50
+ <c:form-input-addon>
51
+ <c:${container} launch-icon="${params.attr.icon ?? 'dotsThree'}" launch-on-end t:title="Query Builder" x-ref="query" x-data="{
52
+ fields: ${jsonStringify(fields, true)},
53
+ builder: '',
54
+ selected: [],
55
+ ${models.join(',\n')},
56
+ ops: { eq: ':', neq: ':-', gt: ':>', gte: ':>=', lt: ':<', lte: ':<=' },
57
+ opsIn (v, neg) { return ':' + (neg ? '-' : '') + '[' + this.expandArray(v) + ']' },
58
+ opsExt (v, neg, ext) {
59
+ let prefix = (neg ? '-' : '') + '~'
60
+ if (ext) prefix += ext
61
+ return ':' + prefix + '\\'' + v + '\\''
62
+ },
63
+ expandArray (val = '') {
64
+ return _.map(val.split(','), item => {
65
+ item = _.trim(item)
66
+ if (Number(item)) return item
67
+ return '\\'' + item + '\\''
68
+ })
69
+ },
70
+ rebuild () {
71
+ const items = []
72
+ for (const sel of this.selected) {
73
+ const key = this[sel + 'Op']
74
+ let val = this[sel + 'Val']
75
+ if (_.isEmpty(val)) continue
76
+ let item
77
+ if (key === 'in') item = this.opsIn(val)
78
+ else if (key === '!in') item = this.opsIn(val, true)
79
+ else if (key === 'contains') item = this.opsExt(val)
80
+ else if (key === '!contains') item = this.opsExt(val, true)
81
+ else if (key === 'starts') item = this.opsExt(val, false, '^')
82
+ else if (key === '!starts') item = this.opsExt(val, true, '^')
83
+ else if (key === 'ends') item = this.opsExt(val, false, '$$')
84
+ else if (key === '!ends') item = this.opsExt(val, true, '$$')
85
+ else if (val.includes(' ')) item = this.ops[key] + '\\'' + val + '\\''
86
+ else item = this.ops[key] + val
87
+ items.push(sel + item)
88
+ }
89
+ this.builder = items.join('+')
90
+ },
91
+ submit (run) {
92
+ if (run) {
93
+ const url = new URL(window.location.href)
94
+ const params = new URLSearchParams(url.search)
95
+ params.set('${qsKey.page}', 1)
96
+ params.set('${qsKey.query}', this.builder ?? '')
97
+ window.location.href = '?' + params.toString()
98
+ } else $dispatch('on-query', this.builder)
99
+ const instance = wbs.getInstance('${upperFirst(container)}', $refs.query)
100
+ instance.hide()
101
+ }
102
+ }" x-init="
103
+ const ops = _.map(fields, f => (f + 'Op'))
104
+ const vals = _.map(fields, f => (f + 'Val'))
105
+ const watcher = ['selected', ...ops, ...vals].join(',')
106
+ $watch(watcher, v => rebuild())
107
+ ">
108
+ <c:grid-row gutter="2">
109
+ <c:grid-col col="12">
110
+ <c:form-textarea x-model="builder" readonly rows="4"/>
111
+ </c:grid-col>
112
+ ${columns.join('\n')}
113
+ </c:grid-row>
114
+ <c:div flex="justify-content:end" margin="top-3">
115
+ <c:btn color="secondary" t:content="Close" dismiss="${container}" />
116
+ <c:btn color="primary" t:content="Apply" margin="start-2" @click="submit()" />
117
+ <c:btn color="primary" t:content="Submit Query" margin="start-2" @click="submit(true)" />
118
+ </c:div>
119
+ </c:${container}>
120
+ </c:form-input-addon>
121
+ <c:form-input-addon>
122
+ <c:btn t:content="Submit" x-data="{
123
+ submit () {
124
+ const val = document.getElementById('${id}').value ?? ''
125
+ const url = new URL(window.location.href)
126
+ const params = new URLSearchParams(url.search)
127
+ params.set('${qsKey.page}', 1)
128
+ params.set('${qsKey.query}', val)
129
+ window.location.href = '?' + params.toString()
130
+ }
131
+ }" @click="submit" @on-submit.window="submit()" />
132
+ </c:form-input-addon>
133
+ </c:form-input>
134
+ `)
135
+ }
136
+
137
+ export default query
@@ -0,0 +1,50 @@
1
+ import { getUrlOpts } from './pagination.js'
2
+
3
+ async function recordsInfo (params = {}) {
4
+ const { attrToObject, groupAttrs, attrToArray } = this.plugin.app.waibuMpa
5
+ const { get, isEmpty, omit, merge } = this.plugin.app.bajo.lib._
6
+ const schema = get(this, 'locals.schema', {})
7
+ if (schema.view.disabled.includes('find')) {
8
+ params.html = ''
9
+ return
10
+ }
11
+ let { count, limit, page, pages } = attrToObject(params.attr.options)
12
+ count = count ?? get(this, 'locals.list.count', 0)
13
+ page = page ?? get(this, 'locals.list.page', 1)
14
+ limit = limit ?? get(this, 'locals.list.limit', 25)
15
+ pages = pages ?? get(this, 'locals.list.pages', 0)
16
+
17
+ params.tag = 'div'
18
+ params.attr.flex = 'justify-center:start align-items:center'
19
+ if (count === 0) {
20
+ params.html = this.req.t('No record found')
21
+ return
22
+ }
23
+ if (!params.attr.dropdown) params.attr.dropdown = true
24
+ const group = groupAttrs(params.attr, ['dropdown'])
25
+ const html = []
26
+ if (params.attr.count) html.push(this.req.t('%s record(s) found', this.req.format(count, 'integer')))
27
+ if (params.attr.pages) {
28
+ if (!isEmpty(html)) html[html.length - 1] += '.'
29
+ html.push(this.req.t('Page %s of %s pages', this.req.format(page, 'integer'), this.req.format(pages, 'integer')))
30
+ }
31
+ if (params.attr.recsPerPage) {
32
+ params.attr.recsPerPageValues = params.attr.recsPerPageValues ?? '10 25 50'
33
+ params.attr.recsPerPageValues = attrToArray(params.attr.recsPerPageValues)
34
+ if (!isEmpty(html)) html[html.length - 1] += ','
35
+ const items = []
36
+ for (const i of params.attr.recsPerPageValues) {
37
+ const attr = { href: this._buildUrl(merge(getUrlOpts.call(this), { params: { limit: i, page: 1 } })), disabled: i === limit }
38
+ items.push(await this.buildTag({ tag: 'dropdownItem', attr, html: i + '' }))
39
+ }
40
+ const attr = group.dropdown
41
+ attr.content = limit + ''
42
+ attr.color = attr.color ?? 'secondary-outline'
43
+ html.push(await this.buildTag({ tag: 'dropdown', attr, html: items.join('\n') }))
44
+ html.push(' ', this.req.t('recs per page'))
45
+ }
46
+ params.attr = omit(params.attr, ['count', 'pages', 'recsPerPage', 'dropdown', 'recsPerPageValues'])
47
+ params.html = html.map(h => `<div class="me-1">${h}</div>`).join('\n')
48
+ }
49
+
50
+ export default recordsInfo
@@ -0,0 +1,161 @@
1
+ function isRightAligned (type) {
2
+ return ['smallint', 'integer', 'float', 'double'].includes(type)
3
+ }
4
+
5
+ const table = {
6
+ handler: async function (params = {}) {
7
+ const { escape } = this.plugin.app.waibu
8
+ const { attrToArray, groupAttrs } = this.plugin.app.waibuMpa
9
+ const { get, omit, set, find, isEmpty, without } = this.plugin.app.bajo.lib._
10
+ const group = groupAttrs(params.attr, ['body', 'head', 'foot'])
11
+ params.attr = group._
12
+
13
+ const data = get(this, 'locals.list.data', [])
14
+ const schema = get(this, 'locals.schema', {})
15
+ if (schema.view.disabled.includes('find')) {
16
+ params.html = ''
17
+ return
18
+ }
19
+ const qsKey = this.plugin.app.waibu.config.qsKey
20
+ let fields = without(get(this, `locals._meta.query.${qsKey.fields}`, '').split(','), '')
21
+ if (isEmpty(fields)) fields = schema.view.fields
22
+ const sort = params.attr.sort ? attrToArray(params.attr.sort) : get(this, `locals._meta.query.${qsKey.sort}`, '')
23
+
24
+ let [sortCol, sortDir] = sort.split(':')
25
+ if (!['-1', '1'].includes(sortDir)) sortDir = '1'
26
+
27
+ let selection
28
+ const canDelete = !schema.view.disabled.includes('remove')
29
+ const canEdit = !schema.view.disabled.includes('update')
30
+ if (canEdit) selection = 'single'
31
+ if (canDelete) selection = 'multi'
32
+ if (selection) params.attr.hover = true
33
+
34
+ params.noTag = true
35
+ const html = []
36
+ let items = []
37
+ // head
38
+ for (const f of schema.view.fields) {
39
+ if (!fields.includes(f)) continue
40
+ const prop = find(schema.properties, { name: f })
41
+ let head = this.req.t(`field.${f}`)
42
+ if (!params.attr.noSort && (schema.sortables ?? []).includes(f)) {
43
+ let sortItem = `${f}:-1`
44
+ let icon = params.attr.sortUpIcon ?? 'caretUp'
45
+ if (f === sortCol) {
46
+ sortItem = `${f}:${sortDir === '1' ? '-1' : '1'}`
47
+ icon = sortDir === '1' ? (params.attr.sortUpIcon ?? 'caretUp') : (params.attr.sortDownIcon ?? 'caretDown')
48
+ }
49
+ const item = set({ page: 1 }, qsKey.sort, sortItem)
50
+ const href = this._buildUrl({ params: item })
51
+ const attr = isRightAligned(prop.type) ? { text: 'align:end' } : {}
52
+ const content = [
53
+ await this.buildTag({ tag: 'div', attr, html: this.req.t(`field.${f}`) }),
54
+ await this.buildTag({ tag: 'a', attr: { icon, href }, prepend: '<div class="ms-1">', append: '</div>' })
55
+ ]
56
+ head = await this.buildTag({ tag: 'div', attr: { flex: 'justify-content:between align-items:end' }, html: content.join('\n') })
57
+ }
58
+ let text = params.attr.headerNowrap ? '' : 'nowrap'
59
+ if (isRightAligned(prop.type)) text += ' align:end'
60
+ const attr = { dataKey: f, dataType: prop.type, text }
61
+ items.push(await this.buildTag({ tag: 'th', attr, html: head }))
62
+ }
63
+ if (items.length > 0 && selection) {
64
+ let item = '<th></th>'
65
+ if (selection === 'multi') {
66
+ const attr = { 'x-model': 'toggleAll', name: '_rtm', noWrapper: true, noLabel: true }
67
+ item = await this.buildTag({ tag: 'formCheck', attr, prepend: '<th>', append: '</th>' })
68
+ } else {
69
+ const attr = { name: 'remove', '@click': 'selected = \'\'', style: { cursor: 'pointer' } }
70
+ item = await this.buildTag({ tag: 'icon', attr, prepend: '<th>', append: '</th>' })
71
+ }
72
+ items.unshift(item)
73
+ }
74
+ const header = await this.buildTag({ tag: 'tr', html: items.join('\n') })
75
+ html.push(await this.buildTag({ tag: 'thead', attr: group.head, html: header }))
76
+ // body
77
+ items = []
78
+ for (const d of data) {
79
+ const lines = []
80
+ if (selection) {
81
+ const tag = selection === 'single' ? 'formRadio' : 'formCheck'
82
+ const attr = { 'x-model': 'selected', name: '_rt', value: d.id, noLabel: true, noWrapper: true }
83
+ lines.push(await this.buildTag({ tag, attr, prepend: '<td>', append: '</td>' }))
84
+ }
85
+ for (const f of schema.view.fields) {
86
+ const prop = find(schema.properties, { name: f })
87
+ if (!fields.includes(f)) continue
88
+ const opts = {}
89
+ if (f === 'lng') opts.longitude = true
90
+ else if (f === 'lat') opts.latitude = true
91
+ let value = this.req.format(d[f], prop.type, opts)
92
+ if (prop.type === 'boolean') {
93
+ value = (await this.buildTag({ tag: 'icon', attr: { name: `circle${d[f] ? 'Check' : ''}` } })) +
94
+ ' ' + (this.req.t(d[f] ? 'Yes' : 'No'))
95
+ } else value = escape(value)
96
+ let dataValue = d[f] ?? ''
97
+ if (['string', 'text'].includes(prop.type)) dataValue = escape(dataValue)
98
+ if (['array', 'object'].includes(prop.type)) dataValue = escape(JSON.stringify(d[f]))
99
+ const attr = { dataValue }
100
+ if (!['object', 'array'].includes(prop.type)) {
101
+ if (isRightAligned(prop.type)) attr.text = 'align:end nowrap'
102
+ else attr.text = 'nowrap'
103
+ }
104
+ const line = await this.buildTag({ tag: 'td', attr, html: value })
105
+ lines.push(line)
106
+ }
107
+ const attr = {}
108
+ if (!schema.view.disabled.includes('update') || !schema.view.disabled.includes('remove')) attr['@click'] = `toggle('${d.id}')`
109
+ if (!schema.view.disabled.includes('get')) attr['@dblclick'] = `goDetails('${d.id}')`
110
+ items.push(await this.buildTag({ tag: 'tr', attr, html: lines.join('\n') }))
111
+ }
112
+ html.push(await this.buildTag({ tag: 'tbody', attr: group.body, html: items.join('\n') }))
113
+ params.attr = omit(params.attr, ['sortUpIcon', 'sortDownIcon', 'noSort', 'selection', 'headerNowrap'])
114
+ const goDetails = `
115
+ goDetails (id) {
116
+ window.location.href = '${this._buildUrl({ base: 'details' })}&id=' + id
117
+ }
118
+ `
119
+ if (selection === 'multi') {
120
+ params.attr['x-data'] = `{
121
+ toggleAll: false,
122
+ selected: [],
123
+ toggle (id) {
124
+ if (this.selected.includes(id)) {
125
+ const idx = this.selected.indexOf(id)
126
+ this.selected.splice(idx, 1)
127
+ } else this.selected.push(id)
128
+ },
129
+ ${goDetails}
130
+ }`
131
+ params.attr['x-init'] = `
132
+ $watch('toggleAll', val => {
133
+ if (val) {
134
+ const els = document.getElementsByName('_rt')
135
+ const items = Array.from(els)
136
+ selected = items.map(el => el.value)
137
+ } else selected = []
138
+ })
139
+ $watch('selected', val => $dispatch('on-selection', val))
140
+ `
141
+ } else if (selection === 'single') {
142
+ params.attr['x-data'] = `{
143
+ selected: '',
144
+ toggle (id) {
145
+ this.selected = id
146
+ },
147
+ ${goDetails}
148
+ }`
149
+ params.attr['x-init'] = `
150
+ $watch('selected', val => $dispatch('on-selection', [val]))
151
+ `
152
+ } else {
153
+ params.attr['x-data'] = `{
154
+ ${goDetails}
155
+ }`
156
+ }
157
+ params.html = await this.buildTag({ tag: 'table', attr: params.attr, html: html.join('\n') })
158
+ }
159
+ }
160
+
161
+ export default table
@@ -0,0 +1,14 @@
1
+ const action = {
2
+ method: ['GET', 'POST'],
3
+ title: 'Model Database',
4
+ handler: async function (req, reply) {
5
+ const { importModule } = this.app.bajo
6
+ const handler = await importModule('waibuDb:/lib/crud/all-handler.js')
7
+ const { model, action } = req.params
8
+ const template = `waibuDb.template:/crud/${action}.html`
9
+ const params = { page: { layout: `waibuAdmin.layout:/crud/${action === 'list' ? 'wide' : 'default'}.html` } }
10
+ return handler.call(this, { model, req, reply, action, params, template })
11
+ }
12
+ }
13
+
14
+ export default action
@@ -0,0 +1,5 @@
1
+ <% for (const ao of addOns) { %>
2
+ <c:div append-to="#sb-addons" margin="bottom-3">
3
+ <c:include resource="<%= ao.resource %>" <% for (const k in ao.data) { print(k + '="' + ao.data[k] + '" ') } %>/>
4
+ </c:div>
5
+ <% } %>
@@ -0,0 +1,12 @@
1
+ <% for (const l of schema.view.layout) { %>
2
+ <c:fieldset card <% if (l.name[0] !== '_') print('t:legend="' + l.name + '"') %> grid-gutter="2">
3
+ <% for (const w of l.widgets) {
4
+ const prop = _.find(schema.properties, { name: w.name })
5
+ const attr = []
6
+ _.forOwn(w.attr, (v, k) => {
7
+ attr.push(`${k}="${v}"`)
8
+ })
9
+ print(`<c:${w.component} ${w.attr.label ? ('label="' + _t(w.attr.label) + '"') : ''} data-type="${prop.type}" label-floating name="${w.name}" ${attr.join(' ')} />`)
10
+ } %>
11
+ </c:fieldset>
12
+ <% } %>
@@ -0,0 +1,19 @@
1
+ <c:form>
2
+ <c:include resource="waibuDb.partial:/crud/_form.html" />
3
+ <c:grid-row gutter="2" margin="top-2">
4
+ <c:grid-col col="6-md">
5
+ <c:wdb-btn-back />
6
+ </c:grid-col>
7
+ <c:grid-col col="6-md">
8
+ <c:div flex="justify-content:end-md align-items:center">
9
+ <c:btn type="reset" color="secondary" t:content="Reset" />
10
+ <c:btn type="submit" color="primary" t:content="Submit" margin="start-2"/>
11
+ </c:div>
12
+ </c:grid-col>
13
+ <c:grid-col col="12" margin="top-4">
14
+ <c:form-switch name="_addmore" t:label="Add more after submit"/>
15
+ <c:form-switch name="_cloneprev" t:label="Clone previous"/>
16
+ </c:grid-col>
17
+ </c:grid-row>
18
+ </c:form>
19
+ <c:include resource="waibuDb.partial:/crud/_addons.html" />
@@ -0,0 +1,21 @@
1
+ <c:div id="main-form">
2
+ <c:include resource="waibuDb.partial:/crud/_form.html" />
3
+ </c:div>
4
+ <c:grid-row gutter="2" margin="top-2">
5
+ <c:grid-col col="6-lg">
6
+ <c:wdb-btn-back />
7
+ <% if (schema.disabled.includes('remove') && schema.disabled.includes('update')) { %>
8
+ <c:wdb-btn-export selector="#main-form" handler="details" launch-margin="start-1"/>
9
+ <% } else { %>
10
+ <c:btn-group margin="start-1">
11
+ <c:wdb-btn-edit />
12
+ <c:wdb-btn-clone />
13
+ <c:wdb-btn-export selector="#main-form" handler="details" launch-on-end/>
14
+ </c:btn-group>
15
+ <% } %>
16
+ </c:grid-col>
17
+ <c:grid-col col="6-lg" flex="justify-content:end-lg align-items:center">
18
+ <c:wdb-btn-delete/>
19
+ </c:grid-col>
20
+ </c:grid-row>
21
+ <c:include resource="waibuDb.partial:/crud/_addons.html" />
@@ -0,0 +1,9 @@
1
+ <c:card>
2
+ <c:card-header>
3
+ <c:card-title t:content="<%= attr.name %>" />
4
+ </c:card-header>
5
+ <c:card-body>
6
+ <c:wdb-echarts style="height: 150px;" option="<%= attr.option %>">
7
+ </c:wdb-echarts>
8
+ </c:card-body>
9
+ </c:card>
@@ -0,0 +1,19 @@
1
+ <c:form id="main-form">
2
+ <c:include resource="waibuDb.partial:/crud/_form.html" />
3
+ <c:grid-row gutter="2" margin="top-2">
4
+ <c:grid-col col="6-lg">
5
+ <c:wdb-btn-back />
6
+ <c:btn-group margin="start-1">
7
+ <c:wdb-btn-details />
8
+ <c:wdb-btn-clone/>
9
+ <c:wdb-btn-export selector="#main-form" handler="edit" launch-on-end/>
10
+ </c:btn-group>
11
+ </c:grid-col>
12
+ <c:grid-col col="6-lg" flex="justify-content:end-lg align-items:center">
13
+ <c:wdb-btn-delete/>
14
+ <c:btn type="reset" color="secondary" t:content="Reset" margin="start-2"/>
15
+ <c:btn type="submit" color="primary" t:content="Submit" margin="start-2"/>
16
+ </c:grid-col>
17
+ </c:grid-row>
18
+ </c:form>
19
+ <c:include resource="waibuDb.partial:/crud/_addons.html" />
@@ -0,0 +1,26 @@
1
+ <c:grid-row gutter="3" margin="bottom-3">
2
+ <c:grid-col col="4-lg">
3
+ <c:wdb-query />
4
+ </c:grid-col>
5
+ <c:grid-col col="8-lg" flex="justify-content:end-lg">
6
+ <c:btn-group margin="end-2">
7
+ <c:wdb-btn-add text="nowrap"/>
8
+ <c:wdb-btn-edit on-list menu="end"/>
9
+ </c:btn-group>
10
+ <c:wdb-btn-delete on-list margin="end-2" />
11
+ <c:btn-group>
12
+ <c:wdb-btn-export selector="#main-table" handler="list"/>
13
+ <c:wdb-btn-columns menu="end"/>
14
+ </c:btn-group>
15
+ </c:grid-col>
16
+ </c:grid-row>
17
+ <c:wdb-table id="main-table" border body-divider strip responsive />
18
+ <c:grid-row gutter="3" margin="top-2">
19
+ <c:grid-col col="6-lg">
20
+ <c:wdb-recs-info pages recs-per-page recs-per-page-values="10 15 25 50" />
21
+ </c:grid-col>
22
+ <c:grid-col col="6-lg" flex="justify-content:end-lg">
23
+ <c:wdb-pagination />
24
+ </c:grid-col>
25
+ </c:grid-row>
26
+ <c:include resource="waibuDb.partial:/crud/_addons.html" />
@@ -0,0 +1,14 @@
1
+ const action = {
2
+ method: ['GET', 'POST'],
3
+ title: 'Database Export',
4
+ handler: async function (req, reply) {
5
+ const { importModule } = this.app.bajo
6
+ const handler = await importModule('waibuDb:/lib/crud/all-handler.js')
7
+ const model = 'SumbaUser'
8
+ const { action } = req.params
9
+ const template = `sumba.template:/crud/${action}.html`
10
+ return handler.call(this, { model, req, reply, action, template })
11
+ }
12
+ }
13
+
14
+ export default action
@@ -0,0 +1,2 @@
1
+ <c:heading move-to="#fullTitle" type="5" t:content="%s - %s|<%= page.modelTitle %>|<%= _t('Add') %>" />
2
+ <c:include resource="waibuDb.partial:/crud/add-handler.html" />
@@ -0,0 +1,2 @@
1
+ <c:heading move-to="#fullTitle" type="5" t:content="%s - %s|<%= page.modelTitle %>|<%= _t('Details') %>" />
2
+ <c:include resource="waibuDb.partial:/crud/details-handler.html" />
@@ -0,0 +1,2 @@
1
+ <c:heading move-to="#fullTitle" type="5" t:content="%s - %s|<%= page.modelTitle %>|<%= _t('Edit') %>" />
2
+ <c:include resource="waibuDb.partial:/crud/edit-handler.html" />
@@ -0,0 +1,2 @@
1
+ <c:heading move-to="#fullTitle" type="5" t:content="%s - %s|<%= page.modelTitle %>|<%= _t('List') %>" />
2
+ <c:include resource="waibuDb.partial:/crud/list-handler.html" />
@@ -0,0 +1,8 @@
1
+ <c:div margin="top-5">
2
+ <c:heading type="3-display" t:content="Action Disabled" margin="bottom-5"/>
3
+ <c:div>
4
+ <c:t value="<%= action %>">Sorry, action '%s' is permanently disabled at database level. For more information,
5
+ please contact your Admin immediately, thank you!</c:t>
6
+ </c:t>
7
+ </c:div>
8
+ </c:div>