├── preview.png ├── wbfolder.wbl ├── QuickTableViewer.qext ├── QuickTableViewer.js ├── LICENSE ├── README.md ├── properties.js └── irregularUtils.js /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristofSchwarz/qsQuickTableViewer/HEAD/preview.png -------------------------------------------------------------------------------- /wbfolder.wbl: -------------------------------------------------------------------------------- 1 | QuickTableViewer.qext; 2 | QuickTableViewer.js; 3 | properties.js; 4 | irregularUtils.js 5 | -------------------------------------------------------------------------------- /QuickTableViewer.qext: -------------------------------------------------------------------------------- 1 | { 2 | "name": "QuickTableViewer", 3 | "description": "This puts a standard table object in place with all columns found in the data model", 4 | "type": "visualization", 5 | "version": "1.0.0", 6 | "icon": "table", 7 | "author": "Christof Schwarz, Ralf Becher", 8 | "homepage": "", 9 | "keywords": "qlik-sense, visualization", 10 | "preview": "preview.png", 11 | "license": "", 12 | "repository": "", 13 | "dependencies": { 14 | "qlik-sense": ">=3.0.x" 15 | } 16 | } -------------------------------------------------------------------------------- /QuickTableViewer.js: -------------------------------------------------------------------------------- 1 | define(["qlik", "jquery", "./properties" 2 | ], function (qlik, $, properties) { 3 | 4 | return { 5 | support: { 6 | snapshot: false, 7 | export: false, 8 | exportData: false 9 | }, 10 | definition: properties, 11 | paint: function ($element, layout) { 12 | var helpUrl = 'https://github.com/ChristofSchwarz/qsQuickTableViewer/blob/master/README.md'; 13 | $element.html( 14 | '
' 15 | + '

This is a placeholder for your table.

' 16 | + '

After making your selections in the accordion menu on the right

' 17 | + '

this will become a standard Qlik Sense table (QlikTableViewer not needed then).

' 18 | + 'More info' 19 | + '
'); 20 | //needed for export 21 | return qlik.Promise.resolve(); 22 | } 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Christof Schwarz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickTableViewer Extension 2 | Qlik Sense Extension to quickly get all fields of a data-model table into a standard Qlik Sense Table object. 3 | 4 | **New 17-Jul-2024** 5 | Note: new version 2024 available 6 | - https://www.linkedin.com/pulse/quicktableviewer-2024-qlik-sense-christof-schwarz-u6yxf/ 7 | - https://www.youtube.com/watch?v=wP5yw4euRuQ 8 | --- 9 | Old version video: https://www.youtube.com/watch?v=NG3t-yurD4g 10 | 11 | **New 16-Mar-2021**: you have an option to turn on/off a show condition on all columns of the table. The condition is checking if the field (still) exists in the datamodel. 12 | If not, it won't stop the chart from showing (as it was before that update), but only hide the respective column. 13 | 14 | **Update 10-Sep-2020**: a checkbox that allows to remove a (table-)prefix from Field Names in the label of the column e.g. Customers.Name -> Name 15 | (everything before the first "." is removed then) 16 | 17 | * Place the extension on the sheet where you like to get the table object 18 | * select from the properties panel on your right which table you would like to see 19 | * you can specify a pattern for both, fields to include (default: *) and fields to exclude (default: %* ... all fields starting with %) 20 | * after you click the button "Get My Table" the object manipulates itself to become a standard Sense table object 21 | 22 | ### Authors: 23 | - Christof Schwarz, formerly Qlik, now data/\bridge 24 | - [Ralf Becher](https://github.com/ralfbecher), formerly TIQ Solutions 25 | 26 | Thanks to Ralf, you can select multiple tables at once to get columns from more than 1 table. The code checks if there are connections in 27 | the data model between the tables before adding its columns to avoid cartesean products with out-of-memory results. Ralf explains this 28 | here --> https://medium.com/@irregularbi/do-my-tables-associate-dc249d59ee89 29 | 30 | ![alt text](https://github.com/ChristofSchwarz/pics/raw/master/quicktableview.gif "Screenshot") 31 | -------------------------------------------------------------------------------- /properties.js: -------------------------------------------------------------------------------- 1 | define(["qlik", "./irregularUtils" 2 | ], function (qlik, utils) { 3 | 'use strict'; 4 | 5 | var settings = { 6 | // property panel definition 7 | mysection: { 8 | label: "Extension Settings", 9 | type: "items", 10 | items: [ 11 | { 12 | "ref": "selectedTable", 13 | "type": "string", 14 | component: { 15 | template: 16 | '
\ 17 |
Choose table(s) to show
\ 18 |
\ 19 | \ 22 |
', 23 | controller: ["$scope", "$element", "$timeout", function (scope, element, timeout) { 24 | scope.tableBackup = []; 25 | scope.table = []; 26 | scope.data.selectedTable = ""; 27 | 28 | // a dummy option to start with 29 | scope.options = [ 30 | { 31 | value: "-", 32 | label: "please wait..." 33 | } 34 | ]; 35 | 36 | scope.selectedTable = function (table) { 37 | // simulate multiple since ctrl-click seems to be blocked in panel 38 | var t = ""; 39 | if (table.length > 0) { 40 | if (table.length === 1) { 41 | t = table[0]; 42 | var i = scope.tableBackup.indexOf(t); 43 | if (i === -1) { 44 | scope.tableBackup.push(t); 45 | } else { 46 | scope.tableBackup.splice(i, 1); 47 | } 48 | } else { 49 | table.forEach(function (t) { 50 | if (scope.tableBackup.indexOf(t) === -1) { 51 | scope.tableBackup.push(t); 52 | } 53 | }); 54 | } 55 | scope.data.selectedTable = scope.tableBackup.join(","); 56 | } else { 57 | scope.data.selectedTable = []; 58 | } 59 | timeout(function () { 60 | scope.table = scope.tableBackup; 61 | }); 62 | } 63 | 64 | // now initialize options (will update scope.options later) 65 | qlik.currApp(this).model.enigmaModel.evaluate("Concat(DISTINCT $Table,CHR(10))") 66 | .then(function (res) { 67 | scope.options = res.split(String.fromCharCode(10)) 68 | .map(function (e) { return { value: e, label: e } }); 69 | }) 70 | .catch(function (err) { 71 | console.error(err); 72 | }); 73 | 74 | }] 75 | } 76 | }, 77 | { 78 | label: "Limit Fields", 79 | type: "integer", 80 | defaultValue: 50, 81 | ref: "limitFields" 82 | }, 83 | { 84 | label: "Include columns (wildcard pattern)", 85 | type: "string", 86 | defaultValue: "*", 87 | ref: "fieldPattern" 88 | }, 89 | { 90 | label: "Ignore columns (wildcard pattern)", 91 | type: "string", 92 | defaultValue: "%*", 93 | ref: "ignoreFieldPattern" 94 | }, { 95 | label: "use '|' as separator", 96 | component: "text" 97 | }, { 98 | label: 'Remove table prefix from label', 99 | type: 'boolean', 100 | ref: 'boolRemovePrefix', 101 | defaultValue: true 102 | }, { 103 | label: 'Hide column if field no longer in datamodel', 104 | type: 'boolean', 105 | ref: "hideColIfNotPresent", 106 | defaultValue: true 107 | }, { 108 | label: "Get my table!", 109 | component: "button", 110 | action: function (context) { utils.convertToTable(qlik, context); } 111 | }, { 112 | label: "by Christof Schwarz & Ralf Becher" 113 | , component: "text" 114 | } 115 | ] 116 | } 117 | }; 118 | 119 | return { 120 | type: "items", 121 | component: "accordion", 122 | items: settings 123 | }; 124 | }); 125 | -------------------------------------------------------------------------------- /irregularUtils.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | 3 | function edgesFromList(arr) { 4 | var res = [], 5 | l = arr.length; 6 | for (var i = 0; i < l; ++i) 7 | for (var j = i + 1; j < l; ++j) 8 | res.push([arr[i], arr[j]]); 9 | return res; 10 | } 11 | 12 | function edgeListToAdjList(edgelist) { 13 | var adjlist = {}; 14 | var i, len, pair, u, v; 15 | for (i = 0, len = edgelist.length; i < len; i += 1) { 16 | pair = edgelist[i]; 17 | u = pair[0]; 18 | v = pair[1]; 19 | if (adjlist[u]) { 20 | adjlist[u].push(v); 21 | } else { 22 | adjlist[u] = [v]; 23 | } 24 | if (adjlist[v]) { 25 | adjlist[v].push(u); 26 | } else { 27 | adjlist[v] = [u]; 28 | } 29 | } 30 | return adjlist; 31 | }; 32 | 33 | // Breadth First Search using adjacency list 34 | function bfs(v, adjlist, visited) { 35 | var q = []; 36 | var current_group = []; 37 | var i, len, adjV, nextVertex; 38 | q.push(v); 39 | visited[v] = true; 40 | while (q.length > 0) { 41 | v = q.shift(); 42 | current_group.push(v); 43 | adjV = adjlist[v]; 44 | for (i = 0, len = adjV.length; i < len; i += 1) { 45 | nextVertex = adjV[i]; 46 | if (!visited[nextVertex]) { 47 | q.push(nextVertex); 48 | visited[nextVertex] = true; 49 | } 50 | } 51 | } 52 | return current_group; 53 | }; 54 | 55 | function checkTableAssiciatons(qlik, app, selectedTables) { 56 | var objId = "", edgeList = [], adjList = {}; 57 | 58 | if (selectedTables.length > 1) { 59 | // check if multiple tables are connected 60 | // create a hypercube with all fields which links to at least 2 tables (result contains no data islands) 61 | return app.model.enigmaModel.createSessionObject({ 62 | "qInfo": { "qType": "HyperCube" }, 63 | "qHyperCubeDef": { 64 | "qDimensions": [{ "qDef": { "qFieldDefs": ["$Field"] } }], 65 | "qMeasures": [{ "qDef": { "qDef": "=Concat({<$Field={\"=Count(distinct $Table)>1\"}>} distinct $Table, ',')" } }], 66 | "qInitialDataFetch": [{ "qWidth": 2, "qHeight": 5000 }] 67 | } 68 | }) 69 | .then(function (obj) { 70 | objId = obj.id; 71 | return obj.getLayout(); 72 | }) 73 | .then(function (layout) { 74 | app.model.enigmaModel.destroySessionObject(objId); 75 | if (layout.qHyperCube.qDataPages.length > 0) { 76 | layout.qHyperCube.qDataPages[0].qMatrix.map(function (row) { 77 | edgeList = edgeList.concat(edgesFromList(row[1].qText.split(","))); 78 | }); 79 | adjList = edgeListToAdjList(edgeList); 80 | var groups = [], visited = {}, v; 81 | // find groups (components) of the graph 82 | for (v in adjList) { 83 | if (adjList.hasOwnProperty(v) && !visited[v]) { 84 | groups.push(bfs(v, adjList, visited)); 85 | } 86 | } 87 | // check if selectedTables are all in same group 88 | var inGroup = 0, l = selectedTables.length; 89 | for (var i = 0; i < groups.length; ++i) { 90 | if (groups[i].length >= l) { 91 | inGroup = 0; 92 | for (var j = 0; j < l; ++j) { 93 | if (groups[i].indexOf(selectedTables[j]) > -1) { 94 | inGroup++; 95 | } 96 | if (inGroup >= l) { 97 | return true; 98 | } 99 | } 100 | } 101 | } 102 | } 103 | return false; 104 | }); 105 | } else { 106 | return qlik.Promise.resolve(true); 107 | } 108 | } 109 | 110 | function convertToTable(qlik, layout) { 111 | if (layout.selectedTable.length == 0) return; 112 | 113 | var selectedTables = layout.selectedTable.split(","); 114 | 115 | var app = qlik.currApp(this); 116 | var ownId = layout.qInfo.qId; 117 | var fieldList; 118 | var thisObj; 119 | var newTable; 120 | console.log('Selected table:', layout.selectedTable); 121 | console.log('Remove prefix:', layout.boolRemovePrefix); 122 | console.log('Fields used:', layout.fieldPattern); 123 | console.log('Fields filtered:', layout.ignoreFieldPattern); 124 | 125 | checkTableAssiciatons(qlik, app, selectedTables) 126 | .then(function (res) { 127 | if (res) { 128 | var selectedTablesStr = selectedTables.map(function (e) { return '"' + e + '"' }).join(","); 129 | 130 | var setModifier = '{<$Table={' + selectedTablesStr + '},' 131 | + '$Field={"(' + layout.fieldPattern + ')"}-{"(' + layout.ignoreFieldPattern + ')"}>}'; 132 | // Use engine-formula to get a field list ... Concat($Field,',') 133 | var fieldQFormula = "=Concat(" + setModifier + " '{\"qDef\":{\"qFieldDefs\":[\"' & $Field & '\"],\"qFieldLabels\":[\"' & " 134 | + (layout.boolRemovePrefix ? "If(Index($Field,'.'),Mid($Field,Index($Field,'.')+1),$Field)" : "$Field") 135 | + "&'\"]}" 136 | + (layout.hideColIfNotPresent ? ", \"qCalcCondition\":{\"qCond\":{\"qv\":\"=Sum($Field=' & CHR(39) & $Field & CHR(39) & ')\"}}" : "") 137 | + "}', ',', $FieldNo)"; 138 | 139 | app.model.enigmaModel.evaluate(fieldQFormula) 140 | .then(function (ret) { 141 | 142 | fieldList = JSON.parse('[' + ret + ']'); 143 | if (layout.limitFields > 0 && fieldList.length > layout.limitFields) { 144 | fieldList = fieldList.slice(0, layout.limitFields) 145 | } 146 | console.log('Field list: ', fieldList); 147 | return app.model.enigmaModel.getObject(ownId); 148 | }).then(function (obj) { 149 | thisObj = obj; 150 | // Use visualization API to create a new table 151 | return app.visualization.create('table', fieldList, 152 | { 153 | title: selectedTables.join(","), 154 | multiline: { wrapTextInCells: false }, 155 | components: [{ 156 | key: "theme", 157 | content: { hoverEffect: true }, 158 | scrollbar: { size: "medium" } 159 | }] 160 | } 161 | ); 162 | }).then(function (obj) { 163 | newTable = obj; 164 | // get properties of new table object 165 | return newTable.model.getProperties(); 166 | }).then(function (prop) { 167 | console.log('newTable properties', prop); 168 | // manipulate the id to match the current extension object's id and 169 | // overwrite current extension object with the new table properties 170 | prop.qInfo.qId = ownId; 171 | return thisObj.setProperties(prop); 172 | }).then(function (ret) { 173 | newTable.close(); 174 | // change the object type to "table" also in the sheet properties 175 | var currSheet = qlik.navigation.getCurrentSheetId(); 176 | return app.model.enigmaModel.getObject(currSheet.sheetId); 177 | }).then(function (sheetObj) { 178 | sheetObj.properties.cells.forEach(function (cell) { 179 | if (cell.name == ownId) cell.type = 'table'; 180 | }); 181 | return sheetObj.setProperties(sheetObj.properties); 182 | }).then(function (ret) { 183 | console.log('Good bye. Object is now a table.'); 184 | }).catch(function (err) { 185 | console.error(err); 186 | }); 187 | } else { 188 | alert( 189 | "QuickViewer Error:\n\nTables don't associate (are islands)! Table Object cannot be created." 190 | ); 191 | } 192 | }); 193 | }; 194 | 195 | return { 196 | convertToTable: convertToTable 197 | }; 198 | 199 | }); 200 | --------------------------------------------------------------------------------