Commit 03ee211b authored by Tejesh's avatar Tejesh 🖖
Browse files

merge dev branch

parent b8eafd10
Pipeline #52885 passed with stage
in 3 minutes and 10 seconds
# Change log
- `0.8.2`: 30 Jun 2018
- [$.urlfilter](#urlfilter) works on forms, inputs & sliders (not just links)
- [$.formhandler()](#formhandler) accepts JavaScript data objects (instead of just a URL) as source
- `0.8.1`: 21 Jun 2018
- [g1.mapviewer](#g1-mapviewer) supports drilldown and color schemes via `scheme:`
- `0.8.0`: 31 May 2018
......
......@@ -358,13 +358,7 @@ Rules:
- Else -> `string`
g1.datafilter(data, {
'sales>': ['100'],
'city': ['London', 'NJ'],
'product': ['Fan']
})
`g1.datafilter(data, filters)` returns the filtered data based on the filters. While urlfilter on [$.formhandler](#formhandler) applies filtering on data server side, `datafilter` applies urlfilter on frontend loaded data.
## g1.datafilter
`g1.datafilter(data, filters)` returns the filtered data based on the filters. While urlfilter on [$.formhandler](#formhandler) applies filtering on data server side, `datafilter` applies urlfilter on frontend loaded data.
......@@ -504,6 +498,7 @@ In the above example, `data-page-size="10"` over-rides `pageSize: 20`.
The full list of options is below. Simple options can be specified as `data-` attributes as well.
- `src`: [FormHandler][formhandler] URL endpoint
- `data`: Array of objects. Dataset for formhandler table. If both `src` and `data` are provided, `data` takes priority.
- `namespace`: (Optional) If the URL has `?name:key=value`, the filter
`key=value` only applies to formhandlers with namespace as `name`.
Filters without a namespace like `?key=value` will apply to all formhandlers.
......@@ -610,6 +605,8 @@ Features to be implemented:
- `args`: the URL query parameters passed to the request
- `options`: applied options to the FormHandler
Note: Make sure `load` event listener is attached before calling `$.formhandler()`
### $.formhandler examples
Render a table using the FormHandler at `./data`:
......@@ -1239,7 +1236,7 @@ Drilldown feature example:
- `drilldown`:
- `rootLayer`: `geojson/topojson` layer that acts as root layer to drilldown further.
- `levels`: Array of objects that provides layer info
- `layerName`: Can be a string or function. Function takes argument as `properties` of parentLayer feature
- `layerName`: Can be a string or function. Function takes argument as `properties` of parentLayer feature
- `layerOptions`: Same as layer options in `layers` option. If `url` is function, `url` takes argument as `properties` of parentLayer feature
- `zoomHandler`: <!-- TODO -->
- `zoomlevel`: must be a `geojson` layer name or function. If given a `geojson` layer name, shows this layer and hides all other `geojson` layers. Optimize this to load only layer that is visible in viewport.
......
{
"name": "g1",
"version": "0.8.1",
"version": "0.8.2",
"description": "Gramex 1.x interaction library",
"license": "UNLICENSED",
"author": "S Anand <s.anand@gramener.com>",
......
import { namespace } from './namespace_util.js'
import { types } from './types.js'
function isEqual(value, compare_with, criteria_satisfied) {
// to handle: ( ...Shape!&... ) or ( ...&Shape&... )
......@@ -11,6 +12,8 @@ function isEqual(value, compare_with, criteria_satisfied) {
function greater_than(value, compare_with, include_equals) {
if ((isNaN(compare_with) && Date.parse(compare_with))) {
compare_with = Date.parse(compare_with)
}
if ((isNaN(value) && Date.parse(value))) {
value = Date.parse(value)
}
return include_equals ? (compare_with >= value) : (compare_with > value)
......@@ -71,6 +74,7 @@ function clone_pluck(source, include_keys, exclude_keys) {
}
export function datafilter(data, filters, dataset_name) {
filters = filters || []
......@@ -80,6 +84,8 @@ export function datafilter(data, filters, dataset_name) {
// url namespace sanitize
filters = namespace(filters, dataset_name)
var data_types = types(data, { convert: true })
// apply WHERE clause
for (var key in filters) {
if (key[0] == '_') continue
......@@ -88,6 +94,13 @@ export function datafilter(data, filters, dataset_name) {
value = (filters[key][0] != "") ? filters[key] : null
var col = key.slice(0, operator_index)
if (data_types[col] == 'number') {
value = value.map(function(val) { return parseFloat(val)})
} else if (data_types[col] == 'boolean') {
value = value.map(function(val){ return String(val) == 'true' ? true : false})
} else if (data_types[col] == 'date') {
value = value.map(function(val) { return Date.parse(val) })
}
result = result.filter(function (row) {
return (typeof row[col] != 'undefined') ? operators[operator](value, row[col]) : true
......
......@@ -4,6 +4,7 @@
import * as default_templates from './formhandler.template.html'
import { parse } from './url.js'
import { namespace } from './namespace_util.js'
import { datafilter } from './datafilter.js'
// Render components in this order. The empty component is the root component.
var components = ['', 'table', 'edit', 'add', 'page', 'size', 'count', 'export', 'filters', 'error', 'table_grid']
......@@ -128,9 +129,10 @@ export function formhandler(js_options) {
_.each(components, function (name) {
compile_template(name, template_data, options, $this, template)
})
addHandler($this, template_data, options, template)
editHandler($this, template_data, options, template)
if (options.add)
addHandler($this, template_data, options, template)
if (options.edit)
editHandler($this, template_data, options, template)
}
function render() {
......@@ -145,17 +147,10 @@ export function formhandler(js_options) {
}, url_args)
$('.loader', $this).removeClass('d-none')
$.ajax(options.src, {
dataType: 'json',
data: args,
traditional: true,
complete: function () {
$('.loader', $this).addClass('d-none')
}
}).done(function (data, status, xhr) {
function done(data, status, xhr) {
var meta = {}
_.each(meta_headers, function (header) {
var val = xhr.getResponseHeader('Fh-Data-' + header)
var val = xhr ? xhr.getResponseHeader('Fh-Data-' + header) : null
if (val !== null)
meta[header] = JSON.parse(val)
})
......@@ -164,13 +159,37 @@ export function formhandler(js_options) {
data = 'data' in result ? result.data : data
meta = 'meta' in result ? result.meta : meta
}
// To support data-src that doesn't poin to formhandler url pattern
if (_.isEmpty(meta) && options.page && options.size) {
meta['offset'] = args._offset ? parseInt(args._offset) : 0
meta['limit'] = parseInt(args._limit)
meta['count'] = data.length
data = datafilter(data, args)
}
draw_table(data, args, meta)
$this.trigger({ type: 'load', formdata: data, meta: meta, args: args, options: options })
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
})
}
if (options.data && typeof(options.data) == 'object') {
options.edit = false
options.add = false
done(options.data)
}
else {
$.ajax(options.src, {
dataType: 'json',
data: args,
traditional: true
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
}).always(function () {
$('.loader', $this).addClass('d-none')
}).done(done)
}
}
modalHandler($this)
......@@ -256,17 +275,13 @@ function addHandler($this, template_data, options, template) {
method: 'POST',
dataType: 'json',
data: data,
complete: options.add.addFunction ? options.add.addFunction() : function () {
// Hide loader
$('.loader', $this).addClass('d-none')
}
}).done(function () {
// TODO: show a bootstrap 4 success/failure message
$('.loader', $this).addClass('d-none')
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
}).always(function () {
if (options.add.addFunction) options.add.addFunction()
$('.loader', $this).addClass('d-none')
})
// update the current view with out re-render the table
......@@ -312,22 +327,20 @@ function editHandler($this, template_data, options, template) {
$.ajax(options.src, {
method: 'PUT',
dataType: 'json',
data: data,
complete: options.edit.editFunction ? options.edit.editFunction() : function () {
$('.loader', $this).addClass('d-none')
}
}).done(function () {
$('.loader', $this).addClass('d-none')
data: data
}).fail(function (xhr, status, message) {
$this.html(template['error']({
message: message
}))
}).always(function () {
if (options.add.editFunction) options.add.editFunction()
$('.loader', $this).addClass('d-none')
})
})
compile_template('table', Object.assign({}, template_data, { isEdit: false }), options, $this, template)
} else if (edit_btn.html().toLowerCase() == 'edit') {
edit_btn.html('Save') // TODO: remove hardcoding of name Save
edit_btn.html('Save') // TODO: remove hardcoding of name Save
compile_template('table', Object.assign({}, template_data, { isEdit: true }), options, $this, template)
$('table tbody input', $this).on('click', function () {
// Attach a class to this row
......@@ -369,5 +382,4 @@ function actionHandler($this, options, template) {
default_action_callback[action](row, rowNo)
}
})
}
......@@ -66,7 +66,6 @@ Each template receives these variables:
cols = cols.filter(function(col) { return col.hide !== true})
var form_id = idcount
%>
<!-- TODO: Abinesh to not override header row in table -->
<table class="table table-sm table-striped">
<thead>
......@@ -184,7 +183,6 @@ Each template receives these variables:
</li>
<% if (lo > 1) { %>
<li class="page-item">
<!-- TODO: Review Shouldn't this be _offset=0 ? -->
<a class="page-link" href="?_offset=">1</a>
</li>
<% if (lo > 2) { %>
......
This diff is collapsed.
......@@ -11,7 +11,7 @@ const port = process.argv.length <= 2 ? 1112 : 1111
const app = express()
.use(express.static(path.resolve(__dirname, '..')))
var formhandler_json_data = JSON.parse(fs.readFileSync('./test/formhandler.json', { encoding: "utf8" }));
var formhandler_json_data = JSON.parse(fs.readFileSync('./test/formhandler_csv.json', { encoding: "utf8" }));
// const router = express.Router()
app.use('/formhandler-data', router.get('/', function (req, res, next) {
......
......@@ -6,12 +6,12 @@ const sales_data = require('./sales-edit.json')
const sales_data_with_date = require('./sales.json')
const product_sales = [
{"ID": "1", "product": "Fan", "sales": "100", "date": "06-10-2018", "city": "NY"},
{"ID": "2", "product": "Fan", "sales": "80", "date": "06-05-2018", "city": "London"},
{"ID": "3", "product": "Fan", "sales": "120", "date": "06-04-2018", "city": "NJ"},
{"ID": "4", "product": "Fan", "sales": "130", "date": "05-15-2018", "city": "London"},
{"ID": "5", "product": "Light", "sales": "500", "date": "06-07-2016", "city": "NY"},
{"ID": "5", "product": "Light", "sales": "100", "date": "06-10-2019", "city": "London"}
{"ID": 1, "product": "Fan", "sales": true, "date": Date.parse("06-10-2018"), "city": "NY"},
{"ID": 2, "product": "Fan", "sales": false, "date": Date.parse("06-05-2018"), "city": "London"},
{"ID": 3, "product": "Fan", "sales": true, "date": Date.parse("06-04-2018"), "city": "NJ"},
{"ID": 4, "product": "Fan", "sales": false, "date": Date.parse("05-15-2018"), "city": "London"},
{"ID": 5, "product": "Light", "sales": true, "date": Date.parse("06-07-2016"), "city": "NY"},
{"ID": 5, "product": "Light", "sales": false, "date": Date.parse("06-10-2019"), "city": "London"}
]
function clone_pluck(source, include_keys, exclude_keys) {
......@@ -42,6 +42,34 @@ test('g1.datafilter test', function(t) {
t.end()
})
t.test('g1.datafilter([data], filter) with one conditions (=) works for numbers', function (t) {
var result = g1.datafilter(continent_data, { "c1": ["50"] })
var length_of_valid_data = 0
for (var row in continent_data) {
if (continent_data[row]["c1"] == 50) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
return val["c1"] == 50
}))
t.end()
})
t.test('g1.datafilter([data], filter) with one conditions (=) works for boolean types', function (t) {
var result = g1.datafilter(product_sales, { "sales": ["true"] })
var length_of_valid_data = 0
for (var row in product_sales) {
if (product_sales[row]["sales"] == true) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
return val["sales"] == true
}))
t.end()
})
t.test('g1.datafilter([data], filter) with one conditions (!) works', function (t) {
var result = g1.datafilter(continent_data, { "ID!": ["AND"] })
var length_of_valid_data = 0
......@@ -135,6 +163,33 @@ test('g1.datafilter test', function(t) {
t.end()
})
t.test('g1.datafilter([data], filter) with one conditions (< for date value) works', function (t) {
var result = g1.datafilter(sales_data_with_date, { "date<": ["1-10-2018"] })
var length_of_valid_data = 0
for (var row in sales_data_with_date) {
if (Date.parse(sales_data_with_date[row]["date"]) < Date.parse("1-10-2018")) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
return Date.parse(val["date"]) < Date.parse("1-10-2018")
}))
t.end()
})
t.test('g1.datafilter([data], filter) with more than one = condition works', function (t) {
var result = g1.datafilter(continent_data, { "c1": ["24", "28", "35"] })
var length_of_valid_data = 0
for (var row in continent_data) {
var val = continent_data[row]["c1"]
if (val == 24 || val == 28 || val == 35) length_of_valid_data++
}
t.equals(result.length, length_of_valid_data)
t.ok(result.every(function (val) {
return (val["c1"] == 24 || val["c1"] == 28 || val["c1"] == 35)
}))
t.end()
})
t.test('g1.datafilter([data], filter) with more than one conditions (>= and <=) works', function (t) {
var result = g1.datafilter(continent_data, { "c1>": ["10"], "c1<": ["30"] })
var length_of_valid_data = 0
......
<!DOCTYPE html>
<html>
<head>
<title>formhandler tests</title>
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="../node_modules/font-awesome/css/font-awesome.min.css">
<style>
.position-relative {
position: relative;
}
.pos-cc {
position: absolute;
top: 45%;
left: 45%;
}
.d-none {
display: none !important;
}
</style>
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
<script src="../node_modules/popper.js/dist/umd/popper.min.js"></script>
<script src="../node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="../node_modules/lodash/lodash.min.js"></script>
<script src="../node_modules/moment/min/moment.min.js"></script>
<script src="../node_modules/numeral/min/numeral.min.js"></script>
<script src="../node_modules/d3/build/d3.js"></script>
<script src="../node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.js"></script>
<script src="../dist/formhandler.min.js"></script>
<script src="tape.js"></script>
</head>
<body>
<script>
tape.onFinish(function () { window.renderComplete = true })
</script>
<div class="external-api"></div>
<div class="external-api2"></div>
<script>
$.ajax('/formhandler-data').done(function(response) {
tape('$().formhandler() datafilter external api test', function(test) {
$('.external-api')
.on('load', function () {
test.equal($('.external-api .count').text().trim(), '200 rows')
test.ok($('.external-api ul.pagination .page-item:nth-of-type(2)').has("disabled"))
test.ok($('.external-api ul.pagination .page-item:nth-of-type(2)').hasClass("active"))
test.equal($('.external-api ul.pagination .page-item:nth-of-type(3) a').attr('href'), '?_offset=' + 100 , 'fdsa')
test.equal($('.external-api ul.pagination .page-item:nth-of-type(4) a').attr('href'), '?_offset=' + 100 , 'fdsa3')
test.equal($('.external-api table tbody tr').length, 100)
test.equal(g1.url.parse(location.href).hash, '')
test.equal($('.external-api table tbody tr').length, 100)
// test edit: option is not valid for external api
test.equal($('.external-api .edit').children().length, 0)
// TODO: write test case when filtering with boolean/number values. No results return
// Fix it and make sure it returns results
//
test.end()
})
.formhandler({
data: response,
edit: true,
pagesize: 100,
columns: [
{
name: '*'
}
]
})
})
tape('$().formhandler() datafilter external api test with options & data overrides src', function (test) {
$('.external-api2')
.on('load', function () {
test.equal($('.external-api2 .count').text().trim(), '200 rows')
test.ok($('.external-api2 ul.pagination .page-item:nth-of-type(2)').has("disabled"))
test.ok($('.external-api2 ul.pagination .page-item:nth-of-type(2)').hasClass("active"))
test.equal($('.external-api2 ul.pagination').children().length, 7)
test.equal($('.external-api2 table tbody tr').length, 10)
// test edit: option is not valid for external api
test.equal($('.external-api2 .edit').children().length, 0)
test.end()
})
.formhandler({
data: response,
src: './formhandler.json',
edit: true,
pageSize: 10
})
})
})
</script>
</body>
</html>
......@@ -37,6 +37,18 @@
<script>
tape.onFinish(function () { window.renderComplete = true })
</script>
<div class="hide_col3" data-src="/test/formhandler.json"></div>
<div class="hide_col2" data-src="/test/formhandler.json"></div>
<div class="hide_col" data-src="/test/formhandler.json"></div>
<div class="cell_template1" data-src="/test/formhandler.json"></div>
<div class="cell_template2" data-src="/test/formhandler.json"></div>
<div class="test_star_overrride" data-src="/test/formhandler.json"></div>
<div class="test-star-without-title" data-src="/test/formhandler.json"></div>
<div class="test-star-with-title" data-src="/test/formhandler.json"></div>
<div class="test_star_overrride" data-src="/formhandler-data"></div>
<div class="test-star-without-title" data-src="/formhandler-data"></div>
<div class="test-star-with-title" data-src="/formhandler-data"></div>
......@@ -113,6 +125,8 @@
})
})
tape('$().formhandler() hide options doesnot display the column', function(t) {
var class_count = 2
t.plan(1 * class_count)
$('.hide_col')
.formhandler({
columns: [
......@@ -124,10 +138,12 @@
})
.on('load', function () {
t.equals($('body > div.hide_col > div.position-relative > div.formhandler > div.table > table > thead').text().trim(), "")
t.end()
})
})
tape('$().formhandler() hide options doesnot display the column (using multiple columns)', function(t) {
var class_count = 2
var current_class_count = 0
t.plan(2 * class_count)
$('.hide_col2')
.formhandler({
columns: [
......@@ -142,12 +158,15 @@
pageSize: 3
})
.on('load', function () {
t.equals($("body > div.hide_col2 > div.position-relative > div.formhandler > div.table > table > thead > tr").length, 1)
current_class_count += 1
t.equals($("body > div.hide_col2 > div.position-relative > div.formhandler > div.table > table > thead > tr").length, current_class_count)
t.ok($("body > div.hide_col2 > div.position-relative > div.formhandler > div.table > table > thead > tr").text().startsWith(" c1"))
t.end()
})
})
tape('$().formhandler() hide options doesnot display the column (using * all columns)', function(t) {
var class_count = 1
var current_class_count = 0
t.plan(2* class_count)
$('.hide_col3')
.formhandler({
columns: [
......@@ -165,9 +184,11 @@
pageSize: 3
})
.on('load', function () {
t.equals($('[data-col="Continent"]', $("body > div.hide_col3 > div.position-relative > div.formhandler > div.table > table > thead > tr")).length, 0)
t.ok($('[data-col]', $("body > div.hide_col3 > div.position-relative > div.formhandler > div.table > table > thead > tr")).length > 0)
t.end()
current_class_count += 1
if(current_class_count == 2) {
t.equals($('[data-col="Continent"]', $("body > div.hide_col3 > div.position-relative > div.formhandler > div.table > table > thead > tr")).length, 0)
t.ok($('[data-col]', $("body > div.hide_col3 > div.position-relative > div.formhandler > div.table > table > thead > tr")).length > 0)
}
})
})
tape('$().formhandler() uses option.src if no data-src exists', function (t) {
......@@ -262,6 +283,9 @@
})
})
tape('$().formhandler() allows updating column info only for specific columns override test', function (t) {
var class_count = 2
var current_class_count = 0
t.plan(6 * class_count)
$('.test_star_overrride')
.formhandler({
columns: [
......@@ -270,17 +294,18 @@
]
})
.on('load', function () {
current_class_count += 1
// no. of columns in meta data must be same as rendered columns
t.equal($('.test_star_overrride thead tr th').length, 17)
t.equal($('.test_star_overrride thead tr th').length, 17 * current_class_count)
// there should not be a column with name 'Continent'
t.notOk($('.test_star_overrride thead tr').text().match(/Continent/))
// there shoudl be a column with name 'CustomTitle'
t.ok($('.test_star_overrride thead tr').text().match(/CustomTitle/))
t.equal($('.test_star_overrride thead tr').text().match(/CustomTitle/).length, 1)
// Rest of 16 columns must have title as 'SameName'
t.equal($('.test_star_overrride thead tr').text().split('SameName').length - 1, 16)
t.equal($('.test_star_overrride thead tr').text().split('SameName').length - 1, 16 * current_class_count)
// column is hideable by default
t.equal($('.test_star_overrride [data-col="Continent"] div:nth-child(1) div a:last-of-type').text().trim(), "Hide")
t.end()
t.equal($('.test_star_overrride [data-col="Continent"] div:nth-child(1) div a:last-of-type').text().trim(), "Hide".repeat(current_class_count))
})
})
tape('$().formhandler() applies {sort: false} option mentioned in options.columns', function (t) {
......@@ -583,6 +608,9 @@
})
tape('$().formhandler() renders template with formatted cell contents', function (t) {
var class_count = 2
var current_class_count = 0
t.plan(3 * class_count)
$('.cell_template1').formhandler({
columns: [
{
......@@ -598,14 +626,17 @@
],
pageSize: 3
}).on('load', function (e) {
t.equal($('.cell_template1 tr:first-of-type td:nth-of-type(1)').text().trim(), 'ID and')
t.equal($('.cell_template1 tr:nth-of-type(2) td:nth-of-type(1)').text().trim(), 'ID are')
t.equal($('.cell_template1 tr:nth-of-type(3) td:nth-of-type(1)').text().trim(), 'ID afg')
t.end()
current_class_count += 1
t.equal($('.cell_template1 tr:first-of-type td:nth-of-type(1)').text().trim(), 'ID and'.repeat(current_class_count))
t.equal($('.cell_template1 tr:nth-of-type(2) td:nth-of-type(1)').text().trim(), 'ID are'.repeat(current_class_count))
t.equal($('.cell_template1 tr:nth-of-type(3) td:nth-of-type(1)').text().trim(), 'ID afg'.repeat(current_class_count))
})
})