waibu-db 2.14.0 → 2.16.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.
@@ -38,6 +38,8 @@
38
38
  "ndjson": "NDJSON",
39
39
  "dataValue": "Data value",
40
40
  "suppressedError": "Error occured and suppressed. Please check error log for details",
41
+ "queryHint%s": "Search by %s",
42
+ "noAttachmentFound": "No attachment found",
41
43
  "op": {
42
44
  "eq": "=",
43
45
  "neq": "≠",
@@ -38,6 +38,8 @@
38
38
  "ndjson": "NDJSON",
39
39
  "dataValue": "Nilai data",
40
40
  "suppressedError": "Kesalahan terjadi dan tidak ditampilkan. Silahkan cek log kesalahan untuk detilnya",
41
+ "queryHint%s": "Cari berdasarkan %s",
42
+ "noAttachmentFound": "Tidak ada lampiran ditemukan",
41
43
  "op": {
42
44
  "eq": "=",
43
45
  "neq": "≠",
@@ -1,29 +1,29 @@
1
1
  <c:grid-row gutter="2">
2
2
  <c:grid-col col="6-lg">
3
- <% if (!schema.view.control.noBackBtn) { %>
4
- <c:wdb-btn-back href="<%= schema.view.control.backHref ?? 'undefined' %>" />
3
+ <% if (!_getSetting('waibuDb:/control/wdbBtnBack/disabled')) { %>
4
+ <c:wdb-btn-back href="<%= _getSetting('waibuDb:/control/wdbBtnBack/href', 'undefined') %>" />
5
5
  <% } %>
6
6
  <% if (schema.disabled.includes('remove') && schema.disabled.includes('update')) { %>
7
- <% if (!schema.view.control.noExportBtn) { %>
7
+ <% if (!_getSetting('waibuDb:/control/wdbBtnExport/disabled')) { %>
8
8
  <c:wdb-btn-export selector="#main-form" handler="details" launch-margin="start-1" />
9
9
  <% } %>
10
10
  <% } else { %>
11
11
  <c:btn-group margin="start-1">
12
- <% if (!schema.view.control.noEditBtn) { %>
13
- <c:wdb-btn-edit href="<%= schema.view.control.editHref ?? 'undefined' %>" />
12
+ <% if (!_getSetting('waibuDb:/control/wdbBtnEdit/disabled')) { %>
13
+ <c:wdb-btn-edit href="<%= _getSetting('waibuDb:/control/wdbBtnEdit/href', 'undefined') %>" />
14
14
  <% } %>
15
- <% if (!schema.view.control.noCloneBtn) { %>
16
- <c:wdb-btn-clone href="<%= schema.view.control.cloneHref ?? 'undefined' %>" />
15
+ <% if (!_getSetting('waibuDb:/control/wdbBtnClone/disabled')) { %>
16
+ <c:wdb-btn-clone href="<%= _getSetting('waibuDb:/control/wdbBtnClone/href', 'undefined') %>" />
17
17
  <% } %>
18
- <% if (!schema.view.control.noExportBtn) { %>
18
+ <% if (!_getSetting('waibuDb:/control/wdbBtnExport/disabled')) { %>
19
19
  <c:wdb-btn-export selector="#main-form" handler="details" launch-on-end no-save/>
20
20
  <% } %>
21
21
  </c:btn-group>
22
22
  <% } %>
23
23
  </c:grid-col>
24
24
  <c:grid-col col="6-lg" flex="justify-content:end-lg align-items:center">
25
- <% if (!schema.view.control.noDeleteBtn) { %>
26
- <c:wdb-btn-delete href="<%= schema.view.control.deleteHref ?? 'undefined' %>" />
25
+ <% if (!_getSetting('waibuDb:/control/wdbBtnDelete/disabled')) { %>
26
+ <c:wdb-btn-delete href="<%= _getSetting('waibuDb:/control/wdbBtnDelete/href', 'undefined') %>" />
27
27
  <% } %>
28
28
  </c:grid-col>
29
29
  </c:grid-row>
@@ -1,28 +1,28 @@
1
1
  <c:grid-row gutter="2">
2
2
  <c:grid-col col="6-lg">
3
- <% if (!schema.view.control.noBackBtn) { %>
4
- <c:wdb-btn-back href="<%= schema.view.control.backHref ?? 'undefined' %>"/>
3
+ <% if (!_getSetting('waibuDb:/control/wdbBtnBack/disabled')) { %>
4
+ <c:wdb-btn-back href="<%= _getSetting('waibuDb:/control/wdbBtnBack/href', 'undefined') %>"/>
5
5
  <% } %>
6
6
  <c:btn-group margin="start-1">
7
- <% if (!schema.view.control.noDetailsBtn) { %>
8
- <c:wdb-btn-details href="<%= schema.view.control.detailsHref ?? 'undefined' %>" />
7
+ <% if (!_getSetting('waibuDb:/control/wdbBtnDetails/disabled')) { %>
8
+ <c:wdb-btn-details href="<%= _getSetting('waibuDb:/control/wdbBtnDetails/href', 'undefined') %>" />
9
9
  <% } %>
10
- <% if (!schema.view.control.noCloneBtn) { %>
11
- <c:wdb-btn-clone href="<%= schema.view.control.cloneHref ?? 'undefined' %>" />
10
+ <% if (!_getSetting('waibuDb:/control/wdbBtnClone/disabled')) { %>
11
+ <c:wdb-btn-clone href="<%= _getSetting('waibuDb:/control/wdbBtnClone/href', 'undefined') %>" />
12
12
  <% } %>
13
- <% if (!schema.view.control.noExportBtn) { %>
13
+ <% if (!_getSetting('waibuDb:/control/wdbBtnExport/disabled')) { %>
14
14
  <c:wdb-btn-export selector="#main-form" handler="edit" launch-on-end no-save />
15
15
  <% } %>
16
16
  </c:btn-group>
17
17
  </c:grid-col>
18
18
  <c:grid-col col="6-lg" flex="justify-content:end-lg align-items:center">
19
- <% if (!schema.view.control.noDeleteBtn) { %>
20
- <c:wdb-btn-delete href="<%= schema.view.control.deleteHref ?? 'undefined' %>"/>
19
+ <% if (!_getSetting('waibuDb:/control/wdbBtnDelete/disabled')) { %>
20
+ <c:wdb-btn-delete href="<%= _getSetting('waibuDb:/control/wdbBtnDelete/href', 'undefined') %>"/>
21
21
  <% } %>
22
- <% if (!schema.view.control.noResetBtn) { %>
22
+ <% if (!_getSetting('waibuDb:/control/wdbBtnReset/disabled')) { %>
23
23
  <c:btn type="reset" color="secondary" t:content="reset" margin="start-2"/>
24
24
  <% } %>
25
- <% if (!schema.view.control.noSubmitBtn) { %>
25
+ <% if (!_getSetting('waibuDb:/control/wdbBtnSubmit/disabled')) { %>
26
26
  <c:btn type="submit" color="primary" t:content="submit" margin="start-2"/>
27
27
  <% } %>
28
28
  </c:grid-col>
@@ -1,9 +1,15 @@
1
- <% for (const att of attachments) { %>
2
- <c:grid-col col="4-lg 6-md">
3
- <% if (attr.readonly) { %>
4
- <a href="<%= decodeURI(att.url) %>" target="_blank"><%= att.fileName %></a>
5
- <% } else { %>
6
- <c:form-check x-model="selected" value="<%= att.fullPath %>" label="<a href='<%= att.url %>' target='_blank'><%= att.fileName %></a>" />
1
+ <% if (attachments.length === 0) { %>
2
+ <c:grid-col col="12">
3
+ <c:alert color="warning" t:content="noAttachmentFound" margin="bottom-1" />
4
+ </c:grid-col>
5
+ <% } else { %>
6
+ <% for (const att of attachments) { %>
7
+ <c:grid-col col="4-lg 6-md">
8
+ <% if (attr.readonly) { %>
9
+ <a href="<%= decodeURI(att.url) %>" target="_blank"><%= att.fileName %></a>
10
+ <% } else { %>
11
+ <c:form-check x-model="selected" value="<%= att.fullPath %>" label="<a href='<%= att.url %>' target='_blank'><%= att.fileName %></a>" />
12
+ <% } %>
13
+ </c:grid-col>
7
14
  <% } %>
8
- </c:grid-col>
9
- <% } %>
15
+ <% } %>
@@ -1,8 +1,8 @@
1
1
  <c:grid-row gutter="3">
2
- <c:grid-col col="4-lg">
2
+ <c:grid-col col="5-lg">
3
3
  <c:wdb-query />
4
4
  </c:grid-col>
5
- <c:grid-col col="8-lg" flex="justify-content:end-lg">
5
+ <c:grid-col col="7-lg" flex="justify-content:end-lg">
6
6
  <c:btn-group margin="end-2">
7
7
  <c:wdb-btn-add text="nowrap"/>
8
8
  <c:wdb-btn-details on-list text="nowrap"/>
@@ -6,15 +6,16 @@ async function wdbBase () {
6
6
  const { get } = this.app.lib._
7
7
  this.schema = get(this, 'component.locals.schema', {})
8
8
  this.formData = get(this, 'component.locals.form', {})
9
- this.model = getModel(this.schema.name)
9
+ this.oldData = get(this, 'component.locals.oldData', {})
10
+ this.model = getModel(this.schema.name, true)
10
11
  }
11
12
 
12
13
  getRef = ({ field, refName, returning } = {}) => {
13
14
  const { get } = this.app.lib._
14
- if (!this.schema) return {}
15
+ if (!this.model) return {}
15
16
  const prop = this.model.getProperty(field)
16
17
  if (!prop) return {}
17
- refName = refName ?? this.params.attr['x-ref'] ?? field.slice(0, -2)
18
+ if (!refName && field.endsWith('Id')) refName = field.slice(0, -2)
18
19
  const key = this.params.attr.refName ?? refName
19
20
  const ref = get(prop, `ref.${key}`, {})
20
21
  if (returning === 'all') return { ref, key }
@@ -23,11 +24,26 @@ async function wdbBase () {
23
24
  }
24
25
 
25
26
  getRefValue = ({ field, data, labelField, refName } = {}) => {
26
- const { isEmpty } = this.app.lib._
27
+ const { get, isEmpty } = this.app.lib._
27
28
  const { ref, key } = this.getRef({ field, refName, returning: 'all' })
28
29
  if (isEmpty(ref)) return undefined
29
30
  labelField = labelField ?? ref.labelField ?? 'id'
30
- return (data ?? this.formData)._ref[key][labelField]
31
+ return (get(data ?? this.formData, `_ref.${key}.${labelField}`))
32
+ }
33
+
34
+ getRefName = (field) => {
35
+ const { get } = this.app.lib._
36
+ let refName = get(this.schema, `view.widget.${field}.attr.ref-name`, this.params.attr.refName)
37
+ if (!refName && this.params.attr[field] && this.params.attr[field].endsWith('Id')) refName = this.params.attr[field].slice(0, -2)
38
+ return refName
39
+ }
40
+
41
+ getSetting = (key, defValue) => {
42
+ const { get, camelCase } = this.app.lib._
43
+ const widgetName = camelCase(this.constructor.name)
44
+ key = key.replaceAll('{self}', widgetName)
45
+ const cfg = this.app.waibu.getSetting(`${this.plugin.ns}:${key}`, { defValue, req: this.component.req })
46
+ return get(this.schema, `view.${key}`, cfg)
31
47
  }
32
48
  }
33
49
  }
@@ -25,7 +25,7 @@ async function btnColumns () {
25
25
  let prop = find(schema.properties, { name: f })
26
26
  if (!prop) prop = find(schema.view.calcFields, { name: f })
27
27
  if (!prop) continue
28
- const attr = { 'x-model': 'selected', label: req.t(get(schema, `view.label.${f}`, `field.${f}`)), value: f }
28
+ const attr = { 'x-model': 'selected', label: req.t(get(schema, `view.label.${f}`, `field.${f}`)), value: f, labelText: 'nowrap' }
29
29
  if (fields.includes(f)) attr.checked = true
30
30
  items.push(await this.component.buildTag({ tag: 'formCheck', attr }))
31
31
  }
@@ -39,17 +39,27 @@ async function btnColumns () {
39
39
  $refs.apply.href = '${href}&${qsKey.fields}=' + selected.join(',')
40
40
  $watch('selected', v => {
41
41
  $refs.apply.href = '${href}&${qsKey.fields}=' + v.join(',')
42
+ if (v.length === 0) $refs.apply.classList.add('disabled')
43
+ else $refs.apply.classList.remove('disabled')
42
44
  })
43
45
  ">`)
44
46
  this.params.attr.menuPrepend = Buffer.from(menuPrepend.join('\n')).toString('base64')
45
- const attr = { size: 'sm', 'x-ref': 'apply', margin: 'top-2', color: this.params.attr.applyColor ?? 'primary', icon: this.params.attr.applyIcon ?? 'arrowsStartEnd', href }
46
- let menuAppend = await this.component.buildTag({ tag: 'btn', attr, html: req.t('apply') })
47
- menuAppend += '\n</form>'
48
- this.params.attr.menuAppend = Buffer.from(menuAppend).toString('base64')
47
+ const btnColor = this.params.attr.applyColor ?? 'primary'
48
+ const sentences = [
49
+ '<c:div flex="justify-content:between" margin="top-2" >',
50
+ ` <c:btn size="sm" x-ref="apply" color="${btnColor}" href="${href}">${req.t('apply')}</c:btn>`,
51
+ ' <c:btn-group>',
52
+ ` <c:btn size="sm" color="${btnColor}-outline" icon="checkAll" @click="selected = all" />`,
53
+ ` <c:btn size="sm" color="${btnColor}-outline" icon="remove" @click="selected = []" />`,
54
+ ' </c:btn-group>',
55
+ '</c:div>'
56
+ ]
57
+ const menuAppend = await this.component.buildSentence(sentences, this.component.locals)
58
+ this.params.attr.menuAppend = Buffer.from(menuAppend + '\n</form>').toString('base64')
49
59
  this.params.attr.autoClose = 'outside'
50
60
  this.params.attr.triggerColor = this.params.attr.color
51
61
  this.params.attr.menuDir = this.params.attr.menuDir ?? 'end'
52
- this.params.attr.menuMax = this.params.attr.menuMax ?? '20'
62
+ this.params.attr.menuMax = this.params.attr.menuMax ?? this.getSetting('control.{self}.menuMax')
53
63
  const html = [...items]
54
64
  this.params.html = await this.component.buildTag({ tag: 'dropdown', attr: this.params.attr, html: html.join('\n') })
55
65
  this.params.noTag = true
@@ -61,7 +61,7 @@ async function btnExport () {
61
61
  }
62
62
  item[key] = value
63
63
  }
64
- return this.ftype === 'csv' ? CSVJSON.json2csv(item) : JSON.stringify(item)
64
+ return this.ftype === 'csv' ? CSVJSON.json2csv(item) : JSON.stringify(item, null, 2)
65
65
  },
66
66
  extractTable (selector) {
67
67
  let items = []
@@ -98,7 +98,7 @@ async function btnExport () {
98
98
  }
99
99
  items.push(item)
100
100
  }
101
- return this.ftype === 'csv' ? CSVJSON.json2csv(items) : JSON.stringify(items)
101
+ return this.ftype === 'csv' ? CSVJSON.json2csv(items) : JSON.stringify(items, null, 2)
102
102
  },
103
103
  async submit () {
104
104
  const instance = wbs.getInstance('Modal', $refs.export)
@@ -1,5 +1,54 @@
1
1
  import wdbBase from '../wdb-base.js'
2
2
 
3
+ async function handleRo (attr = {}, prop = {}, widget = {}) {
4
+ const { get, camelCase, isEmpty, isString } = this.app.lib._
5
+ const { callHandler } = this.app.bajo
6
+ const { escape } = this.app.waibu
7
+ const { req } = this.component
8
+ const dataValue = get(this.formData, `_orig.${prop.name}`, prop.dataValue ?? '')
9
+ let value = get(this.oldData, prop.name, get(this.formData, prop.name, prop.value ?? ''))
10
+ const format = get(this.schema, `view.format.${prop.name}`)
11
+ const formatValue = get(this.schema, `view.formatValue.${prop.name}`)
12
+ const labelField = get(this.schema, `view.widget.${prop.name}.attr.labelField`)
13
+ if (formatValue) value = await formatValue.call(this, value, this.formData, { req })
14
+ else if (prop.ref) {
15
+ value = this.getRefValue({ field: prop.name, labelField, refName: this.getRefName(prop.name) })
16
+ if (format && !isEmpty(value)) attr.href = await format.call(this, value, this.formData, { linkOnly: true })
17
+ } else if (prop.values) {
18
+ const values = isString(prop.values) ? (await callHandler(prop.values)) : prop.values
19
+ value = values.find(v => v.value === dataValue)
20
+ if (value) {
21
+ const key = camelCase(`${prop.name} ${value.text}`)
22
+ value = req.te(key) ? req.t(key) : value.text
23
+ }
24
+ } else if (format && !isEmpty(value)) value = await format.call(this, value, this.formData)
25
+ attr.dataValue = escape(dataValue)
26
+ attr.value = escape(value)
27
+ attr.dataType = prop.type
28
+
29
+ if (['object', 'array', 'text'].includes(prop.type)) {
30
+ attr.style = 'min-height: 100px'
31
+ return await this.component.buildTag({ tag: 'formTextarea', attr, html: value })
32
+ }
33
+ return await this.component.buildTag({ tag: 'formPlaintext', attr, selfCosing: true, noEscape: true })
34
+ }
35
+
36
+ async function handleRw (attr = {}, prop = {}, widget = {}) {
37
+ const { get, has, isPlainObject, isArray } = this.app.lib._
38
+ const { escape } = this.app.waibu
39
+ const { stringifyAttribs } = this.app.waibuMpa
40
+ if (has(attr, 'name') && !has(attr, 'value')) {
41
+ attr.dataType = attr.dataType ?? prop.type
42
+ attr.dataValue = get(this, `formData.${attr.name}`)
43
+ if (isPlainObject(attr.dataValue) || isArray(attr.dataValue)) attr.dataValue = JSON.stringify(attr.dataValue)
44
+ attr.dataValue = escape(attr.dataValue)
45
+ attr.value = widget.component === 'form-plaintext' ? get(this, `oldData.${attr.name}`, attr.dataValue) : attr.dataValue
46
+ }
47
+
48
+ const cmp = prop.ref ? 'wdb-lookup-select' : widget.component
49
+ return `<c:${cmp} ${stringifyAttribs(attr)} data-type="${prop.type}" />`
50
+ }
51
+
3
52
  async function form () {
4
53
  const WdbBase = await wdbBase.call(this)
5
54
 
@@ -7,51 +56,47 @@ async function form () {
7
56
  build = async () => {
8
57
  const { get, find, filter, forOwn, isEmpty } = this.app.lib._
9
58
  const { base64JsonEncode } = this.app.waibu
10
- const { req } = this.component
11
- const schema = get(this, 'component.locals.schema', {})
12
- const data = get(this, 'component.locals.form', {})
13
59
  const body = []
14
- const xModels = get(schema, 'view.x.model', [])
15
- const xOns = get(schema, 'view.x.on', [])
16
- for (const l of schema.view.layout) {
17
- const fields = filter(l.fields, f => schema.view.fields.includes(f))
60
+ const xModels = get(this.schema, 'view.x.model', [])
61
+ const xOns = get(this.schema, 'view.x.on', [])
62
+ for (const l of this.schema.view.layout) {
63
+ const fields = filter(l.fields, f => this.schema.view.fields.includes(f))
18
64
  if (fields.length === 0) continue
19
- body.push(`<c:fieldset ${schema.view.card === false ? '' : 'card'} ${l.name[0] !== '_' ? ('t:legend="' + l.name + '"') : ''} grid-gutter="2">`)
65
+ body.push(`<c:fieldset ${this.schema.view.card === false ? '' : 'card'} ${l.name[0] !== '_' ? ('t:legend="' + l.name + '"') : ''} grid-gutter="2">`)
20
66
  for (const f of fields) {
21
- const w = schema.view.widget[f]
22
- let prop = find(schema.properties, { name: f })
23
- if (!prop) prop = find(schema.view.calcFields, { name: f })
67
+ const widget = this.schema.view.widget[f]
68
+ let prop = find(this.schema.properties, { name: f })
69
+ if (!prop) prop = find(this.schema.view.calcFields, { name: f })
24
70
  if (!prop) continue
25
- const attr = [`x-ref="${w.name}"`]
26
- if (xModels.includes(w.name)) attr.push(`x-model="${w.name}"`)
27
- forOwn(w.attr, (v, k) => {
28
- if (v === true) attr.push(k)
29
- else attr.push(`${k}="${v}"`)
71
+ const attr = {
72
+ 'x-ref': widget.name,
73
+ labelFloating: true,
74
+ name: widget.name
75
+ }
76
+ if (xModels.includes(widget.name)) attr['x-model'] = widget.name
77
+ forOwn(widget.attr, (v, k) => {
78
+ if (v === true) attr[k] = true
79
+ else attr[k] = v
30
80
  })
31
- const xon = filter(xOns, { field: w.name })
81
+ attr.label = this.component.req.t(attr.label)
82
+ const xon = filter(xOns, { field: widget.name })
32
83
  for (const o of xon) {
33
- attr.push(`@${o.bind}="${o.handler}"`)
84
+ attr[`@${o.bind}`] = o.handler
34
85
  }
35
- if (w.componentOpts) attr.push(`c-opts="${base64JsonEncode(w.componentOpts)}"`)
36
- const attributes = `${w.attr.label ? ('t:label="' + w.attr.label + '"') : ''} label-floating name="${w.name}" ${attr.join(' ')}`
37
- if (w.component === 'form-plaintext' || this.params.attr.method !== 'POST') {
38
- let value
39
- if (schema.view.valueFormatter[f]) value = await schema.view.valueFormatter[f].call(this, data[f], data, { req })
40
- else if (prop.ref) value = this.getRefValue({ field: f, labelField: w.attr.labelField })
41
- body.push(`<c:${w.component} ${attributes} data-type="${prop.type}" ${value ? `value="${value}"` : ''} />`)
42
- } else if (prop.ref) {
43
- body.push(`<c:wdb-lookup-select ${attributes} />`)
86
+ if (widget.componentOpts) attr['c-opts'] = base64JsonEncode(widget.componentOpts)
87
+ if (widget.component === 'form-plaintext' || this.params.attr.method !== 'POST') {
88
+ body.push(await handleRo.call(this, attr, prop, widget))
44
89
  } else {
45
- body.push(`<c:${w.component} ${attributes} data-type="${prop.type}" />`)
90
+ body.push(await handleRw.call(this, attr, prop, widget))
46
91
  }
47
92
  }
48
93
  body.push('</c:fieldset>')
49
94
  }
50
95
  const html = await this.component.buildSentence(body, this.component.locals)
51
96
  this.params.html = `${html}\n${this.params.html}`
52
- const xData = get(schema, 'view.x.data', '')
97
+ const xData = get(this.schema, 'view.x.data', '')
53
98
  this.params.attr['x-data'] = isEmpty(xData) ? '' : `{ ${xData} }`
54
- this.params.attr['x-init'] = get(schema, 'view.x.init', '')
99
+ this.params.attr['x-init'] = get(this.schema, 'view.x.init', '')
55
100
 
56
101
  this.params.tag = this.params.attr.tag ?? 'form'
57
102
  }
@@ -9,18 +9,16 @@ async function lookupSelect () {
9
9
  this.params.noTag = true
10
10
  }
11
11
 
12
- returnEmpty = () => {
13
- this.params.html = ''
14
- }
15
-
16
12
  build = async () => {
17
13
  const { isEmpty, get, omit, set, camelCase, kebabCase } = this.app.lib._
18
14
  const { parseQuery } = this.app.dobo
19
15
  const { base64JsonEncode } = this.app.waibu
20
- let refName = get(this.schema, `view.widget.${this.params.attr.name}.attr.refName`, this.params.attr.refName)
21
- if (!refName && this.params.attr.name.endsWith('Id')) refName = this.params.attr.name.slice(0, -2)
22
- const ref = this.getRef({ field: this.params.attr.name, refName })
23
- if (isEmpty(ref)) return this.returnEmpty()
16
+ const ref = this.getRef({ field: this.params.attr.name, refName: this.getRefName(this.params.attr.name) })
17
+ if (isEmpty(ref)) {
18
+ const sentence = `<c:form-input ${Object.entries(this.params.attr).map(([k, v]) => `${kebabCase(k)}="${v}"`).join(' ')} />`
19
+ this.params.html = this.component.buildSentence(sentence, this.component.locals)
20
+ return
21
+ }
24
22
 
25
23
  this.params.attr.url = this.params.attr.url ?? `waibuDb.restapi:/lookup/${kebabCase(ref.model)}`
26
24
  const omitted = ['url', 'searchField', 'labelField', 'valueField']
@@ -7,6 +7,7 @@ async function query () {
7
7
  build = async () => {
8
8
  const { req } = this.component
9
9
  const { generateId } = this.app.lib.aneka
10
+ const { join } = this.app.bajo
10
11
  const { jsonStringify } = this.app.waibuMpa
11
12
  const { find, get, without, isEmpty, filter, upperFirst } = this.app.lib._
12
13
  const qsKey = this.app.waibu.config.qsKey
@@ -60,8 +61,11 @@ async function query () {
60
61
  }
61
62
  this.params.noTag = true
62
63
  const container = this.params.attr.modal ? 'modal' : 'drawer'
64
+ const scanables = (this.model ? this.model.scanables : []).map(item => req.t(`field.${item}`))
65
+ let placeholder = this.params.attr.placeholder
66
+ if (!placeholder) placeholder = scanables.length > 0 ? req.t('queryHint%s', join(scanables, { separator: ', ', lastSeparator: 'or' })) : req.t('query')
63
67
  this.params.html = await this.component.buildSentence(`
64
- <c:form-input type="search" t:placeholder="query" id="${id}" x-data="{ query: '' }" x-init="
68
+ <c:form-input type="search" placeholder="${placeholder}" id="${id}" x-data="{ query: '' }" x-init="
65
69
  const url = new URL(window.location.href)
66
70
  query = url.searchParams.get('${qsKey.query}') ?? ''
67
71
  " x-model="query" @on-query.window="query = $event.detail ?? ''" @keyup.enter="$dispatch('on-submit')">
@@ -21,45 +21,24 @@ async function table () {
21
21
  return get(schema, 'view.noWrap', []).includes(field)
22
22
  }
23
23
 
24
- _defFormatter = async ({ req, key, value, data, schema, params }) => {
25
- const { get, find, camelCase, isEmpty } = this.app.lib._
26
- const { escape } = this.app.waibu
27
- const prop = find(schema.properties, { name: key })
28
- if (!prop) return value
29
- if (prop.type === 'boolean') {
30
- value = (await this.component.buildTag({ tag: 'icon', attr: { name: `circle${data[key] ? 'Check' : 'Cross'}` } })) +
31
- ' ' + (req.t(data[key] ? 'Yes' : 'No'))
32
- } else if (prop.values) {
33
- const values = typeof prop.values === 'string' ? this.propValues[key] : prop.values
34
- const item = find(values, { value }) ?? {}
35
- const ttext = camelCase(`${prop.name} ${item.text}`)
36
- value = escape(req.format(!isEmpty(item) ? (req.te(ttext) ? req.t(ttext) : item.text) : value, prop.type))
37
- if (item && !params.attr.noDataValueRef && !isEmpty(data[key])) value += ` <sup><a href="#" title="${req.t('dataValue')}: ${data[key]}">*</a></sup>`
38
- } else if (['string', 'text'].includes(prop.type)) {
39
- if (!get(schema, 'view.noEscape', []).includes(key)) value = escape(value)
40
- }
41
- return value
42
- }
43
-
44
24
  build = async () => {
45
25
  const { callHandler } = this.app.bajo
46
26
  const { req } = this.component
47
27
  const { escape, attrToArray } = this.app.waibu
48
- const { formatRecord } = this.app.waibuDb
49
28
  const { groupAttrs } = this.app.waibuMpa
50
- const { get, omit, set, find, isEmpty, without, merge } = this.app.lib._
29
+ const { get, omit, set, find, isEmpty, without, merge, camelCase } = this.app.lib._
51
30
  const group = groupAttrs(this.params.attr, ['body', 'head', 'foot'])
52
31
  this.params.attr = group._
53
32
  const prettyUrl = this.params.attr.prettyUrl
54
33
 
55
34
  const schema = get(this, 'component.locals.schema', {})
56
35
  const data = get(this, 'component.locals.list.data', [])
57
- const fdata = await formatRecord.call(this.plugin, { data, req, schema })
58
36
  const filter = get(this, 'component.locals.list.filter', {})
59
37
  const count = get(this, 'component.locals.list.count', 0)
60
38
  // collect prop.values for later use
61
39
  for (const prop of schema.properties) {
62
40
  if (typeof prop.values === 'string') this.propValues[prop.name] = await callHandler(prop.values)
41
+ else if (prop.values) this.propValues[prop.name] = prop.values
63
42
  }
64
43
  if (count === 0 || data.length === 0) {
65
44
  const alert = '<c:alert color="warning" t:content="noRecordFound" margin="top-4"/>'
@@ -87,7 +66,8 @@ async function table () {
87
66
  let selection
88
67
  const canDelete = !disableds.includes('remove')
89
68
  const canEdit = !disableds.includes('update')
90
- if (canEdit) selection = 'single'
69
+ const canDetails = !disableds.includes('get')
70
+ if (canEdit || canDetails) selection = 'single'
91
71
  if (canDelete) selection = 'multi'
92
72
  if (selection) this.params.attr.hover = true
93
73
 
@@ -141,13 +121,12 @@ async function table () {
141
121
  items = []
142
122
  for (const idx in data) {
143
123
  const d = data[idx]
144
- const fd = fdata[idx]
145
124
  const lines = []
146
125
  if (selection) {
147
126
  const tag = selection === 'single' ? 'formRadio' : 'formCheck'
148
- const attr = { 'x-model': 'selected', name: '_rt', value: d.id, noLabel: true, noWrapper: true }
127
+ const attr = { 'x-model': 'selected', name: '_rt', value: d._orig.id, noLabel: true, noWrapper: true }
149
128
  const type = find(schema.properties, { name: 'id' }).type
150
- const prepend = `<td data-value="${d.id}" data-key="id" data-type="${type}">`
129
+ const prepend = `<td data-value="${d._orig.id}" data-key="id" data-type="${type}">`
151
130
  lines.push(await this.component.buildTag({ tag, attr, prepend, append: '</td>' }))
152
131
  }
153
132
  for (const f of schema.view.fields) {
@@ -155,34 +134,47 @@ async function table () {
155
134
  let prop = find(schema.properties, { name: f })
156
135
  if (!prop) prop = find(schema.view.calcFields, { name: f })
157
136
  if (!prop) continue
158
- let dataValue = d[f] ?? ''
137
+ let dataValue = d._orig[f] ?? ''
159
138
  if (!isEmpty(dataValue)) {
160
139
  if (['datetime'].includes(prop.type)) dataValue = escape(dataValue.toISOString())
161
140
  if (['string', 'text'].includes(prop.type)) dataValue = escape(dataValue)
162
141
  if (['array', 'object'].includes(prop.type)) dataValue = escape(JSON.stringify(d[f]))
163
142
  }
164
- let value = this.getRefValue({ field: f, data: fd }) ?? fd[f]
143
+ const refName = get(schema, `view.widget.${f}.attr.refName`)
144
+ let value = this.getRefValue({ field: f, data: d, refName }) ?? d[f]
145
+ const formatValue = get(schema, `view.formatValue.${f}`)
146
+ if (formatValue) {
147
+ value = await formatValue.call(this, value, d, { params: this.params, req })
148
+ dataValue = value
149
+ }
150
+ if (!get(schema, 'view.noEscape', []).includes(f)) value = escape(value)
165
151
  const attr = { dataValue, dataKey: prop.name, dataType: prop.type }
166
152
  if (!disableds.includes('get')) attr.style = { cursor: 'pointer' }
167
- const cellFormatter = get(schema, `view.cellFormatter.${f}`)
168
- if (cellFormatter) merge(attr, await cellFormatter.call(this, dataValue, d, { params: this.params, req }))
153
+ const formatCell = get(schema, `view.formatCell.${f}`)
154
+ if (formatCell) merge(attr, await formatCell.call(this, value, d, { params: this.params, req }))
169
155
  const noWrap = this.isNoWrap(f, schema, group.body.nowrap) ? 'nowrap' : ''
170
156
  if (this.isRightAligned(f, schema)) attr.text = `align:end ${noWrap}`
171
157
  else attr.text = noWrap
172
158
  const lookup = get(schema, `view.lookup.${f}`)
173
159
  if (lookup) {
174
- const item = find(lookup.values, set({}, lookup.id ?? 'id', d[f]))
160
+ const item = find(lookup.values, set({}, lookup.id ?? 'id', dataValue))
175
161
  if (item) value = req.t(item[lookup.field ?? 'name'])
176
162
  }
177
- const formatter = get(schema, `view.formatter.${f}`)
178
- if (formatter) value = await formatter.call(this, value, d, { params: this.params, req })
179
- else value = await this._defFormatter({ req, key: f, schema, value, data: d, params: this.params })
163
+ const format = get(schema, `view.format.${f}`)
164
+ if (format) value = await format.call(this, value, d, { params: this.params, req })
165
+ if (this.propValues[f]) {
166
+ const item = find(this.propValues[f], { value: dataValue })
167
+ if (item) {
168
+ const key = camelCase(`${f} ${item.text}`)
169
+ value = req.te(key) ? req.t(key) : item.text
170
+ }
171
+ }
180
172
  const line = await this.component.buildTag({ tag: 'td', attr, html: value })
181
173
  lines.push(line)
182
174
  }
183
- const attr = { id: `rec-${d.id}` }
184
- if (!disableds.includes('update') || !disableds.includes('remove')) attr['@click'] = `toggle('${d.id}')`
185
- if (!disableds.includes('get')) attr['@dblclick'] = `goDetails('${d.id}')`
175
+ const attr = { id: `rec-${d._orig.id}` }
176
+ if (!disableds.includes('update') || !disableds.includes('remove') || !disableds.includes('get')) attr['@click'] = `toggle('${d._orig.id}')`
177
+ if (!disableds.includes('get')) attr['@dblclick'] = `goDetails('${d._orig.id}')`
186
178
  items.push(await this.component.buildTag({ tag: 'tr', attr, html: lines.join('\n') }))
187
179
  }
188
180
  html.push(await this.component.buildTag({ tag: 'tbody', attr: group.body, html: items.join('\n') }))
@@ -235,7 +227,7 @@ async function table () {
235
227
  }`
236
228
  xInit = `
237
229
  ${xInit}
238
- $watch('selected', val => $dispatch('on-selection', [val]))
230
+ $watch('selected', val => $dispatch('on-selection', _.isEmpty(val) ? [] : [val]))
239
231
  `
240
232
  } else {
241
233
  xData = `{
@@ -244,6 +236,7 @@ async function table () {
244
236
  }
245
237
  this.params.attr['x-data'] = xData
246
238
  this.params.attr['x-init'] = xInit
239
+ this.params.attr.responsive = true
247
240
  this.params.html = await this.component.buildTag({ tag: 'table', attr: this.params.attr, html: html.join('\n') })
248
241
  }
249
242
  }
package/index.js CHANGED
@@ -43,6 +43,11 @@ async function factory (pkgName) {
43
43
  count: false,
44
44
  patchEnabled: false
45
45
  },
46
+ control: {
47
+ wdbBtnColumns: {
48
+ menuMax: 10
49
+ }
50
+ },
46
51
  enableRestApiForModel: false
47
52
  }
48
53
  this.methodMap = {
@@ -200,7 +205,7 @@ async function factory (pkgName) {
200
205
  time: options.time ?? { timeZone }
201
206
  }
202
207
  rec[f] = format(data[f], prop.type, opts)
203
- const vf = get(schema, `view.valueFormatter.${f}`)
208
+ const vf = get(schema, `view.formatValue.${f}`)
204
209
  if (vf) {
205
210
  if (isFunction(vf)) rec[f] = await vf.call(this, data[f], data, { req })
206
211
  else rec[f] = await callHandler(vf, { req, value: data[f], data })
@@ -14,8 +14,8 @@ async function addHandler ({ req, reply, model, params = {}, template, addOnsHan
14
14
  delete req.query.query
15
15
  let def = {}
16
16
  if (req.method === 'GET' && req.query.mode === 'clone' && req.query.id) {
17
- const resp = await getRecord({ model, req, id: req.query.id, options: { fields: map(schema.properties, 'name') } })
18
- def = omit(resp.data, ['id', 'createdAt', 'updatedAt'])
17
+ const resp = await getRecord({ model, req, id: req.query.id, options: { fields: map(schema.properties, 'name'), ...opts } })
18
+ def = omit(resp.data._orig, ['id', 'createdAt', 'updatedAt'])
19
19
  }
20
20
  let form = defaultsDeep(req.body, def)
21
21
  let error
@@ -19,6 +19,9 @@ const handler = {
19
19
  async function allHandler ({ model, action, req, reply, template, params = {}, options = {} }) {
20
20
  const { upperFirst, merge, keys } = this.app.lib._
21
21
  if (!keys(handler).includes(action)) throw this.error('_notFound')
22
+ options.modelOpts = options.modelOpts ?? {}
23
+ options.modelOpts.formatValue = true
24
+ options.modelOpts.retainOriginalValue = true
22
25
  if (['delete', 'export'].includes(action)) {
23
26
  if (req.method === 'GET') throw this.error('_notFound')
24
27
  return await handler[action].call(this, { model, req, reply, options })
@@ -7,7 +7,7 @@ async function editHandler ({ req, reply, model, id, params = {}, template, addO
7
7
  const { buildUrl } = this.app.waibuMpa
8
8
  const { fs } = this.app.lib
9
9
  const { defaultsDeep } = this.app.lib.aneka
10
- const { merge, isEmpty, omit } = this.app.lib._
10
+ const { merge, isEmpty, omit, cloneDeep, isArray, isPlainObject } = this.app.lib._
11
11
  const opts = merge({}, options.modelOpts)
12
12
  let error
13
13
  let resp
@@ -22,7 +22,12 @@ async function editHandler ({ req, reply, model, id, params = {}, template, addO
22
22
  delete req.query.query
23
23
  const old = await getRecord({ model, req, id, options: opts })
24
24
  if (isEmpty(old.data)) return await reply.view(notFoundTpl, params)
25
- form = defaultsDeep(req.body, old.data)
25
+ opts._data = old
26
+ const def = cloneDeep(old.data._orig)
27
+ for (const k in def) {
28
+ if (isArray(def[k]) || isPlainObject(def[k])) def[k] = JSON.stringify(def[k])
29
+ }
30
+ form = defaultsDeep(req.body, def)
26
31
  if (req.method !== 'GET') {
27
32
  form = omit(form, ['_action', '_value'])
28
33
  try {
@@ -49,7 +54,7 @@ async function editHandler ({ req, reply, model, id, params = {}, template, addO
49
54
  }
50
55
  const addOns = addOnsHandler ? await addOnsHandler.call(this.app[req.ns], { req, reply, params, data: resp, schema, error, options }) : undefined
51
56
  const attachments = await attachmentHandler.call(this, { schema, id, options })
52
- merge(params, { form, schema, error, addOns, attachments })
57
+ merge(params, { oldData: old.data, form, schema, error, addOns, attachments })
53
58
  if (schema.template) template = schema.template
54
59
  if (schema.layout) params.page.layout = schema.layout
55
60
  return await reply.view(template, params)
@@ -12,8 +12,9 @@ function getCommons (action, schema, ext, options = {}) {
12
12
  const widget = defaultsDeep(get(ext, `view.${action}.widget`), get(ext, 'common.widget', {}))
13
13
  const noEscape = get(ext, `view.${action}.noEscape`, get(ext, 'common.noEscape', []))
14
14
  const control = defaultsDeep(get(ext, `view.${action}.control`), get(ext, 'common.control', {}))
15
- const valueFormatter = defaultsDeep(get(ext, `view.${action}.valueFormatter`), get(ext, 'common.valueFormatter', {}))
16
- const formatter = defaultsDeep(get(ext, `view.${action}.formatter`), get(ext, 'common.formatter', {}))
15
+ const formatValue = defaultsDeep(get(ext, `view.${action}.formatValue`), get(ext, 'common.formatValue', {}))
16
+ const formatCell = defaultsDeep(get(ext, `view.${action}.formatCell`), get(ext, 'common.formatCell', {}))
17
+ const format = defaultsDeep(get(ext, `view.${action}.format`), get(ext, 'common.format', {}))
17
18
  const card = get(ext, `view.${action}.card`, get(ext, 'common.card', true))
18
19
  let hidden = get(ext, `view.${action}.hidden`, get(ext, 'common.hidden', []))
19
20
  const disabled = get(ext, `view.${action}.disabled`, get(ext, 'common.disabled', []))
@@ -30,8 +31,9 @@ function getCommons (action, schema, ext, options = {}) {
30
31
  set(schema, 'view.calcFields', calcFields)
31
32
  set(schema, 'view.noEscape', noEscape)
32
33
  set(schema, 'view.widget', widget)
33
- set(schema, 'view.valueFormatter', valueFormatter)
34
- set(schema, 'view.formatter', merge({}, defFormatter, formatter))
34
+ set(schema, 'view.formatValue', formatValue)
35
+ set(schema, 'view.formatCell', formatCell)
36
+ set(schema, 'view.format', merge({}, defFormatter, format))
35
37
  set(schema, 'view.stat.aggregate', aggregate)
36
38
  set(schema, 'view.disabled', disabled)
37
39
  set(schema, 'view.control', control)
@@ -6,8 +6,10 @@ async function updateRecord ({ model, req, reply, id, body, options = {}, transa
6
6
 
7
7
  async function handler (trx) {
8
8
  if (opts.trx === true) opts.trx = trx
9
- const resp = await getOneRecord.call(me, mdl, recId, filter, opts)
10
- opts._data = resp.data
9
+ if (!opts._data) {
10
+ const resp = await getOneRecord.call(me, mdl, recId, filter, opts)
11
+ opts._data = resp.data
12
+ }
11
13
  const ret = await mdl.updateRecord(recId, input, opts)
12
14
  if (attachment) ret.data._attachment = await mdl.findAttachment(id, { stats, mimeType })
13
15
  return ret
package/lib/util.js CHANGED
@@ -61,7 +61,7 @@ export async function getOneRecord (model, id, filter, options) {
61
61
  const { cloneDeep, pick, isEmpty } = this.app.lib._
62
62
  let query = cloneDeep(filter.query || {})
63
63
  query = { $and: [query, { id }] }
64
- const opts = pick(options, ['forceNoHidden', 'trx', 'req', 'refs'])
64
+ const opts = pick(options, ['forceNoHidden', 'trx', 'req', 'refs', 'formatValue', 'retainOriginalValue'])
65
65
  opts.dataOnly = false
66
66
  const data = await model.findOneRecord({ query }, opts)
67
67
  if (isEmpty(data.data) && options.throwNotFound) throw this.error('_notFound')
@@ -79,7 +79,8 @@ export async function processHandler ({ action, model, handler, options } = {})
79
79
  }
80
80
 
81
81
  try {
82
- return options.trx === true ? (await model.transaction(handler)) : (await handler())
82
+ if (options.trx === true) return await model.transaction(handler)
83
+ return await handler()
83
84
  } catch (err) {
84
85
  if (options.suppressError.includes(action)) return suppressedReturn.call(this, err)
85
86
  throw err
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waibu-db",
3
- "version": "2.14.0",
3
+ "version": "2.16.0",
4
4
  "description": "DB Helper",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-04-13
4
+
5
+ - [2.16.0] Add ```oldData``` propety to ```WdbBase``` widget
6
+ - [2.16.0] Change ```WdbBase.getSetting()``` to also respect setting from ```waibu.getSetting()```
7
+ - [2.16.0] Rewrite ```WdbForm``` widget entirely
8
+ - [2.16.0] Remove redundant call to get old record in ```updateRecord()```
9
+
10
+ ## 2026-04-11
11
+
12
+ - [2.15.0] Add ```control``` key in config object
13
+ - [2.15.0] Bug fix in ```formatRow()```
14
+ - [2.15.0] Add ```WdbBase.getRefName()```
15
+ - [2.15.0] Add ```WdbBase.getSetting()```
16
+ - [2.15.0] Update placeholder in ```WdbQuery``` based on model's ```scanables``` values
17
+ - [2.15.0] ```getSchemaExt()``` now support ```format```, ```formatValue``` and ```formatCell```
18
+ - [2.15.0] All default handlers now support ```options.formatValue``` and ```options.retainOriginalValue````
19
+
3
20
  ## 2026-04-07
4
21
 
5
22
  - [2.14.0] Add ```wdb-lookup-select``` widget
@@ -31,8 +48,8 @@
31
48
 
32
49
  ## 2026-03-27
33
50
 
34
- - [2.12.1] Bug fix in all ```view.formatter``` & ```view.valueFormatter```
35
- - [2.12.2] Bug fix in ```wdb-form``` & ```wdb-table``` widgets, now correctly use value from formatter if provided
51
+ - [2.12.1] Bug fix in all ```view.format``` & ```view.formatValue```
52
+ - [2.12.2] Bug fix in ```wdb-form``` & ```wdb-table``` widgets, now correctly use value from format if provided
36
53
 
37
54
  ## 2026-03-26
38
55