1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-06-05 21:59:22 +02:00

Compare commits

..

18 Commits

Author SHA1 Message Date
cc941dfc04 chore(release): 0.5.12 2022-07-26 14:59:06 +02:00
1d151e9349 fix: error on schema export 2022-07-26 13:33:57 +02:00
addd9fba28 build(deps): bump ace-builds from 1.4.14 to 1.8.1 2022-07-25 15:25:58 +02:00
a00c19d300 fix: prevent ctrl+a in console 2022-07-25 15:20:15 +02:00
9551afbd2d feat: ability to copy multiple selected rows 2022-07-25 14:56:00 +02:00
1ead76c028 fix: missing defaults on insert row window 2022-07-25 13:09:41 +02:00
d3da15aa13 feat: copy row as SQL INSERT 2022-07-25 12:42:22 +02:00
f3b5de38c4 feat: export table content as SQL INSERT 2022-07-25 12:19:58 +02:00
Askar Kanturin
d4b6d2e9d1 changed readme 2022-07-23 10:20:36 +02:00
Askar Kanturin
e2c106e4e0 fixed typo in readme
i'm no native speaker, but i feel this is a typo
2022-07-23 10:20:36 +02:00
eb60899e6e fix(MySQL): missing quoted identifier for column names in table filter, closes #380 2022-07-22 10:08:33 +02:00
1d367d468d Merge branch 'master' of https://github.com/antares-sql/antares 2022-07-21 11:01:13 +02:00
8ecaedbf6c fix: disable ctrl+alt+(left/right) shortcut on linux 2022-07-21 11:01:10 +02:00
dependabot[bot]
dd1eebd4ec build(deps): bump terser from 5.13.1 to 5.14.2
Bumps [terser](https://github.com/terser/terser) from 5.13.1 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 08:43:24 +02:00
8c83b3f144 fix: missing table on insert new records on session restored tabs 2022-07-20 10:41:44 +02:00
985e5d3527 feat: context menu option to duplicate a table row 2022-07-19 17:48:51 +02:00
78902639eb feat: execute selected query 2022-07-19 15:02:17 +02:00
cb038b374a fix: issue with logger on import/export 2022-07-19 10:10:24 +02:00
27 changed files with 18345 additions and 406 deletions

View File

@@ -2,6 +2,28 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.5.12](https://github.com/antares-sql/antares/compare/v0.5.11...v0.5.12) (2022-07-26)
### Features
* ability to copy multiple selected rows ([9551afb](https://github.com/antares-sql/antares/commit/9551afbd2d7e525c81f28e98e788b92609ce9de4))
* context menu option to duplicate a table row ([985e5d3](https://github.com/antares-sql/antares/commit/985e5d352793d1b3e1981d004b6f494bfbb049bf))
* copy row as SQL INSERT ([d3da15a](https://github.com/antares-sql/antares/commit/d3da15aa1377dcba73927047563f1d0c2d1284ca))
* execute selected query ([7890263](https://github.com/antares-sql/antares/commit/78902639ebb29a8c53f8aa0d2045c74e0646febc))
* export table content as SQL INSERT ([f3b5de3](https://github.com/antares-sql/antares/commit/f3b5de38c4abfd2c1d738e179fc22e6c8b6f9080))
### Bug Fixes
* disable ctrl+alt+(left/right) shortcut on linux ([8ecaedb](https://github.com/antares-sql/antares/commit/8ecaedbf6c2fc0dc56ff2177a87dd6ede74bdd22))
* error on schema export ([1d151e9](https://github.com/antares-sql/antares/commit/1d151e9349fd97576ccd8ef7f88ca789a1f28b65))
* issue with logger on import/export ([cb038b3](https://github.com/antares-sql/antares/commit/cb038b374a4fe85ad569e42eee7af123c925e775))
* missing defaults on insert row window ([1ead76c](https://github.com/antares-sql/antares/commit/1ead76c02889f48bd91cae702820b082ca2ff54b))
* missing table on insert new records on session restored tabs ([8c83b3f](https://github.com/antares-sql/antares/commit/8c83b3f1447354ec63b2a308db05ad4d54659aa7))
* **MySQL:** missing quoted identifier for column names in table filter, closes [#380](https://github.com/antares-sql/antares/issues/380) ([eb60899](https://github.com/antares-sql/antares/commit/eb60899e6e17879c79a7ee7108061e9aca8596f7))
* prevent ctrl+a in console ([a00c19d](https://github.com/antares-sql/antares/commit/a00c19d3003cd43d3ee6e3132728122bb2b24c97))
### [0.5.11](https://github.com/antares-sql/antares/compare/v0.5.10...v0.5.11) (2022-07-19) ### [0.5.11](https://github.com/antares-sql/antares/compare/v0.5.10...v0.5.11) (2022-07-19)

View File

@@ -13,7 +13,7 @@ Antares is an SQL client based on [Electron.js](https://github.com/electron/elec
Our target is to support as many databases as possible, and all major operating systems, including the ARM versions. Our target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite. **At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite.
At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it. However, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible. We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible.
🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest). 🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest).
@@ -40,20 +40,20 @@ We are actively working on it, hoping to provide new cool features, improvements
Why are we developing an SQL client when there are a lot of them on the market? Why are we developing an SQL client when there are a lot of them on the market?
The main goal is to develop a **forever 100% free (without paid premium feature)**, full featured, as possible community driven, cross platform and open source alternative, empowered by JavaScript ecosystem. The main goal is to develop a **forever 100% free (without paid premium feature)**, full featured, as possible community driven, cross platform and open source alternative, empowered by JavaScript ecosystem.
A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenu; productivity comes first. A modern application created with minimalism and simplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenues; productivity comes first.
## Installation ## Installation
Based on your operating system you can have one or more distribution formats to choose based on your preferences. Based on your operating system you can have one or more distribution formats to choose based on your preferences.
Since Antares SQL is a free software we haven't a budget to spend in annual licenses or certificates. This can result that on some platforms you need some additional passages to install this app. Since Antares SQL is a free software we don't have a budget to spend on annual licenses or certificates. This can result that on some platforms you might need to put in some additional work to install this app.
### Linux ### Linux
On Linux you can simply download and run `.AppImage` distributions, install from Snap Store or from AUR. On Linux you can simply download and run the `.AppImage` distribution, install from Snap Store or from AUR.
### Windows ### Windows
On Windows you can choose between Microsoft Store and download `.exe` distribution. The latter lacks of a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt. On Windows you can choose between downloading the app from Microsoft Store or downloading the `.exe` from our [website](https://antares-sql.app/download.html) or [this github repo](https://github.com/Fabio286/antares/releases/latest). Distributions that are not from Microsoft Store are not signed with a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt.
### MacOS ### MacOS

18066
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.5.11", "version": "0.5.12",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.", "description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/antares-sql/antares.git", "repository": "https://github.com/antares-sql/antares.git",
@@ -120,7 +120,7 @@
"@mdi/font": "~6.9.96", "@mdi/font": "~6.9.96",
"@turf/helpers": "~6.5.0", "@turf/helpers": "~6.5.0",
"@vueuse/core": "~8.7.5", "@vueuse/core": "~8.7.5",
"ace-builds": "~1.4.13", "ace-builds": "~1.8.1",
"better-sqlite3": "~7.5.1", "better-sqlite3": "~7.5.1",
"electron-log": "~4.4.1", "electron-log": "~4.4.1",
"electron-store": "~8.0.1", "electron-store": "~8.0.1",

View File

@@ -25,7 +25,7 @@ export const customizations: Customizations = {
functions: true, functions: true,
schedulers: true, schedulers: true,
// Settings // Settings
elementsWrapper: '', elementsWrapper: '`',
stringsWrapper: '"', stringsWrapper: '"',
tableAdd: true, tableAdd: true,
tableTruncateDisableFKCheck: true, tableTruncateDisableFKCheck: true,

View File

@@ -1,14 +0,0 @@
/* eslint-disable no-useless-escape */
// eslint-disable-next-line no-control-regex
const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm;
const regex = new RegExp(pattern);
function sqlEscaper (string: string) {
return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%'];
return r[m.indexOf(char)] || char;
});
}
export { sqlEscaper };

162
src/common/libs/sqlUtils.ts Normal file
View File

@@ -0,0 +1,162 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-useless-escape */
import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
import customizations from '../customizations';
import { ClientCode } from '../interfaces/antares';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER, TEXT_SEARCH } from 'common/fieldTypes';
import hexToBinary, { HexChar } from './hexToBinary';
import { getArrayDepth } from './getArrayDepth';
/**
* Escapes a string fo SQL use
*
* @param { String } string
* @returns { String } Escaped string
*/
export const sqlEscaper = (string: string): string => {
// eslint-disable-next-line no-control-regex
const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm;
const regex = new RegExp(pattern);
return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%'];
return r[m.indexOf(char)] || char;
});
};
export const objectToGeoJSON = (val: any) => {
if (Array.isArray(val)) {
if (getArrayDepth(val) === 1)
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(val.map(arr => arr.reduce((acc: any, curr: any) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
};
export const escapeAndQuote = (val: string, client: ClientCode) => {
const { stringsWrapper: sw } = customizations[client];
// eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP: {[key: string]: string} = {
'\0': '\\0',
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\r': '\\r',
'\x1a': '\\Z',
'"': '\\"',
'\'': '\\\'',
'\\': '\\\\'
};
let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0;
let escapedVal = '';
let match;
while ((match = CHARS_TO_ESCAPE.exec(val))) {
escapedVal += val.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]];
chunkIndex = CHARS_TO_ESCAPE.lastIndex;
}
if (chunkIndex === 0)
return `${sw}${val}${sw}`;
if (chunkIndex < val.length)
return `${sw}${escapedVal + val.slice(chunkIndex)}${sw}`;
return `${sw}${escapedVal}${sw}`;
};
export const valueToSqlString = (args: {
val: any;
client: ClientCode;
field: {type: string; datePrecision: number};
}): string => {
let parsedValue;
const { val, client, field } = args;
const { stringsWrapper: sw } = customizations[client];
if (val === null)
parsedValue = 'NULL';
else if (DATE.includes(field.type)) {
parsedValue = moment(val).isValid()
? escapeAndQuote(moment(val).format('YYYY-MM-DD'), client)
: val;
}
else if (DATETIME.includes(field.type)) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
parsedValue = moment(val).isValid()
? escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`), client)
: escapeAndQuote(val, client);
}
else if ('isArray' in field) {
let localVal;
if (Array.isArray(val))
localVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
else
localVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : '';
parsedValue = `'${localVal}'`;
}
else if (TEXT_SEARCH.includes(field.type))
parsedValue = `'${val.replaceAll('\'', '\'\'')}'`;
else if (BIT.includes(field.type))
parsedValue = `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`;
else if (BLOB.includes(field.type)) {
if (['mysql', 'maria'].includes(client))
parsedValue = `X'${val.toString('hex').toUpperCase()}'`;
else if (client === 'pg')
parsedValue = `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
}
else if (NUMBER.includes(field.type))
parsedValue = val;
else if (FLOAT.includes(field.type))
parsedValue = parseFloat(val);
else if (SPATIAL.includes(field.type)) {
let geoJson;
if (IS_MULTI_SPATIAL.includes(field.type)) {
const features = [];
for (const element of val)
features.push(objectToGeoJSON(element));
geoJson = {
type: 'FeatureCollection',
features
};
}
else
geoJson = objectToGeoJSON(val);
parsedValue = `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`;
}
else if (val === '') parsedValue = `${sw}${sw}`;
else {
parsedValue = typeof val === 'string'
? escapeAndQuote(val, client)
: typeof val === 'object'
? escapeAndQuote(JSON.stringify(val), client)
: val;
}
return parsedValue;
};
export const jsonToSqlInsert = (args: {
json: { [key: string]: any};
client: ClientCode;
fields: { [key: string]: {type: string; datePrecision: number}};
table: string;
}) => {
const { client, json, fields, table } = args;
const { elementsWrapper: ew } = customizations[client];
const fieldNames = Object.keys(json).map(key => `${ew}${key}${ew}`);
const values = Object.keys(json).map(key => (
valueToSqlString({ val: json[key], client, field: fields[key] })
));
return `INSERT INTO ${ew}${table}${ew} (${fieldNames.join(', ')}) VALUES (${values.join(', ')});`;
};

View File

@@ -2,38 +2,57 @@ interface ShortcutRecord {
event: string; event: string;
keys: Electron.Accelerator[]; keys: Electron.Accelerator[];
description: string; description: string;
os: NodeJS.Platform[];
} }
const shortcuts: ShortcutRecord[] = [ const shortcuts: ShortcutRecord[] = [
{ {
event: 'open-new-tab', event: 'open-new-tab',
keys: ['CommandOrControl+T'], keys: ['CommandOrControl+T'],
description: 'Open a new query tab' description: 'Open a new query tab',
os: ['darwin', 'linux', 'win32']
}, },
{ {
event: 'close-tab', event: 'close-tab',
keys: ['CommandOrControl+W'], keys: ['CommandOrControl+W'],
description: 'Close tab' description: 'Close tab',
os: ['darwin', 'linux', 'win32']
}, },
{ {
event: 'next-tab', event: 'next-tab',
keys: ['Alt+CommandOrControl+Right', 'CommandOrControl+PageDown'], keys: ['Alt+CommandOrControl+Right', 'CommandOrControl+PageDown'],
description: 'Next tab' description: 'Next tab',
os: ['darwin', 'win32']
}, },
{ {
event: 'prev-tab', event: 'prev-tab',
keys: ['Alt+CommandOrControl+Left', 'CommandOrControl+PageUp'], keys: ['Alt+CommandOrControl+Left', 'CommandOrControl+PageUp'],
description: 'Previous tab' description: 'Previous tab',
os: ['darwin', 'win32']
},
{
event: 'next-tab',
keys: ['CommandOrControl+PageDown'],
description: 'Next tab',
os: ['linux']
},
{
event: 'prev-tab',
keys: ['CommandOrControl+PageUp'],
description: 'Previous tab',
os: ['linux']
}, },
{ {
event: 'open-connections-modal', event: 'open-connections-modal',
keys: ['Shift+CommandOrControl+Space'], keys: ['Shift+CommandOrControl+Space'],
description: 'Show all connections' description: 'Show all connections',
os: ['darwin', 'linux', 'win32']
}, },
{ {
event: 'toggle-console', event: 'toggle-console',
keys: ['CommandOrControl+F12', 'CommandOrControl+`'], keys: ['CommandOrControl+F12', 'CommandOrControl+`'],
description: 'Toggle console' description: 'Toggle console',
os: ['darwin', 'linux', 'win32']
} }
]; ];
@@ -42,8 +61,9 @@ for (let i = 1; i <= 9; i++) {
{ {
event: `select-tab-${i}`, event: `select-tab-${i}`,
keys: [`CommandOrControl+${i}`], keys: [`CommandOrControl+${i}`],
description: `Select tab number ${i}` description: `Select tab number ${i}`,
os: ['darwin', 'linux', 'win32']
}); });
} }
export { shortcuts }; export { shortcuts, ShortcutRecord };

View File

@@ -4,7 +4,7 @@ import { InsertRowsParams } from 'common/interfaces/tableApis';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import * as moment from 'moment'; import * as moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper'; import { sqlEscaper } from 'common/libs/sqlUtils';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import customizations from 'common/customizations'; import customizations from 'common/customizations';

View File

@@ -1,5 +1,4 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import { webContents } from 'electron';
import mysql from 'mysql2/promise'; import mysql from 'mysql2/promise';
import * as pg from 'pg'; import * as pg from 'pg';
import SSH2Promise from 'ssh2-promise'; import SSH2Promise from 'ssh2-promise';
@@ -7,8 +6,10 @@ import SSH2Promise from 'ssh2-promise';
const queryLogger = ({ sql, cUid }: {sql: string; cUid: string}) => { const queryLogger = ({ sql, cUid }: {sql: string; cUid: string}) => {
// Remove comments, newlines and multiple spaces // Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' '); const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
const mainWindow = webContents.fromId(1); if (process.type !== undefined) {
const mainWindow = require('electron').webContents.fromId(1);
mainWindow.send('query-log', { cUid, sql: escapedSql, date: new Date() }); mainWindow.send('query-log', { cUid, sql: escapedSql, date: new Date() });
}
if (process.env.NODE_ENV === 'development') console.log(escapedSql); if (process.env.NODE_ENV === 'development') console.log(escapedSql);
}; };

View File

@@ -1,12 +1,8 @@
import * as exporter from 'common/interfaces/exporter'; import * as exporter from 'common/interfaces/exporter';
import * as mysql from 'mysql2/promise'; import * as mysql from 'mysql2/promise';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes';
import hexToBinary, { HexChar } from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth';
import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
import { MySQLClient } from '../../clients/MySQLClient'; import { MySQLClient } from '../../clients/MySQLClient';
import { valueToSqlString } from 'common/libs/sqlUtils';
export default class MysqlExporter extends SqlExporter { export default class MysqlExporter extends SqlExporter {
protected _client: MySQLClient; protected _client: MySQLClient;
@@ -122,54 +118,7 @@ ${footer}
const column = notGeneratedColumns[i]; const column = notGeneratedColumns[i];
const val = row[column.name]; const val = row[column.name];
if (val === null) sqlInsertString += 'NULL'; sqlInsertString += valueToSqlString({ val, client: 'mysql', field: column });
else if (DATE.includes(column.type)) {
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
: val;
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val);
}
else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (SPATIAL.includes(column.type)) {
let geoJson;
if (IS_MULTI_SPATIAL.includes(column.type)) {
const features = [];
for (const element of val)
features.push(this._getGeoJSON(element));
geoJson = {
type: 'FeatureCollection',
features
};
}
else
geoJson = this._getGeoJSON(val);
sqlInsertString += `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`;
}
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
if (parseInt(i) !== notGeneratedColumns.length - 1) if (parseInt(i) !== notGeneratedColumns.length - 1)
sqlInsertString += ', '; sqlInsertString += ', ';
@@ -435,17 +384,4 @@ CREATE TABLE \`${view.Name}\`(
return `'${escapedVal}'`; return `'${escapedVal}'`;
} }
/* eslint-disable @typescript-eslint/no-explicit-any */
_getGeoJSON (val: any) {
if (Array.isArray(val)) {
if (getArrayDepth(val) === 1)
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(val.map(arr => arr.reduce((acc: any, curr: any) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
}
/* eslint-enable @typescript-eslint/no-explicit-any */
} }

View File

@@ -1,13 +1,11 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as exporter from 'common/interfaces/exporter'; import * as exporter from 'common/interfaces/exporter';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes';
import hexToBinary, { HexChar } from 'common/libs/hexToBinary';
import * as moment from 'moment';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import * as QueryStream from 'pg-query-stream'; import * as QueryStream from 'pg-query-stream';
import { PostgreSQLClient } from '../../clients/PostgreSQLClient'; import { PostgreSQLClient } from '../../clients/PostgreSQLClient';
import { valueToSqlString } from 'common/libs/sqlUtils';
export default class PostgreSQLExporter extends SqlExporter { export default class PostgreSQLExporter extends SqlExporter {
constructor (client: PostgreSQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) { constructor (client: PostgreSQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) {
@@ -223,47 +221,7 @@ SET row_security = off;\n\n\n`;
const column = columns[i]; const column = columns[i];
const val = row[column.name]; const val = row[column.name];
if (val === null) sqlInsertString += 'NULL'; sqlInsertString += valueToSqlString({ val, client: 'pg', field: column });
else if (DATE.includes(column.type)) {
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
: val;
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val);
}
else if ('isArray' in column) {
let parsedVal;
if (Array.isArray(val))
parsedVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
else
parsedVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : '';
sqlInsertString += `'${parsedVal}'`;
}
else if (TEXT_SEARCH.includes(column.type))
sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`;
else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
if (parseInt(i) !== columns.length - 1) if (parseInt(i) !== columns.length - 1)
sqlInsertString += ', '; sqlInsertString += ', ';

View File

@@ -147,6 +147,7 @@ else {
app.on('browser-window-focus', () => { app.on('browser-window-focus', () => {
// Send registered shortcut events to window // Send registered shortcut events to window
for (const shortcut of shortcuts) { for (const shortcut of shortcuts) {
if (shortcut.os.includes(process.platform)) {
for (const key of shortcut.keys) { for (const key of shortcut.keys) {
globalShortcut.register(key, () => { globalShortcut.register(key, () => {
mainWindow.webContents.send(shortcut.event); mainWindow.webContents.send(shortcut.event);
@@ -154,6 +155,7 @@ else {
}); });
} }
} }
}
if (isDevelopment) { // Dev shortcuts if (isDevelopment) { // Dev shortcuts
globalShortcut.register('Shift+CommandOrControl+F5', () => { globalShortcut.register('Shift+CommandOrControl+F5', () => {

View File

@@ -10,7 +10,7 @@
:key="connection.uid" :key="connection.uid"
:connection="connection" :connection="connection"
/> />
<div class="connection-panel-wrapper"> <div class="connection-panel-wrapper p-relative">
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" /> <WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div> </div>
</div> </div>

View File

@@ -13,7 +13,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, Ref, ref } from 'vue'; import { computed, Ref, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
@@ -40,12 +40,12 @@ const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const editField: Ref<HTMLSelectElement> = ref(null); const editField: Ref<HTMLSelectElement> = ref(null);
const foreignList = ref([]); const foreignList = ref([]);
const currentValue = ref(props.modelValue); const currentValue = ref(null);
const isValidDefault = computed(() => { const isValidDefault = computed(() => {
if (!foreignList.value.length) return true; if (!foreignList.value.length) return true;
if (props.modelValue === null) return false; if (props.modelValue === null) return false;
return foreignList.value.some(foreign => foreign.foreign_column.toString() === props.modelValue.toString()); return foreignList.value.some(foreign => foreign.foreign_column.toString() === props.modelValue?.toString());
}); });
const foreigns = computed(() => { const foreigns = computed(() => {
@@ -66,6 +66,10 @@ const cutText = (val: string) => {
return val.length > 15 ? `${val.substring(0, 15)}...` : val; return val.length > 15 ? `${val.substring(0, 15)}...` : val;
}; };
watch(() => props.modelValue, () => {
currentValue.value = props.modelValue;
});
let foreignDesc: string | false; let foreignDesc: string | false;
const params = { const params = {
uid: selectedWorkspace.value, uid: selectedWorkspace.value,

View File

@@ -112,7 +112,11 @@ import BaseSelect from '@/components/BaseSelect.vue';
const props = defineProps({ const props = defineProps({
tabUid: [String, Number], tabUid: [String, Number],
schema: String,
table: String,
fields: Array as Prop<TableField[]>, fields: Array as Prop<TableField[]>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rowToDuplicate: Object as Prop<any>,
keyUsage: Array as Prop<TableForeign[]> keyUsage: Array as Prop<TableForeign[]>
}); });
@@ -123,8 +127,6 @@ const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace } = workspacesStore;
const { trapRef } = useFocusTrap({ disableAutofocus: true }); const { trapRef } = useFocusTrap({ disableAutofocus: true });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -134,7 +136,6 @@ const nInserts = ref(1);
const isInserting = ref(false); const isInserting = ref(false);
const fakerLocale = ref('en'); const fakerLocale = ref('en');
const workspace = computed(() => getWorkspace(selectedWorkspace.value));
const foreignKeys = computed(() => props.keyUsage.map(key => key.field)); const foreignKeys = computed(() => props.keyUsage.map(key => key.field));
const hasFakes = computed(() => Object.keys(localRow.value).some(field => 'group' in localRow.value[field] && localRow.value[field].group !== 'manual')); const hasFakes = computed(() => Object.keys(localRow.value).some(field => 'group' in localRow.value[field] && localRow.value[field].group !== 'manual'));
@@ -220,8 +221,8 @@ const insertRows = async () => {
try { try {
const { status, response } = await Tables.insertTableFakeRows({ const { status, response } = await Tables.insertTableFakeRows({
uid: selectedWorkspace.value, uid: selectedWorkspace.value,
schema: workspace.value.breadcrumbs.schema, schema: props.schema,
table: workspace.value.breadcrumbs.table, table: props.table,
row: rowToInsert, row: rowToInsert,
repeat: nInserts.value, repeat: nInserts.value,
fields: fieldTypes, fields: fieldTypes,
@@ -284,6 +285,8 @@ onMounted(() => {
const rowObj: {[key: string]: unknown} = {}; const rowObj: {[key: string]: unknown} = {};
if (!props.rowToDuplicate) {
// Set default values
for (const field of props.fields) { for (const field of props.fields) {
let fieldDefault; let fieldDefault;
@@ -323,6 +326,17 @@ onMounted(() => {
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
fieldsToExclude.value = [...fieldsToExclude.value, field.name]; fieldsToExclude.value = [...fieldsToExclude.value, field.name];
} }
}
else {
// Set values to duplicate
for (const field of props.fields) {
if (typeof props.rowToDuplicate[field.name] !== 'object')
rowObj[field.name] = { value: props.rowToDuplicate[field.name] };
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
fieldsToExclude.value = [...fieldsToExclude.value, field.name];
}
}
localRow.value = { ...rowObj }; localRow.value = { ...rowObj };
}); });

View File

@@ -136,7 +136,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Component, computed, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref } from 'vue'; import { Component, computed, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref } from 'vue';
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { arrayToFile } from '../libs/arrayToFile'; import { exportRows } from '../libs/exportRows';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useFocusTrap } from '@/composables/useFocusTrap'; import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
@@ -312,10 +312,10 @@ const closeModal = () => emit('close');
const downloadTable = (format: 'csv' | 'json') => { const downloadTable = (format: 'csv' | 'json') => {
if (!sortedResults.value) return; if (!sortedResults.value) return;
arrayToFile({ exportRows({
type: format, type: format,
content: sortedResults.value, content: sortedResults.value,
filename: 'processes' table: 'processes'
}); });
}; };

View File

@@ -12,7 +12,7 @@
<div class="footer-right-elements"> <div class="footer-right-elements">
<ul class="footer-elements"> <ul class="footer-elements">
<li <li
v-if="workspace.connectionStatus === 'connected' " v-if="workspace?.connectionStatus === 'connected' "
class="footer-element footer-link" class="footer-element footer-link"
@click="toggleConsole()" @click="toggleConsole()"
> >

View File

@@ -560,7 +560,8 @@ setTimeout(() => {
.connection-panel { .connection-panel {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-bottom: 1rem; margin-bottom: .5rem;
margin-top: 1.5rem;
.panel { .panel {
min-width: 450px; min-width: 450px;

View File

@@ -167,11 +167,13 @@ onMounted(() => {
font-size: 95%; font-size: 95%;
opacity: .8; opacity: .8;
font-weight: 700; font-weight: 700;
&:hover {
user-select: text; user-select: text;
} }
} }
} }
} }
}
} }
</style> </style>

View File

@@ -112,6 +112,9 @@
<li class="menu-item"> <li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a> <a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li> </li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('sql')">SQL INSERT</a>
</li>
</ul> </ul>
</div> </div>
<div class="input-group pr-2" :title="t('message.commitMode')"> <div class="input-group pr-2" :title="t('message.commitMode')">
@@ -294,6 +297,10 @@ watch(selectedSchema, () => {
const runQuery = async (query: string) => { const runQuery = async (query: string) => {
if (!query || isQuering.value) return; if (!query || isQuering.value) return;
isQuering.value = true; isQuering.value = true;
const selectedQuery = queryEditor.value.editor.getSelectedText();
if (selectedQuery) query = selectedQuery;
clearTabData(); clearTabData();
queryTable.value.resetSort(); queryTable.value.resetSort();
@@ -442,7 +449,7 @@ const clear = () => {
clearTabData(); clearTabData();
}; };
const downloadTable = (format: 'csv' | 'json') => { const downloadTable = (format: 'csv' | 'json' | 'sql') => {
queryTable.value.downloadTable(format, `${props.tab.type}-${props.tab.index}`); queryTable.value.downloadTable(format, `${props.tab.type}-${props.tab.index}`);
}; };

View File

@@ -14,10 +14,12 @@
:context-event="contextEvent" :context-event="contextEvent"
:selected-rows="selectedRows" :selected-rows="selectedRows"
:selected-cell="selectedCell" :selected-cell="selectedCell"
:mode="mode"
@show-delete-modal="showDeleteConfirmModal" @show-delete-modal="showDeleteConfirmModal"
@set-null="setNull" @set-null="setNull"
@copy-cell="copyCell" @copy-cell="copyCell"
@copy-row="copyRow" @copy-row="copyRow"
@duplicate-row="duplicateRow"
@close-context="closeContext" @close-context="closeContext"
/> />
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs"> <ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
@@ -119,7 +121,7 @@ import { uidGen } from 'common/libs/uidGen';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useConsoleStore } from '@/stores/console'; import { useConsoleStore } from '@/stores/console';
import { arrayToFile } from '../libs/arrayToFile'; import { exportRows } from '../libs/exportRows';
import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import WorkspaceTabQueryTableRow from '@/components/WorkspaceTabQueryTableRow.vue'; import WorkspaceTabQueryTableRow from '@/components/WorkspaceTabQueryTableRow.vue';
@@ -129,6 +131,8 @@ import * as moment from 'moment';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { TableField, QueryResult } from 'common/interfaces/antares'; import { TableField, QueryResult } from 'common/interfaces/antares';
import { TableUpdateParams } from 'common/interfaces/tableApis'; import { TableUpdateParams } from 'common/interfaces/tableApis';
import { jsonToSqlInsert } from 'common/libs/sqlUtils';
import { unproxify } from '@/libs/unproxify';
const { t } = useI18n(); const { t } = useI18n();
@@ -143,12 +147,17 @@ const { consoleHeight } = storeToRefs(consoleStore);
const props = defineProps({ const props = defineProps({
results: Array as Prop<QueryResult[]>, results: Array as Prop<QueryResult[]>,
connUid: String, connUid: String,
mode: String, mode: String as Prop<'table' | 'query'>,
isSelected: Boolean, isSelected: Boolean,
elementType: { type: String, default: 'table' } elementType: { type: String, default: 'table' }
}); });
const emit = defineEmits(['update-field', 'delete-selected', 'hard-sort']); const emit = defineEmits([
'update-field',
'delete-selected',
'hard-sort',
'duplicate-row'
]);
const resultTable: Ref<Component & {updateWindow: () => void}> = ref(null); const resultTable: Ref<Component & {updateWindow: () => void}> = ref(null);
const tableWrapper: Ref<HTMLDivElement> = ref(null); const tableWrapper: Ref<HTMLDivElement> = ref(null);
@@ -170,6 +179,7 @@ const selectedField = ref(null);
const isEditingRow = ref(false); const isEditingRow = ref(false);
const workspaceSchema = computed(() => getWorkspace(props.connUid).breadcrumbs.schema); const workspaceSchema = computed(() => getWorkspace(props.connUid).breadcrumbs.schema);
const workspaceClient = computed(() => getWorkspace(props.connUid).client);
const primaryField = computed(() => { const primaryField = computed(() => {
const primaryFields = fields.value.filter(field => field.key === 'pri'); const primaryFields = fields.value.filter(field => field.key === 'pri');
@@ -405,11 +415,47 @@ const copyCell = () => {
navigator.clipboard.writeText(valueToCopy); navigator.clipboard.writeText(valueToCopy);
}; };
const copyRow = () => { const copyRow = (format: string) => {
let contentToCopy;
if (selectedRows.value.length === 1) {
const row = localResults.value.find((row: any) => selectedRows.value.includes(row._antares_id)); const row = localResults.value.find((row: any) => selectedRows.value.includes(row._antares_id));
const rowToCopy = JSON.parse(JSON.stringify(row)); const rowToCopy = unproxify(row);
delete rowToCopy._antares_id; delete rowToCopy._antares_id;
navigator.clipboard.writeText(JSON.stringify(rowToCopy)); contentToCopy = rowToCopy;
}
else {
contentToCopy = unproxify(localResults.value).filter((row: any) => selectedRows.value.includes(row._antares_id)).map((row: any) => {
delete row._antares_id;
return row;
});
}
if (format === 'json')
navigator.clipboard.writeText(JSON.stringify(contentToCopy));
else if (format === 'sql') {
const sqlInserts = [];
if (!Array.isArray(contentToCopy)) contentToCopy = [contentToCopy];
for (const row of contentToCopy) {
sqlInserts.push(jsonToSqlInsert({
json: row,
client: workspaceClient.value,
fields: fieldsObj.value as {
[key: string]: {type: string; datePrecision: number};
},
table: getTable(resultsetIndex.value)
}));
}
navigator.clipboard.writeText(sqlInserts.join('\n'));
}
};
const duplicateRow = () => {
const row = localResults.value.find((row: any) => selectedRows.value.includes(row._antares_id));
const rowToDuplicate = JSON.parse(JSON.stringify(row));
delete rowToDuplicate._antares_id;
emit('duplicate-row', rowToDuplicate);
}; };
const applyUpdate = (params: TableUpdateParams) => { const applyUpdate = (params: TableUpdateParams) => {
@@ -520,7 +566,7 @@ const selectResultset = (index: number) => {
resultsetIndex.value = index; resultsetIndex.value = index;
}; };
const downloadTable = (format: 'csv' | 'json', filename: string) => { const downloadTable = (format: 'csv' | 'json' | 'sql', table: string) => {
if (!sortedResults.value) return; if (!sortedResults.value) return;
const rows = JSON.parse(JSON.stringify(sortedResults.value)).map((row: any) => { const rows = JSON.parse(JSON.stringify(sortedResults.value)).map((row: any) => {
@@ -528,10 +574,14 @@ const downloadTable = (format: 'csv' | 'json', filename: string) => {
return row; return row;
}); });
arrayToFile({ exportRows({
type: format, type: format,
content: rows, content: rows,
filename fields: fieldsObj.value as {
[key: string]: {type: string; datePrecision: number};
},
client: workspaceClient.value,
table
}); });
}; };

View File

@@ -3,7 +3,7 @@
:context-event="contextEvent" :context-event="contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div v-if="selectedRows.length === 1" class="context-element"> <div class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ t('word.copy') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ t('word.copy') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu"> <div class="context-submenu">
@@ -16,17 +16,27 @@
<i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ t('word.cell', 1) }} <i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ t('word.cell', 1) }}
</span> </span>
</div> </div>
<div <div class="context-element" @click="copyRow('json')">
v-if="selectedRows.length === 1"
class="context-element"
@click="copyRow"
>
<span class="d-flex"> <span class="d-flex">
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ t('word.row', 1) }} <i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ t('word.row', selectedRows.length) }} (JSON)
</span>
</div>
<div class="context-element" @click="copyRow('sql')">
<span class="d-flex">
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ t('word.row', selectedRows.length) }} (SQL INSERT)
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div
v-if="selectedRows.length === 1 && selectedCell.isEditable && mode === 'table'"
class="context-element"
@click="duplicateRow"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ t('word.duplicate') }}
</span>
</div>
<div <div
v-if="selectedRows.length === 1 && selectedCell.isEditable" v-if="selectedRows.length === 1 && selectedCell.isEditable"
class="context-element" class="context-element"
@@ -49,6 +59,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Prop } from 'vue';
import BaseContextMenu from '@/components/BaseContextMenu.vue'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -57,10 +68,18 @@ const { t } = useI18n();
defineProps({ defineProps({
contextEvent: MouseEvent, contextEvent: MouseEvent,
selectedRows: Array, selectedRows: Array,
selectedCell: Object selectedCell: Object,
mode: String as Prop<'table' | 'query'>
}); });
const emit = defineEmits(['show-delete-modal', 'close-context', 'set-null', 'copy-cell', 'copy-row']); const emit = defineEmits([
'show-delete-modal',
'close-context',
'set-null',
'copy-cell',
'copy-row',
'duplicate-row'
]);
const showConfirmModal = () => { const showConfirmModal = () => {
emit('show-delete-modal'); emit('show-delete-modal');
@@ -80,8 +99,13 @@ const copyCell = () => {
closeContext(); closeContext();
}; };
const copyRow = () => { const copyRow = (format: string) => {
emit('copy-row'); emit('copy-row', format);
closeContext();
};
const duplicateRow = () => {
emit('duplicate-row');
closeContext(); closeContext();
}; };
</script> </script>

View File

@@ -82,7 +82,7 @@
v-if="isTable" v-if="isTable"
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:disabled="isQuering" :disabled="isQuering"
@click="showFakerModal" @click="showFakerModal()"
> >
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> <i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $tc('message.insertRow', 2) }}</span> <span>{{ $tc('message.insertRow', 2) }}</span>
@@ -105,6 +105,9 @@
<li class="menu-item"> <li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a> <a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li> </li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('sql')">SQL INSERT</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -153,14 +156,18 @@
:element-type="elementType" :element-type="elementType"
@update-field="updateField" @update-field="updateField"
@delete-selected="deleteSelected" @delete-selected="deleteSelected"
@duplicate-row="showFakerModal"
@hard-sort="hardSort" @hard-sort="hardSort"
/> />
</div> </div>
<ModalFakerRows <ModalFakerRows
v-if="isFakerModal" v-if="isFakerModal"
:fields="fields" :fields="fields"
:row-to-duplicate="rowToDuplicate"
:key-usage="keyUsage" :key-usage="keyUsage"
:tab-uid="tabUid" :tab-uid="tabUid"
:schema="schema"
:table="table"
@hide="hideFakerModal" @hide="hideFakerModal"
@reload="reloadTable" @reload="reloadTable"
/> />
@@ -224,6 +231,7 @@ const filters = ref([]);
const page = ref(1); const page = ref(1);
const pageProxy = ref(1); const pageProxy = ref(1);
const approximateCount = ref(0); const approximateCount = ref(0);
const rowToDuplicate = ref(null);
const workspace = computed(() => { const workspace = computed(() => {
return getWorkspace(props.connection.uid); return getWorkspace(props.connection.uid);
@@ -234,7 +242,7 @@ const customizations = computed(() => {
}); });
const isTable = computed(() => { const isTable = computed(() => {
return !!workspace.value.breadcrumbs.table; return !workspace.value.breadcrumbs.view;
}); });
const fields = computed(() => { const fields = computed(() => {
@@ -329,13 +337,17 @@ const pageChange = (direction: 'prev' | 'next') => {
page.value--; page.value--;
}; };
const showFakerModal = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any
const showFakerModal = (row?: any) => {
console.log(row);
if (isQuering.value) return; if (isQuering.value) return;
isFakerModal.value = true; isFakerModal.value = true;
rowToDuplicate.value = row;
}; };
const hideFakerModal = () => { const hideFakerModal = () => {
isFakerModal.value = false; isFakerModal.value = false;
rowToDuplicate.value = null;
}; };
const onKey = (e: KeyboardEvent) => { const onKey = (e: KeyboardEvent) => {
@@ -349,7 +361,7 @@ const onKey = (e: KeyboardEvent) => {
pageChange('next'); pageChange('next');
if (e.key === 'ArrowLeft') if (e.key === 'ArrowLeft')
pageChange('prev'); pageChange('prev');
if (e.key === 'f') // f if (e.key === 'f')
isSearch.value = !isSearch.value; isSearch.value = !isSearch.value;
} }
} }
@@ -367,7 +379,7 @@ const setRefreshInterval = () => {
} }
}; };
const downloadTable = (format: 'csv' | 'json') => { const downloadTable = (format: 'csv' | 'json' | 'sql') => {
queryTable.value.downloadTable(format, props.table); queryTable.value.downloadTable(format, props.table);
}; };

View File

@@ -97,7 +97,9 @@ const removeRow = (i: number) => {
}; };
const doFilter = () => { const doFilter = () => {
const clausoles = rows.value.filter(el => el.active).map(el => createClausole(el)); const clausoles = rows.value
.filter(el => el.active)
.map(el => createClausole(el));
emit('filter', clausoles); emit('filter', clausoles);
}; };

View File

@@ -1,7 +1,14 @@
export const arrayToFile = (args: { import { ClientCode } from 'common/interfaces/antares';
type: 'csv' | 'json'; import { jsonToSqlInsert } from 'common/libs/sqlUtils';
export const exportRows = (args: {
type: 'csv' | 'json'| 'sql';
content: object[]; content: object[];
filename: string; table: string;
client?: ClientCode;
fields?: {
[key: string]: {type: string; datePrecision: number};
};
}) => { }) => {
let mime; let mime;
let content; let content;
@@ -20,6 +27,23 @@ export const arrayToFile = (args: {
content = csv.join('\n'); content = csv.join('\n');
break; break;
} }
case 'sql': {
mime = 'text/sql';
const sql = [];
for (const row of args.content) {
sql.push(jsonToSqlInsert({
json: row,
client:
args.client,
fields: args.fields,
table: args.table
}));
}
content = sql.join('\n');
break;
}
case 'json': case 'json':
mime = 'application/json'; mime = 'application/json';
content = JSON.stringify(args.content, null, 3); content = JSON.stringify(args.content, null, 3);
@@ -30,7 +54,7 @@ export const arrayToFile = (args: {
const file = new Blob([content], { type: mime }); const file = new Blob([content], { type: mime });
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
downloadLink.download = `${args.filename}.${args.type}`; downloadLink.download = `${args.table}.${args.type}`;
downloadLink.href = window.URL.createObjectURL(file); downloadLink.href = window.URL.createObjectURL(file);
downloadLink.style.display = 'none'; downloadLink.style.display = 'none';
document.body.appendChild(downloadLink); document.body.appendChild(downloadLink);

View File

@@ -4,7 +4,7 @@
"./src/main/**/*", "./src/main/**/*",
"./src/renderer/**/*", "./src/renderer/**/*",
"./src/common/interfaces/antares.ts" "./src/common/interfaces/antares.ts"
], , "src/common/libs/jsonToSql.ts" ],
"exclude": ["./src/renderer/libs/ext-language_tools.js"], "exclude": ["./src/renderer/libs/ext-language_tools.js"],
"compilerOptions": { "compilerOptions": {
"baseUrl": "./", "baseUrl": "./",