diff --git a/SQLiteDB/LibSQLite.ts b/SQLiteDB/LibSQLite.ts index 1c8a7f6..d7b4c04 100644 --- a/SQLiteDB/LibSQLite.ts +++ b/SQLiteDB/LibSQLite.ts @@ -1,259 +1,6 @@ namespace OS { export namespace API { - /** - * Generate SQL expression from input object - * - * Example of input object - * ```ts - * { - * where: { - * id$gte: 10, - * user: "dany'", - * $or: { - * 'user.email': "test@mail.com", - * age$lte: 30, - * $and: { - * 'user.birth$ne': 1986, - * age$not_between: [20,30], - * name$not_like: "%LE" - * } - * } - * }, - * fields: ['name as n', 'id', 'email'], - * order: ['user.name$asc', "id$desc"], - * joins: { - * cid: 'Category.id', - * did: 'Country.id' - * } - *} - * ``` - * This will generate the followings expressions: - * - `( self.name as n,self.id,self.email )` for fields - * - condition: - * ``` - * ( - * ( self.id >= 10 ) AND - * ( self.user = 'dany''' ) AND - * ( - * ( user.email = 'test@mail.com' ) OR - * ( self.age <= 30 ) OR - * ( - * ( user.birth != 1986 ) AND - * ( self.age NOT BETWEEN 20 AND 30 ) AND - * ( self.name NOT LIKE '%LE' ) - * ) - * ) - * ) - * ``` - * - order: `user.name ASC,self.id DESC` - * - joining: - * ``` - * INNER JOIN Category ON self.cid = Category.id - * INNER JOIN Country ON self.did = Country.id - * ``` - * - */ - class SQLiteQueryGenerator { - private _where: string; - private _fields: string; - private _order: string; - private _joins: string; - private _is_joining: boolean; - constructor(obj: GenericObject) - { - this._where = undefined; - this._fields = undefined; - this._order = undefined; - this._joins = undefined; - this._is_joining = false; - if(obj.joins) - { - this._is_joining = true; - this._joins = this.joins(obj.joins); - } - - if(obj.where) - { - this._where = this.where("$and", obj.where); - } - if(obj.fields) - { - this._fields = `( ${obj.fields.map(v=>this.infer_field(v)).join(",")} )`; - } - if(obj.order) - { - this._order = this.order_by(obj.order); - } - } - - private infer_field(k: string) : string - { - if(!this._is_joining || k.indexOf(".") > 0) return k; - return `self.${k}`; - } - - private joins(data: GenericObject): string - { - let joins_arr = []; - for(let k in data) - { - let v = data[k]; - let arr = v.split('.') - if(arr.length != 2) - { - throw new Error(__("Other table name parsing error: {0}", v).__()); - } - joins_arr.push(`INNER JOIN ${arr[0]} ON ${this.infer_field(k)} = ${v}`); - } - return joins_arr.join(" "); - } - - print() - { - console.log(this._fields); - console.log(this._where); - console.log(this._order); - console.log(this._joins); - } - - private order_by(order: string[]): string - { - if(! Array.isArray(order)) - { - throw new Error(__("Invalid type: expect array get {0}", typeof(order)).__()); - - } - return order.map((v,_) => { - const arr = v.split('$'); - if(arr.length != 2) - { - throw new Error(__("Invalid field order format {0}", v).__()); - } - switch(arr[1]) - { - case 'asc': return `${this.infer_field(arr[0])} ASC`; - case 'desc': return `${this.infer_field(arr[0])} DESC`; - default: throw new Error(__("Invalid field order type {0}", v).__()); - } - }).join(","); - } - - private escape_string(s: string) - { - let regex = /[']/g; - var chunkIndex = regex.lastIndex = 0; - var escapedVal = ''; - var match; - - while ((match = regex.exec(s))) { - escapedVal += s.slice(chunkIndex, match.index) + {'\'': '\'\''}[match[0]]; - chunkIndex = regex.lastIndex; - } - - if (chunkIndex === 0) { - // Nothing was escaped - return "'" + s + "'"; - } - - if (chunkIndex < s.length) { - return "'" + escapedVal + s.slice(chunkIndex) + "'"; - } - - return "'" + escapedVal + "'"; - } - - private parse_value(v:any, t: string[]): string - { - if(! (t as any).includes(typeof(v))) - { - throw new Error(__("Invalid type: expect [{0}] get {1}", t.join(","), typeof(v)).__()); - } - switch(typeof(v)) - { - case 'number': return JSON.stringify(v); - case 'string': return this.escape_string(v); - default: throw new Error(__("Un supported value {0} of type {1}", v, typeof(v)).__()); - } - } - - private binary(k: string,v :any) - { - const arr = k.split("$"); - if(arr.length > 2) - { - throw new Error(__("Invalid left hand side format: {0}", k).__()); - } - if(arr.length == 2) - { - switch(arr[1]) - { - case "gt": - return `( ${this.infer_field(arr[0])} > ${this.parse_value(v, ['number'])} )`; - case "gte": - return `( ${this.infer_field(arr[0])} >= ${this.parse_value(v, ['number'])} )`; - case "lt": - return `( ${this.infer_field(arr[0])} < ${this.parse_value(v, ['number'])} )`; - case "lte": - return `( ${this.infer_field(arr[0])} <= ${this.parse_value(v, ['number'])} )`; - case "ne": - return `( ${this.infer_field(arr[0])} != ${this.parse_value(v, ['number', 'string'])} )`; - case "between": - return `( ${this.infer_field(arr[0])} BETWEEN ${this.parse_value(v[0],['number'])} AND ${this.parse_value(v[1],['number'])} )`; - case "not_between": - return `( ${this.infer_field(arr[0])} NOT BETWEEN ${this.parse_value(v[0],['number'])} AND ${this.parse_value(v[1],['number'])} )`; - case "in": - return `( ${this.infer_field(arr[0])} IN [${this.parse_value(v[0],['number'])}, ${this.parse_value(v[1],['number'])}] )`; - case "not_in": - return `( ${this.infer_field(arr[0])} NOT IN [${this.parse_value(v[0],['number'])}, ${this.parse_value(v[1],['number'])}] )`; - case "like": - return `( ${this.infer_field(arr[0])} LIKE ${this.parse_value(v,['string'])} )`; - case "not_like": - return `( ${this.infer_field(arr[0])} NOT LIKE ${this.parse_value(v,['string'])} )`; - default: throw new Error(__("Unsupported operator `{0}`", arr[1]).__()); - } - } - else - { - return `( ${this.infer_field(arr[0])} = ${this.parse_value(v, ['number', 'string'])} )`; - } - - } - - private where(op:string, obj: GenericObject): string - { - let join_op = undefined; - switch(op) - { - case "$and": - join_op = " AND "; - break; - case "$or": - join_op = " OR "; - break; - default: - throw new Error(__("Invalid operator {0}", op).__()); - } - - if(typeof obj !== "object") - { - throw new Error(__("Invalid input data for operator {0}", op).__()); - } - - let arr = []; - for(let k in obj){ - if(k == "$and" || k=="$or") - { - arr.push(this.where(k, obj[k])); - } - else - { - arr.push(this.binary(k, obj[k])); - } - } - return `( ${arr.join(join_op)} )`; - } - } class SQLiteDBCore { static REGISTY: GenericObject; @@ -394,7 +141,7 @@ namespace OS { return this.request(rq); } - create_table(table, scheme): Promise + create_table(table: string, scheme: GenericObject): Promise { let rq = { action: 'create_table', @@ -464,10 +211,9 @@ namespace OS { * - other operations are not supported * * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database * - meta operation will return fileinfo with table scheme information - * - read operation will read all records by filter defined by the filter operation + * - read operation will read all records by filter defined by the filter as parameters * - write operations will insert a new record - * - rm operation will delete records by filter defined by the filter operation - * - filter operation sets the filter for the table + * - rm operation will delete records by filter as parameters * - other operations are not supported * - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id` * - read operation will read the current record @@ -475,12 +221,57 @@ namespace OS { * - rm operation will delete current record * - other operations are not supported * - * Some example of filters: + * Example of filter: * ```ts - * handle.filter = (filter) => { - * filter.fields() - * } - * ``` + * { + * table_name:'contacts'; + * where: { + * id$gte: 10, + * user: "dany'", + * $or: { + * 'user.email': "test@mail.com", + * age$lte: 30, + * $and: { + * 'user.birth$ne': 1986, + * age$not_between: [20,30], + * name$not_like: "%LE" + * } + * } + * }, + * fields: ['name as n', 'id', 'email'], + * order: ['user.name$asc', "id$desc"], + * joins: { + * cid: 'Category.id', + * did: 'Country.id', + * uid: "User.id" + * } + *} + * ``` + * This will generate the followings expressions: + * - `( self.name as n,self.id,self.email )` for fields + * - condition: + * ``` + * ( + * ( contacts.id >= 10 ) AND + * ( contacts.user = 'dany''' ) AND + * ( + * ( user.email = 'test@mail.com' ) OR + * ( contacts.age <= 30 ) OR + * ( + * ( user.birth != 1986 ) AND + * ( contacts.age NOT BETWEEN 20 AND 30 ) AND + * ( contacts.name NOT LIKE '%LE' ) + * ) + * ) + * ) + * ``` + * - order: `user.name ASC,contacts.id DESC` + * - joining: + * ``` + * INNER JOIN Category ON contacts.cid = Category.id + * INNER JOIN Country ON contacts.did = Country.id + * INNER JOIN Country ON contacts.did = Country.id + * ``` * * @class SqliteFileHandle * @extends {BaseFileHandle} diff --git a/SQLiteDB/build/debug/libsqlite.js b/SQLiteDB/build/debug/libsqlite.js index 7095df6..4a7497d 100644 --- a/SQLiteDB/build/debug/libsqlite.js +++ b/SQLiteDB/build/debug/libsqlite.js @@ -3,210 +3,6 @@ var OS; (function (OS) { let API; (function (API) { - /** - * Generate SQL expression from input object - * - * Example of input object - * ```ts - * { - * where: { - * id$gte: 10, - * user: "dany'", - * $or: { - * 'user.email': "test@mail.com", - * age$lte: 30, - * $and: { - * 'user.birth$ne': 1986, - * age$not_between: [20,30], - * name$not_like: "%LE" - * } - * } - * }, - * fields: ['name as n', 'id', 'email'], - * order: ['user.name$asc', "id$desc"], - * joins: { - * cid: 'Category.id', - * did: 'Country.id' - * } - *} - * ``` - * This will generate the followings expressions: - * - `( self.name as n,self.id,self.email )` for fields - * - condition: - * ``` - * ( - * ( self.id >= 10 ) AND - * ( self.user = 'dany''' ) AND - * ( - * ( user.email = 'test@mail.com' ) OR - * ( self.age <= 30 ) OR - * ( - * ( user.birth != 1986 ) AND - * ( self.age NOT BETWEEN 20 AND 30 ) AND - * ( self.name NOT LIKE '%LE' ) - * ) - * ) - * ) - * ``` - * - order: `user.name ASC,self.id DESC` - * - joining: - * ``` - * INNER JOIN Category ON self.cid = Category.id - * INNER JOIN Country ON self.did = Country.id - * ``` - * - */ - class SQLiteQueryGenerator { - constructor(obj) { - this._where = undefined; - this._fields = undefined; - this._order = undefined; - this._joins = undefined; - this._is_joining = false; - if (obj.joins) { - this._is_joining = true; - this._joins = this.joins(obj.joins); - } - if (obj.where) { - this._where = this.where("$and", obj.where); - } - if (obj.fields) { - this._fields = `( ${obj.fields.map(v => this.infer_field(v)).join(",")} )`; - } - if (obj.order) { - this._order = this.order_by(obj.order); - } - } - infer_field(k) { - if (!this._is_joining || k.indexOf(".") > 0) - return k; - return `self.${k}`; - } - joins(data) { - let joins_arr = []; - for (let k in data) { - let v = data[k]; - let arr = v.split('.'); - if (arr.length != 2) { - throw new Error(__("Other table name parsing error: {0}", v).__()); - } - joins_arr.push(`INNER JOIN ${arr[0]} ON ${this.infer_field(k)} = ${v}`); - } - return joins_arr.join(" "); - } - print() { - console.log(this._fields); - console.log(this._where); - console.log(this._order); - console.log(this._joins); - } - order_by(order) { - if (!Array.isArray(order)) { - throw new Error(__("Invalid type: expect array get {0}", typeof (order)).__()); - } - return order.map((v, _) => { - const arr = v.split('$'); - if (arr.length != 2) { - throw new Error(__("Invalid field order format {0}", v).__()); - } - switch (arr[1]) { - case 'asc': return `${this.infer_field(arr[0])} ASC`; - case 'desc': return `${this.infer_field(arr[0])} DESC`; - default: throw new Error(__("Invalid field order type {0}", v).__()); - } - }).join(","); - } - escape_string(s) { - let regex = /[']/g; - var chunkIndex = regex.lastIndex = 0; - var escapedVal = ''; - var match; - while ((match = regex.exec(s))) { - escapedVal += s.slice(chunkIndex, match.index) + { '\'': '\'\'' }[match[0]]; - chunkIndex = regex.lastIndex; - } - if (chunkIndex === 0) { - // Nothing was escaped - return "'" + s + "'"; - } - if (chunkIndex < s.length) { - return "'" + escapedVal + s.slice(chunkIndex) + "'"; - } - return "'" + escapedVal + "'"; - } - parse_value(v, t) { - if (!t.includes(typeof (v))) { - throw new Error(__("Invalid type: expect [{0}] get {1}", t.join(","), typeof (v)).__()); - } - switch (typeof (v)) { - case 'number': return JSON.stringify(v); - case 'string': return this.escape_string(v); - default: throw new Error(__("Un supported value {0} of type {1}", v, typeof (v)).__()); - } - } - binary(k, v) { - const arr = k.split("$"); - if (arr.length > 2) { - throw new Error(__("Invalid left hand side format: {0}", k).__()); - } - if (arr.length == 2) { - switch (arr[1]) { - case "gt": - return `( ${this.infer_field(arr[0])} > ${this.parse_value(v, ['number'])} )`; - case "gte": - return `( ${this.infer_field(arr[0])} >= ${this.parse_value(v, ['number'])} )`; - case "lt": - return `( ${this.infer_field(arr[0])} < ${this.parse_value(v, ['number'])} )`; - case "lte": - return `( ${this.infer_field(arr[0])} <= ${this.parse_value(v, ['number'])} )`; - case "ne": - return `( ${this.infer_field(arr[0])} != ${this.parse_value(v, ['number', 'string'])} )`; - case "between": - return `( ${this.infer_field(arr[0])} BETWEEN ${this.parse_value(v[0], ['number'])} AND ${this.parse_value(v[1], ['number'])} )`; - case "not_between": - return `( ${this.infer_field(arr[0])} NOT BETWEEN ${this.parse_value(v[0], ['number'])} AND ${this.parse_value(v[1], ['number'])} )`; - case "in": - return `( ${this.infer_field(arr[0])} IN [${this.parse_value(v[0], ['number'])}, ${this.parse_value(v[1], ['number'])}] )`; - case "not_in": - return `( ${this.infer_field(arr[0])} NOT IN [${this.parse_value(v[0], ['number'])}, ${this.parse_value(v[1], ['number'])}] )`; - case "like": - return `( ${this.infer_field(arr[0])} LIKE ${this.parse_value(v, ['string'])} )`; - case "not_like": - return `( ${this.infer_field(arr[0])} NOT LIKE ${this.parse_value(v, ['string'])} )`; - default: throw new Error(__("Unsupported operator `{0}`", arr[1]).__()); - } - } - else { - return `( ${this.infer_field(arr[0])} = ${this.parse_value(v, ['number', 'string'])} )`; - } - } - where(op, obj) { - let join_op = undefined; - switch (op) { - case "$and": - join_op = " AND "; - break; - case "$or": - join_op = " OR "; - break; - default: - throw new Error(__("Invalid operator {0}", op).__()); - } - if (typeof obj !== "object") { - throw new Error(__("Invalid input data for operator {0}", op).__()); - } - let arr = []; - for (let k in obj) { - if (k == "$and" || k == "$or") { - arr.push(this.where(k, obj[k])); - } - else { - arr.push(this.binary(k, obj[k])); - } - } - return `( ${arr.join(join_op)} )`; - } - } class SQLiteDBCore { constructor(path) { if (!SQLiteDBCore.REGISTY) { @@ -376,10 +172,9 @@ var OS; * - other operations are not supported * * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database * - meta operation will return fileinfo with table scheme information - * - read operation will read all records by filter defined by the filter operation + * - read operation will read all records by filter defined by the filter as parameters * - write operations will insert a new record - * - rm operation will delete records by filter defined by the filter operation - * - filter operation sets the filter for the table + * - rm operation will delete records by filter as parameters * - other operations are not supported * - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id` * - read operation will read the current record @@ -387,12 +182,57 @@ var OS; * - rm operation will delete current record * - other operations are not supported * - * Some example of filters: + * Example of filter: * ```ts - * handle.filter = (filter) => { - * filter.fields() - * } - * ``` + * { + * table_name:'contacts'; + * where: { + * id$gte: 10, + * user: "dany'", + * $or: { + * 'user.email': "test@mail.com", + * age$lte: 30, + * $and: { + * 'user.birth$ne': 1986, + * age$not_between: [20,30], + * name$not_like: "%LE" + * } + * } + * }, + * fields: ['name as n', 'id', 'email'], + * order: ['user.name$asc', "id$desc"], + * joins: { + * cid: 'Category.id', + * did: 'Country.id', + * uid: "User.id" + * } + *} + * ``` + * This will generate the followings expressions: + * - `( self.name as n,self.id,self.email )` for fields + * - condition: + * ``` + * ( + * ( contacts.id >= 10 ) AND + * ( contacts.user = 'dany''' ) AND + * ( + * ( user.email = 'test@mail.com' ) OR + * ( contacts.age <= 30 ) OR + * ( + * ( user.birth != 1986 ) AND + * ( contacts.age NOT BETWEEN 20 AND 30 ) AND + * ( contacts.name NOT LIKE '%LE' ) + * ) + * ) + * ) + * ``` + * - order: `user.name ASC,contacts.id DESC` + * - joining: + * ``` + * INNER JOIN Category ON contacts.cid = Category.id + * INNER JOIN Country ON contacts.did = Country.id + * INNER JOIN Country ON contacts.did = Country.id + * ``` * * @class SqliteFileHandle * @extends {BaseFileHandle} diff --git a/SQLiteDB/build/debug/main.js b/SQLiteDB/build/debug/main.js index 317e6cf..f66a559 100644 --- a/SQLiteDB/build/debug/main.js +++ b/SQLiteDB/build/debug/main.js @@ -57,47 +57,155 @@ var OS; } }); } - openFile() { - return this.openDialog("FileDialog", { - title: __("Open file"), - mimes: this.meta().mimes - }).then(async (d) => { - this.filehandle = `sqlite://${d.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle(); + async openFile() { + try { + const d_1 = await this.openDialog("FileDialog", { + title: __("Open file"), + mimes: this.meta().mimes + }); + this.filehandle = `sqlite://${d_1.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle(); await this.filehandle.onready(); this.list_tables(); - }) - .catch((e) => { + } + catch (e) { this.error(__("Unable to open database file: {0}", e.toString()), e); - }); + } ; } - newFile() { - return this.openDialog("FileDialog", { - title: __("Save as"), - file: "Untitled.db" - }).then(async (f) => { - var d; - d = f.file.path.asFileHandle(); + async newFile() { + try { + const f = await this.openDialog("FileDialog", { + title: __("Save as"), + file: "Untitled.db" + }); + var d_1 = f.file.path.asFileHandle(); if (f.file.type === "file") { - d = d.parent(); + d_1 = d_1.parent(); } - const target = `${d.path}/${f.name}`.asFileHandle(); + const target = `${d_1.path}/${f.name}`.asFileHandle(); this.filehandle = `sqlite://${target.genealogy.join("/")}`.asFileHandle(); await this.filehandle.onready(); this.list_tables(); - }) - .catch((e) => { + } + catch (e) { this.error(__("Unable to init database file: {0}", e.toString()), e); - }); + } } main() { this.filehandle = undefined; this.tbl_list = this.find("tbl-list"); + this.grid_table = this.find("tb-browser"); + this.grid_scheme = this.find("sch-browser"); + this.grid_table.resizable = true; + this.grid_scheme.resizable = true; + this.grid_scheme.header = [ + { text: __("Field name") }, + { text: __("Field type") }, + ]; + this.btn_loadmore = this.find("bt-load-next"); + this.container = this.find("container"); + this.bindKey("ALT-N", () => { + return this.newFile(); + }); + this.bindKey("ALT-O", () => { + return this.openFile(); + }); + this.container.ontabselect = (e) => { + if (this.container.selectedIndex == 0) { + if (!this.tbl_list.selectedItem) + return; + const scheme = this.tbl_list.selectedItem.data.handle.info.scheme; + if (!scheme) + return; + const data = []; + for (let k in scheme) { + data.push([ + { text: k }, + { text: scheme[k] } + ]); + } + this.grid_scheme.rows = data; + } + }; this.find("bt-add-table").onbtclick = (e) => { + if (!this.filehandle) { + return this.notify(__("Please open a database file")); + } this.openDialog(new NewTableDialog(), { title: __("Create new table") + }) + .then((data) => { + console.log(data); }); }; + this.btn_loadmore.onbtclick = async (e) => { + try { + await this.load_table(); + } + catch (e) { + this.error(__("Error reading table: {0}", e.toString()), e); + } + }; + this.tbl_list.onlistselect = async (_) => { + try { + if (!this.tbl_list.selectedItem) + return; + const handle = this.tbl_list.selectedItem.data.handle; + await handle.onready(); + this.last_max_id = 0; + this.grid_table.rows = []; + const headers = Object.getOwnPropertyNames(handle.info.scheme).map((e) => { + return { text: e }; + }); + this.grid_table.header = headers; + const records = await handle.read({ fields: ["COUNT(*)"] }); + this.n_records = records[0]["(COUNT(*))"]; + await this.load_table(); + this.container.selectedIndex = 1; + } + catch (e) { + this.error(__("Error reading table: {0}", e.toString()), e); + } + }; + this.openFile(); + } + async load_table() { + if (this.grid_table.rows.length >= this.n_records) { + return; + } + if (!this.tbl_list.selectedItem) + return; + const handle = this.tbl_list.selectedItem.data.handle; + await handle.onready(); + const headers = Object.getOwnPropertyNames(handle.info.scheme).map((e) => { + return { text: e }; + }); + // read all records + const records = await handle.read({ + where: { id$gt: this.last_max_id }, + limit: 10 + }); + if (records && records.length > 0) { + for (let e of records) { + const row = []; + if (e.id && e.id > this.last_max_id) { + this.last_max_id = e.id; + } + for (let v in headers) { + let text = e[headers[v].text]; + if (text.length > 100) { + text = text.substring(0, 100); + } + row.push({ + text: text, + record: e + }); + } + this.grid_table.push(row, false); + } + this.grid_table.scroll_to_bottom(); + } + this.btn_loadmore.text = `${this.grid_table.rows.length}/${this.n_records}`; } } application.SQLiteDB = SQLiteDB; @@ -109,9 +217,6 @@ var OS; constructor() { super("NewTableDialog"); } - init() { - console.log(this.constructor.scheme); - } /** * Main entry point * @@ -121,22 +226,31 @@ var OS; super.main(); this.container = this.find("container"); this.find("btnCancel").onbtclick = (e) => this.quit(); - this.find("btnAdd").onbtclick = (e) => this.addField("", "", true); + this.find("btnAdd").onbtclick = (e) => this.addField(); $(this.find("wrapper")); $(this.container) .css("overflow-y", "auto"); + this.addField(); this.find("btnOk").onbtclick = (e) => { - const inputs = $("input", this.scheme); + const input = this.find("txt-tblname"); + if (!input.value || input.value == "") { + return this.notify(__("Please enter table name")); + } + const inputs = $("input", this.container); + const lists = $("afx-list-view", this.container); + if (inputs.length == 0) { + return this.notify(__("Please define table fields")); + } let cdata = {}; - for (let i = 0; i < inputs.length; i += 2) { + for (let i = 0; i < inputs.length; i++) { const key = inputs[i].value.trim(); if (key === "") { - return this.notify(__("Key cannot be empty")); + return this.notify(__("Field name cannot be empty")); } if (cdata[key]) { - return this.notify(__("Duplicate key: {0}", key)); + return this.notify(__("Duplicate field: {0}", key)); } - cdata[key] = inputs[i + 1].value.trim(); + cdata[key] = lists[i].selectedItem.data.text; } if (this.handle) this.handle(cdata); @@ -149,64 +263,55 @@ var OS; * @private * @memberof NewTableDialog */ - addField(key, value, removable) { + addField() { const div = $("
") - .css("width", "100%") .css("display", "flex") .css("flex-direction", "row") .appendTo(this.container); $("") .attr("type", "text") - .css("width", "50%") - .css("height", "25px") - .val(key) + .css("flex", "1") .appendTo(div); - $("") - .css("width", "50%") - .css("height", "25px") + let list = $("") + .css("flex", "1") + .appendTo(div)[0]; + list.uify(this.observable); + list.dropdown = true; + list.data = [ + { text: "TEXT" }, + { text: "INTEGER" }, + { text: "REAL" }, + { text: "NUMERIC" }, + ]; + list.selected = 0; + const btn = $(""); + btn[0].uify(undefined); + btn[0].iconclass = "fa fa-minus"; + btn + .on("click", () => { + div.remove(); + }) .appendTo(div); - if (removable) { - const btn = $(""); - btn[0].uify(undefined); - $("button", btn) - .css("width", "25px") - .css("height", "25px"); - btn[0].iconclass = "fa fa-minus"; - btn - .on("click", () => { - div.remove(); - }) - .appendTo(div); - } - else { - $("
") - .css("width", "25px") - .appendTo(div); - } } } /** * Scheme definition */ NewTableDialog.scheme = `\ - - -
- -
- -
- - -
- - -
-
-
-
-
-
`; + + + + +
+ + +
+ + +
+
+
+
`; })(application = OS.application || (OS.application = {})); })(OS || (OS = {})); @@ -215,210 +320,6 @@ var OS; (function (OS) { let API; (function (API) { - /** - * Generate SQL expression from input object - * - * Example of input object - * ```ts - * { - * where: { - * id$gte: 10, - * user: "dany'", - * $or: { - * 'user.email': "test@mail.com", - * age$lte: 30, - * $and: { - * 'user.birth$ne': 1986, - * age$not_between: [20,30], - * name$not_like: "%LE" - * } - * } - * }, - * fields: ['name as n', 'id', 'email'], - * order: ['user.name$asc', "id$desc"], - * joins: { - * cid: 'Category.id', - * did: 'Country.id' - * } - *} - * ``` - * This will generate the followings expressions: - * - `( self.name as n,self.id,self.email )` for fields - * - condition: - * ``` - * ( - * ( self.id >= 10 ) AND - * ( self.user = 'dany''' ) AND - * ( - * ( user.email = 'test@mail.com' ) OR - * ( self.age <= 30 ) OR - * ( - * ( user.birth != 1986 ) AND - * ( self.age NOT BETWEEN 20 AND 30 ) AND - * ( self.name NOT LIKE '%LE' ) - * ) - * ) - * ) - * ``` - * - order: `user.name ASC,self.id DESC` - * - joining: - * ``` - * INNER JOIN Category ON self.cid = Category.id - * INNER JOIN Country ON self.did = Country.id - * ``` - * - */ - class SQLiteQueryGenerator { - constructor(obj) { - this._where = undefined; - this._fields = undefined; - this._order = undefined; - this._joins = undefined; - this._is_joining = false; - if (obj.joins) { - this._is_joining = true; - this._joins = this.joins(obj.joins); - } - if (obj.where) { - this._where = this.where("$and", obj.where); - } - if (obj.fields) { - this._fields = `( ${obj.fields.map(v => this.infer_field(v)).join(",")} )`; - } - if (obj.order) { - this._order = this.order_by(obj.order); - } - } - infer_field(k) { - if (!this._is_joining || k.indexOf(".") > 0) - return k; - return `self.${k}`; - } - joins(data) { - let joins_arr = []; - for (let k in data) { - let v = data[k]; - let arr = v.split('.'); - if (arr.length != 2) { - throw new Error(__("Other table name parsing error: {0}", v).__()); - } - joins_arr.push(`INNER JOIN ${arr[0]} ON ${this.infer_field(k)} = ${v}`); - } - return joins_arr.join(" "); - } - print() { - console.log(this._fields); - console.log(this._where); - console.log(this._order); - console.log(this._joins); - } - order_by(order) { - if (!Array.isArray(order)) { - throw new Error(__("Invalid type: expect array get {0}", typeof (order)).__()); - } - return order.map((v, _) => { - const arr = v.split('$'); - if (arr.length != 2) { - throw new Error(__("Invalid field order format {0}", v).__()); - } - switch (arr[1]) { - case 'asc': return `${this.infer_field(arr[0])} ASC`; - case 'desc': return `${this.infer_field(arr[0])} DESC`; - default: throw new Error(__("Invalid field order type {0}", v).__()); - } - }).join(","); - } - escape_string(s) { - let regex = /[']/g; - var chunkIndex = regex.lastIndex = 0; - var escapedVal = ''; - var match; - while ((match = regex.exec(s))) { - escapedVal += s.slice(chunkIndex, match.index) + { '\'': '\'\'' }[match[0]]; - chunkIndex = regex.lastIndex; - } - if (chunkIndex === 0) { - // Nothing was escaped - return "'" + s + "'"; - } - if (chunkIndex < s.length) { - return "'" + escapedVal + s.slice(chunkIndex) + "'"; - } - return "'" + escapedVal + "'"; - } - parse_value(v, t) { - if (!t.includes(typeof (v))) { - throw new Error(__("Invalid type: expect [{0}] get {1}", t.join(","), typeof (v)).__()); - } - switch (typeof (v)) { - case 'number': return JSON.stringify(v); - case 'string': return this.escape_string(v); - default: throw new Error(__("Un supported value {0} of type {1}", v, typeof (v)).__()); - } - } - binary(k, v) { - const arr = k.split("$"); - if (arr.length > 2) { - throw new Error(__("Invalid left hand side format: {0}", k).__()); - } - if (arr.length == 2) { - switch (arr[1]) { - case "gt": - return `( ${this.infer_field(arr[0])} > ${this.parse_value(v, ['number'])} )`; - case "gte": - return `( ${this.infer_field(arr[0])} >= ${this.parse_value(v, ['number'])} )`; - case "lt": - return `( ${this.infer_field(arr[0])} < ${this.parse_value(v, ['number'])} )`; - case "lte": - return `( ${this.infer_field(arr[0])} <= ${this.parse_value(v, ['number'])} )`; - case "ne": - return `( ${this.infer_field(arr[0])} != ${this.parse_value(v, ['number', 'string'])} )`; - case "between": - return `( ${this.infer_field(arr[0])} BETWEEN ${this.parse_value(v[0], ['number'])} AND ${this.parse_value(v[1], ['number'])} )`; - case "not_between": - return `( ${this.infer_field(arr[0])} NOT BETWEEN ${this.parse_value(v[0], ['number'])} AND ${this.parse_value(v[1], ['number'])} )`; - case "in": - return `( ${this.infer_field(arr[0])} IN [${this.parse_value(v[0], ['number'])}, ${this.parse_value(v[1], ['number'])}] )`; - case "not_in": - return `( ${this.infer_field(arr[0])} NOT IN [${this.parse_value(v[0], ['number'])}, ${this.parse_value(v[1], ['number'])}] )`; - case "like": - return `( ${this.infer_field(arr[0])} LIKE ${this.parse_value(v, ['string'])} )`; - case "not_like": - return `( ${this.infer_field(arr[0])} NOT LIKE ${this.parse_value(v, ['string'])} )`; - default: throw new Error(__("Unsupported operator `{0}`", arr[1]).__()); - } - } - else { - return `( ${this.infer_field(arr[0])} = ${this.parse_value(v, ['number', 'string'])} )`; - } - } - where(op, obj) { - let join_op = undefined; - switch (op) { - case "$and": - join_op = " AND "; - break; - case "$or": - join_op = " OR "; - break; - default: - throw new Error(__("Invalid operator {0}", op).__()); - } - if (typeof obj !== "object") { - throw new Error(__("Invalid input data for operator {0}", op).__()); - } - let arr = []; - for (let k in obj) { - if (k == "$and" || k == "$or") { - arr.push(this.where(k, obj[k])); - } - else { - arr.push(this.binary(k, obj[k])); - } - } - return `( ${arr.join(join_op)} )`; - } - } class SQLiteDBCore { constructor(path) { if (!SQLiteDBCore.REGISTY) { @@ -588,10 +489,9 @@ var OS; * - other operations are not supported * * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database * - meta operation will return fileinfo with table scheme information - * - read operation will read all records by filter defined by the filter operation + * - read operation will read all records by filter defined by the filter as parameters * - write operations will insert a new record - * - rm operation will delete records by filter defined by the filter operation - * - filter operation sets the filter for the table + * - rm operation will delete records by filter as parameters * - other operations are not supported * - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id` * - read operation will read the current record @@ -599,12 +499,57 @@ var OS; * - rm operation will delete current record * - other operations are not supported * - * Some example of filters: + * Example of filter: * ```ts - * handle.filter = (filter) => { - * filter.fields() - * } - * ``` + * { + * table_name:'contacts'; + * where: { + * id$gte: 10, + * user: "dany'", + * $or: { + * 'user.email': "test@mail.com", + * age$lte: 30, + * $and: { + * 'user.birth$ne': 1986, + * age$not_between: [20,30], + * name$not_like: "%LE" + * } + * } + * }, + * fields: ['name as n', 'id', 'email'], + * order: ['user.name$asc', "id$desc"], + * joins: { + * cid: 'Category.id', + * did: 'Country.id', + * uid: "User.id" + * } + *} + * ``` + * This will generate the followings expressions: + * - `( self.name as n,self.id,self.email )` for fields + * - condition: + * ``` + * ( + * ( contacts.id >= 10 ) AND + * ( contacts.user = 'dany''' ) AND + * ( + * ( user.email = 'test@mail.com' ) OR + * ( contacts.age <= 30 ) OR + * ( + * ( user.birth != 1986 ) AND + * ( contacts.age NOT BETWEEN 20 AND 30 ) AND + * ( contacts.name NOT LIKE '%LE' ) + * ) + * ) + * ) + * ``` + * - order: `user.name ASC,contacts.id DESC` + * - joining: + * ``` + * INNER JOIN Category ON contacts.cid = Category.id + * INNER JOIN Country ON contacts.did = Country.id + * INNER JOIN Country ON contacts.did = Country.id + * ``` * * @class SqliteFileHandle * @extends {BaseFileHandle} diff --git a/SQLiteDB/build/debug/scheme.html b/SQLiteDB/build/debug/scheme.html index c0197cc..3b9996c 100644 --- a/SQLiteDB/build/debug/scheme.html +++ b/SQLiteDB/build/debug/scheme.html @@ -1,5 +1,5 @@ - + @@ -7,12 +7,22 @@ - + - + + + + +
+ + + +
+
+
diff --git a/SQLiteDB/main.ts b/SQLiteDB/main.ts index 7dc8e65..9b87313 100644 --- a/SQLiteDB/main.ts +++ b/SQLiteDB/main.ts @@ -10,7 +10,12 @@ namespace OS { private filehandle: API.VFS.BaseFileHandle; private tbl_list: GUI.tag.ListViewTag; - + private grid_table: GUI.tag.GridViewTag; + private grid_scheme: GUI.tag.GridViewTag; + private container: GUI.tag.TabContainerTag; + private last_max_id: number; + private n_records: number; + private btn_loadmore: GUI.tag.ButtonTag; constructor(args: AppArgumentsType[]) { super("SQLiteDB", args); } @@ -62,48 +67,177 @@ namespace OS { } }) } - private openFile() { - return this.openDialog("FileDialog", { - title: __("Open file"), - mimes: this.meta().mimes - }).then(async (d) => { - this.filehandle = `sqlite://${d.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle(); + private async openFile() { + try { + const d_1 = await this.openDialog("FileDialog",{ + title: __("Open file"), + mimes: this.meta().mimes + }); + this.filehandle=`sqlite://${d_1.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle(); await this.filehandle.onready(); this.list_tables(); - }) - .catch((e) => { - this.error(__("Unable to open database file: {0}", e.toString()), e); - });; + } + catch(e) + { + this.error(__("Unable to open database file: {0}",e.toString()),e); + }; } - private newFile() { - return this.openDialog("FileDialog", { - title: __("Save as"), - file: "Untitled.db" - }).then(async (f) => { - var d; - d = f.file.path.asFileHandle(); - if (f.file.type === "file") { - d = d.parent(); + private async newFile() { + try { + const f = await this.openDialog("FileDialog",{ + title: __("Save as"), + file: "Untitled.db" + }); + var d_1 = f.file.path.asFileHandle(); + if(f.file.type==="file") { + d_1=d_1.parent(); } - const target = `${d.path}/${f.name}`.asFileHandle(); - this.filehandle = `sqlite://${target.genealogy.join("/")}`.asFileHandle(); + const target=`${d_1.path}/${f.name}`.asFileHandle(); + this.filehandle=`sqlite://${target.genealogy.join("/")}`.asFileHandle(); await this.filehandle.onready(); this.list_tables(); - }) - .catch((e) => { - this.error(__("Unable to init database file: {0}", e.toString()), e); - }); + } + catch(e) { + this.error(__("Unable to init database file: {0}",e.toString()),e); + } } main(): void { this.filehandle = undefined; this.tbl_list = this.find("tbl-list") as GUI.tag.ListViewTag; + this.grid_table = this.find("tb-browser") as GUI.tag.GridViewTag; + this.grid_scheme = this.find("sch-browser") as GUI.tag.GridViewTag; + this.grid_table.resizable = true; + this.grid_scheme.resizable = true; + this.grid_scheme.header = [ + { text: __("Field name") }, + { text: __("Field type") }, + ]; + this.btn_loadmore = this.find("bt-load-next") as GUI.tag.ButtonTag; + this.container = this.find("container") as GUI.tag.TabContainerTag; + this.bindKey("ALT-N", () => { + return this.newFile(); + }); + this.bindKey("ALT-O", () => { + return this.openFile(); + }); + + this.container.ontabselect = (e) => { + if(this.container.selectedIndex == 0) + { + if(!this.tbl_list.selectedItem) + return; + const scheme = this.tbl_list.selectedItem.data.handle.info.scheme; + if(!scheme) + return; + const data = []; + for(let k in scheme) + { + data.push([ + { text: k}, + {text: scheme[k]} + ]) + } + this.grid_scheme.rows = data; + } + } + (this.find("bt-add-table") as GUI.tag.ButtonTag).onbtclick = (e) => { + if(!this.filehandle) + { + return this.notify(__("Please open a database file")); + } this.openDialog(new NewTableDialog(), { title: __("Create new table") + }) + .then((data) => { + console.log(data); }); } + this.btn_loadmore.onbtclick = async (e) => { + try + { + await this.load_table(); + } + catch(e) + { + this.error(__("Error reading table: {0}", e.toString()),e); + } + } + this.tbl_list.onlistselect = async (_) => { + try + { + if(!this.tbl_list.selectedItem) + return; + const handle: API.VFS.BaseFileHandle = this.tbl_list.selectedItem.data.handle; + await handle.onready(); + this.last_max_id = 0; + this.grid_table.rows = []; + const headers = + Object.getOwnPropertyNames(handle.info.scheme).map((e)=>{ + return {text: e} + }); + this.grid_table.header = headers; + const records = await handle.read({fields:["COUNT(*)"]}); + this.n_records = records[0]["(COUNT(*))"]; + await this.load_table(); + this.container.selectedIndex = 1; + } + catch(e) + { + this.error(__("Error reading table: {0}", e.toString()),e); + } + } + this.openFile(); + } + + private async load_table() + { + if(this.grid_table.rows.length >= this.n_records) + { + return; + } + if(!this.tbl_list.selectedItem) + return; + const handle: API.VFS.BaseFileHandle = this.tbl_list.selectedItem.data.handle; + await handle.onready(); + const headers = + Object.getOwnPropertyNames(handle.info.scheme).map((e)=>{ + return {text: e} + }); + // read all records + const records = await handle.read({ + where:{ id$gt: this.last_max_id }, + limit: 10 + }); + + if(records && records.length > 0) + { + for(let e of records){ + const row = []; + if(e.id && e.id > this.last_max_id) + { + this.last_max_id = e.id; + } + for(let v in headers) + { + let text:string = e[headers[v].text]; + if(text.length > 100) + { + text = text.substring(0,100); + } + row.push({ + text: text, + record: e + }); + } + this.grid_table.push(row, false); + } + (this.grid_table as any).scroll_to_bottom(); + } + + this.btn_loadmore.text = `${this.grid_table.rows.length}/${this.n_records}`; } } @@ -135,22 +269,36 @@ namespace OS { super.main(); this.container = this.find("container") as HTMLDivElement; (this.find("btnCancel") as GUI.tag.ButtonTag).onbtclick = (e) => this.quit(); - (this.find("btnAdd") as GUI.tag.ButtonTag).onbtclick = (e) => this.addField("", "", true); + (this.find("btnAdd") as GUI.tag.ButtonTag).onbtclick = (e) => this.addField(); $(this.find("wrapper")) $(this.container) .css("overflow-y", "auto"); + this.addField(); (this.find("btnOk") as GUI.tag.ButtonTag).onbtclick = (e) => { - const inputs = $("input", this.scheme) as JQuery; + const input = this.find("txt-tblname") as GUI.tag.InputTag; + + if(!input.value || input.value == "") + { + return this.notify(__("Please enter table name")); + } + + const inputs = $("input", this.container) as JQuery; + const lists = $("afx-list-view", this.container) as JQuery; + if(inputs.length == 0) + { + return this.notify(__("Please define table fields")); + } + let cdata: GenericObject = {}; - for (let i = 0; i < inputs.length; i += 2) { + for (let i = 0; i < inputs.length; i ++) { const key = inputs[i].value.trim(); if (key === "") { - return this.notify(__("Key cannot be empty")); + return this.notify(__("Field name cannot be empty")); } if (cdata[key]) { - return this.notify(__("Duplicate key: {0}", key)); + return this.notify(__("Duplicate field: {0}", key)); } - cdata[key] = inputs[i + 1].value.trim(); + cdata[key] = lists[i].selectedItem.data.text; } if (this.handle) this.handle(cdata); @@ -165,41 +313,35 @@ namespace OS { * @private * @memberof NewTableDialog */ - private addField(key: string, value: string, removable: boolean): void { + private addField(): void { const div = $("
") - .css("width", "100%") .css("display", "flex") .css("flex-direction", "row") .appendTo(this.container); $("") .attr("type", "text") - .css("width", "50%") - .css("height", "25px") - .val(key) + .css("flex", "1") .appendTo(div); - $("") - .css("width", "50%") - .css("height", "25px") + let list = $("") + .css("flex", "1") + .appendTo(div)[0] as GUI.tag.ListViewTag; + list.uify(this.observable); + list.dropdown = true; + list.data = [ + {text:"TEXT"}, + {text:"INTEGER"}, + {text:"REAL"}, + {text:"NUMERIC"}, + ]; + list.selected = 0; + const btn = $(""); + btn[0].uify(undefined); + (btn[0] as GUI.tag.ButtonTag).iconclass = "fa fa-minus"; + btn + .on("click", () => { + div.remove(); + }) .appendTo(div); - if (removable) { - const btn = $(""); - btn[0].uify(undefined); - $("button", btn) - .css("width", "25px") - .css("height", "25px"); - (btn[0] as GUI.tag.ButtonTag).iconclass = "fa fa-minus"; - btn - .on("click", () => { - div.remove(); - }) - .appendTo(div); - } - else { - $("
") - .css("width", "25px") - .appendTo(div); - } - } } @@ -208,23 +350,19 @@ namespace OS { * Scheme definition */ NewTableDialog.scheme = `\ - - -
- -
- -
- - -
- - -
-
-
-
-
-
`; + + + + +
+ + +
+ + +
+
+
+
`; } } \ No newline at end of file diff --git a/SQLiteDB/scheme.html b/SQLiteDB/scheme.html index c0197cc..3b9996c 100644 --- a/SQLiteDB/scheme.html +++ b/SQLiteDB/scheme.html @@ -1,5 +1,5 @@ - + @@ -7,12 +7,22 @@ - + - + + + + +
+ + + +
+
+
diff --git a/libantosdk/README.md b/libantosdk/README.md index 94dbc21..5772d60 100644 --- a/libantosdk/README.md +++ b/libantosdk/README.md @@ -2,6 +2,7 @@ AntOSDK: development API for AntOS based applications/projects ## Change logs +- 0.0.18: Add some public API on grid view - 0.0.17: Use lastest AntOS d.ts file - 0.0.16: Fix jquery encoding error - 0.0.15: App name differ from libname, update AntOS API diff --git a/libantosdk/build/debug/README.md b/libantosdk/build/debug/README.md index 94dbc21..5772d60 100644 --- a/libantosdk/build/debug/README.md +++ b/libantosdk/build/debug/README.md @@ -2,6 +2,7 @@ AntOSDK: development API for AntOS based applications/projects ## Change logs +- 0.0.18: Add some public API on grid view - 0.0.17: Use lastest AntOS d.ts file - 0.0.16: Fix jquery encoding error - 0.0.15: App name differ from libname, update AntOS API diff --git a/libantosdk/build/debug/core/ts/antos.d.ts b/libantosdk/build/debug/core/ts/antos.d.ts index 73a82a3..efc6b63 100644 --- a/libantosdk/build/debug/core/ts/antos.d.ts +++ b/libantosdk/build/debug/core/ts/antos.d.ts @@ -475,6 +475,7 @@ declare namespace OS { * @extends {BaseDialog} */ class BasicDialog extends BaseDialog { + ['constructor']: typeof BasicDialog; /** * Placeholder for the UI scheme to be rendered. This can * be either the string definition of the scheme or @@ -4421,6 +4422,14 @@ declare namespace OS { * @memberof ListViewTag */ private _selectedItems; + /** + * The anchor element that the list view positioned on + * This is helpful when rendering dropdown list + * @private + * @type{HTMLElement} + * @memberof ListViewTag + */ + private _anchor; /** * Data placeholder of the list * @@ -5539,6 +5548,18 @@ declare namespace OS { * @memberof LabelTag */ set iconclass(v: string); + /** + * Set the CSS class of the label icon on the right side + * + * @memberof LabelTag + */ + set iconclass_end(v: string); + /** + * Set the CSS class of the label icon on the right side + * + * @memberof LabelTag + */ + set iconclass$(v: string); /** * Setter: Set the text of the label * @@ -6295,13 +6316,25 @@ declare namespace OS { * @memberof GridViewTag */ delete(row: GridRowTag): void; + /** + * Scroll the grid view to bottom + * + * @memberof GridViewTag + */ + scroll_to_bottom(): void; + /** + * Scroll the grid view to top + * + * @memberof GridViewTag + */ + scroll_to_top(): void; /** * Push a row to the grid * * @param {GenericObject[]} row list of cell data * @param {boolean} flag indicates where the row is add to beginning or end * of the row - * @memberof GridViewTags + * @memberof GridViewTag */ push(row: GenericObject[], flag: boolean): void; /** @@ -6622,6 +6655,19 @@ declare namespace OS { * @memberof ButtonTag */ set iconclass(v: string); + /** + * Set the icon class on the right side of the button, this property + * allows to style the button icon using CSS + * + * @memberof ButtonTag + */ + set iconclass$(v: string); + /** + * Set the CSS class of the label icon on the right side + * + * @memberof ButtonTag + */ + set iconclass_end(v: string); /** * Setter: Set the text of the button * diff --git a/libantosdk/build/debug/package.json b/libantosdk/build/debug/package.json index 17289e9..da101de 100644 --- a/libantosdk/build/debug/package.json +++ b/libantosdk/build/debug/package.json @@ -7,7 +7,7 @@ "author": "Xuan Sang LE", "email": "mrsang@iohub.dev" }, - "version": "0.0.17-a", + "version": "0.0.18-a", "category": "Development", "iconclass": "fa fa-cog", "mimes": [ diff --git a/libantosdk/build/release/libantosdk.zip b/libantosdk/build/release/libantosdk.zip index 6043e36..ee93e6d 100644 Binary files a/libantosdk/build/release/libantosdk.zip and b/libantosdk/build/release/libantosdk.zip differ diff --git a/libantosdk/core/ts/antos.d.ts b/libantosdk/core/ts/antos.d.ts index 73a82a3..efc6b63 100644 --- a/libantosdk/core/ts/antos.d.ts +++ b/libantosdk/core/ts/antos.d.ts @@ -475,6 +475,7 @@ declare namespace OS { * @extends {BaseDialog} */ class BasicDialog extends BaseDialog { + ['constructor']: typeof BasicDialog; /** * Placeholder for the UI scheme to be rendered. This can * be either the string definition of the scheme or @@ -4421,6 +4422,14 @@ declare namespace OS { * @memberof ListViewTag */ private _selectedItems; + /** + * The anchor element that the list view positioned on + * This is helpful when rendering dropdown list + * @private + * @type{HTMLElement} + * @memberof ListViewTag + */ + private _anchor; /** * Data placeholder of the list * @@ -5539,6 +5548,18 @@ declare namespace OS { * @memberof LabelTag */ set iconclass(v: string); + /** + * Set the CSS class of the label icon on the right side + * + * @memberof LabelTag + */ + set iconclass_end(v: string); + /** + * Set the CSS class of the label icon on the right side + * + * @memberof LabelTag + */ + set iconclass$(v: string); /** * Setter: Set the text of the label * @@ -6295,13 +6316,25 @@ declare namespace OS { * @memberof GridViewTag */ delete(row: GridRowTag): void; + /** + * Scroll the grid view to bottom + * + * @memberof GridViewTag + */ + scroll_to_bottom(): void; + /** + * Scroll the grid view to top + * + * @memberof GridViewTag + */ + scroll_to_top(): void; /** * Push a row to the grid * * @param {GenericObject[]} row list of cell data * @param {boolean} flag indicates where the row is add to beginning or end * of the row - * @memberof GridViewTags + * @memberof GridViewTag */ push(row: GenericObject[], flag: boolean): void; /** @@ -6622,6 +6655,19 @@ declare namespace OS { * @memberof ButtonTag */ set iconclass(v: string); + /** + * Set the icon class on the right side of the button, this property + * allows to style the button icon using CSS + * + * @memberof ButtonTag + */ + set iconclass$(v: string); + /** + * Set the CSS class of the label icon on the right side + * + * @memberof ButtonTag + */ + set iconclass_end(v: string); /** * Setter: Set the text of the button * diff --git a/libantosdk/package.json b/libantosdk/package.json index 17289e9..da101de 100644 --- a/libantosdk/package.json +++ b/libantosdk/package.json @@ -7,7 +7,7 @@ "author": "Xuan Sang LE", "email": "mrsang@iohub.dev" }, - "version": "0.0.17-a", + "version": "0.0.18-a", "category": "Development", "iconclass": "fa fa-cog", "mimes": [ diff --git a/packages.json b/packages.json index 735d39e..0b1fc65 100644 --- a/packages.json +++ b/packages.json @@ -225,7 +225,7 @@ "description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/libantosdk/README.md", "category": "Development", "author": "Xuan Sang LE", - "version": "0.0.17-a", + "version": "0.0.18-a", "dependencies": [], "download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/libantosdk/build/release/libantosdk.zip" },