6 Commits

3 arquivos alterados com 105 adições e 62 exclusões
Visão dividida
  1. +21
    -17
      src/components/Table.js
  2. +10
    -1
      src/postgrest.js
  3. +74
    -44
      src/util/make_model.js

+ 21
- 17
src/components/Table.js Ver arquivo

@@ -87,10 +87,11 @@ export default initial_vnode => {
model.select(offset, limit)
}

return [
m('table', {
style: vnode.attrs.show ? undefined : {display: 'none'},
}, [
return m('main', {
style: vnode.attrs.show ? undefined : {display: 'none'}
}, [
// the table
m('table', [
// always show table header
m('thead', m('tr', [
vnode.attrs.serial ? m('th.centered', '序号') : undefined,
@@ -120,21 +121,24 @@ export default initial_vnode => {

return m(`td${column.class || ''}`, {style: column.style || undefined}, v)
})
]))) : m('', 'Empty')
]))) : undefined
]),

// page navigation
pages.length > 1 && vnode.attrs.show ?
m('.centered',
m('.pagination.centered', pages.map((page, i) => page.show ? m('a', {
class: page.active ? 'active' : '',
onclick: e => {
offset = i*limit
model.select(offset, limit)
}
}, i+1) : undefined))
) : undefined
]
// status area
model.data(offset, limit).length ? undefined : (
model.loading() ? (vnode.attrs.loading_indicator || m('', 'Loading')) : (vnode.attrs.empty_indicator || m('', 'Empty'))
),

// the page navigation
(pages.length > 1) ? m('.centered', m('.pagination.centered', pages.map(
(page, i) => page.show ? m('a', {
class: page.active ? 'active' : '',
onclick: e => {
offset = i*limit
model.select(offset, limit)
}
}, i+1) : undefined))) : undefined
])
}
}
}

+ 10
- 1
src/postgrest.js Ver arquivo

