waibu-db 2.14.0 → 2.15.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 (!_.get(schema, 'view.control.wdbBtnBack.disabled')) { %>
4
+ <c:wdb-btn-back href="<%= _.get(schema, 'view.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 (!_.get(schema, 'view.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 (!_.get(schema, 'view.control.wdbBtnEdit.disabled')) { %>
13
+ <c:wdb-btn-edit href="<%= _.get(schema, 'view.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 (!_.get(schema, 'view.control.wdbBtnClone.disabled')) { %>
16
+ <c:wdb-btn-clone href="<%= _.get(schema, 'view.control.wdbBtnClone.href', 'undefined') %>" />
17
17
  <% } %>
18
- <% if (!schema.view.control.noExportBtn) { %>
18
+ <% if (!_.get(schema, 'view.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 (!_.get(schema, 'view.control.wdbBtnDelete.disabled')) { %>
26
+ <c:wdb-btn-delete href="<%= _.get(schema, 'view.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 (!_.get(schema, 'view.control.wdbBtnBack.disabled')) { %>
4
+ <c:wdb-btn-back href="<%= _.get(schema, 'view.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 (!_.get(schema, 'view.control.wdbBtnDetails.disabled')) { %>
8
+ <c:wdb-btn-details href="<%= _.get(schema, 'view.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 (!_.get(schema, 'view.control.wdbBtnClone.disabled')) { %>
11
+ <c:wdb-btn-clone href="<%= _.get(schema, 'view.control.wdbBtnClone.href', 'undefined') %>" />
12
12
  <% } %>
13
- <% if (!schema.view.control.noExportBtn) { %>
13
+ <% if (!_.get(schema, 'view.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 (!_.get(schema, 'view.control.wdbBtnDelete.disabled')) { %>
20
+ <c:wdb-btn-delete href="<%= _.get(schema, 'view.control.wdbBtnDelete.href', 'undefined') %>"/>
21
21
  <% } %>
22
- <% if (!schema.view.control.noResetBtn) { %>
22
+ <% if (!_.get(schema, 'view.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 (!_.get(schema, 'view.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
+ <% } %>
@@ -6,15 +6,15 @@ 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.model = getModel(this.schema.name, true)
10
10
  }
11
11
 
12
12
  getRef = ({ field, refName, returning } = {}) => {
13
13
  const { get } = this.app.lib._
14
- if (!this.schema) return {}
14
+ if (!this.model) return {}
15
15
  const prop = this.model.getProperty(field)
16
16
  if (!prop) return {}
17
- refName = refName ?? this.params.attr['x-ref'] ?? field.slice(0, -2)
17
+ if (!refName && field.endsWith('Id')) refName = field.slice(0, -2)
18
18
  const key = this.params.attr.refName ?? refName
19
19
  const ref = get(prop, `ref.${key}`, {})
20
20
  if (returning === 'all') return { ref, key }
@@ -23,11 +23,27 @@ async function wdbBase () {
23
23
  }
24
24
 
25
25
  getRefValue = ({ field, data, labelField, refName } = {}) => {
26
- const { isEmpty } = this.app.lib._
26
+ const { get, isEmpty } = this.app.lib._
27
27
  const { ref, key } = this.getRef({ field, refName, returning: 'all' })
28
28
  if (isEmpty(ref)) return undefined
29
29
  labelField = labelField ?? ref.labelField ?? 'id'
30
- return (data ?? this.formData)._ref[key][labelField]
30
+ return (get(data ?? this.formData, `_ref.${key}.${labelField}`))
31
+ }
32
+
33
+ getRefName = (field) => {
34
+ const { get } = this.app.lib._
35
+ let refName = get(this.schema, `view.widget.${field}.attr.ref-name`, this.params.attr.refName)
36
+ if (!refName && this.params.attr[field] && this.params.attr[field].endsWith('Id')) refName = this.params.attr[field].slice(0, -2)
37
+ return refName
38
+ }
39
+
40
+ getSetting = (key, defaultValue) => {
41
+ const { req } = this.component
42
+ const { get, camelCase } = this.app.lib._
43
+ const widgetName = camelCase(this.constructor.name)
44
+ key = key.replaceAll('{self}', widgetName)
45
+ const config = req.getSetting(`${this.plugin.ns}:${key}`, defaultValue)
46
+ return get(this.schema, `view.${key}`, config)
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)
@@ -8,19 +8,18 @@ async function form () {
8
8
  const { get, find, filter, forOwn, isEmpty } = this.app.lib._
9
9
  const { base64JsonEncode } = this.app.waibu
10
10
  const { req } = this.component
11
- const schema = get(this, 'component.locals.schema', {})
12
11
  const data = get(this, 'component.locals.form', {})
13
12
  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))
13
+ const xModels = get(this.schema, 'view.x.model', [])
14
+ const xOns = get(this.schema, 'view.x.on', [])
15
+ for (const l of this.schema.view.layout) {
16
+ const fields = filter(l.fields, f => this.schema.view.fields.includes(f))
18
17
  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">`)
18
+ body.push(`<c:fieldset ${this.schema.view.card === false ? '' : 'card'} ${l.name[0] !== '_' ? ('t:legend="' + l.name + '"') : ''} grid-gutter="2">`)
20
19
  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 })
20
+ const w = this.schema.view.widget[f]
21
+ let prop = find(this.schema.properties, { name: f })
22
+ if (!prop) prop = find(this.schema.view.calcFields, { name: f })
24
23
  if (!prop) continue
25
24
  const attr = [`x-ref="${w.name}"`]
26
25
  if (xModels.includes(w.name)) attr.push(`x-model="${w.name}"`)
@@ -36,9 +35,14 @@ async function form () {
36
35
  const attributes = `${w.attr.label ? ('t:label="' + w.attr.label + '"') : ''} label-floating name="${w.name}" ${attr.join(' ')}`
37
36
  if (w.component === 'form-plaintext' || this.params.attr.method !== 'POST') {
38
37
  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}"` : ''} />`)
38
+ let link
39
+ if (this.schema.view.formatValue[f]) value = await this.schema.view.formatValue[f].call(this, data[f], data, { req })
40
+ else if (prop.ref) {
41
+ value = this.getRefValue({ field: f, labelField: w.attr.labelField, refName: this.getRefName(f) })
42
+ const format = get(this.schema, `view.format.${f}`)
43
+ if (format && !isEmpty(value)) link = await format.call(this.model, value, data, { linkOnly: true })
44
+ }
45
+ body.push(`<c:${w.component} ${attributes} data-type="${prop.type}" ${value ? `value="${value}"` : ''} ${link ? `href="${link}"` : ''} />`)
42
46
  } else if (prop.ref) {
43
47
  body.push(`<c:wdb-lookup-select ${attributes} />`)
44
48
  } else {
@@ -49,9 +53,9 @@ async function form () {
49
53
  }
50
54
  const html = await this.component.buildSentence(body, this.component.locals)
51
55
  this.params.html = `${html}\n${this.params.html}`
52
- const xData = get(schema, 'view.x.data', '')
56
+ const xData = get(this.schema, 'view.x.data', '')
53
57
  this.params.attr['x-data'] = isEmpty(xData) ? '' : `{ ${xData} }`
54
- this.params.attr['x-init'] = get(schema, 'view.x.init', '')
58
+ this.params.attr['x-init'] = get(this.schema, 'view.x.init', '')
55
59
 
56
60
  this.params.tag = this.params.attr.tag ?? 'form'
57
61
  }
@@ -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, { 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,31 +21,10 @@ 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
29
  const { get, omit, set, find, isEmpty, without, merge } = this.app.lib._
51
30
  const group = groupAttrs(this.params.attr, ['body', 'head', 'foot'])
@@ -54,7 +33,6 @@ async function table () {
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
@@ -87,7 +65,8 @@ async function table () {
87
65
  let selection
88
66
  const canDelete = !disableds.includes('remove')
89
67
  const canEdit = !disableds.includes('update')
90
- if (canEdit) selection = 'single'
68
+ const canDetails = !disableds.includes('get')
69
+ if (canEdit || canDetails) selection = 'single'
91
70
  if (canDelete) selection = 'multi'
92
71
  if (selection) this.params.attr.hover = true
93
72
 
@@ -141,13 +120,12 @@ async function table () {
141
120
  items = []
142
121
  for (const idx in data) {
143
122
  const d = data[idx]
144
- const fd = fdata[idx]
145
123
  const lines = []
146
124
  if (selection) {
147
125
  const tag = selection === 'single' ? 'formRadio' : 'formCheck'
148
- const attr = { 'x-model': 'selected', name: '_rt', value: d.id, noLabel: true, noWrapper: true }
126
+ const attr = { 'x-model': 'selected', name: '_rt', value: d._orig.id, noLabel: true, noWrapper: true }
149
127
  const type = find(schema.properties, { name: 'id' }).type
150
- const prepend = `<td data-value="${d.id}" data-key="id" data-type="${type}">`
128
+ const prepend = `<td data-value="${d._orig.id}" data-key="id" data-type="${type}">`
151
129
  lines.push(await this.component.buildTag({ tag, attr, prepend, append: '</td>' }))
152
130
  }
153
131
  for (const f of schema.view.fields) {
@@ -155,17 +133,24 @@ async function table () {
155
133
  let prop = find(schema.properties, { name: f })
156
134
  if (!prop) prop = find(schema.view.calcFields, { name: f })
157
135
  if (!prop) continue
158
- let dataValue = d[f] ?? ''
136
+ let dataValue = d._orig[f] ?? ''
159
137
  if (!isEmpty(dataValue)) {
160
138
  if (['datetime'].includes(prop.type)) dataValue = escape(dataValue.toISOString())
161
139
  if (['string', 'text'].includes(prop.type)) dataValue = escape(dataValue)
162
140
  if (['array', 'object'].includes(prop.type)) dataValue = escape(JSON.stringify(d[f]))
163
141
  }
164
- let value = this.getRefValue({ field: f, data: fd }) ?? fd[f]
142
+ const refName = get(schema, `view.widget.${f}.attr.refName`)
143
+ let value = this.getRefValue({ field: f, data: d, refName }) ?? d[f]
144
+ const formatValue = get(schema, `view.formatValue.${f}`)
145
+ if (formatValue) {
146
+ value = await formatValue.call(this, value, d, { params: this.params, req })
147
+ dataValue = value
148
+ }
149
+ if (!get(schema, 'view.noEscape', []).includes(f)) value = escape(value)
165
150
  const attr = { dataValue, dataKey: prop.name, dataType: prop.type }
166
151
  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 }))
152
+ const formatCell = get(schema, `view.formatCell.${f}`)
153
+ if (formatCell) merge(attr, await formatCell.call(this, value, d, { params: this.params, req }))
169
154
  const noWrap = this.isNoWrap(f, schema, group.body.nowrap) ? 'nowrap' : ''
170
155
  if (this.isRightAligned(f, schema)) attr.text = `align:end ${noWrap}`
171
156
  else attr.text = noWrap
@@ -174,15 +159,14 @@ async function table () {
174
159
  const item = find(lookup.values, set({}, lookup.id ?? 'id', d[f]))
175
160
  if (item) value = req.t(item[lookup.field ?? 'name'])
176
161
  }
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 })
162
+ const format = get(schema, `view.format.${f}`)
163
+ if (format) value = await format.call(this, value, d, { params: this.params, req })
180
164
  const line = await this.component.buildTag({ tag: 'td', attr, html: value })
181
165
  lines.push(line)
182
166
  }
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}')`
167
+ const attr = { id: `rec-${d._orig.id}` }
168
+ if (!disableds.includes('update') || !disableds.includes('remove') || !disableds.includes('get')) attr['@click'] = `toggle('${d._orig.id}')`
169
+ if (!disableds.includes('get')) attr['@dblclick'] = `goDetails('${d._orig.id}')`
186
170
  items.push(await this.component.buildTag({ tag: 'tr', attr, html: lines.join('\n') }))
187
171
  }
188
172
  html.push(await this.component.buildTag({ tag: 'tbody', attr: group.body, html: items.join('\n') }))
@@ -235,7 +219,7 @@ async function table () {
235
219
  }`
236
220
  xInit = `
237
221
  ${xInit}
238
- $watch('selected', val => $dispatch('on-selection', [val]))
222
+ $watch('selected', val => $dispatch('on-selection', _.isEmpty(val) ? [] : [val]))
239
223
  `
240
224
  } else {
241
225
  xData = `{
@@ -244,6 +228,7 @@ async function table () {
244
228
  }
245
229
  this.params.attr['x-data'] = xData
246
230
  this.params.attr['x-init'] = xInit
231
+ this.params.attr.responsive = true
247
232
  this.params.html = await this.component.buildTag({ tag: 'table', attr: this.params.attr, html: html.join('\n') })
248
233
  }
249
234
  }
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 })
@@ -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 })
@@ -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)
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')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waibu-db",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "DB Helper",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-04-11
4
+
5
+ - [2.15.0] Add ```control``` key in config object
6
+ - [2.15.0] Bug fix in ```formatRow()```
7
+ - [2.15.0] Add ```WdbBase.getRefName()```
8
+ - [2.15.0] Add ```WdbBase.getSetting()```
9
+ - [2.15.0] Update placeholder in ```WdbQuery``` based on model's ```scanables``` values
10
+ - [2.15.0] ```getSchemaExt()``` now support ```format```, ```formatValue``` and ```formatCell```
11
+ - [2.15.0] All default handlers now support ```options.formatValue``` and ```options.retainOriginalValue````
12
+
3
13
  ## 2026-04-07
4
14
 
5
15
  - [2.14.0] Add ```wdb-lookup-select``` widget
@@ -31,8 +41,8 @@
31
41
 
32
42
  ## 2026-03-27
33
43
 
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
44
+ - [2.12.1] Bug fix in all ```view.format``` & ```view.formatValue```
45
+ - [2.12.2] Bug fix in ```wdb-form``` & ```wdb-table``` widgets, now correctly use value from format if provided
36
46
 
37
47
  ## 2026-03-26
38
48