waibu-db
Version:
247 lines (236 loc) • 12.1 kB
JavaScript
import wdbBase from '../wdb-base.js'
async function table () {
const WdbBase = await wdbBase.call(this)
return class WdbDataTable extends WdbBase {
isRightAligned = (field) => {
const { get, find } = this.app.lib._
const prop = find(this.schema.properties, { name: field })
if (!prop) return false
let value = get(this.schema, 'view.alignEnd', []).includes(field)
if (!value) value = ['smallint', 'integer', 'float', 'double'].includes(prop.type)
return value
}
isNoWrap = (field, bodyNowrap) => {
if (bodyNowrap) return true
const { get } = this.app.lib._
return get(this.schema, 'view.noWrap', []).includes(field)
}
build = async () => {
const { req, buildTag, buildSentence, buildUrl, locals = {} } = this.component
const { escape, attrToArray } = this.app.waibu
const { groupAttrs } = this.app.waibuMpa
const { isHtmlLink } = this.app.bajoExtra
const { getTruncated } = this.app.bajoTemplate
const { get, omit, set, find, isEmpty, without, merge, intersection, isPlainObject } = this.app.lib._
const { isSet } = this.app.lib.aneka
const group = groupAttrs(this.params.attr, ['body', 'head', 'foot'])
this.params.attr = group._
const prettyUrl = this.params.attr.prettyUrl
const hasXSite = get(req, 'routeOptions.config.xSite')
const data = get(locals, 'list.data', [])
const filter = get(locals, 'list.filter', {})
const count = get(locals, 'list.count', 0)
const allowed = await this.isActionAllowed('READ')
if (!allowed) {
const alert = '<c:alert color="error" t:content="accessDenied" margin="top-4"/>'
this.params.noTag = true
this.params.html = await buildSentence(alert)
return
}
if (count === 0 || data.length === 0) {
const alert = '<c:alert color="warning" t:content="noRecordFound" margin="top-4"/>'
this.params.noTag = true
this.params.html = await buildSentence(alert)
return
}
const disableds = get(this.schema, 'view.disabled', [])
if (disableds.includes('find')) {
this.params.html = ''
return
}
const qsKey = this.app.waibu.config.qsKey
let fields = without(get(locals, `_meta.query.${qsKey.fields}`, '').split(','), '')
if (isEmpty(fields)) fields = without(this.schema.view.fields, 'id')
if (data.length > 0) {
fields = intersection(fields, Object.keys(data[0]))
}
let sort = this.params.attr.sort ? attrToArray(this.params.attr.sort) : get(locals, `_meta.query.${qsKey.sort}`, '')
if (isEmpty(sort) && filter.sort) {
const keys = Object.keys(filter.sort)
if (keys.length > 0) sort = `${keys[0]}:${filter.sort[keys[0]]}`
}
let [sortCol, sortDir] = sort.split(':')
if (!['-1', '1'].includes(sortDir)) sortDir = '1'
let selection = this.params.attr.selection
if (!isSet(selection)) {
const canDelete = !disableds.includes('remove') && (await this.isActionAllowed('REMOVE'))
const canEdit = !disableds.includes('update') && (await this.isActionAllowed('UPDATE'))
const canDetails = !disableds.includes('get') && (await this.isActionAllowed('READ'))
if (canEdit || canDetails) selection = 'single'
if (canDelete) selection = 'multi'
if (selection) this.params.attr.hover = true
} else if (!['single', 'multi'].includes(selection)) selection = false
this.params.noTag = true
const html = []
let items = []
// head
for (const f of this.schema.view.fields) {
if (!fields.includes(f)) continue
let prop = find(this.schema.properties, { name: f })
if (!prop) prop = find(this.schema.view.calcFields, { name: f })
if (!prop) continue
let head = req.t(get(this.schema, `view.label.${f}`, `field.${f}`))
if (!this.params.attr.noSort && (this.schema.sortables ?? []).includes(f)) {
let sortItem = `${f}:-1`
let icon = this.params.attr.sortUpIcon ?? 'caretUp'
if (f === sortCol) {
sortItem = `${f}:${sortDir === '1' ? '-1' : '1'}`
icon = sortDir === '1' ? (this.params.attr.sortUpIcon ?? 'caretUp') : (this.params.attr.sortDownIcon ?? 'caretDown')
}
const item = set({ page: 1 }, qsKey.sort, sortItem)
const href = buildUrl({ params: item })
const attr = this.isRightAligned(f, this.schema) ? { text: 'align:end' } : {}
const content = [
await buildTag({ tag: 'div', attr, html: head }),
await buildTag({ tag: 'a', attr: { icon, href, noIconLink: true }, prepend: '<div class="ms-1">', append: '</div>' })
]
head = await buildTag({ tag: 'div', attr: { flex: 'justify-content:between align-items:end' }, html: content.join('\n') })
}
let text = this.params.attr.headerNowrap ? '' : 'nowrap'
if (text === '' && this.isNoWrap(f, group.body.nowrap)) text = 'nowrap'
if (this.isRightAligned(f)) text += ' align:end'
const attr = { dataKey: f, dataType: prop.type, text }
items.push(await buildTag({ tag: 'th', attr, html: head }))
}
if (items.length > 0 && selection) {
let item = '<th></th>'
if (selection === 'multi') {
const attr = { 'x-model': 'toggleAll', name: '_rtm', noWrapper: true, noLabel: true }
item = await buildTag({ tag: 'formCheck', attr, prepend: '<th>', append: '</th>' })
} else {
const attr = { name: 'remove', '@click': 'selected = \'\'' }
if (!disableds.includes('get')) attr.style = { cursor: 'pointer' }
item = await buildTag({ tag: 'icon', attr, prepend: '<th>', append: '</th>' })
}
items.unshift(item)
}
const header = await buildTag({ tag: 'tr', html: items.join('\n') })
html.push(await buildTag({ tag: 'thead', attr: group.head, html: header }))
// body
items = []
for (const idx in data) {
const d = data[idx]
const lines = []
if (selection) {
const tag = selection === 'single' ? 'formRadio' : 'formCheck'
const attr = { 'x-model': 'selected', name: '_rt', value: d.id, noLabel: true, noWrapper: true }
const type = find(this.schema.properties, { name: 'id' }).type
const prepend = `<td data-value="${d.id}" data-key="id" data-type="${type}">`
lines.push(await buildTag({ tag, attr, prepend, append: '</td>' }))
}
for (const f of this.schema.view.fields) {
if (!fields.includes(f)) continue
const prop = find(this.schema.properties, { name: f })
if (!prop) continue
let dataValue = d[f]
if (['datetime'].includes(prop.type) && dataValue instanceof Date && !isNaN(dataValue)) dataValue = escape(dataValue.toISOString())
else if (['string', 'text', 'array', 'object'].includes(prop.type)) dataValue = escape(dataValue)
let value = this.getRefValue({ field: f, data: d, refName: this.getRefName(f) }) ?? get(d, `_fmt.${f}`, d[f])
const attr = { dataValue, dataKey: prop.name, dataType: prop.type }
if (!disableds.includes('get')) attr.style = { cursor: 'pointer' }
const formatCell = get(this.schema, `view.formatCell.${f}`)
if (formatCell) merge(attr, await formatCell.call(this, value, d, { params: this.params, req }))
const noWrap = this.isNoWrap(f, group.body.nowrap) ? 'nowrap' : ''
if (this.isRightAligned(f)) attr.text = `align:end ${noWrap}`
else attr.text = `${noWrap}`
if (!hasXSite && d._immutable && d._immutable.length > 0) {
if (d._immutable[0] === '*' || d._immutable.includes(f)) attr.text += ' color:body-tertiary'
}
if (f === 'id') attr.text += ' color:body-tertiary'
const format = get(this.schema, `view.format.${f}`)
if (format) {
const formatted = await format.call(this, value, d, { params: this.params, req })
if (isPlainObject(formatted) && formatted.href) {
const text = await buildTag({ tag: 'div', attr: {}, html: formatted.value })
const link = await buildTag({ tag: 'a', attr: { text: 'color:white', icon: 'link', href: formatted.href, noIconLink: true } })
const badge = await buildTag({ tag: 'badge', attr: { text: 'background:primary', rounded: 'type:pill' }, html: link, prepend: '<div class="ms-2">', append: '</div>' })
const line = await buildTag({ tag: 'div', attr: { flex: 'justify-content:between align-items:end' }, html: `${text}\n${badge}` })
lines.push(await buildTag({ tag: 'td', attr, html: line }))
continue
}
value = formatted
}
if (['object', 'array'].includes(prop.type) && !isHtmlLink(value)) value = getTruncated(value, 20) // TODO: should be handle by css instead
if (!get(this.schema, 'view.noEscape', []).includes(f) && !isHtmlLink(value)) value = escape(value)
const line = await buildTag({ tag: 'td', attr, html: value })
lines.push(line)
}
const attr = { id: `rec-${d.id}` }
if (!disableds.includes('update') || !disableds.includes('remove') || !disableds.includes('get')) attr['@click'] = `toggle('${d.id}')`
if (!disableds.includes('get')) attr['@dblclick'] = `goDetails('${d.id}')`
items.push(await buildTag({ tag: 'tr', attr, html: lines.join('\n') }))
}
html.push(await buildTag({ tag: 'tbody', attr: group.body, html: items.join('\n') }))
this.params.attr = omit(this.params.attr, ['sortUpIcon', 'sortDownIcon', 'noSort', 'selection', 'headerNowrap'])
let xData = `
goDetails (id) {
let url = '${this.params.attr.detailsHref ?? buildUrl({ base: 'details', prettyUrl })}'
if (url === '#') return
if (url.indexOf('/:id') > -1) url = url.replace('/:id', '/' + id)
else url += '&id=' + id
window.location.href = url
}
`
const xDataView = get(this.schema, 'view.x.data', '')
if (!isEmpty(xDataView)) xData += `, ${xDataView}`
let xInit = ''
const xInitView = get(this.schema, 'view.x.init', '')
if (!isEmpty(xInitView)) xInit += `${xInitView}\n`
if (selection === 'multi') {
xData = `{
${xData},
toggleAll: false,
selected: [],
toggle (id) {
if (this.selected.includes(id)) {
const idx = this.selected.indexOf(id)
this.selected.splice(idx, 1)
} else this.selected.push(id)
}
}`
xInit = `
${xInit}
$watch('toggleAll', val => {
if (val) {
const els = document.getElementsByName('_rt')
const items = Array.from(els)
selected = items.map(el => el.value)
} else selected = []
})
$watch('selected', val => $dispatch('on-selection', val))
`
} else if (selection === 'single') {
xData = `{
${xData},
selected: '',
toggle (id) {
this.selected = id
}
}`
xInit = `
${xInit}
$watch('selected', val => $dispatch('on-selection', _.isEmpty(val) ? [] : [val]))
`
} else {
xData = `{
${xData}
}`
}
this.params.attr['x-data'] = xData
this.params.attr['x-init'] = xInit
this.params.attr.responsive = true
this.params.html = await buildTag({ tag: 'table', attr: this.params.attr, html: html.join('\n') })
}
}
}
export default table