');
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 | 
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 |
--------------------------------------------------------------------------------