From 8bbca3c8ce606b5a2d45eaaf6f0c5ebda7268d71 Mon Sep 17 00:00:00 2001 From: Visal In Date: Sun, 23 Jul 2023 21:06:52 +0800 Subject: [PATCH 1/3] feat: export result into excel and csv --- package-lock.json | 180 +++++++++++++- package.json | 4 +- src/libs/ApplyQueryResultChanges.ts | 9 +- src/libs/GenerateSqlFromChanges.test.ts | 10 +- src/libs/GenerateSqlFromChanges.ts | 9 +- src/libs/SaveCSVFile.ts | 11 + src/libs/SaveExcelFile.ts | 8 + src/libs/SqlRunnerManager.ts | 7 +- src/libs/TransformResult.ts | 25 +- src/main/ipc/other.ts | 31 +++ src/main/preload.ts | 19 ++ src/renderer/App.css | 6 + .../ContextMenu/AttachedContextMenu.tsx | 54 ++++ src/renderer/components/ContextMenu/index.tsx | 9 +- .../components/OptimizeTable/index.tsx | 4 +- src/renderer/components/SelectField/index.tsx | 62 +++++ .../components/SelectField/styles.module.scss | 32 +++ .../DatabaseScreen/ExportModal/index.tsx | 230 ++++++++++++++++++ .../QueryMultipleResultViewer.tsx | 4 +- .../QueryResultViewer/QueryResultAction.tsx | 24 +- .../QueryResultViewer/QueryResultTable.tsx | 10 +- .../QueryResultViewer/index.tsx | 4 +- .../screens/DatabaseScreen/QueryWindow.tsx | 15 +- src/types/SqlResult.ts | 4 - test.xlsx | Bin 0 -> 16000 bytes 25 files changed, 690 insertions(+), 81 deletions(-) create mode 100644 src/libs/SaveCSVFile.ts create mode 100644 src/libs/SaveExcelFile.ts create mode 100644 src/renderer/components/ContextMenu/AttachedContextMenu.tsx create mode 100644 src/renderer/components/SelectField/index.tsx create mode 100644 src/renderer/components/SelectField/styles.module.scss create mode 100644 src/renderer/screens/DatabaseScreen/ExportModal/index.tsx create mode 100644 test.xlsx diff --git a/package-lock.json b/package-lock.json index 7370d4c..bc46519 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "querymaster", - "version": "0.1.5", + "version": "0.1.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "querymaster", - "version": "0.1.5", + "version": "0.1.9", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -18,6 +18,7 @@ "@fortawesome/free-solid-svg-icons": "^6.3.0", "@fortawesome/react-fontawesome": "^0.2.0", "@lezer/highlight": "^1.1.3", + "@types/xlsx": "^0.0.36", "@uiw/codemirror-theme-atomone": "^4.19.9", "@uiw/codemirror-theme-bbedit": "^4.19.9", "@uiw/codemirror-theme-vscode": "^4.21.7", @@ -36,7 +37,8 @@ "react-router-dom": "^6.8.1", "react-syntax-highlighter": "^15.5.0", "sqlstring": "^2.3.3", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@electron/notarize": "^1.2.3", @@ -4509,6 +4511,15 @@ "@types/node": "*" } }, + "node_modules/@types/xlsx": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/xlsx/-/xlsx-0.0.36.tgz", + "integrity": "sha512-mvfrKiKKMErQzLMF8ElYEH21qxWCZtN59pHhWGmWCWFJStYdMWjkDSAy6mGowFxHXaXZWe5/TW7pBUiWclIVOw==", + "deprecated": "This is a stub types definition for xlsx (https://github.com/sheetjs/js-xlsx). xlsx provides its own type definitions, so you don't need @types/xlsx installed!", + "dependencies": { + "xlsx": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -5112,6 +5123,14 @@ "node": ">= 10.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -6433,6 +6452,18 @@ } ] }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6712,6 +6743,14 @@ "@codemirror/view": "^6.0.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -7038,6 +7077,17 @@ "buffer": "^5.1.0" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -10076,6 +10126,14 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -16467,6 +16525,17 @@ "node": ">= 0.6" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ssri": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", @@ -18428,6 +18497,22 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -18494,6 +18579,26 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -21935,6 +22040,14 @@ "@types/node": "*" } }, + "@types/xlsx": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/xlsx/-/xlsx-0.0.36.tgz", + "integrity": "sha512-mvfrKiKKMErQzLMF8ElYEH21qxWCZtN59pHhWGmWCWFJStYdMWjkDSAy6mGowFxHXaXZWe5/TW7pBUiWclIVOw==", + "requires": { + "xlsx": "*" + } + }, "@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -22381,6 +22494,11 @@ "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", "dev": true }, + "adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==" + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -23398,6 +23516,15 @@ "integrity": "sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ==", "dev": true }, + "cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "requires": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -23591,6 +23718,11 @@ "@codemirror/view": "^6.0.0" } }, + "codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==" + }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -23851,6 +23983,11 @@ "buffer": "^5.1.0" } }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -26126,6 +26263,11 @@ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true }, + "frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==" + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -30840,6 +30982,14 @@ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==" }, + "ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "requires": { + "frac": "~1.1.2" + } + }, "ssri": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", @@ -32247,6 +32397,16 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==" + }, + "word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==" + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -32287,6 +32447,20 @@ "dev": true, "requires": {} }, + "xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "requires": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + } + }, "xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/package.json b/package.json index 1885285..7b916ff 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@fortawesome/free-solid-svg-icons": "^6.3.0", "@fortawesome/react-fontawesome": "^0.2.0", "@lezer/highlight": "^1.1.3", + "@types/xlsx": "^0.0.36", "@uiw/codemirror-theme-atomone": "^4.19.9", "@uiw/codemirror-theme-bbedit": "^4.19.9", "@uiw/codemirror-theme-vscode": "^4.21.7", @@ -73,7 +74,8 @@ "react-router-dom": "^6.8.1", "react-syntax-highlighter": "^15.5.0", "sqlstring": "^2.3.3", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "xlsx": "^0.18.5" }, "optionalDependencies": { "dmg-license": "^1.0.11" diff --git a/src/libs/ApplyQueryResultChanges.ts b/src/libs/ApplyQueryResultChanges.ts index 3d7ff40..e3accbc 100644 --- a/src/libs/ApplyQueryResultChanges.ts +++ b/src/libs/ApplyQueryResultChanges.ts @@ -1,15 +1,16 @@ -import { QueryRowBasedResult } from 'types/SqlResult'; +import { QueryResult } from 'types/SqlResult'; import { ResultChangeCollectorItem } from './ResultChangeCollector'; export default function applyQueryResultChanges( - result: QueryRowBasedResult, + result: QueryResult, changes: ResultChangeCollectorItem[] -): QueryRowBasedResult { +): QueryResult { const newResult = { ...result }; + const headers = result.headers; for (const change of changes) { for (const col of change.cols) { - newResult.rows[change.row][col.col] = col.value; + newResult.rows[change.row][headers[col.col].name] = col.value; } } diff --git a/src/libs/GenerateSqlFromChanges.test.ts b/src/libs/GenerateSqlFromChanges.test.ts index d05e84d..8c5fe10 100644 --- a/src/libs/GenerateSqlFromChanges.test.ts +++ b/src/libs/GenerateSqlFromChanges.test.ts @@ -1,4 +1,4 @@ -import { QueryRowBasedResult } from 'types/SqlResult'; +import { QueryResult } from 'types/SqlResult'; import generateSqlFromChanges from './GenerateSqlFromChanges'; import ResultChangeCollector from './ResultChangeCollector'; import { SqlStatementPlan } from 'types/SqlStatement'; @@ -24,7 +24,7 @@ test('Generate Sql from changes with primary key', () => { }, } as unknown as DatabaseSchema; - const data: QueryRowBasedResult = { + const data: QueryResult = { keys: {}, error: null, headers: [ @@ -45,9 +45,9 @@ test('Generate Sql from changes with primary key', () => { }, ], rows: [ - [1, 'Henry', 25], - [2, 'Jenny', 20], - [3, 'Zeus', 30], + { id: 1, name: 'Henry', age: 25 }, + { id: 2, name: 'Jenny', age: 20 }, + { id: 3, name: 'Zeus', age: 30 }, ], }; diff --git a/src/libs/GenerateSqlFromChanges.ts b/src/libs/GenerateSqlFromChanges.ts index 3885521..559d0bd 100644 --- a/src/libs/GenerateSqlFromChanges.ts +++ b/src/libs/GenerateSqlFromChanges.ts @@ -1,4 +1,4 @@ -import { QueryResultHeader, QueryRowBasedResult } from 'types/SqlResult'; +import { QueryResult, QueryResultHeader } from 'types/SqlResult'; import { DatabaseSchema } from 'types/SqlSchema'; import { ResultChangeCollectorItem } from './ResultChangeCollector'; import { SqlStatementPlan } from 'types/SqlStatement'; @@ -66,7 +66,7 @@ export function getUpdatableTable( function getSqlPlanFromChange( change: ResultChangeCollectorItem, - data: QueryRowBasedResult, + data: QueryResult, updatable: UpdatableTableDict ): SqlStatementPlan[] { const changedTable: Record = {}; @@ -92,7 +92,8 @@ function getSqlPlanFromChange( where: updatable[tableName].reduce((a, b) => { return { ...a, - [b.columnNames]: data.rows[change.row][b.columnNumber], + [b.columnNames]: + data.rows[change.row][headers[b.columnNumber].name], }; }, {}), }; @@ -106,7 +107,7 @@ function getSqlPlanFromChange( export default function generateSqlFromChanges( schema: DatabaseSchema, - currentData: QueryRowBasedResult, + currentData: QueryResult, changes: ResultChangeCollectorItem[] ): SqlStatementPlan[] { const updatableTables = getUpdatableTable(currentData.headers, schema); diff --git a/src/libs/SaveCSVFile.ts b/src/libs/SaveCSVFile.ts new file mode 100644 index 0000000..8f5f923 --- /dev/null +++ b/src/libs/SaveCSVFile.ts @@ -0,0 +1,11 @@ +import xlsx from 'xlsx'; +import fs from 'fs'; + +export default function saveCsvFile(fileName: string, records: object[]) { + const workbook = xlsx.utils.book_new(); + const worksheet = xlsx.utils.json_to_sheet(records); + xlsx.utils.book_append_sheet(workbook, worksheet); + + const csv = xlsx.utils.sheet_to_csv(worksheet); + fs.writeFileSync(fileName, csv); +} diff --git a/src/libs/SaveExcelFile.ts b/src/libs/SaveExcelFile.ts new file mode 100644 index 0000000..185c22d --- /dev/null +++ b/src/libs/SaveExcelFile.ts @@ -0,0 +1,8 @@ +import xlsx from 'xlsx'; + +export default function saveExcelFile(fileName: string, records: object[]) { + const workbook = xlsx.utils.book_new(); + const worksheet = xlsx.utils.json_to_sheet(records); + xlsx.utils.book_append_sheet(workbook, worksheet); + xlsx.writeFile(workbook, fileName); +} diff --git a/src/libs/SqlRunnerManager.ts b/src/libs/SqlRunnerManager.ts index 5711bc3..a5e65d0 100644 --- a/src/libs/SqlRunnerManager.ts +++ b/src/libs/SqlRunnerManager.ts @@ -1,5 +1,5 @@ import { SqlQueryCallback } from 'drivers/SQLLikeConnection'; -import { QueryResult, QueryRowBasedResult } from 'types/SqlResult'; +import { QueryResult } from 'types/SqlResult'; import { SqlStatement } from 'types/SqlStatement'; import { Parser, AST } from 'node-sql-parser'; @@ -22,11 +22,6 @@ export interface SqlStatementResult { result: QueryResult; } -export interface SqlStatementRowBasedResult { - statement: SqlStatement; - result: QueryRowBasedResult; -} - interface SqlExecuteOption { onStart?: () => void; skipProtection?: boolean; diff --git a/src/libs/TransformResult.ts b/src/libs/TransformResult.ts index c7c8a2a..39c652d 100644 --- a/src/libs/TransformResult.ts +++ b/src/libs/TransformResult.ts @@ -1,30 +1,7 @@ import { DatabaseSchemas } from 'types/SqlSchema'; -import { - SqlStatementResult, - SqlStatementRowBasedResult, -} from './SqlRunnerManager'; +import { SqlStatementResult } from './SqlRunnerManager'; import { QueryResultHeader } from 'types/SqlResult'; -export function transformResultToRowBasedResult( - statements: SqlStatementResult[] -): SqlStatementRowBasedResult[] { - return statements.map((statement) => { - const rows = statement.result.resultHeader - ? [] - : statement.result.rows.map((row) => - statement.result.headers.map((header) => row[header.name]) - ); - - return { - ...statement, - result: { - ...statement.result, - rows, - }, - }; - }); -} - function findMatchColumn( schema: DatabaseSchemas, header: QueryResultHeader diff --git a/src/main/ipc/other.ts b/src/main/ipc/other.ts index b9656f9..da1e443 100644 --- a/src/main/ipc/other.ts +++ b/src/main/ipc/other.ts @@ -5,7 +5,11 @@ import { ipcMain, Menu, MenuItemConstructorOptions, + SaveDialogSyncOptions, + shell, } from 'electron'; +import saveCsvFile from './../../libs/SaveCSVFile'; +import saveExcelFile from './../../libs/SaveExcelFile'; function recursiveAttachClick( items: MenuItemConstructorOptions[], @@ -55,6 +59,33 @@ export default class OtherIpcHandler { } } ); + + ipcMain.handle( + 'show-save-dialog', + (_, [options]: [SaveDialogSyncOptions]) => { + if (this.window) { + return dialog.showSaveDialogSync(this.window, options); + } + } + ); + + ipcMain.handle( + 'save-csv-file', + (_, [fileName, records]: [string, object[]]) => { + saveCsvFile(fileName, records); + } + ); + + ipcMain.handle( + 'save-excel-file', + (_, [fileName, records]: [string, object[]]) => { + saveExcelFile(fileName, records); + } + ); + + ipcMain.handle('show-item-in-folder', (_, [fileName]: [string]) => { + shell.showItemInFolder(fileName); + }); } attachWindow(window: BrowserWindow) { diff --git a/src/main/preload.ts b/src/main/preload.ts index 0cced1b..e101c35 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -7,6 +7,7 @@ import { IpcRendererEvent, MessageBoxSyncOptions, MenuItemConstructorOptions, + SaveDialogSyncOptions, } from 'electron'; import { ProgressInfo, @@ -52,6 +53,24 @@ const electronHandler = { showMessageBox: (options: MessageBoxSyncOptions): Promise => ipcRenderer.invoke('show-message-box', [options]), + // ---------------------------------- + // Related to File O/I + // ---------------------------------- + showSaveDialog: ( + options: SaveDialogSyncOptions + ): Promise => + ipcRenderer.invoke('show-save-dialog', [options]), + + showFileInFolder: (fileName: string) => + ipcRenderer.invoke('show-item-in-folder', [fileName]), + + saveCsvFile: (fileName: string, records: object[]) => + ipcRenderer.invoke('save-csv-file', [fileName, records]), + + saveExcelFile: (fileName: string, records: object[]) => + ipcRenderer.invoke('save-excel-file', [fileName, records]), + // ---------------------------------- + setNativeMenu: (options: MenuItemConstructorOptions[]) => ipcRenderer.invoke('set-menu', [options]), diff --git a/src/renderer/App.css b/src/renderer/App.css index 9ccd8e0..25d9950 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -149,3 +149,9 @@ body { .scroll:hover::-webkit-scrollbar-thumb { background: var(--color-scrollbar-hover); } + +hr { + border: 0; + background: var(--color-surface-hover); + height: 1px; +} diff --git a/src/renderer/components/ContextMenu/AttachedContextMenu.tsx b/src/renderer/components/ContextMenu/AttachedContextMenu.tsx new file mode 100644 index 0000000..cdcb5dd --- /dev/null +++ b/src/renderer/components/ContextMenu/AttachedContextMenu.tsx @@ -0,0 +1,54 @@ +import { ReactElement, useEffect, useRef, useState } from 'react'; +import ContextMenu, { ContextMenuItemProps } from '.'; + +interface AttachedContextMenuProps { + activator: (props: { isOpened: boolean }) => ReactElement; + items: ContextMenuItemProps[]; +} + +export default function AttachedContextMenu({ + activator, + items, +}: AttachedContextMenuProps) { + const wrapperRef = useRef(null); + const [minWidth, setMinWidth] = useState(300); + + const [open, setOpen] = useState(false); + const [x, setX] = useState(0); + const [y, setY] = useState(0); + + useEffect(() => { + if (wrapperRef.current) { + setMinWidth(wrapperRef.current.getBoundingClientRect().width); + } + }, [open, wrapperRef]); + + return ( + <> +
{ + if (!open) { + setOpen(true); + const bound = e.currentTarget.getBoundingClientRect(); + setX(bound.left); + setY(bound.bottom); + } + }} + > + {activator({ isOpened: open })} +
+ { + setOpen(false); + }} + > + {items.map((item, idx) => ( + + ))} + + + ); +} diff --git a/src/renderer/components/ContextMenu/index.tsx b/src/renderer/components/ContextMenu/index.tsx index 72e354e..09fd009 100644 --- a/src/renderer/components/ContextMenu/index.tsx +++ b/src/renderer/components/ContextMenu/index.tsx @@ -40,9 +40,11 @@ export default function ContextMenu({ children, status, onClose, + minWidth, }: PropsWithChildren<{ status: ContextMenuStatus; onClose: () => void; + minWidth?: number; }>) { const menuRef = useRef(null); const [menuWidth, setMenuWidth] = useState(0); @@ -67,13 +69,16 @@ export default function ContextMenu({ } }, [status.open]); - const viewportWidth = window.innerWidth || document.documentElement.clientWidth; - const viewportHeight = window.innerHeight || document.documentElement.clientHeight; + const viewportWidth = + window.innerWidth || document.documentElement.clientWidth; + const viewportHeight = + window.innerHeight || document.documentElement.clientHeight; const menuStyle: CSSProperties = { visibility: status.open ? 'visible' : 'hidden', top: Math.min(status.y, viewportHeight - menuHeight - 10), left: Math.min(status.x, viewportWidth - menuWidth - 10), + minWidth, }; return ( diff --git a/src/renderer/components/OptimizeTable/index.tsx b/src/renderer/components/OptimizeTable/index.tsx index 7900129..a3266da 100644 --- a/src/renderer/components/OptimizeTable/index.tsx +++ b/src/renderer/components/OptimizeTable/index.tsx @@ -9,7 +9,7 @@ import React, { import styles from './styles.module.css'; interface OptimizeTableProps { - data: unknown[][]; + data: unknown[]; headers: { name: string; initialSize: number; @@ -280,7 +280,7 @@ export default function OptimizeTable({ visibleDebounce.rowEnd - visibleDebounce.rowStart ) .fill(false) - .map(() => new Array(data[0].length).fill(false)); + .map(() => new Array(headers.length).fill(false)); const cells = windowArray.map((row, rowIndex) => { const absoluteRowIndex = rowIndex + visibleDebounce.rowStart; diff --git a/src/renderer/components/SelectField/index.tsx b/src/renderer/components/SelectField/index.tsx new file mode 100644 index 0000000..f45091d --- /dev/null +++ b/src/renderer/components/SelectField/index.tsx @@ -0,0 +1,62 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faChevronDown, + faChevronRight, +} from '@fortawesome/free-solid-svg-icons'; +import styles from './styles.module.scss'; +import AttachedContextMenu from '../ContextMenu/AttachedContextMenu'; +import { ReactElement } from 'react'; + +interface SelectFieldProps { + value?: string; + items: { icon?: ReactElement; text: string; value: string }[]; + onChange?: (value: string) => void; +} + +export default function SelectField({ + value, + items, + onChange, +}: SelectFieldProps) { + const selected = items.find((item) => item.value === value); + + return ( +
+ { + return ( +
+ {selected ? ( + <> + {selected.icon && ( +
+ {selected.icon} +     +
+ )} +
{selected.text}
+ + ) : ( +
Please select
+ )} +
+ +
+
+ ); + }} + items={items.map((item) => ({ + text: item.text, + icon: item.icon, + onClick: () => { + if (onChange) { + onChange(item.value); + } + }, + }))} + /> +
+ ); +} diff --git a/src/renderer/components/SelectField/styles.module.scss b/src/renderer/components/SelectField/styles.module.scss new file mode 100644 index 0000000..571dbda --- /dev/null +++ b/src/renderer/components/SelectField/styles.module.scss @@ -0,0 +1,32 @@ +.input { + font-size: 1rem; + width: 100%; + font-family: inherit; + outline: none; + border: 0; + background: var(--color-surface); + color: var(--color-text); + + label { + display: block; + margin-bottom: 5px; + font-weight: 500; + } +} + +.content { + display: flex; + border: 2px solid var(--color-input-border); + border-radius: 4px; + padding: 10px; + + &:focus-within { + border: 2px solid var(--color-text-link); + + .action_icon { + img { + opacity: 1; + } + } + } +} diff --git a/src/renderer/screens/DatabaseScreen/ExportModal/index.tsx b/src/renderer/screens/DatabaseScreen/ExportModal/index.tsx new file mode 100644 index 0000000..27b6b94 --- /dev/null +++ b/src/renderer/screens/DatabaseScreen/ExportModal/index.tsx @@ -0,0 +1,230 @@ +import { + faFileExcel, + faFileCsv, + faCircleCheck, + faTimesCircle, + faSpinner, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useCallback, useState } from 'react'; +import { faEllipsis } from '@fortawesome/free-solid-svg-icons'; +import Button from 'renderer/components/Button'; +import Modal from 'renderer/components/Modal'; +import SelectField from 'renderer/components/SelectField'; +import Stack from 'renderer/components/Stack'; +import TextField from 'renderer/components/TextField'; +import { QueryResult } from 'types/SqlResult'; + +const EXPORT_OPTIONS = [ + { + text: 'Excel (*.xlsx)', + icon: , + value: 'excel', + }, + { + text: 'Comma Separated Value (*.csv)', + icon: , + value: 'csv', + }, +]; + +interface ExportModalProps { + data: QueryResult; + onClose: () => void; +} + +function ExportModalConfig({ + onExport, +}: { + onExport: (fileName: string, format: string) => void; +}) { + const [format, setFormat] = useState('excel'); + const [fileName, setFileName] = useState(''); + + const onBrowseFileClicked = useCallback(() => { + window.electron + .showSaveDialog({ + filters: [ + format === 'excel' + ? { + name: 'Excel', + extensions: ['xlsx'], + } + : { + name: 'CSV', + extensions: ['csv'], + }, + ], + }) + .then((value) => setFileName(value ?? '')); + }, [format]); + + return ( + <> + + + + } + actionClick={onBrowseFileClicked} + /> + + + + + + + ); +} + +function ExportModalLoading() { + return ( + <> + +
+
+ + +
+
Exporting...
+
+
+
+ + + + + ); +} + +function ExportModalSuccess({ + fileName, + onClose, +}: { + fileName: string; + onClose: () => void; +}) { + return ( + <> + +
+
+
+ + +
+
Successful export to
+
+ {fileName} +
+
+
+
+
+ + + + + + ); +} + +function ExportModalError({ onClose }: { onClose: () => void }) { + return ( + <> + +
+
+
+ + +
+
There is something wrong
+
+
+
+
+ + + + + ); +} + +export default function ExportModal({ data, onClose }: ExportModalProps) { + const [successFilename, setSuccessFilename] = useState(''); + const [stage, setStage] = useState<'CONFIG' | 'SAVING' | 'SUCCESS' | 'ERROR'>( + 'CONFIG' + ); + + const onExportClicked = useCallback( + (fileName: string, format: string) => { + setStage('SAVING'); + setSuccessFilename(fileName); + + if (format === 'excel') { + window.electron + .saveExcelFile(fileName, data.rows) + .then(() => setStage('SUCCESS')) + .catch(() => { + setStage('ERROR'); + }); + } else if (format === 'csv') { + window.electron + .saveCsvFile(fileName, data.rows) + .then(() => setStage('SUCCESS')) + .catch(() => { + setStage('ERROR'); + }); + } else { + setStage('ERROR'); + } + }, + [data, setStage, setSuccessFilename] + ); + + return ( + { + onClose(); + }} + > + {stage === 'CONFIG' && } + {stage === 'SAVING' && } + {stage === 'SUCCESS' && ( + + )} + {stage === 'ERROR' && } + + ); +} diff --git a/src/renderer/screens/DatabaseScreen/QueryMultipleResultViewer.tsx b/src/renderer/screens/DatabaseScreen/QueryMultipleResultViewer.tsx index 186f57e..f32a087 100644 --- a/src/renderer/screens/DatabaseScreen/QueryMultipleResultViewer.tsx +++ b/src/renderer/screens/DatabaseScreen/QueryMultipleResultViewer.tsx @@ -1,10 +1,10 @@ -import { SqlStatementRowBasedResult } from 'libs/SqlRunnerManager'; import WindowTab from 'renderer/components/WindowTab'; import QueryResultViewer from './QueryResultViewer'; import { useMemo, useState, memo } from 'react'; +import { SqlStatementResult } from 'libs/SqlRunnerManager'; interface QueryMultipleResultViewerProps { - value: SqlStatementRowBasedResult[]; + value: SqlStatementResult[]; } export default memo(function QueryMultipleResultViewer({ diff --git a/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultAction.tsx b/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultAction.tsx index 5bc8ad1..d7ab28c 100644 --- a/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultAction.tsx +++ b/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultAction.tsx @@ -6,12 +6,14 @@ import Button from 'renderer/components/Button'; import { useQueryResultChange } from 'renderer/contexts/QueryResultChangeProvider'; import { useSchmea } from 'renderer/contexts/SchemaProvider'; import { useSqlExecute } from 'renderer/contexts/SqlExecuteProvider'; -import { QueryRowBasedResult } from 'types/SqlResult'; +import { QueryResult } from 'types/SqlResult'; import styles from './styles.module.scss'; +import ButtonGroup from 'renderer/components/ButtonGroup'; +import ExportModal from '../ExportModal'; interface QueryResultActionProps { - result: QueryRowBasedResult; - onResultChange: React.Dispatch>; + result: QueryResult; + onResultChange: React.Dispatch>; page: number; pageSize: number; onPageChange: React.Dispatch>; @@ -25,6 +27,7 @@ export default function QueryResultAction({ onPageChange, }: QueryResultActionProps) { const [changeCount, setChangeCount] = useState(0); + const [showExportModal, setShowExportModal] = useState(false); const { clearChange, collector } = useQueryResultChange(); const { schema, currentDatabase } = useSchmea(); const { runner } = useSqlExecute(); @@ -72,10 +75,19 @@ export default function QueryResultAction({ return (
+ {showExportModal && ( + setShowExportModal(false)} /> + )} +
- + + + +
diff --git a/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx b/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx index 0491edf..5e5b1bd 100644 --- a/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx +++ b/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import styles from './styles.module.scss'; import TableCell from 'renderer/screens/DatabaseScreen/QueryResultViewer/TableCell/TableCell'; -import { QueryResultHeader, QueryRowBasedResult } from 'types/SqlResult'; +import { QueryResult, QueryResultHeader } from 'types/SqlResult'; import { getUpdatableTable } from 'libs/GenerateSqlFromChanges'; import { useSchmea } from 'renderer/contexts/SchemaProvider'; import { useContextMenu } from 'renderer/contexts/ContextMenuProvider'; @@ -11,7 +11,7 @@ import OptimizeTable from 'renderer/components/OptimizeTable'; import Icon from 'renderer/components/Icon'; interface QueryResultTableProps { - result: QueryRowBasedResult; + result: QueryResult; page: number; pageSize: number; } @@ -105,8 +105,8 @@ function QueryResultTable({ result, page, pageSize }: QueryResultTableProps) { // Check the last 100 records const maxLength = Math.max( ...result.rows.slice(0, 100).map((row) => { - if (typeof row[idx] === 'string') - return (row[idx] as string).length; + if (typeof row[header.name] === 'string') + return (row[header.name] as string).length; return 10; }) ); @@ -135,7 +135,7 @@ function QueryResultTable({ result, page, pageSize }: QueryResultTableProps) { return ( ([]); + const [result, setResult] = useState([]); const [queryKeyCounter, setQueryKeyCounter] = useState(0); const [loading, setLoading] = useState(false); const { schema, currentDatabase } = useSchmea(); @@ -122,11 +119,7 @@ export default function QueryWindow({ } ) .then((r) => { - setResult( - transformResultToRowBasedResult( - transformResultHeaderUseSchema(r, schema) - ) - ); + setResult(transformResultHeaderUseSchema(r, schema)); setQueryKeyCounter((prev) => prev + 1); }) .catch((e) => { diff --git a/src/types/SqlResult.ts b/src/types/SqlResult.ts index 65726d5..340a06d 100644 --- a/src/types/SqlResult.ts +++ b/src/types/SqlResult.ts @@ -34,7 +34,3 @@ export interface QueryResult> extends QueryResultCommon { rows: T[]; } - -export interface QueryRowBasedResult extends QueryResultCommon { - rows: unknown[][]; -} diff --git a/test.xlsx b/test.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..58c5984e46e4b2dabfbf5fcf7926391441d867d4 GIT binary patch literal 16000 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 literal 0 HcmV?d00001 From 24054310a99c49c830ef0b2d2f6dae8581725be9 Mon Sep 17 00:00:00 2001 From: Visal In Date: Sun, 23 Jul 2023 21:16:23 +0800 Subject: [PATCH 2/3] fix: fixing code smell --- src/renderer/components/SelectField/index.tsx | 53 ++++++++++--------- .../DatabaseScreen/ExportModal/index.tsx | 2 +- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/renderer/components/SelectField/index.tsx b/src/renderer/components/SelectField/index.tsx index f45091d..926bcf0 100644 --- a/src/renderer/components/SelectField/index.tsx +++ b/src/renderer/components/SelectField/index.tsx @@ -5,7 +5,7 @@ import { } from '@fortawesome/free-solid-svg-icons'; import styles from './styles.module.scss'; import AttachedContextMenu from '../ContextMenu/AttachedContextMenu'; -import { ReactElement } from 'react'; +import { ReactElement, useCallback } from 'react'; interface SelectFieldProps { value?: string; @@ -20,33 +20,36 @@ export default function SelectField({ }: SelectFieldProps) { const selected = items.find((item) => item.value === value); + const activator = useCallback( + ({ isOpened }: { isOpened: boolean }) => { + return ( +
+ {selected ? ( + <> + {selected.icon && ( +
+ {selected.icon} +     +
+ )} +
{selected.text}
+ + ) : ( +
Please select
+ )} +
+ +
+
+ ); + }, + [selected] + ); + return (
{ - return ( -
- {selected ? ( - <> - {selected.icon && ( -
- {selected.icon} -     -
- )} -
{selected.text}
- - ) : ( -
Please select
- )} -
- -
-
- ); - }} + activator={activator} items={items.map((item) => ({ text: item.text, icon: item.icon, diff --git a/src/renderer/screens/DatabaseScreen/ExportModal/index.tsx b/src/renderer/screens/DatabaseScreen/ExportModal/index.tsx index 27b6b94..fbb9b7a 100644 --- a/src/renderer/screens/DatabaseScreen/ExportModal/index.tsx +++ b/src/renderer/screens/DatabaseScreen/ExportModal/index.tsx @@ -4,10 +4,10 @@ import { faCircleCheck, faTimesCircle, faSpinner, + faEllipsis, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useCallback, useState } from 'react'; -import { faEllipsis } from '@fortawesome/free-solid-svg-icons'; import Button from 'renderer/components/Button'; import Modal from 'renderer/components/Modal'; import SelectField from 'renderer/components/SelectField'; From add3df6423b18ae3be32e5e730402fcd9db04392 Mon Sep 17 00:00:00 2001 From: Visal In Date: Sun, 23 Jul 2023 21:20:22 +0800 Subject: [PATCH 3/3] fix: remove @types/xlsx as xlsx already provided its own type --- package-lock.json | 18 ------------------ package.json | 1 - release/app/package-lock.json | 4 ++-- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc46519..a8340fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@fortawesome/free-solid-svg-icons": "^6.3.0", "@fortawesome/react-fontawesome": "^0.2.0", "@lezer/highlight": "^1.1.3", - "@types/xlsx": "^0.0.36", "@uiw/codemirror-theme-atomone": "^4.19.9", "@uiw/codemirror-theme-bbedit": "^4.19.9", "@uiw/codemirror-theme-vscode": "^4.21.7", @@ -4511,15 +4510,6 @@ "@types/node": "*" } }, - "node_modules/@types/xlsx": { - "version": "0.0.36", - "resolved": "https://registry.npmjs.org/@types/xlsx/-/xlsx-0.0.36.tgz", - "integrity": "sha512-mvfrKiKKMErQzLMF8ElYEH21qxWCZtN59pHhWGmWCWFJStYdMWjkDSAy6mGowFxHXaXZWe5/TW7pBUiWclIVOw==", - "deprecated": "This is a stub types definition for xlsx (https://github.com/sheetjs/js-xlsx). xlsx provides its own type definitions, so you don't need @types/xlsx installed!", - "dependencies": { - "xlsx": "*" - } - }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -22040,14 +22030,6 @@ "@types/node": "*" } }, - "@types/xlsx": { - "version": "0.0.36", - "resolved": "https://registry.npmjs.org/@types/xlsx/-/xlsx-0.0.36.tgz", - "integrity": "sha512-mvfrKiKKMErQzLMF8ElYEH21qxWCZtN59pHhWGmWCWFJStYdMWjkDSAy6mGowFxHXaXZWe5/TW7pBUiWclIVOw==", - "requires": { - "xlsx": "*" - } - }, "@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", diff --git a/package.json b/package.json index 7b916ff..75f57de 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "@fortawesome/free-solid-svg-icons": "^6.3.0", "@fortawesome/react-fontawesome": "^0.2.0", "@lezer/highlight": "^1.1.3", - "@types/xlsx": "^0.0.36", "@uiw/codemirror-theme-atomone": "^4.19.9", "@uiw/codemirror-theme-bbedit": "^4.19.9", "@uiw/codemirror-theme-vscode": "^4.21.7", diff --git a/release/app/package-lock.json b/release/app/package-lock.json index de053ac..d71f51f 100644 --- a/release/app/package-lock.json +++ b/release/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "query-master", - "version": "0.1.5", + "version": "0.1.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "query-master", - "version": "0.1.5", + "version": "0.1.9", "hasInstallScript": true, "license": "MIT" }