@@ -110,11 +110,20 @@ export default (api_root, auth={}) => {

// 2. return values on Prefer
// if (['POST', 'PATCH'].includes(options.method) && options.headers && !options.headers.Prefer)
if (['POST', 'PATCH'].includes(options.method))
if (['POST', 'PATCH'].includes(options.method)) {
// if the config is already set
let _old_config = options.config

options.config = xhr => {
// apply the provided xhr config
let _old_config_xhr = _old_config(xhr)
if (_old_config_xhr)
xhr = _old_config_xhr

xhr.setRequestHeader('Prefer', 'return=representation')
return xhr
}
}

// normalize params: since options.params is an object, it cannot support
// duplicated query keys, while postgrest supports that.


+ 74
- 44
src/util/make_model.js Ver arquivo

@@ -37,20 +37,25 @@ export default (options={}) => {

// private model cache & meta-info
let _cache = {
// upstream (data) cache
// upstream data cache
data: null,
count: null,
upstream_limit: null,
upstream_limit: null
}

// local cache
// model state
let _state = {
// model ambient that determines the data content
ambient: null,
last_ambient: null,
// async states
xhr: null,
promise: null,
loading: false,
loading_offset: null,
loading_limit: null
}

// some random variables to make things work
let _xhr = null
let _promise = null

// construct the model
let _model = {
// reflection methods
@@ -58,24 +63,34 @@ export default (options={}) => {
cache() { return _cache },
data(offset=0, limit=Infinity) { return _cache.data && _cache.data.slice(offset, offset+limit) || [] },
ambient_changed() {
return _cache.ambient != _cache.last_ambient
return _state.ambient != _state.last_ambient
},
loading() {
return _state.loading
},
reset() {
let ambient_queries = [

// clean state and cache if necessary
clean() {
// always clean async state
_state.xhr = null
_state.promise = null
_state.loading = false
_state.loading_offset = null
_state.loading_limit = null

// update ambient state
_state.last_ambient = _state.ambient
_state.ambient =JSON.stringify([
...(_configs.selects ? _configs.selects : []),
...(_configs.wheres ? _configs.wheres.map(where => ({label: where.label, op: where.op, value: typeof where.value == 'function' ? where.value() : where.value})) : []),
...(_configs.order ? _configs.order : [])
]

_cache.last_ambient = _cache.ambient
_cache.ambient =JSON.stringify(ambient_queries)
])

// clean cache if ambient changes
if (this.ambient_changed()) {
_cache.data = []
_cache.count = null
_cache.upstream_limit = null
_xhr = null
//this.fully_loaded = false
}
},

@@ -89,24 +104,30 @@ export default (options={}) => {
if (this.data(offset, limit).length > 0 && !this.data(offset, limit).includes(undefined))
return Promise.resolve(this.data(offset, limit))

// be lazy 2: if there is a promise, return it if ambient matches or
// cancel it
if (_promise != null)
return _promise
// be lazy 2: if the model is loading the same slice of data with the
// same ambient, return the current promise; otherwise cancel it
if (this.loading()) {
if (!this.ambient_changed() && _state.loading_offset == offset && _state.loading_limit == limit)
return _state.promise
else {
if (_state.xhr != null) _state.xhr.abort()
_state.xhr = null
}
}

// TODO
// ambient = select + order + limit/offset
// if ambient is changed, cancel currently loading xhr; otherwise return
// current promise
// update state
_state.loading = true
_state.loading_offset = offset
_state.loading_limit = limit

// now the hard work
_promise = _configs.api.request({
// construct the promise
_state.promise = _configs.api.request({
method: 'GET',
url: _configs.endpoint,
headers: _cache.count == null ? {
Prefer: 'count=exact'
} : {},
config: xhr => _xhr = xhr,
config: xhr => _state.xhr = xhr,
queries: [
// transform model state to postgest queries
// selects
@@ -135,7 +156,7 @@ export default (options={}) => {
]
}).then(response => {
// gather begin/end/count
let [_range, _count] = _xhr.getResponseHeader('content-range').split('/')
let [_range, _count] = _state.xhr.getResponseHeader('content-range').split('/')
let [_begin, _end] = _range.split('-').map(v => ~~v)

// update count if presented
@@ -161,15 +182,13 @@ export default (options={}) => {
if (_end - _begin + 1 < limit)
console.warn('The response range is narrower than requested, probably due to an upstream hard limit.')

// clean model state
_promise = null
this.reset()

// return data
return _cache.data.slice(_begin, _end + 1)
}).finally(() => {
this.clean()
})

return _promise
return _state.promise
},

select_all() {
@@ -177,14 +196,27 @@ export default (options={}) => {
if (this.data().length == _cache.count && !this.data().includes(undefined))
return Promise.resolve(this.data())

// be lazy 2: if there is a promise, return it if ambient matches or
// cancel it
if (_promise != null)
return _promise
// be lazy 2: if the model is loading the same slice of data with the
// same ambient, return the current promise; otherwise cancel it
if (this.loading()) {
if (!this.ambient_changed() && _state.loading_offset == offset && _state.loading_limit == limit)
return _state.promise
else {
if (_state.xhr != null) _state.xhr.abort()
_state.xhr = null
}
}

// update state
_state.loading = true
_state.loading_offset = 0
_state.loading_limit = Infinity

_promise = _configs.api.request({
// construct the promise
_state.promise = _configs.api.request({
method: 'POST',
url: '/rpc/select_all',
config: xhr => _state.xhr = xhr,
body: {
endpoint: _configs.endpoint,
selects: _configs.selects || [],
@@ -201,13 +233,11 @@ export default (options={}) => {

return data
})

// clean state
_promise = null
this.reset()
}).finally(() => {
this.clean()
})

return _promise
return _state.promise
},

export(options={}) {
@@ -268,6 +298,6 @@ export default (options={}) => {
}

// initialize model
_model.reset()
_model.clean()
return _model
}

Carregando…
Cancelar
Salvar