diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/bootstrap-table-accent-neutralise.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/bootstrap-table-accent-neutralise.js new file mode 100644 index 0000000000..42c8d2896f --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/bootstrap-table-accent-neutralise.js @@ -0,0 +1,170 @@ +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @update: zhixin wen + */ + +!($ => { + const diacriticsMap = {} + const defaultAccentsDiacritics = [ + {base: 'A', letters: '\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F'}, + {base: 'AA',letters: '\uA732'}, + {base: 'AE',letters: '\u00C6\u01FC\u01E2'}, + {base: 'AO',letters: '\uA734'}, + {base: 'AU',letters: '\uA736'}, + {base: 'AV',letters: '\uA738\uA73A'}, + {base: 'AY',letters: '\uA73C'}, + {base: 'B', letters: '\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181'}, + {base: 'C', letters: '\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E'}, + {base: 'D', letters: '\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779'}, + {base: 'DZ',letters: '\u01F1\u01C4'}, + {base: 'Dz',letters: '\u01F2\u01C5'}, + {base: 'E', letters: '\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E'}, + {base: 'F', letters: '\u0046\u24BB\uFF26\u1E1E\u0191\uA77B'}, + {base: 'G', letters: '\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E'}, + {base: 'H', letters: '\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D'}, + {base: 'I', letters: '\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197'}, + {base: 'J', letters: '\u004A\u24BF\uFF2A\u0134\u0248'}, + {base: 'K', letters: '\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2'}, + {base: 'L', letters: '\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780'}, + {base: 'LJ',letters: '\u01C7'}, + {base: 'Lj',letters: '\u01C8'}, + {base: 'M', letters: '\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C'}, + {base: 'N', letters: '\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4'}, + {base: 'NJ',letters: '\u01CA'}, + {base: 'Nj',letters: '\u01CB'}, + {base: 'O', letters: '\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C'}, + {base: 'OI',letters: '\u01A2'}, + {base: 'OO',letters: '\uA74E'}, + {base: 'OU',letters: '\u0222'}, + {base: 'OE',letters: '\u008C\u0152'}, + {base: 'oe',letters: '\u009C\u0153'}, + {base: 'P', letters: '\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754'}, + {base: 'Q', letters: '\u0051\u24C6\uFF31\uA756\uA758\u024A'}, + {base: 'R', letters: '\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782'}, + {base: 'S', letters: '\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784'}, + {base: 'T', letters: '\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786'}, + {base: 'TZ',letters: '\uA728'}, + {base: 'U', letters: '\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244'}, + {base: 'V', letters: '\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245'}, + {base: 'VY',letters: '\uA760'}, + {base: 'W', letters: '\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72'}, + {base: 'X', letters: '\u0058\u24CD\uFF38\u1E8A\u1E8C'}, + {base: 'Y', letters: '\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE'}, + {base: 'Z', letters: '\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762'}, + {base: 'a', letters: '\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250'}, + {base: 'aa',letters: '\uA733'}, + {base: 'ae',letters: '\u00E6\u01FD\u01E3'}, + {base: 'ao',letters: '\uA735'}, + {base: 'au',letters: '\uA737'}, + {base: 'av',letters: '\uA739\uA73B'}, + {base: 'ay',letters: '\uA73D'}, + {base: 'b', letters: '\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253'}, + {base: 'c', letters: '\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184'}, + {base: 'd', letters: '\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A'}, + {base: 'dz',letters: '\u01F3\u01C6'}, + {base: 'e', letters: '\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD'}, + {base: 'f', letters: '\u0066\u24D5\uFF46\u1E1F\u0192\uA77C'}, + {base: 'g', letters: '\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F'}, + {base: 'h', letters: '\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265'}, + {base: 'hv',letters: '\u0195'}, + {base: 'i', letters: '\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131'}, + {base: 'j', letters: '\u006A\u24D9\uFF4A\u0135\u01F0\u0249'}, + {base: 'k', letters: '\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3'}, + {base: 'l', letters: '\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747'}, + {base: 'lj',letters: '\u01C9'}, + {base: 'm', letters: '\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F'}, + {base: 'n', letters: '\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5'}, + {base: 'nj',letters: '\u01CC'}, + {base: 'o', letters: '\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275'}, + {base: 'oi',letters: '\u01A3'}, + {base: 'ou',letters: '\u0223'}, + {base: 'oo',letters: '\uA74F'}, + {base: 'p',letters: '\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755'}, + {base: 'q',letters: '\u0071\u24E0\uFF51\u024B\uA757\uA759'}, + {base: 'r',letters: '\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783'}, + {base: 's',letters: '\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B'}, + {base: 't',letters: '\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787'}, + {base: 'tz',letters: '\uA729'}, + {base: 'u',letters: '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289'}, + {base: 'v',letters: '\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C'}, + {base: 'vy',letters: '\uA761'}, + {base: 'w',letters: '\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73'}, + {base: 'x',letters: '\u0078\u24E7\uFF58\u1E8B\u1E8D'}, + {base: 'y',letters: '\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF'}, + {base: 'z',letters: '\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763'} + ] + + const initNeutraliser = () => { + for (const diacritic of defaultAccentsDiacritics) { + const letters = diacritic.letters + for (let i = 0; i < letters.length; i++) { + diacriticsMap[letters[i]] = diacritic.base + } + } + } + + /* eslint-disable no-control-regex */ + const removeDiacritics = str => str.replace(/[^\u0000-\u007E]/g, a => diacriticsMap[a] || a) + + $.extend($.fn.bootstrapTable.defaults, { + searchAccentNeutralise: false + }) + + $.BootstrapTable = class extends $.BootstrapTable { + init () { + if (this.options.searchAccentNeutralise) { + initNeutraliser() + } + super.init() + } + + initSearch () { + if (this.options.sidePagination !== 'server') { + let s = this.searchText && this.searchText.toLowerCase() + const f = $.isEmptyObject(this.filterColumns) ? null : this.filterColumns + + // Check filter + this.data = f ? this.options.data.filter((item, i) => { + for (const key in f) { + if (item[key] !== f[key]) { + return false + } + } + return true + }) : this.options.data + + this.data = s ? this.options.data.filter((item, i) => { + for (let [key, value] of Object.entries(item)) { + key = $.isNumeric(key) ? parseInt(key, 10) : key + const column = this.columns[this.fieldsColumnsIndex[key]] + const j = this.header.fields.indexOf(key) + + if (column && column.searchFormatter) { + value = $.fn.bootstrapTable.utils.calculateObjectValue(column, + this.header.formatters[j], [value, item, i], value) + } + + const index = this.header.fields.indexOf(key) + if (index !== -1 && this.header.searchables[index] && typeof value === 'string') { + if (this.options.searchAccentNeutralise) { + value = removeDiacritics(value) + s = removeDiacritics(s) + } + if (this.options.strictSearch) { + if ((`${value}`).toLowerCase() === s) { + return true + } + } else { + if ((`${value}`).toLowerCase().includes(s)) { + return true + } + } + } + } + return false + }) : this.data + } + } + } +})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/extension.json new file mode 100644 index 0000000000..ce69cfdb5f --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/accent-neutralise/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Accent Neutralise", + "version": "1.0.0", + "description": "Plugin to neutralise the words.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/accent-neutralise", + "example": "#", + + "plugins": [{ + "name": "bootstrap-table-accent-neutralise", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/accent-neutralise" + }], + + "author": { + "name": "djhvscf", + "image": "https://avatars1.githubusercontent.com/u/4496763" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/addrbar/bootstrap-table-addrbar.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/addrbar/bootstrap-table-addrbar.js new file mode 100644 index 0000000000..e33307f565 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/addrbar/bootstrap-table-addrbar.js @@ -0,0 +1,116 @@ +/** + * @author: general + * @website: note.generals.space + * @email: generals.space@gmail.com + * @github: https://github.com/generals-space/bootstrap-table-addrbar + * @update: zhixin wen + */ + +($ => { + /* + * function: 获取浏览器地址栏中的指定参数. + * key: 参数名 + * url: 默认为当前地址栏 + */ + function _GET (key, url = window.location.search) { + /* + * 注意这里正则表达式的书写方法 + * (^|&)key匹配: 直接以key开始或以&key开始的字符串 + * 同理(&|$)表示以&结束或是直接结束的字符串 + * ...当然, 我并不知道这种用法. + */ + const reg = new RegExp(`(^|&)${key}=([^&]*)(&|$)`) + const result = url.substr(1).match(reg) + + if (result) { + return decodeURIComponent(result[2]) + } + return null + } + + /* + * function: 根据给定参数生成url地址 + * var dic = {name: 'genreal', age: 24} + * var url = 'https://www.baidu.com?age=22'; + * _buildUrl(dic, url); + * 将得到"https://www.baidu.com?age=24&name=genreal" + * 哦, 忽略先后顺序吧... + * + * 补充: 可以参考浏览器URLSearchParams对象, 更加方便和强大. + * 考虑到兼容性, 暂时不使用这个工具. + */ + + function _buildUrl (dict, url = window.location.search) { + for (const [key, val] of Object.entries(dict)) { + // 搜索name=general这种形式的字符串(&是分隔符) + const pattern = `${key}=([^&]*)` + const targetStr = `${key}=${val}` + + /* + * 如果目标url中包含了key键, 我们需要将它替换成我们自己的val + * 不然就直接添加好了. + */ + if (url.match(pattern)) { + const tmp = new RegExp(`(${key}=)([^&]*)`, 'gi') + url = url.replace(tmp, targetStr) + } else { + const seperator = url.match('[?]') ? '&' : '?' + url = url + seperator + targetStr + } + } + if (location.hash) { + url += location.hash + } + return url + } + + $.BootstrapTable = class extends $.BootstrapTable { + init () { + // 拥有addrbar选项并且其值为true的才会继续执行 + if (this.options.addrbar) { + // 标志位, 初始加载后关闭 + this.addrbarInit = true + const _prefix = this.options.addrPrefix || '' + + // 优先级排序: 用户指定值最优先, 未指定时从地址栏获取, 未获取到时采用默认值 + this.options.pageSize = this.options.pageSize || ( + _GET(`${_prefix}limit`) ? parseInt(_GET(`${_prefix}limit`)) : $.BootstrapTable.DEFAULTS.pageSize + ) + this.options.pageNumber = this.options.pageNumber || ( + _GET(`${_prefix}page`) ? parseInt(_GET(`${_prefix}page`)) : $.BootstrapTable.DEFAULTS.pageNumber + ) + this.options.sortOrder = this.options.sortOrder || ( + _GET(`${_prefix}order`) ? _GET(`${_prefix}order`) : $.BootstrapTable.DEFAULTS.sortOrder + ) + this.options.sortName = this.options.sortName || ( + _GET(`${_prefix}sort`) ? _GET(`${_prefix}sort`) : 'id' + ) + this.options.searchText = this.options.searchText || ( + _GET(`${_prefix}search`) ? _GET(`${_prefix}search`) : $.BootstrapTable.DEFAULTS.searchText + ) + + const _onLoadSuccess = this.options.onLoadSuccess + + this.options.onLoadSuccess = data => { + if (this.addrbarInit) { + this.addrbarInit = false + } else { + const params = {} + params[`${_prefix}page`] = this.options.pageNumber, + params[`${_prefix}limit`] = this.options.pageSize, + params[`${_prefix}order`] = this.options.sortOrder, + params[`${_prefix}sort`] = this.options.sortName, + params[`${_prefix}search`] = this.options.searchText + // h5提供的修改浏览器地址栏的方法 + window.history.pushState({}, '', _buildUrl(params)) + } + + if (_onLoadSuccess) { + _onLoadSuccess.call(this, data) + } + } + } + super.init() + } + } +})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/auto-refresh/bootstrap-table-auto-refresh.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/auto-refresh/bootstrap-table-auto-refresh.js new file mode 100644 index 0000000000..42edf633e9 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/auto-refresh/bootstrap-table-auto-refresh.js @@ -0,0 +1,78 @@ +/** + * @author: Alec Fenichel + * @webSite: https://fenichelar.com + * @update: zhixin wen + */ + +($ => { + const Utils = $.fn.bootstrapTable.utils + + $.extend($.fn.bootstrapTable.defaults, { + autoRefresh: false, + autoRefreshInterval: 60, + autoRefreshSilent: true, + autoRefreshStatus: true, + autoRefreshFunction: null + }) + + $.extend($.fn.bootstrapTable.defaults.icons, { + autoRefresh: Utils.bootstrapVersion === 4 ? 'fa-clock' : 'glyphicon-time icon-time' + }) + + $.extend($.fn.bootstrapTable.locales, { + formatAutoRefresh () { + return 'Auto Refresh' + } + }) + + $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales) + + $.BootstrapTable = class extends $.BootstrapTable { + init (...args) { + super.init(...args) + + if (this.options.autoRefresh && this.options.autoRefreshStatus) { + this.options.autoRefreshFunction = setInterval(() => { + this.refresh({silent: this.options.autoRefreshSilent}) + }, this.options.autoRefreshInterval * 1000) + } + } + + initToolbar (...args) { + super.initToolbar(...args) + + if (this.options.autoRefresh) { + const $btnGroup = this.$toolbar.find('>.btn-group') + let $btnAutoRefresh = $btnGroup.find('.auto-refresh') + + if (!$btnAutoRefresh.length) { + $btnAutoRefresh = $(` + + `).appendTo($btnGroup) + + $btnAutoRefresh.on('click', $.proxy(this.toggleAutoRefresh, this)) + } + } + } + + toggleAutoRefresh () { + if (this.options.autoRefresh) { + if (this.options.autoRefreshStatus) { + clearInterval(this.options.autoRefreshFunction) + this.$toolbar.find('>.btn-group').find('.auto-refresh').removeClass('active') + } else { + this.options.autoRefreshFunction = setInterval(() => { + this.refresh({silent: this.options.autoRefreshSilent}) + }, this.options.autoRefreshInterval * 1000) + this.$toolbar.find('>.btn-group').find('.auto-refresh').addClass('active') + } + this.options.autoRefreshStatus = !this.options.autoRefreshStatus + } + } + } +})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/auto-refresh/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/auto-refresh/extension.json new file mode 100644 index 0000000000..182561bc1e --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/auto-refresh/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Auto Refresh", + "version": "1.0.0", + "description": "Plugin to automatically refresh the table on an interval.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/auto-refresh", + "example": "#", + + "plugins": [{ + "name": "bootstrap-table-auto-refresh", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/auto-refresh" + }], + + "author": { + "name": "fenichaler", + "image": "https://avatars.githubusercontent.com/u/3437075" + } +} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/cookie/bootstrap-table-cookie.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/cookie/bootstrap-table-cookie.js new file mode 100644 index 0000000000..09447880ea --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/cookie/bootstrap-table-cookie.js @@ -0,0 +1,401 @@ +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @update zhixin wen + */ + +($ => { + const UtilsCookie = { + cookieIds: { + sortOrder: 'bs.table.sortOrder', + sortName: 'bs.table.sortName', + pageNumber: 'bs.table.pageNumber', + pageList: 'bs.table.pageList', + columns: 'bs.table.columns', + searchText: 'bs.table.searchText', + filterControl: 'bs.table.filterControl', + filterBy: 'bs.table.filterBy' + }, + getCurrentHeader (that) { + let header = that.$header + if (that.options.height) { + header = that.$tableHeader + } + + return header + }, + getCurrentSearchControls (that) { + let searchControls = 'select, input' + if (that.options.height) { + searchControls = 'table select, table input' + } + + return searchControls + }, + cookieEnabled () { + return !!(navigator.cookieEnabled) + }, + inArrayCookiesEnabled (cookieName, cookiesEnabled) { + let index = -1 + + for (let i = 0; i < cookiesEnabled.length; i++) { + if (cookieName.toLowerCase() === cookiesEnabled[i].toLowerCase()) { + index = i + break + } + } + + return index + }, + setCookie (that, cookieName, cookieValue) { + if ((!that.options.cookie) || (!UtilsCookie.cookieEnabled()) || (that.options.cookieIdTable === '')) { + return + } + + if (UtilsCookie.inArrayCookiesEnabled(cookieName, that.options.cookiesEnabled) === -1) { + return + } + + cookieName = `${that.options.cookieIdTable}.${cookieName}` + + switch (that.options.cookieStorage) { + case 'cookieStorage': + document.cookie = [ + cookieName, '=', encodeURIComponent(cookieValue), + `; expires=${UtilsCookie.calculateExpiration(that.options.cookieExpire)}`, + that.options.cookiePath ? `; path=${that.options.cookiePath}` : '', + that.options.cookieDomain ? `; domain=${that.options.cookieDomain}` : '', + that.options.cookieSecure ? '; secure' : '' + ].join('') + break + case 'localStorage': + localStorage.setItem(cookieName, cookieValue) + break + case 'sessionStorage': + sessionStorage.setItem(cookieName, cookieValue) + break + default: + return false + } + + return true + }, + getCookie (that, tableName, cookieName) { + if (!cookieName) { + return null + } + + if (UtilsCookie.inArrayCookiesEnabled(cookieName, that.options.cookiesEnabled) === -1) { + return null + } + + cookieName = `${tableName}.${cookieName}` + + switch (that.options.cookieStorage) { + case 'cookieStorage': + const value = `; ${document.cookie}` + const parts = value.split(`; ${cookieName}=`) + return parts.length === 2 ? decodeURIComponent(parts.pop().split(';').shift()) : null + case 'localStorage': + return localStorage.getItem(cookieName) + case 'sessionStorage': + return sessionStorage.getItem(cookieName) + default: + return null + } + }, + deleteCookie (that, tableName, cookieName) { + cookieName = `${tableName}.${cookieName}` + + switch (that.options.cookieStorage) { + case 'cookieStorage': + document.cookie = [ + encodeURIComponent(cookieName), '=', + '; expires=Thu, 01 Jan 1970 00:00:00 GMT', + that.options.cookiePath ? `; path=${that.options.cookiePath}` : '', + that.options.cookieDomain ? `; domain=${that.options.cookieDomain}` : '' + ].join('') + break + case 'localStorage': + localStorage.removeItem(cookieName) + break + case 'sessionStorage': + sessionStorage.removeItem(cookieName) + break + default: + return false + } + return true + }, + calculateExpiration (cookieExpire) { + const time = cookieExpire.replace(/[0-9]*/, '') // s,mi,h,d,m,y + cookieExpire = cookieExpire.replace(/[A-Za-z]{1,2}/, '') // number + + switch (time.toLowerCase()) { + case 's': + cookieExpire = +cookieExpire + break + case 'mi': + cookieExpire *= 60 + break + case 'h': + cookieExpire = cookieExpire * 60 * 60 + break + case 'd': + cookieExpire = cookieExpire * 24 * 60 * 60 + break + case 'm': + cookieExpire = cookieExpire * 30 * 24 * 60 * 60 + break + case 'y': + cookieExpire = cookieExpire * 365 * 24 * 60 * 60 + break + default: + cookieExpire = undefined + break + } + if (!cookieExpire) { + return '' + } + const d = new Date() + d.setTime(d.getTime() + cookieExpire * 1000) + return d.toGMTString() + }, + initCookieFilters (bootstrapTable) { + setTimeout(() => { + const parsedCookieFilters = JSON.parse(UtilsCookie.getCookie(bootstrapTable, bootstrapTable.options.cookieIdTable, UtilsCookie.cookieIds.filterControl)) + + if (!bootstrapTable.options.filterControlValuesLoaded && parsedCookieFilters) { + + const cachedFilters = {} + const header = UtilsCookie.getCurrentHeader(bootstrapTable) + const searchControls = UtilsCookie.getCurrentSearchControls(bootstrapTable) + + const applyCookieFilters = (element, filteredCookies) => { + $(filteredCookies).each((i, cookie) => { + if (cookie.text !== '') { + $(element).val(cookie.text) + cachedFilters[cookie.field] = cookie.text + } + }) + } + + header.find(searchControls).each(function () { + const field = $(this).closest('[data-field]').data('field') + const filteredCookies = parsedCookieFilters.filter(cookie => cookie.field === field) + + applyCookieFilters(this, filteredCookies) + }) + + bootstrapTable.initColumnSearch(cachedFilters) + bootstrapTable.options.filterControlValuesLoaded = true + bootstrapTable.initServer() + } + }, 250) + } + } + + $.extend($.fn.bootstrapTable.defaults, { + cookie: false, + cookieExpire: '2h', + cookiePath: null, + cookieDomain: null, + cookieSecure: null, + cookieIdTable: '', + cookiesEnabled: [ + 'bs.table.sortOrder', 'bs.table.sortName', + 'bs.table.pageNumber', 'bs.table.pageList', + 'bs.table.columns', 'bs.table.searchText', + 'bs.table.filterControl', 'bs.table.filterBy' + ], + cookieStorage: 'cookieStorage', // localStorage, sessionStorage + // internal variable + filterControls: [], + filterControlValuesLoaded: false + }) + + $.fn.bootstrapTable.methods.push('getCookies') + $.fn.bootstrapTable.methods.push('deleteCookie') + + $.extend($.fn.bootstrapTable.utils, { + setCookie: UtilsCookie.setCookie, + getCookie: UtilsCookie.getCookie + }) + + $.BootstrapTable = class extends $.BootstrapTable { + init () { + // FilterBy logic + const filterByCookie = JSON.parse(UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.filterBy)) + this.filterColumns = filterByCookie ? filterByCookie : {} + + // FilterControl logic + this.options.filterControls = [] + this.options.filterControlValuesLoaded = false + + this.options.cookiesEnabled = typeof this.options.cookiesEnabled === 'string' ? + this.options.cookiesEnabled.replace('[', '').replace(']', '') + .replace(/ /g, '').toLowerCase().split(',') : + this.options.cookiesEnabled + + if (this.options.filterControl) { + const that = this + this.$el.on('column-search.bs.table', (e, field, text) => { + let isNewField = true + + for (let i = 0; i < that.options.filterControls.length; i++) { + if (that.options.filterControls[i].field === field) { + that.options.filterControls[i].text = text + isNewField = false + break + } + } + if (isNewField) { + that.options.filterControls.push({ + field, + text + }) + } + + UtilsCookie.setCookie(that, UtilsCookie.cookieIds.filterControl, JSON.stringify(that.options.filterControls)) + }).on('created-controls.bs.table', UtilsCookie.initCookieFilters(that)) + } + super.init() + } + + initServer (...args) { + if ( + this.options.cookie && + this.options.filterControl && + !this.options.filterControlValuesLoaded + ) { + const cookie = JSON.parse(UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.filterControl)) + if (cookie) { + return + } + } + super.initServer(...args) + } + + initTable (...args) { + super.initTable(...args) + this.initCookie() + } + + onSort (...args) { + super.onSort(...args) + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.sortOrder, this.options.sortOrder) + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.sortName, this.options.sortName) + } + + onPageNumber (...args) { + super.onPageNumber(...args) + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, this.options.pageNumber) + } + + onPageListChange (...args) { + super.onPageListChange(...args) + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageList, this.options.pageSize) + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, this.options.pageNumber) + } + + onPagePre (...args) { + super.onPagePre(...args) + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, this.options.pageNumber) + } + + onPageNext (...args) { + super.onPageNext(...args) + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, this.options.pageNumber) + } + + toggleColumn (...args) { + super.toggleColumn(...args) + + const visibleColumns = [] + + $.each(this.columns, (i, column) => { + if (column.visible) { + visibleColumns.push(column.field) + } + }) + + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.columns, JSON.stringify(visibleColumns)) + } + + selectPage (page) { + super.selectPage(page) + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, page) + } + + onSearch (event) { + super.onSearch(event) + + if ($(event.currentTarget).parent().hasClass('search')) { + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.searchText, this.searchText) + } + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, this.options.pageNumber) + } + + filterBy (...args) { + super.filterBy(...args) + UtilsCookie.setCookie(this, UtilsCookie.cookieIds.filterBy, JSON.stringify(this.filterColumns)) + } + + initCookie () { + if (!this.options.cookie) { + return + } + + if ((this.options.cookieIdTable === '') || (this.options.cookieExpire === '') || (!UtilsCookie.cookieEnabled())) { + console.error('Configuration error. Please review the cookieIdTable and the cookieExpire property. If the properties are correct, then this browser does not support cookies.') + this.options.cookie = false // Make sure that the cookie extension is disabled + return + } + + const sortOrderCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortOrder) + const sortOrderNameCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortName) + const pageNumberCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.pageNumber) + const pageListCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.pageList) + const columnsCookie = JSON.parse(UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.columns)) + const searchTextCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.searchText) + + // sortOrder + this.options.sortOrder = sortOrderCookie ? sortOrderCookie : this.options.sortOrder + // sortName + this.options.sortName = sortOrderNameCookie ? sortOrderNameCookie : this.options.sortName + // pageNumber + this.options.pageNumber = pageNumberCookie ? +pageNumberCookie : this.options.pageNumber + // pageSize + this.options.pageSize = pageListCookie ? pageListCookie === this.options.formatAllRows() ? pageListCookie : +pageListCookie : this.options.pageSize + // searchText + this.options.searchText = searchTextCookie ? searchTextCookie : '' + + if (columnsCookie) { + $.each(this.columns, (i, column) => { + column.visible = $.inArray(column.field, columnsCookie) !== -1 + }) + } + } + + getCookies () { + const bootstrapTable = this + const cookies = {} + $.each(UtilsCookie.cookieIds, (key, value) => { + cookies[key] = UtilsCookie.getCookie(bootstrapTable, bootstrapTable.options.cookieIdTable, value) + if (key === 'columns') { + cookies[key] = JSON.parse(cookies[key]) + } + }) + return cookies + } + + deleteCookie (cookieName) { + if ((cookieName === '') || (!UtilsCookie.cookieEnabled())) { + return + } + + UtilsCookie.deleteCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds[cookieName]) + } + } + +})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/cookie/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/cookie/extension.json new file mode 100644 index 0000000000..6422de402a --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/cookie/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Cookie", + "version": "1.2.1", + "description": "Plugin to use the cookie of the browser.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/cookie", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/cookie.html", + + "plugins": [{ + "name": "bootstrap-table-cookie", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/cookie" + }], + + "author": { + "name": "djhvscf", + "image": "https://avatars1.githubusercontent.com/u/4496763" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/copy-rows/bootstrap-table-copy-rows.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/copy-rows/bootstrap-table-copy-rows.js new file mode 100644 index 0000000000..78f62d61d4 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/copy-rows/bootstrap-table-copy-rows.js @@ -0,0 +1,102 @@ +/** + * @author Homer Glascock + * @version: v1.0.0 + */ + + !function ($) { + "use strict"; + + var calculateObjectValue = $.fn.bootstrapTable.utils.calculateObjectValue, + sprintf = $.fn.bootstrapTable.utils.sprintf; + + var copytext = function (text) { + var textField = document.createElement('textarea'); + $(textField).html(text); + document.body.appendChild(textField); + textField.select(); + + try { + document.execCommand('copy'); + } + catch (e) { + console.log("Oops, unable to copy"); + } + $(textField).remove(); + }; + + $.extend($.fn.bootstrapTable.defaults, { + copyBtn: false, + copyWHiddenBtn: false, + copyDelemeter: ", " + }); + + $.fn.bootstrapTable.methods.push('copyColumnsToClipboard', 'copyColumnsToClipboardWithHidden'); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _initToolbar = BootstrapTable.prototype.initToolbar; + + BootstrapTable.prototype.initToolbar = function () { + + _initToolbar.apply(this, Array.prototype.slice.apply(arguments)); + + var that = this, + $btnGroup = this.$toolbar.find('>.btn-group'); + + if (this.options.clickToSelect || this.options.singleSelect) { + + if (this.options.copyBtn) { + var copybtn = ""; + $btnGroup.append(copybtn); + $btnGroup.find('#copyBtn').click(function () { that.copyColumnsToClipboard(); }); + } + + if (this.options.copyWHiddenBtn) { + var copyhiddenbtn = ""; + $btnGroup.append(copyhiddenbtn); + $btnGroup.find('#copyWHiddenBtn').click(function () { that.copyColumnsToClipboardWithHidden(); }); + } + } + }; + + BootstrapTable.prototype.copyColumnsToClipboard = function () { + var that = this, + ret = "", + delimet = this.options.copyDelemeter; + + $.each(that.getSelections(), function (index, row) { + $.each(that.options.columns[0], function (indy, column) { + if (column.field !== "state" && column.field !== "RowNumber" && column.visible) { + if (row[column.field] !== null) { + ret += calculateObjectValue(column, that.header.formatters[indy], [row[column.field], row, index], row[column.field]); + } + ret += delimet; + } + }); + + ret += "\r\n"; + }); + + copytext(ret); + }; + + BootstrapTable.prototype.copyColumnsToClipboardWithHidden = function () { + var that = this, + ret = "", + delimet = this.options.copyDelemeter; + + $.each(that.getSelections(), function (index, row) { + $.each(that.options.columns[0], function (indy, column) { + if (column.field != "state" && column.field !== "RowNumber") { + if (row[column.field] !== null) { + ret += calculateObjectValue(column, that.header.formatters[indy], [row[column.field], row, index], row[column.field]); + } + ret += delimet; + } + }); + + ret += "\r\n"; + }); + + copytext(ret); + }; +}(jQuery); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/copy-rows/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/copy-rows/extension.json new file mode 100644 index 0000000000..4deb78b288 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/copy-rows/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Copy Rows", + "version": "1.0.0", + "description": "Allows pushing of selected column data to the clipboard.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/copy-rows", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/copy-rows.html", + + "plugins": [{ + "name": "copy-rows", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/copy-rows" + }], + + "author": { + "name": "Homer Glascock", + "image": "https://avatars1.githubusercontent.com/u/5546710" + } +} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/defer-url/bootstrap-table-defer-url.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/defer-url/bootstrap-table-defer-url.js new file mode 100644 index 0000000000..25d2a17358 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/defer-url/bootstrap-table-defer-url.js @@ -0,0 +1,32 @@ +/** + * When using server-side processing, the default mode of operation for + * bootstrap-table is to simply throw away any data that currently exists in the + * table and make a request to the server to get the first page of data to + * display. This is fine for an empty table, but if you already have the first + * page of data displayed in the plain HTML, it is a waste of resources. As + * such, you can use data-defer-url instead of data-url to allow you to instruct + * bootstrap-table to not make that initial request, rather it will use the data + * already on the page. + * + * @author: Ruben Suarez + * @webSite: http://rubensa.eu.org + * @version: v1.0.0 + */ + +(function($) { + 'use strict'; + + $.extend($.fn.bootstrapTable.defaults, { + deferUrl : undefined + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, _init = BootstrapTable.prototype.init; + + BootstrapTable.prototype.init = function() { + _init.apply(this, Array.prototype.slice.apply(arguments)); + + if (this.options.deferUrl) { + this.options.url = this.options.deferUrl; + } + } +})(jQuery); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/defer-url/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/defer-url/extension.json new file mode 100644 index 0000000000..4895fb21d0 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/defer-url/extension.json @@ -0,0 +1,17 @@ +{ + "name": "DeferURL", + "version": "1.0.0", + "description": "Plugin to defer server side processing.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/defer-url", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/defer-url.html", + + "plugins": [{ + "name": "bootstrap-table-defer-url", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/defer-url" + }], + + "author": { + "name": "rubensa", + "image": "https://avatars1.githubusercontent.com/u/1469340" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/editable/bootstrap-table-editable.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/editable/bootstrap-table-editable.js new file mode 100644 index 0000000000..134b5546ca --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/editable/bootstrap-table-editable.js @@ -0,0 +1,149 @@ +/** + * @author zhixin wen + * extensions: https://github.com/vitalets/x-editable + */ + +($ => { + const Utils = $.fn.bootstrapTable.utils + + $.extend($.fn.bootstrapTable.defaults, { + editable: true, + onEditableInit () { + return false + }, + onEditableSave (field, row, oldValue, $el) { + return false + }, + onEditableShown (field, row, $el, editable) { + return false + }, + onEditableHidden (field, row, $el, reason) { + return false + } + }) + + $.extend($.fn.bootstrapTable.Constructor.EVENTS, { + 'editable-init.bs.table': 'onEditableInit', + 'editable-save.bs.table': 'onEditableSave', + 'editable-shown.bs.table': 'onEditableShown', + 'editable-hidden.bs.table': 'onEditableHidden' + }) + + $.BootstrapTable = class extends $.BootstrapTable { + initTable () { + super.initTable() + + if (!this.options.editable) { + return + } + + $.each(this.columns, (i, column) => { + if (!column.editable) { + return + } + + const editableOptions = {} + const editableDataMarkup = [] + const editableDataPrefix = 'editable-' + const processDataOptions = (key, value) => { + // Replace camel case with dashes. + const dashKey = key.replace(/([A-Z])/g, $1 => `-${$1.toLowerCase()}`) + if (dashKey.indexOf(editableDataPrefix) === 0) { + editableOptions[dashKey.replace(editableDataPrefix, 'data-')] = value + } + } + + $.each(this.options, processDataOptions) + + column.formatter = column.formatter || (value => value) + column._formatter = column._formatter ? column._formatter : column.formatter + column.formatter = (value, row, index) => { + const result = Utils.calculateObjectValue(column, + column._formatter, [value, row, index], value) + + $.each(column, processDataOptions) + + $.each(editableOptions, (key, value) => { + editableDataMarkup.push(` ${key}="${value}"`) + }) + + let _dont_edit_formatter = false + if (column.editable.hasOwnProperty('noeditFormatter')) { + _dont_edit_formatter = column.editable.noeditFormatter(value, row, index) + } + + if (_dont_edit_formatter === false) { + return `` + } + return _dont_edit_formatter + } + }) + } + + initBody (fixedScroll) { + super.initBody(fixedScroll) + + if (!this.options.editable) { + return + } + + $.each(this.columns, (i, column) => { + if (!column.editable) { + return + } + + const data = this.getData() + const $field = this.$body.find(`a[data-name="${column.field}"]`) + + $field.each((i, element) => { + const $element = $(element) + const $tr = $element.closest('tr') + const index = $tr.data('index') + const row = data[index] + + const editableOpts = Utils.calculateObjectValue(column, + column.editable, [index, row, $element], {}) + + $element.editable(editableOpts) + }) + + $field.off('save').on('save', ({currentTarget}, {submitValue}) => { + const $this = $(currentTarget) + const data = this.getData() + const index = $this.parents('tr[data-index]').data('index') + const row = data[index] + const oldValue = row[column.field] + + $this.data('value', submitValue) + row[column.field] = submitValue + this.trigger('editable-save', column.field, row, oldValue, $this) + this.resetFooter() + }) + + $field.off('shown').on('shown', ({currentTarget}, editable) => { + const $this = $(currentTarget) + const data = this.getData() + const index = $this.parents('tr[data-index]').data('index') + const row = data[index] + + this.trigger('editable-shown', column.field, row, $this, editable) + }) + + $field.off('hidden').on('hidden', ({currentTarget}, reason) => { + const $this = $(currentTarget) + const data = this.getData() + const index = $this.parents('tr[data-index]').data('index') + const row = data[index] + + this.trigger('editable-hidden', column.field, row, $this, reason) + }) + }) + this.trigger('editable-init') + } + } + +})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/editable/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/editable/extension.json new file mode 100644 index 0000000000..ea6106b693 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/editable/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Table Editable", + "version": "1.1.0", + "description": "Use the x-editable to in-place editing your table.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/editable", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/editable.html", + + "plugins": [{ + "name": "x-editable", + "url": "https://github.com/vitalets/x-editable" + }], + + "author": { + "name": "wenzhixin", + "image": "https://avatars1.githubusercontent.com/u/2117018" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/export/bootstrap-table-export.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/export/bootstrap-table-export.js new file mode 100644 index 0000000000..2f07479ba5 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/export/bootstrap-table-export.js @@ -0,0 +1,225 @@ +/** + * @author zhixin wen + * extensions: https://github.com/hhurz/tableExport.jquery.plugin + */ + +($ => { + const Utils = $.fn.bootstrapTable.utils + + const bootstrap = { + 3: { + icons: { + export: 'glyphicon-export icon-share' + }, + html: { + dropmenu: '', + dropitem: '
  • %s
  • ' + } + }, + 4: { + icons: { + export: 'fa-download' + }, + html: { + dropmenu: '', + dropitem: '%s' + } + } + }[Utils.bootstrapVersion] + + const TYPE_NAME = { + json: 'JSON', + xml: 'XML', + png: 'PNG', + csv: 'CSV', + txt: 'TXT', + sql: 'SQL', + doc: 'MS-Word', + excel: 'MS-Excel', + xlsx: 'MS-Excel (OpenXML)', + powerpoint: 'MS-Powerpoint', + pdf: 'PDF' + } + + $.extend($.fn.bootstrapTable.defaults, { + showExport: false, + exportDataType: 'basic', // basic, all, selected + exportTypes: ['json', 'xml', 'csv', 'txt', 'sql', 'excel'], + exportOptions: {}, + exportFooter: false + }) + + $.extend($.fn.bootstrapTable.defaults.icons, { + export: bootstrap.icons.export + }) + + $.extend($.fn.bootstrapTable.locales, { + formatExport () { + return 'Export data' + } + }) + $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales) + + $.fn.bootstrapTable.methods.push('exportTable') + + $.BootstrapTable = class extends $.BootstrapTable { + initToolbar () { + const o = this.options + + this.showToolbar = this.showToolbar || o.showExport + + super.initToolbar() + + if (!this.options.showExport) { + return + } + const $btnGroup = this.$toolbar.find('>.btn-group') + this.$export = $btnGroup.find('div.export') + + if (this.$export.length) { + this.updateExportButton() + return + } + this.$export = $(` +
    + + ${bootstrap.html.dropmenu} +
    + `).appendTo($btnGroup) + + this.updateExportButton() + + const $menu = this.$export.find('.dropdown-menu') + let exportTypes = o.exportTypes + + if (typeof exportTypes === 'string') { + const types = exportTypes.slice(1, -1).replace(/ /g, '').split(',') + exportTypes = types.map(t => t.slice(1, -1)) + } + for (const type of exportTypes) { + if (TYPE_NAME.hasOwnProperty(type)) { + $menu.append(Utils.sprintf(bootstrap.html.dropitem, type, TYPE_NAME[type])) + } + } + + $menu.find('>li, >a').click(({currentTarget}) => { + const type = $(currentTarget).data('type') + const exportOptions = { + type, + escape: false + } + + this.exportTable(exportOptions) + }) + } + + exportTable (options) { + const o = this.options + const stateField = this.header.stateField + const isCardView = o.cardView + + const doExport = callback => { + if (stateField) { + this.hideColumn(stateField) + } + if (isCardView) { + this.toggleView() + } + + const data = this.getData() + if (o.exportFooter) { + const $footerRow = this.$tableFooter.find('tr').first() + const footerData = {} + const footerHtml = [] + + $.each($footerRow.children(), (index, footerCell) => { + const footerCellHtml = $(footerCell).children('.th-inner').first().html() + footerData[this.columns[index].field] = footerCellHtml === ' ' ? null : footerCellHtml + + // grab footer cell text into cell index-based array + footerHtml.push(footerCellHtml) + }) + + this.append(footerData) + + const $lastTableRow = this.$body.children().last() + + $.each($lastTableRow.children(), (index, lastTableRowCell) => { + $(lastTableRowCell).html(footerHtml[index]) + }) + } + + this.$el.tableExport($.extend({ + onAfterSaveToFile: () => { + if (o.exportFooter) { + this.load(data) + } + + if (stateField) { + this.showColumn(stateField) + } + if (isCardView) { + this.toggleView() + } + + callback() + } + }, o.exportOptions, options)) + } + + if (o.exportDataType === 'all' && o.pagination) { + const eventName = o.sidePagination === 'server' + ? 'post-body.bs.table' : 'page-change.bs.table' + this.$el.one(eventName, () => { + doExport(() => { + this.togglePagination() + }) + }) + this.togglePagination() + } else if (o.exportDataType === 'selected') { + let data = this.getData() + let selectedData = this.getSelections() + if (!selectedData.length) { + return + } + + if (o.sidePagination === 'server') { + data = { + total: o.totalRows, + [this.options.dataField]: data + } + selectedData = { + total: selectedData.length, + [this.options.dataField]: selectedData + } + } + + this.load(selectedData) + doExport(() => { + this.load(data) + }) + } else { + doExport() + } + } + + updateSelected () { + super.updateSelected() + this.updateExportButton() + } + + updateExportButton () { + if (this.options.exportDataType === 'selected') { + this.$export.find('> button') + .prop('disabled', !this.getSelections().length) + } + } + } +})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/export/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/export/extension.json new file mode 100644 index 0000000000..5714a5fa0a --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/export/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Table Export", + "version": "1.1.0", + "description": "Export your table data to JSON, XML, CSV, TXT, SQL, Word, Excel, PNG, PDF.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/export", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/export.html", + + "plugins": [{ + "name": "tableExport.jquery.plugin", + "url": "https://github.com/hhurz/tableExport.jquery.plugin" + }], + + "author": { + "name": "wenzhixin", + "image": "https://avatars1.githubusercontent.com/u/2117018" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.css new file mode 100644 index 0000000000..2ad2a73b28 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.css @@ -0,0 +1,13 @@ +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @version: v2.1.1 + */ + +.no-filter-control { + height: 34px; +} + +.filter-control { + margin: 0 2px 2px 2px; +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.js new file mode 100644 index 0000000000..18db04da06 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.js @@ -0,0 +1,917 @@ +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @version: v2.2.0 + */ + +($ => { + const Utils = $.fn.bootstrapTable.utils + const UtilsFilterControl = { + getOptionsFromSelectControl (selectControl) { + return selectControl.get(selectControl.length - 1).options + }, + + hideUnusedSelectOptions (selectControl, uniqueValues) { + const options = UtilsFilterControl.getOptionsFromSelectControl( + selectControl + ) + + for (let i = 0; i < options.length; i++) { + if (options[i].value !== '') { + if (!uniqueValues.hasOwnProperty(options[i].value)) { + selectControl + .find(Utils.sprintf('option[value=\'%s\']', options[i].value)) + .hide() + } else { + selectControl + .find(Utils.sprintf('option[value=\'%s\']', options[i].value)) + .show() + } + } + } + }, + addOptionToSelectControl (selectControl, _value, text) { + const value = $.trim(_value) + const $selectControl = $(selectControl.get(selectControl.length - 1)) + if ( + !UtilsFilterControl.existOptionInSelectControl(selectControl, value) + ) { + $selectControl.append( + $('') + .attr('value', value) + .text( + $('
    ') + .html(text) + .text() + ) + ) + } + }, + sortSelectControl (selectControl) { + const $selectControl = $(selectControl.get(selectControl.length - 1)) + const $opts = $selectControl.find('option:gt(0)') + + $opts.sort((a, b) => { + let aa = $(a).text().toLowerCase() + let bb = $(b).text().toLowerCase() + if ($.isNumeric(a) && $.isNumeric(b)) { + // Convert numerical values from string to float. + aa = parseFloat(aa) + bb = parseFloat(bb) + } + return aa > bb ? 1 : aa < bb ? -1 : 0 + }) + + $selectControl.find('option:gt(0)').remove() + $selectControl.append($opts) + }, + existOptionInSelectControl (selectControl, value) { + const options = UtilsFilterControl.getOptionsFromSelectControl( + selectControl + ) + for (let i = 0; i < options.length; i++) { + if (options[i].value === value.toString()) { + // The value is not valid to add + return true + } + } + + // If we get here, the value is valid to add + return false + }, + fixHeaderCSS ({ $tableHeader }) { + $tableHeader.css('height', '77px') + }, + getCurrentHeader ({ $header, options, $tableHeader }) { + let header = $header + if (options.height) { + header = $tableHeader + } + + return header + }, + getCurrentSearchControls ({ options }) { + let searchControls = 'select, input' + if (options.height) { + searchControls = 'table select, table input' + } + + return searchControls + }, + getCursorPosition (el) { + if (Utils.isIEBrowser()) { + if ($(el).is('input[type=text]')) { + let pos = 0 + if ('selectionStart' in el) { + pos = el.selectionStart + } else if ('selection' in document) { + el.focus() + const Sel = document.selection.createRange() + const SelLength = document.selection.createRange().text.length + Sel.moveStart('character', -el.value.length) + pos = Sel.text.length - SelLength + } + return pos + } + return -1 + + } + return -1 + }, + setCursorPosition (el) { + $(el).val(el.value) + }, + copyValues (that) { + const header = UtilsFilterControl.getCurrentHeader(that) + const searchControls = UtilsFilterControl.getCurrentSearchControls(that) + + that.options.valuesFilterControl = [] + + header.find(searchControls).each(function () { + that.options.valuesFilterControl.push({ + field: $(this) + .closest('[data-field]') + .data('field'), + value: $(this).val(), + position: UtilsFilterControl.getCursorPosition($(this).get(0)), + hasFocus: $(this).is(':focus') + }) + }) + }, + setValues (that) { + let field = null + let result = [] + const header = UtilsFilterControl.getCurrentHeader(that) + const searchControls = UtilsFilterControl.getCurrentSearchControls(that) + + if (that.options.valuesFilterControl.length > 0) { + // Callback to apply after settings fields values + let fieldToFocusCallback = null + header.find(searchControls).each(function (index, ele) { + field = $(this) + .closest('[data-field]') + .data('field') + result = that.options.valuesFilterControl.filter(valueObj => valueObj.field === field) + + if (result.length > 0) { + $(this).val(result[0].value) + if (result[0].hasFocus) { + // set callback if the field had the focus. + fieldToFocusCallback = ((fieldToFocus, carretPosition) => { + // Closure here to capture the field and cursor position + const closedCallback = () => { + fieldToFocus.focus() + UtilsFilterControl.setCursorPosition(fieldToFocus, carretPosition) + } + return closedCallback + })($(this).get(0), result[0].position) + } + } + }) + + // Callback call. + if (fieldToFocusCallback !== null) { + fieldToFocusCallback() + } + } + }, + collectBootstrapCookies () { + const cookies = [] + const foundCookies = document.cookie.match(/(?:bs.table.)(\w*)/g) + + if (foundCookies) { + $.each(foundCookies, (i, _cookie) => { + let cookie = _cookie + if (/./.test(cookie)) { + cookie = cookie.split('.').pop() + } + + if ($.inArray(cookie, cookies) === -1) { + cookies.push(cookie) + } + }) + return cookies + } + }, + escapeID (id) { + return String(id).replace(/(:|\.|\[|\]|,)/g, '\\$1') + }, + isColumnSearchableViaSelect ({ filterControl, searchable }) { + return filterControl && + filterControl.toLowerCase() === 'select' && + searchable + }, + isFilterDataNotGiven ({ filterData }) { + return filterData === undefined || + filterData.toLowerCase() === 'column' + }, + hasSelectControlElement (selectControl) { + return selectControl && selectControl.length > 0 + }, + initFilterSelectControls (that) { + const data = that.data + const itemsPerPage = that.pageTo < that.options.data.length ? that.options.data.length : that.pageTo + const z = that.options.pagination + ? that.options.sidePagination === 'server' + ? that.pageTo + : that.options.totalRows + : that.pageTo + + $.each(that.header.fields, (j, field) => { + const column = that.columns[that.fieldsColumnsIndex[field]] + const selectControl = $(`.bootstrap-table-filter-control-${UtilsFilterControl.escapeID(column.field)}`) + + if ( + UtilsFilterControl.isColumnSearchableViaSelect(column) && + UtilsFilterControl.isFilterDataNotGiven(column) && + UtilsFilterControl.hasSelectControlElement(selectControl) + ) { + if (selectControl.get(selectControl.length - 1).options.length === 0) { + // Added the default option + UtilsFilterControl.addOptionToSelectControl(selectControl, '', column.filterControlPlaceholder) + } + + const uniqueValues = {} + for (let i = 0; i < z; i++) { + // Added a new value + const fieldValue = data[i][field] + const formattedValue = Utils.calculateObjectValue(that.header, that.header.formatters[j], [fieldValue, data[i], i], fieldValue) + + uniqueValues[formattedValue] = fieldValue + } + + // eslint-disable-next-line guard-for-in + for (const key in uniqueValues) { + UtilsFilterControl.addOptionToSelectControl(selectControl, uniqueValues[key], key) + } + + UtilsFilterControl.sortSelectControl(selectControl) + + if (that.options.hideUnusedSelectOptions) { + UtilsFilterControl.hideUnusedSelectOptions(selectControl, uniqueValues) + } + } + }) + + that.trigger('created-controls') + }, + getFilterDataMethod (objFilterDataMethod, searchTerm) { + const keys = Object.keys(objFilterDataMethod) + for (let i = 0; i < keys.length; i++) { + if (keys[i] === searchTerm) { + return objFilterDataMethod[searchTerm] + } + } + return null + }, + createControls (that, header) { + let addedFilterControl = false + let isVisible + let html + + $.each(that.columns, (i, column) => { + isVisible = 'hidden' + html = [] + + if (!column.visible) { + return + } + + if (!column.filterControl) { + html.push('
    ') + } else { + html.push('
    ') + + const nameControl = column.filterControl.toLowerCase() + if (column.searchable && that.options.filterTemplate[nameControl]) { + addedFilterControl = true + isVisible = 'visible' + html.push( + that.options.filterTemplate[nameControl]( + that, + column.field, + isVisible, + column.filterControlPlaceholder + ? column.filterControlPlaceholder + : '', + `filter-control-${i}` + ) + ) + } + } + + $.each(header.children().children(), (i, tr) => { + const $tr = $(tr) + if ($tr.data('field') === column.field) { + $tr.find('.fht-cell').append(html.join('')) + return false + } + }) + + if ( + column.filterData !== undefined && + column.filterData.toLowerCase() !== 'column' + ) { + const filterDataType = UtilsFilterControl.getFilterDataMethod( + /* eslint-disable no-use-before-define */ + filterDataMethods, + column.filterData.substring(0, column.filterData.indexOf(':')) + ) + let filterDataSource + let selectControl + + if (filterDataType !== null) { + filterDataSource = column.filterData.substring( + column.filterData.indexOf(':') + 1, + column.filterData.length + ) + selectControl = $( + `.bootstrap-table-filter-control-${UtilsFilterControl.escapeID(column.field)}` + ) + + UtilsFilterControl.addOptionToSelectControl(selectControl, '', column.filterControlPlaceholder) + filterDataType(filterDataSource, selectControl) + } else { + throw new SyntaxError( + 'Error. You should use any of these allowed filter data methods: var, json, url.' + + ' Use like this: var: {key: "value"}' + ) + } + + let variableValues + let key + // eslint-disable-next-line default-case + switch (filterDataType) { + case 'url': + $.ajax({ + url: filterDataSource, + dataType: 'json', + success (data) { + // eslint-disable-next-line guard-for-in + for (const key in data) { + UtilsFilterControl.addOptionToSelectControl(selectControl, key, data[key]) + } + UtilsFilterControl.sortSelectControl(selectControl) + } + }) + break + case 'var': + variableValues = window[filterDataSource] + // eslint-disable-next-line guard-for-in + for (key in variableValues) { + UtilsFilterControl.addOptionToSelectControl(selectControl, key, variableValues[key]) + } + UtilsFilterControl.sortSelectControl(selectControl) + break + case 'jso': + variableValues = JSON.parse(filterDataSource) + // eslint-disable-next-line guard-for-in + for (key in variableValues) { + UtilsFilterControl.addOptionToSelectControl(selectControl, key, variableValues[key]) + } + UtilsFilterControl.sortSelectControl(selectControl) + break + } + } + }) + + if (addedFilterControl) { + header.off('keyup', 'input').on('keyup', 'input', (event, obj) => { + // Simulate enter key action from clear button + event.keyCode = obj ? obj.keyCode : event.keyCode + + if (that.options.searchOnEnterKey && event.keyCode !== 13) { + return + } + + if ($.inArray(event.keyCode, [37, 38, 39, 40]) > -1) { + return + } + + const $currentTarget = $(event.currentTarget) + + if ($currentTarget.is(':checkbox') || $currentTarget.is(':radio')) { + return + } + + clearTimeout(event.currentTarget.timeoutId || 0) + event.currentTarget.timeoutId = setTimeout(() => { + that.onColumnSearch(event) + }, that.options.searchTimeOut) + }) + + header.off('change', 'select').on('change', 'select', event => { + if (that.options.searchOnEnterKey && event.keyCode !== 13) { + return + } + + if ($.inArray(event.keyCode, [37, 38, 39, 40]) > -1) { + return + } + + clearTimeout(event.currentTarget.timeoutId || 0) + event.currentTarget.timeoutId = setTimeout(() => { + that.onColumnSearch(event) + }, that.options.searchTimeOut) + }) + + header.off('mouseup', 'input').on('mouseup', 'input', function (event) { + const $input = $(this) + const oldValue = $input.val() + + if (oldValue === '') { + return + } + + setTimeout(() => { + const newValue = $input.val() + + if (newValue === '') { + clearTimeout(event.currentTarget.timeoutId || 0) + event.currentTarget.timeoutId = setTimeout(() => { + that.onColumnSearch(event) + }, that.options.searchTimeOut) + } + }, 1) + }) + + if (header.find('.date-filter-control').length > 0) { + $.each(that.columns, (i, { filterControl, field, filterDatepickerOptions }) => { + if ( + filterControl !== undefined && + filterControl.toLowerCase() === 'datepicker' + ) { + header + .find( + `.date-filter-control.bootstrap-table-filter-control-${field}` + ) + .datepicker(filterDatepickerOptions) + .on('changeDate', ({ currentTarget }) => { + $(currentTarget).val( + currentTarget.value + ) + // Fired the keyup event + $(currentTarget).keyup() + }) + } + }) + } + } else { + header.find('.filterControl').hide() + } + }, + getDirectionOfSelectOptions (_alignment) { + const alignment = _alignment === undefined ? 'left' : _alignment.toLowerCase() + + switch (alignment) { + case 'left': + return 'ltr' + case 'right': + return 'rtl' + case 'auto': + return 'auto' + default: + return 'ltr' + } + } + } + const filterDataMethods = { + var (filterDataSource, selectControl) { + const variableValues = window[filterDataSource] + // eslint-disable-next-line guard-for-in + for (const key in variableValues) { + UtilsFilterControl.addOptionToSelectControl(selectControl, key, variableValues[key]) + } + UtilsFilterControl.sortSelectControl(selectControl) + }, + url (filterDataSource, selectControl) { + $.ajax({ + url: filterDataSource, + dataType: 'json', + success (data) { + // eslint-disable-next-line guard-for-in + for (const key in data) { + UtilsFilterControl.addOptionToSelectControl(selectControl, key, data[key]) + } + UtilsFilterControl.sortSelectControl(selectControl) + } + }) + }, + json (filterDataSource, selectControl) { + const variableValues = JSON.parse(filterDataSource) + // eslint-disable-next-line guard-for-in + for (const key in variableValues) { + UtilsFilterControl.addOptionToSelectControl(selectControl, key, variableValues[key]) + } + UtilsFilterControl.sortSelectControl(selectControl) + } + } + + const bootstrap = { + 3: { + icons: { + clear: 'glyphicon-trash icon-clear' + } + }, + 4: { + icons: { + clear: 'fa-trash icon-clear' + } + } + }[Utils.bootstrapVersion] + + $.extend($.fn.bootstrapTable.defaults, { + filterControl: false, + onColumnSearch (field, text) { + return false + }, + onCreatedControls () { + return true + }, + filterShowClear: false, + alignmentSelectControlOptions: undefined, + filterTemplate: { + input (that, field, isVisible, placeholder) { + return Utils.sprintf( + '', + field, + isVisible, + placeholder + ) + }, + select ({ options }, field, isVisible) { + return Utils.sprintf( + '', + field, + isVisible, + UtilsFilterControl.getDirectionOfSelectOptions( + options.alignmentSelectControlOptions + ) + ) + }, + datepicker (that, field, isVisible) { + return Utils.sprintf( + '', + field, + isVisible + ) + } + }, + disableControlWhenSearch: false, + searchOnEnterKey: false, + // internal variables + valuesFilterControl: [] + }) + + $.extend($.fn.bootstrapTable.columnDefaults, { + filterControl: undefined, + filterData: undefined, + filterDatepickerOptions: undefined, + filterStrictSearch: false, + filterStartsWithSearch: false, + filterControlPlaceholder: '' + }) + + $.extend($.fn.bootstrapTable.Constructor.EVENTS, { + 'column-search.bs.table': 'onColumnSearch', + 'created-controls.bs.table': 'onCreatedControls' + }) + + $.extend($.fn.bootstrapTable.defaults.icons, { + clear: bootstrap.icons.clear + }) + + $.extend($.fn.bootstrapTable.locales, { + formatClearFilters () { + return 'Clear Filters' + } + }) + + $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales) + + $.fn.bootstrapTable.methods.push('triggerSearch') + $.fn.bootstrapTable.methods.push('clearFilterControl') + + $.BootstrapTable = class extends $.BootstrapTable { + init () { + // Make sure that the filterControl option is set + if (this.options.filterControl) { + const that = this + + // Make sure that the internal variables are set correctly + this.options.valuesFilterControl = [] + + this.$el + .on('reset-view.bs.table', () => { + // Create controls on $tableHeader if the height is set + if (!that.options.height) { + return + } + + // Avoid recreate the controls + if ( + that.$tableHeader.find('select').length > 0 || + that.$tableHeader.find('input').length > 0 + ) { + return + } + + UtilsFilterControl.createControls(that, that.$tableHeader) + }) + .on('post-header.bs.table', () => { + UtilsFilterControl.setValues(that) + }) + .on('post-body.bs.table', () => { + if (that.options.height) { + UtilsFilterControl.fixHeaderCSS(that) + } + }) + .on('column-switch.bs.table', () => { + UtilsFilterControl.setValues(that) + }) + .on('load-success.bs.table', () => { + that.EnableControls(true) + }) + .on('load-error.bs.table', () => { + that.EnableControls(true) + }) + } + + super.init() + } + + initToolbar () { + this.showToolbar = + this.showToolbar || + (this.options.filterControl && this.options.filterShowClear) + + super.initToolbar() + + if (this.options.filterControl && this.options.filterShowClear) { + const $btnGroup = this.$toolbar.find('>.btn-group') + let $btnClear = $btnGroup.find('.filter-show-clear') + + if (!$btnClear.length) { + $btnClear = $( + [ + Utils.sprintf( + '' + ].join('') + ).appendTo($btnGroup) + + $btnClear + .off('click') + .on('click', $.proxy(this.clearFilterControl, this)) + } + } + } + + initHeader () { + super.initHeader() + + if (!this.options.filterControl) { + return + } + UtilsFilterControl.createControls(this, this.$header) + } + initBody () { + super.initBody() + + UtilsFilterControl.initFilterSelectControls(this) + } + + initSearch () { + const that = this + const fp = $.isEmptyObject(that.filterColumnsPartial) + ? null + : that.filterColumnsPartial + + if (fp === null || Object.keys(fp).length <= 1) { + super.initSearch() + } + + if (this.options.sidePagination === 'server') { + return + } + + if (fp === null) { + return + } + + // Check partial column filter + that.data = fp + ? that.options.data.filter((item, i) => { + const itemIsExpected = [] + Object.keys(item).forEach((key, index) => { + const thisColumn = that.columns[that.fieldsColumnsIndex[key]] + const fval = (fp[key] || '').toLowerCase() + let value = item[key] + + if (fval === '') { + itemIsExpected.push(true) + } else { + // Fix #142: search use formated data + if (thisColumn && thisColumn.searchFormatter) { + value = $.fn.bootstrapTable.utils.calculateObjectValue( + that.header, + that.header.formatters[$.inArray(key, that.header.fields)], + [value, item, i], + value + ) + } + + if ($.inArray(key, that.header.fields) !== -1) { + if (typeof value === 'string' || typeof value === 'number') { + if (thisColumn.filterStrictSearch) { + if (value.toString().toLowerCase() === fval.toString().toLowerCase()) { + itemIsExpected.push(true) + } else { + itemIsExpected.push(false) + } + } else if (thisColumn.filterStartsWithSearch) { + if ((`${value}`).toLowerCase().indexOf(fval) === 0) { + itemIsExpected.push(true) + } else { + itemIsExpected.push(false) + } + } else { + if ((`${value}`).toLowerCase().includes(fval)) { + itemIsExpected.push(true) + } else { + itemIsExpected.push(false) + } + } + } + } + } + }) + + return !itemIsExpected.includes(false) + }) + : that.data + } + + initColumnSearch (filterColumnsDefaults) { + UtilsFilterControl.copyValues(this) + + if (filterColumnsDefaults) { + this.filterColumnsPartial = filterColumnsDefaults + this.updatePagination() + + // eslint-disable-next-line guard-for-in + for (const filter in filterColumnsDefaults) { + this.trigger('column-search', filter, filterColumnsDefaults[filter]) + } + } + } + + onColumnSearch (event) { + if ($.inArray(event.keyCode, [37, 38, 39, 40]) > -1) { + return + } + + UtilsFilterControl.copyValues(this) + const text = $.trim($(event.currentTarget).val()) + const $field = $(event.currentTarget) + .closest('[data-field]') + .data('field') + + if ($.isEmptyObject(this.filterColumnsPartial)) { + this.filterColumnsPartial = {} + } + if (text) { + this.filterColumnsPartial[$field] = text + } else { + delete this.filterColumnsPartial[$field] + } + + // if the searchText is the same as the previously selected column value, + // bootstrapTable will not try searching again (even though the selected column + // may be different from the previous search). As a work around + // we're manually appending some text to bootrap's searchText field + // to guarantee that it will perform a search again when we call this.onSearch(event) + this.searchText += 'randomText' + + this.options.pageNumber = 1 + this.EnableControls(false) + this.onSearch(event) + this.trigger('column-search', $field, text) + } + + clearFilterControl () { + if (this.options.filterControl && this.options.filterShowClear) { + const that = this + const cookies = UtilsFilterControl.collectBootstrapCookies() + const header = UtilsFilterControl.getCurrentHeader(that) + const table = header.closest('table') + const controls = header.find(UtilsFilterControl.getCurrentSearchControls(that)) + const search = that.$toolbar.find('.search input') + let hasValues = false + let timeoutId = 0 + + $.each(that.options.valuesFilterControl, (i, item) => { + hasValues = hasValues ? true : item.value !== '' + item.value = '' + }) + + UtilsFilterControl.setValues(that) + + // clear cookies once the filters are clean + clearTimeout(timeoutId) + timeoutId = setTimeout(() => { + if (cookies && cookies.length > 0) { + $.each(cookies, (i, item) => { + if (that.deleteCookie !== undefined) { + that.deleteCookie(item) + } + }) + } + }, that.options.searchTimeOut) + + // If there is not any value in the controls exit this method + if (!hasValues) { + return + } + + // Clear each type of filter if it exists. + // Requires the body to reload each time a type of filter is found because we never know + // which ones are going to be present. + if (controls.length > 0) { + this.filterColumnsPartial = {} + $(controls[0]).trigger( + controls[0].tagName === 'INPUT' ? 'keyup' : 'change', { keyCode: 13 } + ) + } else { + return + } + + if (search.length > 0) { + that.resetSearch() + } + + // use the default sort order if it exists. do nothing if it does not + if ( + that.options.sortName !== table.data('sortName') || + that.options.sortOrder !== table.data('sortOrder') + ) { + const sorter = header.find( + Utils.sprintf( + '[data-field="%s"]', + $(controls[0]) + .closest('table') + .data('sortName') + ) + ) + if (sorter.length > 0) { + that.onSort({ type: 'keypress', currentTarget: sorter }) + $(sorter) + .find('.sortable') + .trigger('click') + } + } + } + } + + triggerSearch () { + const header = UtilsFilterControl.getCurrentHeader(this) + const searchControls = UtilsFilterControl.getCurrentSearchControls(this) + + header.find(searchControls).each(function () { + const el = $(this) + if (el.is('select')) { + el.change() + } else { + el.keyup() + } + }) + } + + EnableControls (enable) { + if ( + this.options.disableControlWhenSearch && + this.options.sidePagination === 'server' + ) { + const header = UtilsFilterControl.getCurrentHeader(this) + const searchControls = UtilsFilterControl.getCurrentSearchControls(this) + + if (!enable) { + header.find(searchControls).prop('disabled', 'disabled') + } else { + header.find(searchControls).removeProp('disabled') + } + } + } + } +})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/extension.json new file mode 100644 index 0000000000..b0e31694bb --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/filter-control/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Filter Control", + "version": "2.1.0", + "description": "Plugin to add input/select element on the top of the columns in order to filter the data.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/filter-control", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/filter-control.html", + + "plugins": [{ + "name": "bootstrap-table-filter-control", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/filter-control" + }], + + "author": { + "name": "djhvscf", + "image": "https://avatars1.githubusercontent.com/u/4496763" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/fixed-columns/bootstrap-table-fixed-columns.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/fixed-columns/bootstrap-table-fixed-columns.css new file mode 100644 index 0000000000..69d79006b9 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/fixed-columns/bootstrap-table-fixed-columns.css @@ -0,0 +1,27 @@ +.fixed-table-header-columns, +.fixed-table-body-columns { + position: absolute; + background-color: #fff; + box-sizing: border-box; + overflow: hidden; + z-index: 1; +} + +.fixed-table-header-columns { + z-index: 2; +} + +.fixed-table-header-columns .table, +.fixed-table-body-columns .table { + border-right: 1px solid #ddd; +} + +.fixed-table-header-columns .table.table-no-bordered, +.fixed-table-body-columns .table.table-no-bordered { + border-right: 1px solid transparent; +} + +.fixed-table-body-columns table { + position: absolute; + animation: none; +} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/fixed-columns/bootstrap-table-fixed-columns.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/fixed-columns/bootstrap-table-fixed-columns.js new file mode 100644 index 0000000000..e05ec2cca0 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/fixed-columns/bootstrap-table-fixed-columns.js @@ -0,0 +1,115 @@ +/** + * @author zhixin wen + */ + +($ => { + $.extend($.fn.bootstrapTable.defaults, { + fixedColumns: false, + fixedNumber: 1 + }) + + $.BootstrapTable = class extends $.BootstrapTable { + + fitHeader (...args) { + super.fitHeader(...args) + + if (!this.options.fixedColumns) { + return + } + + if (this.$el.is(':hidden')) { + return + } + + this.$container.find('.fixed-table-header-columns').remove() + this.$fixedHeader = $('
    ') + this.$fixedHeader.append(this.$tableHeader.find('>table').clone(true)) + this.$tableHeader.after(this.$fixedHeader) + + const width = this.getFixedColumnsWidth() + + this.$fixedHeader.css({ + top: 0, + width, + height: this.$tableHeader.outerHeight(true) + }) + + this.initFixedColumnsBody() + + this.$fixedBody.css({ + top: this.$tableHeader.outerHeight(true), + width, + height: this.$tableBody.outerHeight(true) - 1 + }) + + this.initFixedColumnsEvents() + } + + initBody (...args) { + super.initBody(...args) + + if (!this.options.fixedColumns) { + return + } + + if (this.options.showHeader && this.options.height) { + return + } + + this.initFixedColumnsBody() + + this.$fixedBody.css({ + top: 0, + width: this.getFixedColumnsWidth(), + height: this.$tableHeader.outerHeight(true) + this.$tableBody.outerHeight(true) + }) + + this.initFixedColumnsEvents() + } + + initFixedColumnsBody () { + this.$container.find('.fixed-table-body-columns').remove() + this.$fixedBody = $('
    ') + this.$fixedBody.append(this.$tableBody.find('>table').clone(true)) + this.$tableBody.after(this.$fixedBody) + } + + getFixedColumnsWidth () { + const visibleFields = this.getVisibleFields() + let width = 0 + + for (let i = 0; i < this.options.fixedNumber; i++) { + width += this.$header.find(`th[data-field="${visibleFields[i]}"]`).outerWidth(true) + } + + return width + 1 + } + + initFixedColumnsEvents () { + // events + this.$tableBody.off('scroll.fixed-columns').on('scroll.fixed-columns', e => { + this.$fixedBody.find('table').css('top', -$(e.currentTarget).scrollTop()) + }) + + this.$body.find('> tr[data-index]').off('hover').hover(e => { + const index = $(e.currentTarget).data('index') + this.$fixedBody.find(`tr[data-index="${index}"]`) + .css('background-color', $(e.currentTarget).css('background-color')) + }, e => { + const index = $(e.currentTarget).data('index') + const $tr = this.$fixedBody.find(`tr[data-index="${index}"]`) + $tr.attr('style', $tr.attr('style').replace(/background-color:.*;/, '')) + }) + + this.$fixedBody.find('tr[data-index]').off('hover').hover(e => { + const index = $(e.currentTarget).data('index') + this.$body.find(`tr[data-index="${index}"]`) + .css('background-color', $(e.currentTarget).css('background-color')) + }, e => { + const index = $(e.currentTarget).data('index') + const $tr = this.$body.find(`> tr[data-index="${index}"]`) + $tr.attr('style', $tr.attr('style').replace(/background-color:.*;/, '')) + }) + } + } +})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.css new file mode 100644 index 0000000000..449bc73aa4 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.css @@ -0,0 +1,11 @@ +.bootstrap-table .table > tbody > tr.groupBy { + cursor: pointer; +} + +.bootstrap-table .table > tbody > tr.groupBy.expanded { + +} + +.bootstrap-table .table > tbody > tr.hidden + tr.detail-view { + display: none; +} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.js new file mode 100644 index 0000000000..0118ee2996 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.js @@ -0,0 +1,230 @@ +/** + * @author: Yura Knoxville + * @version: v1.1.0 + */ + +(function ($) { + + 'use strict'; + + var initBodyCaller, + tableGroups; + + // it only does '%s', and return '' when arguments are undefined + var sprintf = function (str) { + var args = arguments, + flag = true, + i = 1; + + str = str.replace(/%s/g, function () { + var arg = args[i++]; + + if (typeof arg === 'undefined') { + flag = false; + return ''; + } + return arg; + }); + return flag ? str : ''; + }; + + var groupBy = function (array , f) { + var groups = {}; + array.forEach(function(o) { + var group = f(o); + groups[group] = groups[group] || []; + groups[group].push(o); + }); + + return groups; + }; + + $.extend($.fn.bootstrapTable.defaults, { + groupBy: false, + groupByField: '', + groupByFormatter: undefined + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _initSort = BootstrapTable.prototype.initSort, + _initBody = BootstrapTable.prototype.initBody, + _updateSelected = BootstrapTable.prototype.updateSelected; + + BootstrapTable.prototype.initSort = function () { + _initSort.apply(this, Array.prototype.slice.apply(arguments)); + + var that = this; + tableGroups = []; + + if ((this.options.groupBy) && (this.options.groupByField !== '')) { + + if ((this.options.sortName != this.options.groupByField)) { + this.data.sort(function(a, b) { + return a[that.options.groupByField].localeCompare(b[that.options.groupByField]); + }); + } + + var that = this; + var groups = groupBy(that.data, function (item) { + return [item[that.options.groupByField]]; + }); + + var index = 0; + $.each(groups, function(key, value) { + tableGroups.push({ + id: index, + name: key, + data: value + }); + + value.forEach(function(item) { + if (!item._data) { + item._data = {}; + } + + item._data['parent-index'] = index; + }); + + index++; + }); + } + } + + BootstrapTable.prototype.initBody = function () { + initBodyCaller = true; + + _initBody.apply(this, Array.prototype.slice.apply(arguments)); + + if ((this.options.groupBy) && (this.options.groupByField !== '')) { + var that = this, + checkBox = false, + visibleColumns = 0; + + this.columns.forEach(function(column) { + if (column.checkbox) { + checkBox = true; + } else { + if (column.visible) { + visibleColumns += 1; + } + } + }); + + if (this.options.detailView && !this.options.cardView) { + visibleColumns += 1; + } + + tableGroups.forEach(function(item){ + var html = []; + + html.push(sprintf('', item.id)); + + if (that.options.detailView && !that.options.cardView) { + html.push(''); + } + + if (checkBox) { + html.push('', + '', + '' + ); + } + var formattedValue = item.name; + if (typeof(that.options.groupByFormatter) == "function") { + formattedValue = that.options.groupByFormatter(item.name, item.id, item.data); + } + html.push('', formattedValue, '' + ); + + html.push(''); + + that.$body.find('tr[data-parent-index='+item.id+']:first').before($(html.join(''))); + }); + + this.$selectGroup = []; + this.$body.find('[name="btSelectGroup"]').each(function() { + var self = $(this); + + that.$selectGroup.push({ + group: self, + item: that.$selectItem.filter(function () { + return ($(this).closest('tr').data('parent-index') === + self.closest('tr').data('group-index')); + }) + }); + }); + + this.$container.off('click', '.groupBy') + .on('click', '.groupBy', function() { + $(this).toggleClass('expanded'); + that.$body.find('tr[data-parent-index='+$(this).closest('tr').data('group-index')+']').toggleClass('hidden'); + }); + + this.$container.off('click', '[name="btSelectGroup"]') + .on('click', '[name="btSelectGroup"]', function (event) { + event.stopImmediatePropagation(); + + var self = $(this); + var checked = self.prop('checked'); + that[checked ? 'checkGroup' : 'uncheckGroup']($(this).closest('tr').data('group-index')); + }); + } + + initBodyCaller = false; + this.updateSelected(); + }; + + BootstrapTable.prototype.updateSelected = function () { + if (!initBodyCaller) { + _updateSelected.apply(this, Array.prototype.slice.apply(arguments)); + + if ((this.options.groupBy) && (this.options.groupByField !== '')) { + this.$selectGroup.forEach(function (item) { + var checkGroup = item.item.filter(':enabled').length === + item.item.filter(':enabled').filter(':checked').length; + + item.group.prop('checked', checkGroup); + }); + } + } + }; + + BootstrapTable.prototype.getGroupSelections = function (index) { + var that = this; + + return $.grep(this.data, function (row) { + return (row[that.header.stateField] && (row._data['parent-index'] === index)); + }); + }; + + BootstrapTable.prototype.checkGroup = function (index) { + this.checkGroup_(index, true); + }; + + BootstrapTable.prototype.uncheckGroup = function (index) { + this.checkGroup_(index, false); + }; + + BootstrapTable.prototype.checkGroup_ = function (index, checked) { + var rows; + var filter = function() { + return ($(this).closest('tr').data('parent-index') === index); + }; + + if (!checked) { + rows = this.getGroupSelections(index); + } + + this.$selectItem.filter(filter).prop('checked', checked); + + this.updateRows(); + this.updateSelected(); + if (checked) { + rows = this.getGroupSelections(index); + } + this.trigger(checked ? 'check-all' : 'uncheck-all', rows); + }; + +})(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/extension.json new file mode 100644 index 0000000000..2b948bbd30 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by-v2/extension.json @@ -0,0 +1,12 @@ +{ + "name": "Group By V2", + "version": "1.0.0", + "description": "Group the data by field", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/group-by-v2", + "example": "", + "plugins": [], + "author": { + "name": "Knoxvillekm", + "image": "https://avatars3.githubusercontent.com/u/11072464" + } +} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.css new file mode 100644 index 0000000000..fce5a9a7b1 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.css @@ -0,0 +1,53 @@ +table.treetable tbody tr td { + cursor: default; +} + +table.treetable span { + background-position: center left; + background-repeat: no-repeat; + padding: .2em 0 .2em 1.5em; +} + +table.treetable tr.collapsed span.indenter a { + background-image: url(data:image/png;base64,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); + padding-right: 12px; +} + +table.treetable tr.expanded span.indenter a { + background-image: url(data:image/png;base64,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); + padding-right: 12px; +} + +table.treetable tr.branch { + background-color: #f9f9f9; +} + +table.treetable tr.selected { + background-color: #3875d7; + color: #fff; +} + +table.treetable tr span.indenter a { + outline: none; /* Expander shows outline after upgrading to 3.0 (#141) */ +} + +table.treetable tr.collapsed.selected span.indenter a { + background-image: url(data:image/png;base64,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); +} + +table.treetable tr.expanded.selected span.indenter a { + background-image: url(data:image/png;base64,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); +} + +table.treetable tr.accept { + background-color: #a3bce4; + color: #fff +} + +table.treetable tr.collapsed.accept td span.indenter a { + background-image: url(data:image/x-png;base64,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); +} + +table.treetable tr.expanded.accept td span.indenter a { + background-image: url(data:image/x-png;base64,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); +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.js new file mode 100644 index 0000000000..35c3b61a79 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.js @@ -0,0 +1,243 @@ +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @version: v1.1.0 + */ + +!function ($) { + + 'use strict'; + + var originalRowAttr, + dataTTId = 'data-tt-id', + dataTTParentId = 'data-tt-parent-id', + obj = {}, + parentId = undefined; + + var getParentRowId = function (that, id) { + var parentRows = that.$body.find('tr').not('[' + 'data-tt-parent-id]'); + + for (var i = 0; i < parentRows.length; i++) { + if (i === id) { + return $(parentRows[i]).attr('data-tt-id'); + } + } + + return undefined; + }; + + var sumData = function (that, data) { + var sumRow = {}; + $.each(data, function (i, row) { + if (!row.IsParent) { + for (var prop in row) { + if (!isNaN(parseFloat(row[prop]))) { + if (that.columns[that.fieldsColumnsIndex[prop]].groupBySumGroup) { + if (sumRow[prop] === undefined) { + sumRow[prop] = 0; + } + sumRow[prop] += +row[prop]; + } + } + } + } + }); + return sumRow; + }; + + var rowAttr = function (row, index) { + //Call the User Defined Function + originalRowAttr.apply([row, index]); + + obj[dataTTId.toString()] = index; + + if (!row.IsParent) { + obj[dataTTParentId.toString()] = parentId === undefined ? index : parentId; + } else { + parentId = index; + delete obj[dataTTParentId.toString()]; + } + + return obj; + }; + + var setObjectKeys = function () { + // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys + Object.keys = function (o) { + if (o !== Object(o)) { + throw new TypeError('Object.keys called on a non-object'); + } + var k = [], + p; + for (p in o) { + if (Object.prototype.hasOwnProperty.call(o, p)) { + k.push(p); + } + } + return k; + } + }; + + var getDataArrayFromItem = function (that, item) { + var itemDataArray = []; + for (var i = 0; i < that.options.groupByField.length; i++) { + itemDataArray.push(item[that.options.groupByField[i]]); + } + + return itemDataArray; + }; + + var getNewRow = function (that, result, index) { + var newRow = {}; + for (var i = 0; i < that.options.groupByField.length; i++) { + newRow[that.options.groupByField[i].toString()] = result[index][0][that.options.groupByField[i]]; + } + + newRow.IsParent = true; + + return newRow; + }; + + var groupBy = function (array, f) { + var groups = {}; + $.each(array, function (i, o) { + var group = JSON.stringify(f(o)); + groups[group] = groups[group] || []; + groups[group].push(o); + }); + return Object.keys(groups).map(function (group) { + return groups[group]; + }); + }; + + var makeGrouped = function (that, data) { + var newData = [], + sumRow = {}; + + var result = groupBy(data, function (item) { + return getDataArrayFromItem(that, item); + }); + + for (var i = 0; i < result.length; i++) { + result[i].unshift(getNewRow(that, result, i)); + if (that.options.groupBySumGroup) { + sumRow = sumData(that, result[i]); + if (!$.isEmptyObject(sumRow)) { + result[i].push(sumRow); + } + } + } + + newData = newData.concat.apply(newData, result); + + if (!that.options.loaded && newData.length > 0) { + that.options.loaded = true; + that.options.originalData = that.options.data; + that.options.data = newData; + } + + return newData; + }; + + $.extend($.fn.bootstrapTable.defaults, { + groupBy: false, + groupByField: [], + groupBySumGroup: false, + groupByInitExpanded: undefined, //node, 'all' + //internal variables + loaded: false, + originalData: undefined + }); + + $.fn.bootstrapTable.methods.push('collapseAll', 'expandAll', 'refreshGroupByField'); + + $.extend($.fn.bootstrapTable.COLUMN_DEFAULTS, { + groupBySumGroup: false + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _init = BootstrapTable.prototype.init, + _initData = BootstrapTable.prototype.initData; + + BootstrapTable.prototype.init = function () { + //Temporal validation + if (!this.options.sortName) { + if ((this.options.groupBy) && (this.options.groupByField.length > 0)) { + var that = this; + + // Compatibility: IE < 9 and old browsers + if (!Object.keys) { + $.fn.bootstrapTable.utils.objectKeys(); + } + + //Make sure that the internal variables are set correctly + this.options.loaded = false; + this.options.originalData = undefined; + + originalRowAttr = this.options.rowAttributes; + this.options.rowAttributes = rowAttr; + this.$el.off('post-body.bs.table').on('post-body.bs.table', function () { + that.$el.treetable({ + expandable: true, + onNodeExpand: function () { + if (that.options.height) { + that.resetHeader(); + } + }, + onNodeCollapse: function () { + if (that.options.height) { + that.resetHeader(); + } + } + }, true); + + if (that.options.groupByInitExpanded !== undefined) { + if (typeof that.options.groupByInitExpanded === 'number') { + that.expandNode(that.options.groupByInitExpanded); + } else if (that.options.groupByInitExpanded.toLowerCase() === 'all') { + that.expandAll(); + } + } + }); + } + } + _init.apply(this, Array.prototype.slice.apply(arguments)); + }; + + BootstrapTable.prototype.initData = function (data, type) { + //Temporal validation + if (!this.options.sortName) { + if ((this.options.groupBy) && (this.options.groupByField.length > 0)) { + + this.options.groupByField = typeof this.options.groupByField === 'string' ? + this.options.groupByField.replace('[', '').replace(']', '') + .replace(/ /g, '').toLowerCase().split(',') : this.options.groupByField; + + data = makeGrouped(this, data ? data : this.options.data); + } + } + _initData.apply(this, [data, type]); + }; + + BootstrapTable.prototype.expandAll = function () { + this.$el.treetable('expandAll'); + }; + + BootstrapTable.prototype.collapseAll = function () { + this.$el.treetable('collapseAll'); + }; + + BootstrapTable.prototype.expandNode = function (id) { + id = getParentRowId(this, id); + if (id !== undefined) { + this.$el.treetable('expandNode', id); + } + }; + + BootstrapTable.prototype.refreshGroupByField = function (groupByFields) { + if (!$.fn.bootstrapTable.utils.compareObjects(this.options.groupByField, groupByFields)) { + this.options.groupByField = groupByFields; + this.load(this.options.originalData); + } + }; +}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/extension.json new file mode 100644 index 0000000000..1d6d838808 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Group By", + "version": "1.1.0", + "description": "Plugin to group the data by fields.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/group-by", + "example": "#", + + "plugins": [{ + "name": "bootstrap-table-group-by", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/group-by" + }], + + "author": { + "name": "djhvscf", + "image": "https://avatars1.githubusercontent.com/u/4496763" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/i18n-enhance/bootstrap-table-i18n-enhance.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/i18n-enhance/bootstrap-table-i18n-enhance.js new file mode 100644 index 0000000000..4f36d21bbf --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/i18n-enhance/bootstrap-table-i18n-enhance.js @@ -0,0 +1,35 @@ +/** + * @author: Jewway + * @version: v1.0.0 + */ + +!function ($) { + 'use strict'; + + var BootstrapTable = $.fn.bootstrapTable.Constructor; + + BootstrapTable.prototype.changeTitle = function (locale) { + $.each(this.options.columns, function (idx, columnList) { + $.each(columnList, function (idx, column) { + if (column.field) { + column.title = locale[column.field]; + } + }); + }); + this.initHeader(); + this.initBody(); + this.initToolbar(); + }; + + BootstrapTable.prototype.changeLocale = function (localeId) { + this.options.locale = localeId; + this.initLocale(); + this.initPagination(); + this.initBody(); + this.initToolbar(); + }; + + $.fn.bootstrapTable.methods.push('changeTitle'); + $.fn.bootstrapTable.methods.push('changeLocale'); + +}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/i18n-enhance/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/i18n-enhance/extension.json new file mode 100644 index 0000000000..a2b018029c --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/i18n-enhance/extension.json @@ -0,0 +1,17 @@ +{ + "name": "i18n Enhance", + "version": "1.0.0", + "description": "Plugin to add i18n API in order to change column's title and table locale.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/i18n-enhance", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/i18n-enhance.html", + + "plugins": [{ + "name": "bootstrap-table-i18n-enhance", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/i18n-enhance" + }], + + "author": { + "name": "Jewway", + "image": "https://avatars0.githubusercontent.com/u/3501899" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/key-events/bootstrap-table-key-events.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/key-events/bootstrap-table-key-events.js new file mode 100644 index 0000000000..87ddad1286 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/key-events/bootstrap-table-key-events.js @@ -0,0 +1,80 @@ +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @version: v1.0.0 + * + * @update zhixin wen + */ + +!function ($) { + + 'use strict'; + + $.extend($.fn.bootstrapTable.defaults, { + keyEvents: false + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _init = BootstrapTable.prototype.init; + + BootstrapTable.prototype.init = function () { + _init.apply(this, Array.prototype.slice.apply(arguments)); + this.initKeyEvents(); + }; + + BootstrapTable.prototype.initKeyEvents = function () { + if (this.options.keyEvents) { + var that = this; + + $(document).off('keydown').on('keydown', function (e) { + var $search = that.$toolbar.find('.search input'), + $refresh = that.$toolbar.find('button[name="refresh"]'), + $toggle = that.$toolbar.find('button[name="toggle"]'), + $paginationSwitch = that.$toolbar.find('button[name="paginationSwitch"]'); + + if (document.activeElement === $search.get(0) || !$.contains(document.activeElement ,that.$toolbar.get(0))) { + return true; + } + + switch (e.keyCode) { + case 83: //s + if (!that.options.search) { + return; + } + $search.focus(); + return false; + case 82: //r + if (!that.options.showRefresh) { + return; + } + $refresh.click(); + return false; + case 84: //t + if (!that.options.showToggle) { + return; + } + $toggle.click(); + return false; + case 80: //p + if (!that.options.showPaginationSwitch) { + return; + } + $paginationSwitch.click(); + return false; + case 37: // left + if (!that.options.pagination) { + return; + } + that.prevPage(); + return false; + case 39: // right + if (!that.options.pagination) { + return; + } + that.nextPage(); + return; + } + }); + } + }; +}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/key-events/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/key-events/extension.json new file mode 100644 index 0000000000..966f6f8f29 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/key-events/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Key Events", + "version": "1.0.0", + "description": "Plugin to support the key events in the bootstrap table.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/key-events", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/key-events.html", + + "plugins": [{ + "name": "bootstrap-table-key-events", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/key-events" + }], + + "author": { + "name": "djhvscf", + "image": "https://avatars1.githubusercontent.com/u/4496763" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/mobile/bootstrap-table-mobile.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/mobile/bootstrap-table-mobile.js new file mode 100644 index 0000000000..21e2501a84 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/mobile/bootstrap-table-mobile.js @@ -0,0 +1,136 @@ +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @version: v1.1.0 + */ + +!function ($) { + + 'use strict'; + + var showHideColumns = function (that, checked) { + if (that.options.columnsHidden.length > 0 ) { + $.each(that.columns, function (i, column) { + if (that.options.columnsHidden.indexOf(column.field) !== -1) { + if (column.visible !== checked) { + that.toggleColumn(that.fieldsColumnsIndex[column.field], checked, true); + } + } + }); + } + }; + + var resetView = function (that) { + if (that.options.height || that.options.showFooter) { + setTimeout(function(){ + that.resetView.call(that); + }, 1); + } + }; + + var changeView = function (that, width, height) { + if (that.options.minHeight) { + if ((width <= that.options.minWidth) && (height <= that.options.minHeight)) { + conditionCardView(that); + } else if ((width > that.options.minWidth) && (height > that.options.minHeight)) { + conditionFullView(that); + } + } else { + if (width <= that.options.minWidth) { + conditionCardView(that); + } else if (width > that.options.minWidth) { + conditionFullView(that); + } + } + + resetView(that); + }; + + var conditionCardView = function (that) { + changeTableView(that, false); + showHideColumns(that, false); + }; + + var conditionFullView = function (that) { + changeTableView(that, true); + showHideColumns(that, true); + }; + + var changeTableView = function (that, cardViewState) { + that.options.cardView = cardViewState; + that.toggleView(); + }; + + var debounce = function(func,wait) { + var timeout; + return function() { + var context = this, + args = arguments; + var later = function() { + timeout = null; + func.apply(context,args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }; + + $.extend($.fn.bootstrapTable.defaults, { + mobileResponsive: false, + minWidth: 562, + minHeight: undefined, + heightThreshold: 100, // just slightly larger than mobile chrome's auto-hiding toolbar + checkOnInit: true, + columnsHidden: [] + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _init = BootstrapTable.prototype.init; + + BootstrapTable.prototype.init = function () { + _init.apply(this, Array.prototype.slice.apply(arguments)); + + if (!this.options.mobileResponsive) { + return; + } + + if (!this.options.minWidth) { + return; + } + + if (this.options.minWidth < 100 && this.options.resizable) { + console.log("The minWidth when the resizable extension is active should be greater or equal than 100"); + this.options.minWidth = 100; + } + + var that = this, + old = { + width: $(window).width(), + height: $(window).height() + }; + + $(window).on('resize orientationchange',debounce(function (evt) { + // reset view if height has only changed by at least the threshold. + var height = $(this).height(), + width = $(this).width(); + + if (Math.abs(old.height - height) > that.options.heightThreshold || old.width != width) { + changeView(that, width, height); + old = { + width: width, + height: height + }; + } + },200)); + + if (this.options.checkOnInit) { + var height = $(window).height(), + width = $(window).width(); + changeView(this, width, height); + old = { + width: width, + height: height + }; + } + }; +}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/mobile/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/mobile/extension.json new file mode 100644 index 0000000000..433eb77fe9 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/mobile/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Mobile", + "version": "1.1.0", + "description": "Plugin to support the responsive feature.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/mobile", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/mobile.html", + + "plugins": [{ + "name": "bootstrap-table-mobile", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/mobile" + }], + + "author": { + "name": "djhvscf", + "image": "https://avatars1.githubusercontent.com/u/4496763" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/bootstrap-table-multi-toggle.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/bootstrap-table-multi-toggle.js new file mode 100644 index 0000000000..4cb110bd89 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/bootstrap-table-multi-toggle.js @@ -0,0 +1,88 @@ +/** + * @author Homer Glascock + * @version: v1.0.0 + */ + + !function ($) { + "use strict"; + + var sprintf = $.fn.bootstrapTable.utils.sprintf; + + var reInit = function (self) { + self.initHeader(); + self.initSearch(); + self.initPagination(); + self.initBody(); + }; + + $.extend($.fn.bootstrapTable.defaults, { + showToggleBtn: false, + multiToggleDefaults: [], //column names go here + }); + + $.fn.bootstrapTable.methods.push('hideAllColumns', 'showAllColumns'); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _initToolbar = BootstrapTable.prototype.initToolbar; + + BootstrapTable.prototype.initToolbar = function () { + + _initToolbar.apply(this, Array.prototype.slice.apply(arguments)); + + var that = this, + $btnGroup = this.$toolbar.find('>.btn-group'); + + if (typeof this.options.multiToggleDefaults === 'string') { + this.options.multiToggleDefaults = JSON.parse(this.options.multiToggleDefaults); + } + + if (this.options.showToggleBtn && this.options.showColumns) { + var showbtn = "", + hidebtn = ""; + + $btnGroup.append(showbtn + hidebtn); + + $btnGroup.find('#showAllBtn').click(function () { that.showAllColumns(); + $btnGroup.find('#hideAllBtn').toggleClass('hidden'); + $btnGroup.find('#showAllBtn').toggleClass('hidden'); + }); + $btnGroup.find('#hideAllBtn').click(function () { that.hideAllColumns(); + $btnGroup.find('#hideAllBtn').toggleClass('hidden'); + $btnGroup.find('#showAllBtn').toggleClass('hidden'); + }); + } + }; + + BootstrapTable.prototype.hideAllColumns = function () { + var that = this, + defaults = that.options.multiToggleDefaults; + + $.each(this.columns, function (index, column) { + //if its one of the defaults dont touch it + if (defaults.indexOf(column.field) == -1 && column.switchable) { + column.visible = false; + var $items = that.$toolbar.find('.keep-open input').prop('disabled', false); + $items.filter(sprintf('[value="%s"]', index)).prop('checked', false); + } + }); + + reInit(that); + }; + + BootstrapTable.prototype.showAllColumns = function () { + var that = this; + $.each(this.columns, function (index, column) { + if (column.switchable) { + column.visible = true; + } + + var $items = that.$toolbar.find('.keep-open input').prop('disabled', false); + $items.filter(sprintf('[value="%s"]', index)).prop('checked', true); + }); + + reInit(that); + + that.toggleColumn(0, that.columns[0].visible, false); + }; + +}(jQuery); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/extension.json new file mode 100644 index 0000000000..e27efdc881 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/multi-column-toggle/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Multi Column Toggle", + "version": "1.0.0", + "description": "Allows hiding and showing of multiple columns at once.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multi-column-toggle", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/multi-column-toggle.html", + + "plugins": [{ + "name": "multi-column-toggle", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multi-column-toggle" + }], + + "author": { + "name": "Homer Glascock", + "image": "https://avatars1.githubusercontent.com/u/5546710" + } +} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/bootstrap-table-multiple-search.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/bootstrap-table-multiple-search.js new file mode 100644 index 0000000000..a8e264e41f --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/bootstrap-table-multiple-search.js @@ -0,0 +1,71 @@ +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @version: v1.0.0 + */ + +!function ($) { + + 'use strict'; + + $.extend($.fn.bootstrapTable.defaults, { + multipleSearch: false, + delimeter: " " + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _initSearch = BootstrapTable.prototype.initSearch; + + BootstrapTable.prototype.initSearch = function () { + if (this.options.multipleSearch) { + if (this.searchText === undefined) { + return; + } + var strArray = this.searchText.split(this.options.delimeter), + that = this, + f = $.isEmptyObject(this.filterColumns) ? null : this.filterColumns, + dataFiltered = []; + + if (strArray.length === 1) { + _initSearch.apply(this, Array.prototype.slice.apply(arguments)); + } else { + for (var i = 0; i < strArray.length; i++) { + var str = strArray[i].trim(); + dataFiltered = str ? $.grep(dataFiltered.length === 0 ? this.options.data : dataFiltered, function (item, i) { + for (var key in item) { + key = $.isNumeric(key) ? parseInt(key, 10) : key; + var value = item[key], + column = that.columns[that.fieldsColumnsIndex[key]], + j = $.inArray(key, that.header.fields); + + // Fix #142: search use formated data + if (column && column.searchFormatter) { + value = $.fn.bootstrapTable.utils.calculateObjectValue(column, + that.header.formatters[j], [value, item, i], value); + } + + var index = $.inArray(key, that.header.fields); + if (index !== -1 && that.header.searchables[index] && (typeof value === 'string' || typeof value === 'number')) { + if (that.options.strictSearch) { + if ((value + '').toLowerCase() === str) { + return true; + } + } else { + if ((value + '').toLowerCase().indexOf(str) !== -1) { + return true; + } + } + } + } + return false; + }) : this.data; + } + + this.data = dataFiltered; + } + } else { + _initSearch.apply(this, Array.prototype.slice.apply(arguments)); + } + }; + +}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/extension.json new file mode 100644 index 0000000000..5160d1a98b --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-search/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Multiple Search", + "version": "1.0.0", + "description": "Plugin to support the multiple search.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-search", + "example": "#", + + "plugins": [{ + "name": "bootstrap-table-multiple-search", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-search" + }], + + "author": { + "name": "djhvscf", + "image": "https://avatars1.githubusercontent.com/u/4496763" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.css new file mode 100644 index 0000000000..81da76b5f3 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.css @@ -0,0 +1,17 @@ +.multiple-select-row-selected { + background: lightBlue +} + +.table tbody tr:hover td, +.table tbody tr:hover th { + background-color: transparent; +} + + +.table-striped tbody tr:nth-child(odd):hover td { + background-color: #F9F9F9; +} + +.fixed-table-container tbody .selected td { + background: lightBlue; +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.js new file mode 100644 index 0000000000..597b28b8f7 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/bootstrap-table-multiple-selection-row.js @@ -0,0 +1,127 @@ +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @version: v1.0.0 + */ + +!function ($) { + + 'use strict'; + + document.onselectstart = function() { + return false; + }; + + var getTableObjectFromCurrentTarget = function (currentTarget) { + currentTarget = $(currentTarget); + return currentTarget.is("table") ? currentTarget : currentTarget.parents().find(".table"); + }; + + var getRow = function (target) { + target = $(target); + return target.parent().parent(); + }; + + var onRowClick = function (e) { + var that = getTableObjectFromCurrentTarget(e.currentTarget); + + if (window.event.ctrlKey) { + toggleRow(e.currentTarget, that, false, false); + } + + if (window.event.button === 0) { + if (!window.event.ctrlKey && !window.event.shiftKey) { + clearAll(that); + toggleRow(e.currentTarget, that, false, false); + } + + if (window.event.shiftKey) { + selectRowsBetweenIndexes([that.bootstrapTable("getOptions").multipleSelectRowLastSelectedRow.rowIndex, e.currentTarget.rowIndex], that) + } + } + }; + + var onCheckboxChange = function (e) { + var that = getTableObjectFromCurrentTarget(e.currentTarget); + clearAll(that); + toggleRow(getRow(e.currentTarget), that, false, false); + }; + + var toggleRow = function (row, that, clearAll, useShift) { + if (clearAll) { + row = $(row); + that.bootstrapTable("getOptions").multipleSelectRowLastSelectedRow = undefined; + row.removeClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass); + that.bootstrapTable("uncheck", row.data("index")); + } else { + that.bootstrapTable("getOptions").multipleSelectRowLastSelectedRow = row; + row = $(row); + if (useShift) { + row.addClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass); + that.bootstrapTable("check", row.data("index")); + } else { + if(row.hasClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass)) { + row.removeClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass) + that.bootstrapTable("uncheck", row.data("index")); + } else { + row.addClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass); + that.bootstrapTable("check", row.data("index")); + } + } + } + }; + + var selectRowsBetweenIndexes = function (indexes, that) { + indexes.sort(function(a, b) { + return a - b; + }); + + for (var i = indexes[0]; i <= indexes[1]; i++) { + toggleRow(that.bootstrapTable("getOptions").multipleSelectRowRows[i-1], that, false, true); + } + }; + + var clearAll = function (that) { + for (var i = 0; i < that.bootstrapTable("getOptions").multipleSelectRowRows.length; i++) { + toggleRow(that.bootstrapTable("getOptions").multipleSelectRowRows[i], that, true, false); + } + }; + + $.extend($.fn.bootstrapTable.defaults, { + multipleSelectRow: false, + multipleSelectRowCssClass: 'multiple-select-row-selected', + //internal variables used by the extension + multipleSelectRowLastSelectedRow: undefined, + multipleSelectRowRows: [] + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _init = BootstrapTable.prototype.init, + _initBody = BootstrapTable.prototype.initBody; + + BootstrapTable.prototype.init = function () { + if (this.options.multipleSelectRow) { + var that = this; + + //Make sure that the internal variables have the correct value + this.options.multipleSelectRowLastSelectedRow = undefined; + this.options.multipleSelectRowRows = []; + + this.$el.on("post-body.bs.table", function (e) { + setTimeout(function () { + that.options.multipleSelectRowRows = that.$body.children(); + that.options.multipleSelectRowRows.click(onRowClick); + that.options.multipleSelectRowRows.find("input[type=checkbox]").change(onCheckboxChange); + }, 1); + }); + } + + _init.apply(this, Array.prototype.slice.apply(arguments)); + }; + + BootstrapTable.prototype.clearAllMultipleSelectionRow = function () { + clearAll(this); + }; + + $.fn.bootstrapTable.methods.push('clearAllMultipleSelectionRow'); +}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/extension.json new file mode 100644 index 0000000000..69d4a9effa --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-selection-row/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Multiple Selection Row", + "version": "1.0.0", + "description": "Plugin to enable the multiple selection row. You can use the ctrl+click to select one row or use ctrl+shift+click to select a range of rows.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-selection-row", + "example": "", + + "plugins": [{ + "name": "bootstrap-table-multiple-selection-row", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-selection-row" + }], + + "author": { + "name": "djhvscf", + "image": "https://avatars1.githubusercontent.com/u/4496763" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-sort/bootstrap-table-multiple-sort.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-sort/bootstrap-table-multiple-sort.js new file mode 100644 index 0000000000..13493f87e9 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-sort/bootstrap-table-multiple-sort.js @@ -0,0 +1,412 @@ +/** + * @author Nadim Basalamah + * @version: v1.1.0 + * https://github.com/dimbslmh/bootstrap-table/tree/master/src/extensions/multiple-sort/bootstrap-table-multiple-sort.js + * Modification: ErwannNevou + */ + +(function($) { + 'use strict'; + + var isSingleSort = false; + + var showSortModal = function(that) { + var _selector = that.sortModalSelector, + _id = '#' + _selector; + + if (!$(_id).hasClass("modal")) { + var sModal = ' '; + + $('body').append($(sModal)); + + that.$sortModal = $(_id); + var $rows = that.$sortModal.find('tbody > tr'); + + that.$sortModal.off('click', '#add').on('click', '#add', function() { + var total = that.$sortModal.find('.multi-sort-name:first option').length, + current = that.$sortModal.find('tbody tr').length; + + if (current < total) { + current++; + that.addLevel(); + that.setButtonStates(); + } + }); + + that.$sortModal.off('click', '#delete').on('click', '#delete', function() { + var total = that.$sortModal.find('.multi-sort-name:first option').length, + current = that.$sortModal.find('tbody tr').length; + + if (current > 1 && current <= total) { + current--; + that.$sortModal.find('tbody tr:last').remove(); + that.setButtonStates(); + } + }); + + that.$sortModal.off('click', '.btn-primary').on('click', '.btn-primary', function() { + var $rows = that.$sortModal.find('tbody > tr'), + $alert = that.$sortModal.find('div.alert'), + fields = [], + results = []; + + + that.options.sortPriority = $.map($rows, function(row) { + var $row = $(row), + name = $row.find('.multi-sort-name').val(), + order = $row.find('.multi-sort-order').val(); + + fields.push(name); + + return { + sortName: name, + sortOrder: order + }; + }); + + var sorted_fields = fields.sort(); + + for (var i = 0; i < fields.length - 1; i++) { + if (sorted_fields[i + 1] == sorted_fields[i]) { + results.push(sorted_fields[i]); + } + } + + if (results.length > 0) { + if ($alert.length === 0) { + $alert = ''; + $($alert).insertBefore(that.$sortModal.find('.bars')); + } + } else { + if ($alert.length === 1) { + $($alert).remove(); + } + + that.$sortModal.modal('hide'); + that.options.sortName = ''; + + if (that.options.sidePagination === 'server') { + var t = that.options.queryParams; + that.options.queryParams = function(params) { + params.multiSort = that.options.sortPriority; + return t(params); + }; + isSingleSort=false; + that.initServer(that.options.silentSort); + return; + } + that.onMultipleSort(); + + } + }); + + if (that.options.sortPriority === null || that.options.sortPriority.length === 0) { + if (that.options.sortName) { + that.options.sortPriority = [{ + sortName: that.options.sortName, + sortOrder: that.options.sortOrder + }]; + } + } + + if (that.options.sortPriority !== null && that.options.sortPriority.length > 0) { + if ($rows.length < that.options.sortPriority.length && typeof that.options.sortPriority === 'object') { + for (var i = 0; i < that.options.sortPriority.length; i++) { + that.addLevel(i, that.options.sortPriority[i]); + } + } + } else { + that.addLevel(0); + } + + that.setButtonStates(); + } + }; + + $.fn.bootstrapTable.methods.push('multipleSort'); + + $.extend($.fn.bootstrapTable.defaults, { + showMultiSort: false, + showMultiSortButton: true, + sortPriority: null, + onMultipleSort: function() { + return false; + } + }); + + $.extend($.fn.bootstrapTable.defaults.icons, { + sort: 'glyphicon-sort', + plus: 'glyphicon-plus', + minus: 'glyphicon-minus' + }); + + $.extend($.fn.bootstrapTable.Constructor.EVENTS, { + 'multiple-sort.bs.table': 'onMultipleSort' + }); + + $.extend($.fn.bootstrapTable.locales, { + formatMultipleSort: function() { + return 'Multiple Sort'; + }, + formatAddLevel: function() { + return 'Add Level'; + }, + formatDeleteLevel: function() { + return 'Delete Level'; + }, + formatColumn: function() { + return 'Column'; + }, + formatOrder: function() { + return 'Order'; + }, + formatSortBy: function() { + return 'Sort by'; + }, + formatThenBy: function() { + return 'Then by'; + }, + formatSort: function() { + return 'Sort'; + }, + formatCancel: function() { + return 'Cancel'; + }, + formatDuplicateAlertTitle: function() { + return 'Duplicate(s) detected!'; + }, + formatDuplicateAlertDescription: function() { + return 'Please remove or change any duplicate column.'; + }, + formatSortOrders: function() { + return { + asc: 'Ascending', + desc: 'Descending' + }; + } + }); + + $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _initToolbar = BootstrapTable.prototype.initToolbar; + + BootstrapTable.prototype.initToolbar = function() { + this.showToolbar = this.showToolbar || this.options.showMultiSort; + var that = this, + sortModalSelector = 'sortModal_' + this.$el.attr('id'), + sortModalId = '#' + sortModalSelector; + this.$sortModal = $(sortModalId); + this.sortModalSelector = sortModalSelector; + + _initToolbar.apply(this, Array.prototype.slice.apply(arguments)); + + if (that.options.sidePagination === 'server' && !isSingleSort && that.options.sortPriority !== null){ + var t = that.options.queryParams; + that.options.queryParams = function(params) { + params.multiSort = that.options.sortPriority; + return t(params); + }; + } + + if (this.options.showMultiSort) { + var $btnGroup = this.$toolbar.find('>.btn-group').first(), + $multiSortBtn = this.$toolbar.find('div.multi-sort'); + + if (!$multiSortBtn.length && this.options.showMultiSortButton) { + $multiSortBtn = ' '; + + $btnGroup.append($multiSortBtn); + + showSortModal(that); + } + + this.$el.on('sort.bs.table', function() { + isSingleSort = true; + }); + + this.$el.on('multiple-sort.bs.table', function() { + isSingleSort = false; + }); + + this.$el.on('load-success.bs.table', function() { + if (!isSingleSort && that.options.sortPriority !== null && typeof that.options.sortPriority === 'object' && that.options.sidePagination !== 'server') { + that.onMultipleSort(); + } + }); + + this.$el.on('column-switch.bs.table', function(field, checked) { + for (var i = 0; i < that.options.sortPriority.length; i++) { + if (that.options.sortPriority[i].sortName === checked) { + that.options.sortPriority.splice(i, 1); + } + } + + that.assignSortableArrows(); + that.$sortModal.remove(); + showSortModal(that); + }); + + this.$el.on('reset-view.bs.table', function() { + if (!isSingleSort && that.options.sortPriority !== null && typeof that.options.sortPriority === 'object') { + that.assignSortableArrows(); + } + }); + } + }; + + BootstrapTable.prototype.multipleSort = function() { + var that = this; + if (!isSingleSort && that.options.sortPriority !== null && typeof that.options.sortPriority === 'object' && that.options.sidePagination !== 'server') { + that.onMultipleSort(); + } + }; + + BootstrapTable.prototype.onMultipleSort = function() { + var that = this; + + var cmp = function(x, y) { + return x > y ? 1 : x < y ? -1 : 0; + }; + + var arrayCmp = function(a, b) { + var arr1 = [], + arr2 = []; + + for (var i = 0; i < that.options.sortPriority.length; i++) { + var order = that.options.sortPriority[i].sortOrder === 'desc' ? -1 : 1, + aa = a[that.options.sortPriority[i].sortName], + bb = b[that.options.sortPriority[i].sortName]; + + if (aa === undefined || aa === null) { + aa = ''; + } + if (bb === undefined || bb === null) { + bb = ''; + } + if ($.isNumeric(aa) && $.isNumeric(bb)) { + aa = parseFloat(aa); + bb = parseFloat(bb); + } + if (typeof aa !== 'string') { + aa = aa.toString(); + } + + arr1.push( + order * cmp(aa, bb)); + arr2.push( + order * cmp(bb, aa)); + } + + return cmp(arr1, arr2); + }; + + this.data.sort(function(a, b) { + return arrayCmp(a, b); + }); + + this.initBody(); + this.assignSortableArrows(); + this.trigger('multiple-sort'); + }; + + BootstrapTable.prototype.addLevel = function(index, sortPriority) { + var text = index === 0 ? this.options.formatSortBy() : this.options.formatThenBy(); + + this.$sortModal.find('tbody') + .append($('') + .append($('').text(text)) + .append($('').append($(''))) + ); + + var $multiSortName = this.$sortModal.find('.multi-sort-name').last(), + $multiSortOrder = this.$sortModal.find('.multi-sort-order').last(); + + $.each(this.columns, function(i, column) { + if (column.sortable === false || column.visible === false) { + return true; + } + $multiSortName.append(''); + }); + + $.each(this.options.formatSortOrders(), function(value, order) { + $multiSortOrder.append(''); + }); + + if (sortPriority !== undefined) { + $multiSortName.find('option[value="' + sortPriority.sortName + '"]').attr("selected", true); + $multiSortOrder.find('option[value="' + sortPriority.sortOrder + '"]').attr("selected", true); + } + }; + + BootstrapTable.prototype.assignSortableArrows = function() { + var that = this, + headers = that.$header.find('th'); + + for (var i = 0; i < headers.length; i++) { + for (var c = 0; c < that.options.sortPriority.length; c++) { + if ($(headers[i]).data('field') === that.options.sortPriority[c].sortName) { + $(headers[i]).find('.sortable').removeClass('desc asc').addClass(that.options.sortPriority[c].sortOrder); + } + } + } + }; + + BootstrapTable.prototype.setButtonStates = function() { + var total = this.$sortModal.find('.multi-sort-name:first option').length, + current = this.$sortModal.find('tbody tr').length; + + if (current == total) { + this.$sortModal.find('#add').attr('disabled', 'disabled'); + } + if (current > 1) { + this.$sortModal.find('#delete').removeAttr('disabled'); + } + if (current < total) { + this.$sortModal.find('#add').removeAttr('disabled'); + } + if (current == 1) { + this.$sortModal.find('#delete').attr('disabled', 'disabled'); + } + }; +})(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-sort/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-sort/extension.json new file mode 100644 index 0000000000..580082a26b --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/multiple-sort/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Multiple Sort", + "version": "1.1.0", + "description": "Plugin to support the multiple sort.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-sort", + "example": "#", + + "plugins": [{ + "name": "bootstrap-table-multiple-sort", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-sort" + }], + + "author": { + "name": "dimbslmh", + "image": "https://avatars1.githubusercontent.com/u/745635" + } +} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/bootstrap-table-natural-sorting.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/bootstrap-table-natural-sorting.js new file mode 100644 index 0000000000..30e6521760 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/bootstrap-table-natural-sorting.js @@ -0,0 +1,67 @@ +/** + * @author: Brian Huisman + * @webSite: http://www.greywyvern.com + * @version: v1.0.0 + * JS functions to allow natural sorting on bootstrap-table columns + * add data-sorter="alphanum" or data-sorter="numericOnly" to any th + * + * @update Dennis Hernández + * @update Duane May + */ + +function alphanum(a, b) { + function chunkify(t) { + var tz = [], + x = 0, + y = -1, + n = 0, + i, + j; + + while (i = (j = t.charAt(x++)).charCodeAt(0)) { + var m = (i === 46 || (i >= 48 && i <= 57)); + if (m !== n) { + tz[++y] = ""; + n = m; + } + tz[y] += j; + } + return tz; + } + + function stringfy(v) { + if (typeof(v) === "number") { + v = "" + v; + } + if (!v) { + v = ""; + } + return v; + } + + var aa = chunkify(stringfy(a)); + var bb = chunkify(stringfy(b)); + + for (x = 0; aa[x] && bb[x]; x++) { + if (aa[x] !== bb[x]) { + var c = Number(aa[x]), + d = Number(bb[x]); + + if (c == aa[x] && d == bb[x]) { + return c - d; + } else { + return (aa[x] > bb[x]) ? 1 : -1; + } + } + } + return aa.length - bb.length; +} + +function numericOnly(a, b) { + function stripNonNumber(s) { + s = s.replace(new RegExp(/[^0-9]/g), ""); + return parseInt(s, 10); + } + + return stripNonNumber(a) - stripNonNumber(b); +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/extension.json new file mode 100644 index 0000000000..06bf4e4ece --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/natural-sorting/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Natural Sorting", + "version": "1.0.0", + "description": "Plugin to support the natural sorting.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/natural-sorting", + "example": "#", + + "plugins": [{ + "name": "bootstrap-table-natural-sorting", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/natural-sorting" + }], + + "author": { + "name": "GreyWyvern", + "image": "https://avatars1.githubusercontent.com/u/137631" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/page-jump-to/bootstrap-table-page-jump-to.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/page-jump-to/bootstrap-table-page-jump-to.css new file mode 100644 index 0000000000..3db1ade83f --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/page-jump-to/bootstrap-table-page-jump-to.css @@ -0,0 +1,8 @@ +.jumpto input { + height: 31px; + width: 50px; + margin-left: 5px; + margin-right: 5px; + text-align: center; + display: inline-block; +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/page-jump-to/bootstrap-table-page-jump-to.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/page-jump-to/bootstrap-table-page-jump-to.js new file mode 100644 index 0000000000..f09d5cd324 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/page-jump-to/bootstrap-table-page-jump-to.js @@ -0,0 +1,50 @@ +/** + * @author Jay + */ + +(function ($) { + 'use strict'; + var sprintf = $.fn.bootstrapTable.utils.sprintf; + + $.extend($.fn.bootstrapTable.defaults, { + showJumpto: false, + exportOptions: {} + }); + + $.extend($.fn.bootstrapTable.locales, { + formatJumpto: function () { + return 'GO'; + } + }); + $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _initPagination = BootstrapTable.prototype.initPagination; + + BootstrapTable.prototype.initPagination = function () { + _initPagination.apply(this, Array.prototype.slice.apply(arguments)); + + if (this.options.showJumpto) { + var that = this, + $pageGroup = this.$pagination.find('ul.pagination'), + $jumpto = $pageGroup.find('li.jumpto'); + + if (!$jumpto.length) { + $jumpto = $([ + '
  • ', + '', + '', + '
  • '].join('')).appendTo($pageGroup); + + $jumpto.find('button').click(function () { + that.selectPage(parseInt($jumpto.find('input').val())); + }); + } + } + }; +})(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/LICENSE b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/LICENSE new file mode 100644 index 0000000000..608dadfb85 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/LICENSE @@ -0,0 +1,21 @@ +(The MIT License) + +Copyright (c) 2019 doug-the-guy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/README.md b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/README.md new file mode 100644 index 0000000000..c5019431f2 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/README.md @@ -0,0 +1,92 @@ +# Bootstrap Table Pipelining + +Use Plugin: [bootstrap-table-pipeline] + +This plugin enables client side data caching for server side requests which will +eliminate the need to issue a new request every page change. This will allow +for a performance balance for a large data set between returning all data at once +(client side paging) and a new server side request (server side paging). + +There are two new options: +- usePipeline: enables this feature +- pipelineSize: the size of each cache window + +The size of the pipeline must be evenly divisible by the current page size. This is +assured by rounding up to the nearest evenly divisible value. For example, if +the pipeline size is 4990 and the current page size is 25, then pipeline size will +be dynamically set to 5000. + +The cache windows are computed based on the pipeline size and the total number of rows +returned by the server side query. For example, with pipeline size 500 and total rows +1300, the cache windows will be: + +[{'lower': 0, 'upper': 499}, {'lower': 500, 'upper': 999}, {'lower': 1000, 'upper': 1499}] + +Using the limit (i.e. the pipelineSize) and offset parameters, the server side request +**MUST** return only the data in the requested cache window **AND** the total number of rows. +To wit, the server side code must use the offset and limit parameters to prepare the response +data. + +On a page change, the new offset is checked if it is within the current cache window. If so, +the requested page data is returned from the cached data set. Otherwise, a new server side +request will be issued for the new cache window. + +The current cached data is only invalidated on these events: + - sorting + - searching + - page size change + - page change moves into a new cache window + +There are two new events: +- cached-data-hit.bs.table: issued when cached data is used on a page change +- cached-data-reset.bs.table: issued when the cached data is invalidated and new server side request is issued + +## Features + +* Created with Bootstrap 4 + +## Usage + +``` +# assumed import of bootstrap and bootstrap-table assets + +... + + + + + + +
    TypeValueDate
    +``` + +## Options + +### usePipeline + +* type: Boolean +* description: Set true to enable pipelining +* default: `false` + +## pipelineSize + +* type: Integer +* description: Size of each cache window. Must be greater than 0 +* default: `1000` + +## Events + +### onCachedDataHit(cached-data-hit.bs.table) + +* Fires when paging was able to use the locally cached data. + +### onCachedDataReset(cached-data-reset.bs.table) + +* Fires when the locally cached data needed to be reset (i.e. on sorting, searching, page size change or paged out of current cache window) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/bootstrap-table-pipeline.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/bootstrap-table-pipeline.js new file mode 100644 index 0000000000..954ee28aaf --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/bootstrap-table-pipeline.js @@ -0,0 +1,330 @@ +/** + * @author doug-the-guy + * @version v1.0.0 + * + * Boostrap Table Pipeline + * ----------------------- + * + * This plugin enables client side data caching for server side requests which will + * eliminate the need to issue a new request every page change. This will allow + * for a performance balance for a large data set between returning all data at once + * (client side paging) and a new server side request (server side paging). + * + * There are two new options: + * - usePipeline: enables this feature + * - pipelineSize: the size of each cache window + * + * The size of the pipeline must be evenly divisible by the current page size. This is + * assured by rounding up to the nearest evenly divisible value. For example, if + * the pipeline size is 4990 and the current page size is 25, then pipeline size will + * be dynamically set to 5000. + * + * The cache windows are computed based on the pipeline size and the total number of rows + * returned by the server side query. For example, with pipeline size 500 and total rows + * 1300, the cache windows will be: + * + * [{'lower': 0, 'upper': 499}, {'lower': 500, 'upper': 999}, {'lower': 1000, 'upper': 1499}] + * + * Using the limit (i.e. the pipelineSize) and offset parameters, the server side request + * **MUST** return only the data in the requested cache window **AND** the total number of rows. + * To wit, the server side code must use the offset and limit parameters to prepare the response + * data. + * + * On a page change, the new offset is checked if it is within the current cache window. If so, + * the requested page data is returned from the cached data set. Otherwise, a new server side + * request will be issued for the new cache window. + * + * The current cached data is only invalidated on these events: + * * sorting + * * searching + * * page size change + * * page change moves into a new cache window + * + * There are two new events: + * - cached-data-hit.bs.table: issued when cached data is used on a page change + * - cached-data-reset.bs.table: issued when the cached data is invalidated and a + * new server side request is issued + * + **/ + +(function ($) { + + 'use strict'; + + var Utils = $.fn.bootstrapTable.utils; + + $.extend($.fn.bootstrapTable.defaults, { + usePipeline: false, + pipelineSize: 1000, + onCachedDataHit: function(data) { + return false; + }, + onCachedDataReset: function(data){ + return false; + } + }); + + $.extend($.fn.bootstrapTable.Constructor.EVENTS, { + 'cached-data-hit.bs.table': 'onCachedDataHit', + 'cached-data-reset.bs.table': 'onCachedDataReset' + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _init = BootstrapTable.prototype.init, + _initServer = BootstrapTable.prototype.initServer, + _onSearch = BootstrapTable.prototype.onSearch, + _onSort = BootstrapTable.prototype.onSort, + _onPageListChange = BootstrapTable.prototype.onPageListChange; + + BootstrapTable.prototype.init = function () { + // needs to be called before initServer() + this.initPipeline(); + _init.apply(this, Array.prototype.slice.apply(arguments)); + }; + + BootstrapTable.prototype.initPipeline = function() { + this.cacheRequestJSON = {}; + this.cacheWindows = []; + this.currWindow = 0; + this.resetCache = true; + }; + + BootstrapTable.prototype.onSearch = function(event) { + /* force a cache reset on search */ + if (this.options.usePipeline) { + this.resetCache = true; + } + _onSearch.apply(this, Array.prototype.slice.apply(arguments)); + }; + + BootstrapTable.prototype.onSort = function(event) { + /* force a cache reset on sort */ + if (this.options.usePipeline) { + this.resetCache = true; + } + _onSort.apply(this, Array.prototype.slice.apply(arguments)); + }; + + BootstrapTable.prototype.onPageListChange = function (event) { + /* rebuild cache window on page size change */ + var target = $(event.currentTarget); + var newPageSize = parseInt(target.text()); + this.options.pipelineSize = this.calculatePipelineSize(this.options.pipelineSize, newPageSize); + this.resetCache = true; + _onPageListChange.apply(this, Array.prototype.slice.apply(arguments)); + }; + + BootstrapTable.prototype.calculatePipelineSize = function(pipelineSize, pageSize) { + /* calculate pipeline size by rounding up to the nearest value evenly divisible + * by the pageSize */ + if(pageSize == 0) return 0; + return Math.ceil(pipelineSize/pageSize) * pageSize; + }; + + BootstrapTable.prototype.setCacheWindows = function() { + /* set cache windows based on the total number of rows returned by server side + * request and the pipelineSize */ + this.cacheWindows = []; + var numWindows = this.options.totalRows / this.options.pipelineSize; + for(var i = 0; i <= numWindows; i++){ + var b = i * this.options.pipelineSize; + this.cacheWindows[i] = {'lower': b, 'upper': b + this.options.pipelineSize - 1}; + } + }; + + BootstrapTable.prototype.setCurrWindow = function(offset) { + /* set the current cache window index, based on where the current offset falls */ + this.currWindow = 0; + for(var i = 0; i < this.cacheWindows.length; i++){ + if(this.cacheWindows[i].lower <= offset && offset <= this.cacheWindows[i].upper){ + this.currWindow = i; + break; + } + } + }; + + BootstrapTable.prototype.drawFromCache = function(offset, limit) { + /* draw rows from the cache using offset and limit */ + var res = $.extend(true, {}, this.cacheRequestJSON); + var drawStart = offset - this.cacheWindows[this.currWindow].lower; + var drawEnd = drawStart + limit; + res.rows = res.rows.slice(drawStart, drawEnd); + return res; + }; + + BootstrapTable.prototype.initServer = function(silent, query, url){ + /* determine if requested data is in cache (on paging) or if + * a new ajax request needs to be issued (sorting, searching, paging + * moving outside of cached data, page size change) + * initial version of this extension will entirely override base initServer + **/ + + var data = {}; + var index = this.header.fields.indexOf(this.options.sortName); + + var params = { + searchText: this.searchText, + sortName: this.options.sortName, + sortOrder: this.options.sortOrder + }; + + var request = null; + + if (this.header.sortNames[index]) { + params.sortName = this.header.sortNames[index]; + } + + if (this.options.pagination && this.options.sidePagination === 'server') { + params.pageSize = this.options.pageSize === this.options.formatAllRows() + ? this.options.totalRows : this.options.pageSize + params.pageNumber = this.options.pageNumber + } + + if (!(url || this.options.url) && !this.options.ajax) { + return; + } + + var useAjax = true; + if (this.options.queryParamsType === 'limit') { + params = { + searchText: params.searchText, + sortName: params.sortName, + sortOrder: params.sortOrder + } + if (this.options.pagination && this.options.sidePagination === 'server') { + params.limit = this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize; + params.offset = (this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize) * (this.options.pageNumber - 1); + if (this.options.usePipeline) { + // if cacheWindows is empty, this is the initial request + if(!this.cacheWindows.length){ + useAjax = true; + params.drawOffset = params.offset; + // cache exists: determine if the page request is entirely within the current cached window + } else { + var w = this.cacheWindows[this.currWindow]; + // case 1: reset cache but stay within current window (e.g. column sort) + // case 2: move outside of the current window (e.g. search or paging) + // since each cache window is aligned with the current page size + // checking if params.offset is outside the current window is sufficient. + // need to requery for preceding or succeeding cache window + // also handle case + if(this.resetCache || (params.offset < w.lower || params.offset > w.upper)){ + useAjax = true; + this.setCurrWindow(params.offset); + // store the relative offset for drawing the page data afterwards + params.drawOffset = params.offset; + // now set params.offset to the lower bound of the new cache window + // the server will return that whole cache window + params.offset = this.cacheWindows[this.currWindow].lower; + // within current cache window + } else { + useAjax = false; + } + } + } else { + if (params.limit === 0) { + delete params.limit; + } + } + } + } + + // force an ajax call - this is on search, sort or page size change + if (this.resetCache) { + useAjax = true; + this.resetCache = false; + } + + if(this.options.usePipeline && useAjax) { + /* in this scenario limit is used on the server to get the cache window + * and drawLimit is used to get the page data afterwards */ + params.drawLimit = params.limit; + params.limit = this.options.pipelineSize; + } + + // cached results can be used + if(!useAjax) { + var res = this.drawFromCache(params.offset, params.limit); + this.load(res); + this.trigger('load-success', res); + this.trigger('cached-data-hit', res); + return; + } + // cached results can't be used + // continue base initServer code + if (!($.isEmptyObject(this.filterColumnsPartial))) { + params.filter = JSON.stringify(this.filterColumnsPartial, null); + } + + data = Utils.calculateObjectValue(this.options, this.options.queryParams, [params], data); + + $.extend(data, query || {}); + + // false to stop request + if (data === false) { + return; + } + + if (!silent) { + this.$tableLoading.show(); + } + var self = this; + + request = $.extend({}, Utils.calculateObjectValue(null, this.options.ajaxOptions), { + type: this.options.method, + url: url || this.options.url, + data: this.options.contentType === 'application/json' && this.options.method === 'post' + ? JSON.stringify(data) : data, + cache: this.options.cache, + contentType: this.options.contentType, + dataType: this.options.dataType, + success: function(res){ + res = Utils.calculateObjectValue(self.options, self.options.responseHandler, [res], res); + // cache results if using pipelining + if(self.options.usePipeline){ + // store entire request in cache + self.cacheRequestJSON = $.extend(true, {}, res); + // this gets set in load() also but needs to be set before + // setting cacheWindows + self.options.totalRows = res[self.options.totalField]; + // if this is a search, potentially less results will be returned + // so cache windows need to be rebuilt. Otherwise it + // will come out the same + self.setCacheWindows(); + self.setCurrWindow(params.drawOffset); + // just load data for the page + res = self.drawFromCache(params.drawOffset, params.drawLimit); + self.trigger('cached-data-reset', res); + } + self.load(res); + self.trigger('load-success', res); + if (!silent) self.$tableLoading.hide(); + }, + error: function(res){ + var data = []; + if (self.options.sidePagination === 'server') { + data = {}; + data[self.options.totalField] = 0; + data[self.options.dataField] = []; + } + self.load(data); + self.trigger('load-error', res.status, res); + if (!silent) self.$tableLoading.hide(); + } + }); + + if (this.options.ajax) { + Utils.calculateObjectValue(this, this.options.ajax, [request], null); + } else { + if (this._xhr && this._xhr.readyState !== 4) { + this._xhr.abort(); + } + this._xhr = $.ajax(request); + } + } + + $.fn.bootstrapTable.methods.push(); + + + +})(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/extension.json new file mode 100644 index 0000000000..de569c848c --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/pipeline/extension.json @@ -0,0 +1,18 @@ +{ + "name": "Pipeline", + "version": "1.0.0", + "description": "Plugin to support a hybrid approach to server/client side paging.", + "url": "", + "example": "#", + + "plugins": [{ + "name": "bootstrap-table-pipeline", + "url": "" + }], + + "author": { + "name": "doug-the-guy", + "image": "" + } +} + diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/print/bootstrap-table-print.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/print/bootstrap-table-print.js new file mode 100644 index 0000000000..1254609fbe --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/print/bootstrap-table-print.js @@ -0,0 +1,149 @@ +(function ($) { + 'use strict'; + + var sprintf = $.fn.bootstrapTable.utils.sprintf; + + function printPageBuilderDefault(table) { + return '' + + '' + + 'Print Table' + + '

    Printed on: ' + new Date + '

    ' + + '
    ' + table + "
    "; + } + $.extend($.fn.bootstrapTable.defaults, { + showPrint: false, + printAsFilteredAndSortedOnUI: true, //boolean, when true - print table as sorted and filtered on UI. + //Please note that if true is set, along with explicit predefined print options for filtering and sorting (printFilter, printSortOrder, printSortColumn)- then they will be applied on data already filtered and sorted by UI controls. + //For printing data as filtered and sorted on UI - do not set these 3 options:printFilter, printSortOrder, printSortColumn + + printSortColumn: undefined , //String, set column field name to be sorted by + printSortOrder: 'asc', //String: 'asc' , 'desc' - relevant only if printSortColumn is set + printPageBuilder: function(table){return printPageBuilderDefault(table)} // function, receive html element as string, returns html string for printing. by default delegates to function printPageBuilderDefault(table). used for styling and adding header or footer + }); + $.extend($.fn.bootstrapTable.COLUMN_DEFAULTS, { + printFilter: undefined, //set value to filter by in print page + printIgnore: false, //boolean, set true to ignore this column in the print page + printFormatter:undefined //function(value, row, index), formats the cell value for this column in the printed table. Function behaviour is similar to the 'formatter' column option + + }); + $.extend($.fn.bootstrapTable.defaults.icons, { + print: 'glyphicon-print icon-share' + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _initToolbar = BootstrapTable.prototype.initToolbar; + + BootstrapTable.prototype.initToolbar = function () { + this.showToolbar = this.showToolbar || this.options.showPrint; + + _initToolbar.apply(this, Array.prototype.slice.apply(arguments)); + + if (this.options.showPrint) { + var that = this, + $btnGroup = this.$toolbar.find('>.btn-group'), + $print = $btnGroup.find('button.bs-print'); + + if (!$print.length) { + $print = $([ + ''].join('')).appendTo($btnGroup); + + $print.click(function () { + function formatValue(row, i, column ) { + var value = row[column.field]; + if (typeof column.printFormatter === 'function') { + return column.printFormatter.apply(column, [value, row, i]); + } + else { + return typeof value === 'undefined' ? "-" : value; + } + } + + function buildTable(data, columnsArray) { + var html = ['
    ']; + for (var k = 0; k < columnsArray.length; k++) { + var columns = columnsArray[k]; + html.push(''); + for (var h = 0; h < columns.length; h++) { + if (!columns[h].printIgnore) { + html.push( + '%s', columns[h].title) + ); + } + } + html.push(''); + } + html.push(''); + for (var i = 0; i < data.length; i++) { + html.push(''); + for(var l = 0; l < columnsArray.length; l++) { + var columns = columnsArray[l]; + for(var j = 0; j < columns.length; j++) { + if (!columns[j].printIgnore && columns[j].field) { + html.push(''); + } + } + } + html.push(''); + } + html.push('
    ', formatValue(data[i], i, columns[j]), '
    '); + return html.join(''); + } + function sortRows(data,colName,sortOrder) { + if(!colName){ + return data; + } + var reverse = sortOrder != 'asc'; + reverse = -((+reverse) || -1); + return data.sort(function (a, b) { + return reverse * (a[colName].localeCompare(b[colName])); + }); + } + function filterRow(row,filters) { + for (var index = 0; index < filters.length; ++index) { + if(row[filters[index].colName]!=filters[index].value) { + return false; + } + } + return true; + } + function filterRows(data,filters) { + return data.filter(function (row) { + return filterRow(row,filters) + }); + } + function getColumnFilters(columns) { + return !columns || !columns[0] ? [] : columns[0].filter(function (col) { + return col.printFilter; + }).map(function (col) { + return {colName:col.field, value:col.printFilter}; + }); + } + var doPrint = function (data) { + data=filterRows(data,getColumnFilters(that.options.columns)); + data=sortRows(data,that.options.printSortColumn,that.options.printSortOrder); + var table=buildTable(data,that.options.columns); + var newWin = window.open(""); + newWin.document.write(that.options.printPageBuilder.call(this, table)); + newWin.print(); + newWin.close(); + }; + doPrint(that.options.printAsFilteredAndSortedOnUI? that.getData() : that.options.data.slice(0)); + }); + } + } + }; +})(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-columns/bootstrap-table-reorder-columns.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-columns/bootstrap-table-reorder-columns.js new file mode 100644 index 0000000000..9e0aa6b14c --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/reorder-columns/bootstrap-table-reorder-columns.js @@ -0,0 +1,181 @@ +/** + * @author: Dennis Hernández + * @webSite: http://djhvscf.github.io/Blog + * @version: v1.1.0 + */ + +!function ($) { + + 'use strict'; + + //From MDN site, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter + var filterFn = function () { + if (!Array.prototype.filter) { + Array.prototype.filter = function(fun/*, thisArg*/) { + 'use strict'; + + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== 'function') { + throw new TypeError(); + } + + var res = []; + var thisArg = arguments.length >= 2 ? arguments[1] : void 0; + for (var i = 0; i < len; i++) { + if (i in t) { + var val = t[i]; + + // NOTE: Technically this should Object.defineProperty at + // the next index, as push can be affected by + // properties on Object.prototype and Array.prototype. + // But that method's new, and collisions should be + // rare, so use the more-compatible alternative. + if (fun.call(thisArg, val, i, t)) { + res.push(val); + } + } + } + + return res; + }; + } + }; + + $.extend($.fn.bootstrapTable.defaults, { + reorderableColumns: false, + maxMovingRows: 10, + onReorderColumn: function (headerFields) { + return false; + }, + dragaccept: null + }); + + $.extend($.fn.bootstrapTable.Constructor.EVENTS, { + 'reorder-column.bs.table': 'onReorderColumn' + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _initHeader = BootstrapTable.prototype.initHeader, + _toggleColumn = BootstrapTable.prototype.toggleColumn, + _toggleView = BootstrapTable.prototype.toggleView, + _resetView = BootstrapTable.prototype.resetView; + + BootstrapTable.prototype.initHeader = function () { + _initHeader.apply(this, Array.prototype.slice.apply(arguments)); + + if (!this.options.reorderableColumns) { + return; + } + + this.makeRowsReorderable(); + }; + + BootstrapTable.prototype.toggleColumn = function () { + _toggleColumn.apply(this, Array.prototype.slice.apply(arguments)); + + if (!this.options.reorderableColumns) { + return; + } + + this.makeRowsReorderable(); + }; + + BootstrapTable.prototype.toggleView = function () { + _toggleView.apply(this, Array.prototype.slice.apply(arguments)); + + if (!this.options.reorderableColumns) { + return; + } + + if (this.options.cardView) { + return; + } + + this.makeRowsReorderable(); + }; + + BootstrapTable.prototype.resetView = function () { + _resetView.apply(this, Array.prototype.slice.apply(arguments)); + + if (!this.options.reorderableColumns) { + return; + } + + this.makeRowsReorderable(); + }; + + BootstrapTable.prototype.makeRowsReorderable = function () { + var that = this; + try { + $(this.$el).dragtable('destroy'); + } catch (e) {} + $(this.$el).dragtable({ + maxMovingRows: that.options.maxMovingRows, + dragaccept: that.options.dragaccept, + clickDelay:200, + beforeStop: function() { + var ths = [], + formatters = [], + columns = [], + columnsHidden = [], + columnIndex = -1, + optionsColumns = []; + that.$header.find('th').each(function (i) { + ths.push($(this).data('field')); + formatters.push($(this).data('formatter')); + }); + + //Exist columns not shown + if (ths.length < that.columns.length) { + columnsHidden = $.grep(that.columns, function (column) { + return !column.visible; + }); + for (var i = 0; i < columnsHidden.length; i++) { + ths.push(columnsHidden[i].field); + formatters.push(columnsHidden[i].formatter); + } + } + + for (var i = 0; i < this.length; i++ ) { + columnIndex = that.fieldsColumnsIndex[ths[i]]; + if (columnIndex !== -1) { + that.columns[columnIndex].fieldIndex = i; + columns.push(that.columns[columnIndex]); + that.columns.splice(columnIndex, 1); + } + } + + that.columns = that.columns.concat(columns); + + filterFn(); //Support
    '); + } else { + var filterClass = column.filter.class ? ' ' + column.filter.class : ''; + html = $('
    '); + + if (column.searchable) { + enableFilter = true; + isVisible = 'visible' + } + + if (column.filter.template) { + html.append(column.filter.template(that, column, isVisible)); + } else { + var $filter = $(that.options.filterTemplate[column.filter.type.toLowerCase()](that, column, isVisible)); + + switch (column.filter.type) { + case 'input': + var cpLock = true; + $filter.off('compositionstart').on('compositionstart', function (event) { + cpLock = false; + }); + + $filter.off('compositionend').on('compositionend', function (event) { + cpLock = true; + var $input = $(this); + clearTimeout(timeoutId); + timeoutId = setTimeout(function () { + that.onColumnSearch(event, column.field, $input.val()); + }, that.options.searchTimeOut); + }); + + $filter.off('keyup').on('keyup', function (event) { + if (cpLock) { + var $input = $(this); + clearTimeout(timeoutId); + timeoutId = setTimeout(function () { + that.onColumnSearch(event, column.field, $input.val()); + }, that.options.searchTimeOut); + } + }); + + $filter.off('mouseup').on('mouseup', function (event) { + var $input = $(this), + oldValue = $input.val(); + + if (oldValue === "") { + return; + } + + setTimeout(function () { + var newValue = $input.val(); + + if (newValue === "") { + clearTimeout(timeoutId); + timeoutId = setTimeout(function () { + that.onColumnSearch(event, column.field, newValue); + }, that.options.searchTimeOut); + } + }, 1); + }); + break; + case 'select': + $filter.on('select2:select', function (event) { + that.onColumnSearch(event, column.field, $(this).val()); + }); + + $filter.on("select2:unselecting", function (event) { + var $select2 = $(this); + event.preventDefault(); + $select2.val(null).trigger('change'); + that.searchText = undefined; + that.onColumnSearch(event, column.field, $select2.val()); + }); + break; + } + + html.append($filter); + } + } + + $.each(header.children().children(), function (i, tr) { + tr = $(tr); + if (tr.data('field') === column.field) { + tr.find('.fht-cell').append(html); + return false; + } + }); + }); + + if (!enableFilter) { + header.find('.filter').hide(); + } + } + + function initSelect2(that) { + var $header = getCurrentHeader(that); + + $.each(that.columns, function (idx, column) { + if (column.filter && column.filter.type === 'select') { + var $selectEle = $header.find('select[data-filter-field="' + column.field + '"]'); + + if ($selectEle.length > 0 && !$selectEle.data().select2) { + var select2Opts = { + placeholder: "", + allowClear: true, + data: column.filter.data, + dropdownParent: that.$el.closest(".bootstrap-table") + }; + + $selectEle.select2(select2Opts); + } + } + }); + } + + $.extend($.fn.bootstrapTable.defaults, { + filter: false, + filterValues: {}, + filterTemplate: { + input: function (instance, column, isVisible) { + return ''; + }, + select: function (instance, column, isVisible) { + return ''; + } + }, + onColumnSearch: function (field, text) { + return false; + } + }); + + $.extend($.fn.bootstrapTable.COLUMN_DEFAULTS, { + filter: undefined + }); + + $.extend($.fn.bootstrapTable.Constructor.EVENTS, { + 'column-search.bs.table': 'onColumnSearch' + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _init = BootstrapTable.prototype.init, + _initHeader = BootstrapTable.prototype.initHeader, + _initSearch = BootstrapTable.prototype.initSearch; + + BootstrapTable.prototype.init = function () { + //Make sure that the filtercontrol option is set + if (this.options.filter) { + var that = this; + + if (that.options.filterTemplate) { + that.options.filterTemplate = $.extend({}, $.fn.bootstrapTable.defaults.filterTemplate, that.options.filterTemplate); + } + + if (!$.isEmptyObject(that.options.filterValues)) { + that.filterColumnsPartial = that.options.filterValues; + that.options.filterValues = {}; + } + + this.$el.on('reset-view.bs.table', function () { + //Create controls on $tableHeader if the height is set + if (!that.options.height) { + return; + } + + //Avoid recreate the controls + if (that.$tableHeader.find('select').length > 0 || that.$tableHeader.find('input').length > 0) { + return; + } + + createFilter(that, that.$tableHeader); + }).on('post-header.bs.table', function () { + var timeoutId = 0; + + initSelect2(that); + clearTimeout(timeoutId); + timeoutId = setTimeout(function () { + initFilterValues(that); + }, that.options.searchTimeOut - 1000); + }).on('column-switch.bs.table', function (field, checked) { + initFilterValues(that); + }); + } + + _init.apply(this, Array.prototype.slice.apply(arguments)); + }; + + BootstrapTable.prototype.initHeader = function () { + _initHeader.apply(this, Array.prototype.slice.apply(arguments)); + if (this.options.filter) { + createFilter(this, this.$header); + } + }; + + BootstrapTable.prototype.initSearch = function () { + var that = this, + filterValues = that.filterColumnsPartial; + + // Filter for client + if (that.options.sidePagination === 'client') { + this.data = $.grep(this.data, function (row, idx) { + for (var field in filterValues) { + var column = that.columns[that.fieldsColumnsIndex[field]], + filterValue = filterValues[field].toLowerCase(), + rowValue = row[field]; + + rowValue = $.fn.bootstrapTable.utils.calculateObjectValue( + that.header, + that.header.formatters[$.inArray(field, that.header.fields)], [rowValue, row, idx], rowValue); + + if (column.filterStrictSearch) { + if (!($.inArray(field, that.header.fields) !== -1 && + (typeof rowValue === 'string' || typeof rowValue === 'number') && + rowValue.toString().toLowerCase() === filterValue.toString().toLowerCase())) { + return false; + } + } else { + if (!($.inArray(field, that.header.fields) !== -1 && + (typeof rowValue === 'string' || typeof rowValue === 'number') && + (rowValue + '').toLowerCase().indexOf(filterValue) !== -1)) { + return false; + } + } + } + + return true; + }); + } + + _initSearch.apply(this, Array.prototype.slice.apply(arguments)); + }; + + BootstrapTable.prototype.onColumnSearch = function (event, field, value) { + if ($.isEmptyObject(this.filterColumnsPartial)) { + this.filterColumnsPartial = {}; + } + + if (value) { + this.filterColumnsPartial[field] = value; + } else { + delete this.filterColumnsPartial[field]; + } + + this.options.pageNumber = 1; + this.onSearch(event); + this.trigger('column-search', field, value); + }; + + BootstrapTable.prototype.setSelect2Data = function (field, data) { + var that = this, + $header = getCurrentHeader(that), + $selectEle = $header.find('select[data-filter-field=\"' + field + '\"]'); + $selectEle.empty(); + $selectEle.select2({ + data: data, + placeholder: "", + allowClear: true, + dropdownParent: that.$el.closest(".bootstrap-table") + }); + + $.each(this.columns, function (idx, column) { + if (column.field === field) { + column.filter.data = data; + return false; + } + }); + }; + + BootstrapTable.prototype.setFilterValues = function (values) { + this.filterColumnsPartial = values; + }; + + $.fn.bootstrapTable.methods.push('setSelect2Data'); + $.fn.bootstrapTable.methods.push('setFilterValues'); + +}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/extension.json new file mode 100644 index 0000000000..edbc589c0b --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/select2-filter/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Select2 Filter", + "version": "1.1.0", + "description": "Plugin to add select2 filter on the top of the columns in order to filter the data.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/select2-filter", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/select2-filter.html", + + "plugins": [{ + "name": "bootstrap-table-select2-filter", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/select2-filter" + }], + + "author": { + "name": "Jewway", + "image": "https://avatars0.githubusercontent.com/u/3501899" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/bootstrap-table-sticky-header.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/bootstrap-table-sticky-header.css new file mode 100644 index 0000000000..0b4c850f5e --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/bootstrap-table-sticky-header.css @@ -0,0 +1,22 @@ +/** + * @author vincent loh + * @update zhixin wen + */ + +.fix-sticky { + position: fixed !important; + overflow: hidden; + z-index: 100; +} + +.fix-sticky table thead { + background: #fff; +} + +.fix-sticky table thead.thead-light { + background: #e9ecef; +} + +.fix-sticky table thead.thead-light { + background: #212529; +} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/bootstrap-table-sticky-header.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/bootstrap-table-sticky-header.js new file mode 100644 index 0000000000..bf6ba74660 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/bootstrap-table-sticky-header.js @@ -0,0 +1,76 @@ +/** + * @author vincent loh + * @update J Manuel Corona + * @update zhixin wen + */ + +($ => { + const Utils = $.fn.bootstrapTable.utils + + $.extend($.fn.bootstrapTable.defaults, { + stickyHeader: false, + stickyHeaderOffsetY: 0 + }) + + const hiddenClass = Utils.bootstrapVersion === 4 ? 'd-none' : 'hidden' + + $.BootstrapTable = class extends $.BootstrapTable { + initHeader (...args) { + super.initHeader(...args) + + if (!this.options.stickyHeader) { + return + } + + this.$el.before('
    ') + this.$el.before('
    ') + this.$el.after('
    ') + this.$header.addClass('sticky-header') + + // clone header just once, to be used as sticky header + // deep clone header, using source header affects tbody>td width + this.$stickyContainer = this.$tableBody.find('.sticky-header-container') + this.$stickyBegin = this.$tableBody.find('.sticky_anchor_begin') + this.$stickyEnd = this.$tableBody.find('.sticky_anchor_end') + this.$stickyHeader = this.$header.clone(true, true) + + // render sticky on window scroll or resize + $(window).on('resize.sticky-header-table', () => this.renderStickyHeader()) + $(window).on('scroll.sticky-header-table', () => this.renderStickyHeader()) + this.$tableBody.off('scroll').on('scroll', () => this.matchPositionX()) + } + + renderStickyHeader () { + const top = $(window).scrollTop() + // top anchor scroll position, minus header height + const start = this.$stickyBegin.offset().top - this.options.stickyHeaderOffsetY + // bottom anchor scroll position, minus header height, minus sticky height + const end = this.$stickyEnd.offset().top - this.options.stickyHeaderOffsetY - this.$header.height() + // show sticky when top anchor touches header, and when bottom anchor not exceeded + if (top > start && top <= end) { + // ensure clone and source column widths are the same + this.$stickyHeader.find('tr:eq(0)').find('th').each((index, el) => { + $(el).css('min-width', this.$header.find('tr:eq(0)').find('th').eq(index).css('width')) + }) + // match bootstrap table style + this.$stickyContainer.removeClass(hiddenClass).addClass('fix-sticky fixed-table-container') + // stick it in position + this.$stickyContainer.css('top', `${this.options.stickyHeaderOffsetY}px`) + // create scrollable container for header + this.$stickyTable = $('') + this.$stickyTable.addClass(this.options.classes) + // append cloned header to dom + this.$stickyContainer.html(this.$stickyTable.append(this.$stickyHeader)) + // match clone and source header positions when left-right scroll + this.matchPositionX() + } else { + this.$stickyContainer.removeClass('fix-sticky').addClass(hiddenClass) + } + } + + matchPositionX () { + this.$stickyContainer.scrollLeft(this.$tableBody.scrollLeft()) + } + } + +})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/extension.json new file mode 100644 index 0000000000..70ae30ff8b --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/sticky-header/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Sticky Header", + "version": "1.0.0", + "description": "An extension which provides a sticky header for table columns when scrolling on a long page and / or table. Works for tables with many columns and narrow width with horizontal scrollbars too.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/sticky-header", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/sticky-header.html", + + "plugins": [{ + "name": "bootstrap-table-sticky-header", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/sticky-header" + }], + + "author": { + "name": "vinzloh", + "image": "https://avatars0.githubusercontent.com/u/5501845" + } +} diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/bootstrap-table-toolbar.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/bootstrap-table-toolbar.js new file mode 100644 index 0000000000..6c83c5ebb3 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/bootstrap-table-toolbar.js @@ -0,0 +1,228 @@ +/** + * @author: aperez + * @version: v2.0.0 + * + * @update Dennis Hernández + * @update zhixin wen + */ + +($ => { + const Utils = $.fn.bootstrapTable.utils + + const bootstrap = { + 3: { + icons: { + advancedSearchIcon: 'glyphicon-chevron-down' + }, + html: { + modalHeader: ` + + ` + } + }, + 4: { + icons: { + advancedSearchIcon: 'fa-chevron-down' + }, + html: { + modalHeader: ` + + ` + } + } + }[Utils.bootstrapVersion] + + $.extend($.fn.bootstrapTable.defaults, { + advancedSearch: false, + idForm: 'advancedSearch', + actionForm: '', + idTable: undefined, + onColumnAdvancedSearch (field, text) { + return false + } + }) + + $.extend($.fn.bootstrapTable.defaults.icons, { + advancedSearchIcon: bootstrap.icons.advancedSearchIcon + }) + + $.extend($.fn.bootstrapTable.Constructor.EVENTS, { + 'column-advanced-search.bs.table': 'onColumnAdvancedSearch' + }) + + $.extend($.fn.bootstrapTable.locales, { + formatAdvancedSearch () { + return 'Advanced search' + }, + formatAdvancedCloseButton () { + return 'Close' + } + }) + + $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales) + + $.BootstrapTable = class extends $.BootstrapTable { + initToolbar () { + const o = this.options + + this.showToolbar = this.showToolbar || + (o.search && + o.advancedSearch && + o.idTable) + + super.initToolbar() + + if (!o.search || !o.advancedSearch || !o.idTable) { + return + } + + this.$toolbar.find('>.btn-group').append(` + + `) + + this.$toolbar.find('button[name="advancedSearch"]').off('click').on('click', () => this.showAvdSearch()) + } + + showAvdSearch () { + const o = this.options + + if (!$(`#avdSearchModal_${o.idTable}`).hasClass('modal')) { + $('body').append(` + + `) + + let timeoutId = 0 + + $(`#avdSearchModalContent_${o.idTable}`).append(this.createFormAvd().join('')) + + $(`#${o.idForm}`).off('keyup blur', 'input').on('keyup blur', 'input', e => { + if (o.sidePagination === 'server') { + this.onColumnAdvancedSearch(e) + } else { + clearTimeout(timeoutId) + timeoutId = setTimeout(() => { + this.onColumnAdvancedSearch(e) + }, o.searchTimeOut) + } + }) + + $(`#btnCloseAvd_${o.idTable}`).click(() => { + $(`#avdSearchModal_${o.idTable}`).modal('hide') + if (o.sidePagination === 'server') { + this.options.pageNumber = 1 + this.updatePagination() + this.trigger('column-advanced-search', this.filterColumnsPartial) + } + }) + + $(`#avdSearchModal_${o.idTable}`).modal() + } else { + $(`#avdSearchModal_${o.idTable}`).modal() + } + } + + createFormAvd () { + const o = this.options + const html = [`
    `] + + for (const column of this.columns) { + if (!column.checkbox && column.visible && column.searchable) { + html.push(` +
    + +
    + +
    +
    + `) + } + } + + html.push('') + + return html + } + + initSearch () { + super.initSearch() + + if (!this.options.advancedSearch || this.options.sidePagination === 'server') { + return + } + + const fp = $.isEmptyObject(this.filterColumnsPartial) ? null : this.filterColumnsPartial + + this.data = fp ? $.grep(this.data, (item, i) => { + for (const [key, v] of Object.entries(fp)) { + const fval = v.toLowerCase() + let value = item[key] + const index = this.header.fields.indexOf(key) + value = Utils.calculateObjectValue(this.header, + this.header.formatters[index], [value, item, i], value) + + if ( + !(index !== -1 && + (typeof value === 'string' || typeof value === 'number') && + (`${value}`).toLowerCase().includes(fval)) + ) { + return false + } + } + return true + }) : this.data + } + + onColumnAdvancedSearch (e) { + const text = $.trim($(e.currentTarget).val()) + const $field = $(e.currentTarget)[0].id + + if ($.isEmptyObject(this.filterColumnsPartial)) { + this.filterColumnsPartial = {} + } + if (text) { + this.filterColumnsPartial[$field] = text + } else { + delete this.filterColumnsPartial[$field] + } + + if (this.options.sidePagination !== 'server') { + this.options.pageNumber = 1 + this.onSearch(e) + this.updatePagination() + this.trigger('column-advanced-search', $field, text) + } + } + } +})(jQuery) diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/extension.json new file mode 100644 index 0000000000..f33ae2c475 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Toolbar", + "version": "2.0.0", + "description": "Plugin to support the advanced search.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/toolbar", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/toolbar.html", + + "plugins": [{ + "name": "bootstrap-table-toolbar", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/toolbar" + }], + + "author": { + "name": "djhvscf", + "image": "https://avatars1.githubusercontent.com/u/4496763" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.css b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.css new file mode 100644 index 0000000000..481ca89cd8 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.css @@ -0,0 +1 @@ +.table:not(.table-condensed)>tbody>tr>td.treenode{padding-top:0;padding-bottom:0;border-bottom:solid #fff 1px}.table:not(.table-condensed)>tbody>tr:last-child>td.treenode{border-bottom:none}.treenode .text{float:left;display:block;padding-top:6px;padding-bottom:6px}.treenode .vertical,.treenode .vertical.last{float:left;display:block;width:1px;border-left:dashed silver 1px;height:38px;margin-left:8px}.treenode .vertical.last{height:15px}.treenode .space,.treenode .node{float:left;display:block;width:15px;height:5px;margin-top:15px}.treenode .node{border-top:dashed silver 1px} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.js new file mode 100644 index 0000000000..a8ff876d11 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.js @@ -0,0 +1,130 @@ +/** + * @author: KingYang + * @webSite: https://github.com/kingyang + * @version: v1.0.0 + */ + +! function ($) { + + 'use strict'; + + $.extend($.fn.bootstrapTable.defaults, { + treeShowField: null, + idField: 'id', + parentIdField: 'pid', + treeVerticalcls: 'vertical', + treeVerticalLastcls: 'vertical last', + treeSpacecls: 'space', + treeNodecls: 'node', + treeCellcls: 'treenode', + treeTextcls: 'text', + onTreeFormatter: function (row) { + var that = this, + options = that.options, + level = row._level || 0, + plevel = row._parent && row._parent._level || 0, + paddings = []; + for (var i = 0; i < plevel; i++) { + paddings.push(''); + paddings.push(''); + } + + for (var i = plevel; i < level; i++) { + if (row._last && i === (level - 1)) { + paddings.push(''); + } else { + paddings.push(''); + } + paddings.push(''); + } + return paddings.join(''); + }, onGetNodes: function (row, data) { + var that = this; + var nodes = []; + $.each(data, function (i, item) { + if (row[that.options.idField] === item[that.options.parentIdField]) { + nodes.push(item); + } + }); + return nodes; + }, + onCheckLeaf: function (row, data) { + if (row.isLeaf !== undefined) { + return row.isLeaf; + } + return !row._nodes || !row._nodes.length; + }, onCheckRoot: function (row, data) { + var that = this; + return !row[that.options.parentIdField]; + } + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _initRow = BootstrapTable.prototype.initRow, + _initHeader = BootstrapTable.prototype.initHeader; + + BootstrapTable.prototype.initHeader = function () { + var that = this; + _initHeader.apply(that, Array.prototype.slice.apply(arguments)); + var treeShowField = that.options.treeShowField; + if (treeShowField) { + $.each(this.header.fields, function (i, field) { + if (treeShowField === field) { + that.treeEnable = true; + var _formatter = that.header.formatters[i]; + var _class = [that.options.treeCellcls]; + if (that.header.classes[i]) { + _class.push(that.header.classes[i].split('"')[1] || ''); + } + that.header.classes[i] = ' class="' + _class.join(' ') + '"'; + that.header.formatters[i] = function (value, row, index) { + var colTree = [that.options.onTreeFormatter.apply(that, [row])]; + colTree.push(''); + if (_formatter) { + colTree.push(_formatter.apply(this, Array.prototype.slice.apply(arguments))); + } else { + colTree.push(value); + } + colTree.push(''); + return colTree.join(''); + }; + return false; + } + }); + } + }; + + var initNode = function (item, idx, data, parentDom) { + var that = this; + var nodes = that.options.onGetNodes.apply(that, [item, data]); + item._nodes = nodes; + parentDom.append(_initRow.apply(that, [item, idx, data, parentDom])); + var len = nodes.length - 1; + for (var i = 0; i <= len; i++) { + var node = nodes[i]; + node._level = item._level + 1; + node._parent = item; + if (i === len) + node._last = 1; + initNode.apply(that, [node, $.inArray(node, data), data, parentDom]); + } + }; + + + BootstrapTable.prototype.initRow = function (item, idx, data, parentDom) { + var that = this; + if (that.treeEnable) { + if (that.options.onCheckRoot.apply(that, [item, data])) { + if (item._level === undefined) { + item._level = 0; + } + initNode.apply(that, [item, idx, data, parentDom]); + return true; + } + return false; + + } + return _initRow.apply(that, Array.prototype.slice.apply(arguments)); + }; + +} (jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.less b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.less new file mode 100644 index 0000000000..fc8ceda3b9 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.less @@ -0,0 +1,43 @@ + .table:not(.table-condensed) > tbody > tr > td.treenode { + padding-top: 0; + padding-bottom: 0; + border-bottom: solid #fff 1px; + } + + .table:not(.table-condensed) > tbody > tr:last-child > td.treenode { + border-bottom: none; + } + + .treenode { + .text { + float: left; + display: block; + padding-top: 6px; + padding-bottom: 6px; + } + .vertical, + .vertical.last { + float: left; + display: block; + width: 1px; + border-left: dashed silver 1px; + height: 38px; + margin-left: 8px; + } + .vertical.last { + height: 15px; + } + + .space, + .node { + float: left; + display: block; + width: 15px; + height: 5px; + margin-top: 15px; + } + + .node { + border-top: dashed silver 1px; + } + } \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/extension.json new file mode 100644 index 0000000000..72ddaa5a13 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/extension.json @@ -0,0 +1,17 @@ +{ + "name": "Tree column", + "version": "1.0.0", + "description": "Plugin to support display tree data column.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/tree-column", + "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/tree-column.html", + + "plugins": [{ + "name": "bootstrap-table-reorder-rows", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/tree-column" + }], + + "author": { + "name": "KingYang", + "image": "https://avatars3.githubusercontent.com/u/1540211" + } +} \ No newline at end of file diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/icon.png b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/icon.png new file mode 100644 index 0000000000..e7453922fe Binary files /dev/null and b/InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/icon.png differ diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.js b/InvenTree/InvenTree/static/bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.js new file mode 100644 index 0000000000..68ea17e872 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/treegrid/bootstrap-table-treegrid.js @@ -0,0 +1,111 @@ +/** + * @author: YL + * @version: v1.0.0 + */ +!function ($) { + 'use strict'; + $.extend($.fn.bootstrapTable.defaults, { + treeShowField: null, + idField: 'id', + parentIdField: 'pid', + rootParentId: null, + onGetNodes: function (row, data) { + var that = this; + var nodes = []; + $.each(data, function (i, item) { + if (row[that.options.idField] === item[that.options.parentIdField]) { + nodes.push(item); + } + }); + return nodes; + }, + onCheckRoot: function (row, data) { + var that = this; + return that.options.rootParentId === row[that.options.parentIdField] || + !row[that.options.parentIdField]; + } + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _init = BootstrapTable.prototype.init, + _initRow = BootstrapTable.prototype.initRow, + _initHeader = BootstrapTable.prototype.initHeader, + _rowStyle = null; + + BootstrapTable.prototype.init = function () { + _rowStyle = this.options.rowStyle; + _init.apply(this, Array.prototype.slice.apply(arguments)); + }; + + // td + BootstrapTable.prototype.initHeader = function () { + var that = this; + _initHeader.apply(that, Array.prototype.slice.apply(arguments)); + var treeShowField = that.options.treeShowField; + if (treeShowField) { + $.each(this.header.fields, function (i, field) { + if (treeShowField === field) { + that.treeEnable = true; + return false; + } + }); + } + }; + + var initTr = function (item, idx, data, parentDom) { + var that = this; + var nodes = that.options.onGetNodes.apply(that, [item, data]); + item._nodes = nodes; + parentDom.append(_initRow.apply(that, [item, idx, data, parentDom])); + + // init sub node + var len = nodes.length - 1; + for (var i = 0; i <= len; i++) { + var node = nodes[i]; + node._level = item._level + 1; + node._parent = item; + if (i === len) + node._last = 1; + // jquery.treegrid.js + that.options.rowStyle = function (item, idx) { + var res = _rowStyle.apply(that, Array.prototype.slice.apply(arguments)); + var id = item[that.options.idField] ? item[that.options.idField] : 0; + var pid = item[that.options.parentIdField] ? item[that.options.parentIdField] : 0; + res.classes = [ + res.classes || '', + 'treegrid-' + id, + 'treegrid-parent-' + pid + ].join(' '); + return res; + }; + initTr.apply(that, [node, $.inArray(node, data), data, parentDom]); + } + }; + + // tr + BootstrapTable.prototype.initRow = function (item, idx, data, parentDom) { + var that = this; + if (that.treeEnable) { + // init root node + if (that.options.onCheckRoot.apply(that, [item, data])) { + if (item._level === undefined) { + item._level = 0; + } + // jquery.treegrid.js + that.options.rowStyle = function (item, idx) { + var res = _rowStyle.apply(that, Array.prototype.slice.apply(arguments)); + var x = item[that.options.idField] ? item[that.options.idField] : 0; + res.classes = [ + res.classes || '', + 'treegrid-' + x + ].join(' '); + return res; + }; + initTr.apply(that, [item, idx, data, parentDom]); + return true; + } + return false; + } + return _initRow.apply(that, Array.prototype.slice.apply(arguments)); + }; +}(jQuery); diff --git a/InvenTree/InvenTree/static/bootstrap-table/extensions/treegrid/extension.json b/InvenTree/InvenTree/static/bootstrap-table/extensions/treegrid/extension.json new file mode 100644 index 0000000000..2c07e1f487 --- /dev/null +++ b/InvenTree/InvenTree/static/bootstrap-table/extensions/treegrid/extension.json @@ -0,0 +1,17 @@ +{ + "name": "treegrid", + "version": "1.0.0", + "description": "Plugin to support the jquery treegrid.", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/treegrid", + "example": "https://github.com/wenzhixin/bootstrap-table-examples/blob/master/extensions/treegrid.html", + + "plugins": [{ + "name": "bootstrap-table-treegrid", + "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/treegrid" + }], + + "author": { + "name": "foreveryang321", + "image": "https://avatars0.githubusercontent.com/u/5868190" + } +} \ No newline at end of file