Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ jobs:
- name: Run SQL clipboard test
run: yarn run test:sql-in-clipboard

- name: Run result Markdown formatter test
run: yarn run test:result-markdown

- name: Run active workspace tab locator contract
run: yarn run test:active-tab-locator

Expand Down
1 change: 1 addition & 0 deletions chat2db-community-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"test:database-object-sorting": "tsx src/blocks/NewTree/utils/sortTreeNodes.test.ts",
"test:execution-console": "tsx src/blocks/SearchResult/components/ExecutionConsole/executionConsolePreferences.test.ts",
"test:redis-explorer": "tsx src/blocks/RedisAllData/redisExplorer.test.ts",
"test:result-markdown": "tsx src/blocks/SearchResult/components/ResultSetTable/event/onContextmenuCell/handleCopyAsMarkdown.test.ts",
"test:result-inspector": "tsx src/store/workspace/utils/resultInspector.test.ts && yarn test:search-result-tab-selection",
"test:search-result-query-composer": "tsx src/blocks/SearchResult/components/ScreeningResult/queryComposer.test.ts",
"test:search-result-tab-selection": "tsx src/blocks/SearchResult/tabSelection.test.ts",
Expand Down
1 change: 1 addition & 0 deletions chat2db-community-client/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ and installers are written under `jpackage/` and are not frontend source files.
```bash
yarn run lint
yarn run test:i18n
yarn run test:result-markdown
yarn run test:sql-in-clipboard
```

Expand Down
4 changes: 2 additions & 2 deletions chat2db-community-client/scripts/i18n-source-hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"es-ES": {
"ai.ts": "391a6e14c58d8401bf3bc245eca5bdd4b66efd8225a877bfbbcae01a1c1c019a",
"chat.ts": "0cbe009eacbaa95a9a29bbc69588cb547e90b563667c53edec3e54ee64f5667b",
"common.ts": "91dad744d8a1a7b829c61e0dc47e6912953f106f2c96bab1ad0d9858278badcd",
"common.ts": "9ae18f30c9bec0784fc6dca322c63dd440d4d3281e223dbddfd811685f63a7cd",
"connection.ts": "41d07fd10c9550ae51995620cc998de5e99add35edb31c2bb592864c76c13688",
"dashboard.ts": "b217bbe260c7674d4efad1dd1b61136653858884f5b9b2134cfdf7d1de5a44ee",
"editTable.ts": "0f7a808f50053cd52b183d156bb4fd46bb125b8058f8774deb61c7f02894a350",
Expand All @@ -32,7 +32,7 @@
"ko-KR": {
"ai.ts": "391a6e14c58d8401bf3bc245eca5bdd4b66efd8225a877bfbbcae01a1c1c019a",
"chat.ts": "0cbe009eacbaa95a9a29bbc69588cb547e90b563667c53edec3e54ee64f5667b",
"common.ts": "91dad744d8a1a7b829c61e0dc47e6912953f106f2c96bab1ad0d9858278badcd",
"common.ts": "9ae18f30c9bec0784fc6dca322c63dd440d4d3281e223dbddfd811685f63a7cd",
"connection.ts": "41d07fd10c9550ae51995620cc998de5e99add35edb31c2bb592864c76c13688",
"dashboard.ts": "b217bbe260c7674d4efad1dd1b61136653858884f5b9b2134cfdf7d1de5a44ee",
"editTable.ts": "0f7a808f50053cd52b183d156bb4fd46bb125b8058f8774deb61c7f02894a350",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ export enum ContextmenuType {
tabSplitField = 'tabSplitField',
// Tab-separated value field
tabSplitFieldAndValue = 'tabSplitFieldAndValue',
// Markdown table
markdownTable = 'markdownTable',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { formatSelectionAsMarkdown, type IMarkdownSelectionCell } from './markdownTable';

function assertEqual(actual: any, expected: any, message: string) {
if (actual !== expected) {
throw new Error(`${message}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}
}

const selection: IMarkdownSelectionCell[][] = [
[
{ row: 2, col: 2, field: 'name', value: 'Grace | Hopper' },
{ row: 2, col: 1, field: 'id', dataValue: 2 },
],
[
{ row: 1, col: 2, field: 'name', value: 'Ada\nLovelace' },
{ row: 1, col: 1, field: 'id', value: 1 },
],
];

assertEqual(
formatSelectionAsMarkdown(selection, (cell) => ({ id: 'User ID', name: 'Name | bio' })[String(cell.field)]),
[
'| User ID | Name \\| bio |',
'| --- | --- |',
'| 1 | Ada<br>Lovelace |',
'| 2 | Grace \\| Hopper |',
].join('\n'),
'formats selected cells by row and column with Markdown escaping',
);

assertEqual(
formatSelectionAsMarkdown([
[{ row: 3, col: 1, field: 'id', value: null }],
[{ row: 4, col: 2, field: 'note', value: '' }],
]),
['| id | note |', '| --- | --- |', '| NULL | |', '| | |'].join('\n'),
'preserves sparse selections and distinguishes NULL from an empty string',
);

assertEqual(
formatSelectionAsMarkdown([[{ row: 1, col: 1, field: 'value', value: 'left\\|right' }]]),
['| value |', '| --- |', '| left\\\\\\|right |'].join('\n'),
'preserves a backslash followed by a pipe',
);

assertEqual(
formatSelectionAsMarkdown(
[[{ row: 1, col: 1, field: 'html', value: '<script>alert("x")</script> & next\nline' }]],
() => '<Header & value>',
),
[
'| &lt;Header &amp; value&gt; |',
'| --- |',
'| &lt;script&gt;alert("x")&lt;/script&gt; &amp; next<br>line |',
].join('\n'),
'escapes source HTML before inserting generated line-break markup',
);

assertEqual(
formatSelectionAsMarkdown([[{ row: 0, col: 1, field: 'id', value: 'id' }]]),
null,
'ignores header-only selections',
);

console.log('copy selection as Markdown tests passed');
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { copyToClipboard } from '@/utils';
import { formatSelectionAsMarkdown } from './markdownTable';
import type { ITableInstance } from '@/blocks/CanvasTable/typings';

const handleCopyAsMarkdown = (tableInstance: ITableInstance) => {
const columns = tableInstance.columns as any[];
const markdown = formatSelectionAsMarkdown(tableInstance.getSelectedCellInfos() || [], (cell) => {
const column = columns.find((item) => String(item.field) === String(cell.field));
return column?.title ?? cell.field ?? '';
});

if (!markdown) {
return false;
}

return copyToClipboard(markdown);
};

export default handleCopyAsMarkdown;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import handleViewUpdateData from './handleViewUpdateData';
import handleSetNull from './handleSetNull';
import handleCopyRow from './handleCopyRow';
import handleCopyAsSqlInValues from './handleCopyAsSqlInValues';
import handleCopyAsMarkdown from './handleCopyAsMarkdown';
import i18n from '@/i18n';
import { copyToClipboard } from '@/utils';
import { downloadLargeCellValue } from '@/utils/file';
Expand Down Expand Up @@ -193,6 +194,13 @@ const onContextmenuCell = (props: IOnContextmenuEvent) => {
handleCopyRow({ tableInstance, selectEvent, type: ContextmenuType.tabSplitFieldAndValue });
},
},
{
key: ContextmenuType.markdownTable,
label: i18n('common.button.markdownTable'),
onClick: () => {
handleCopyAsMarkdown(tableInstance);
},
},
],
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export interface IMarkdownSelectionCell {
col: number;
row: number;
field?: unknown;
value?: unknown;
dataValue?: unknown;
}

const escapeMarkdownCell = (value: unknown) => {
if (value === null || value === undefined) {
return 'NULL';
}

return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\r\n|\r|\n/g, '<br>')
.replace(/\\/g, '\\\\')
.replace(/\|/g, '\\|');
};

export const formatSelectionAsMarkdown = (
selectedCells: IMarkdownSelectionCell[][],
getColumnTitle: (cell: IMarkdownSelectionCell) => unknown = (cell) => cell.field,
) => {
const cells = selectedCells.flat().filter((cell) => cell.row > 0 && cell.col > 0);
if (!cells.length) {
return null;
}

const columns = [...new Set(cells.map((cell) => cell.col))].sort((left, right) => left - right);
const rows = [...new Set(cells.map((cell) => cell.row))].sort((left, right) => left - right);
const cellByPosition = new Map(cells.map((cell) => [`${cell.row}:${cell.col}`, cell]));

const header = columns.map((col) => {
const cell = cells.find((item) => item.col === col);
return escapeMarkdownCell(cell ? getColumnTitle(cell) : '');
});
const separator = columns.map(() => '---');
const body = rows.map((row) =>
columns.map((col) => {
const cell = cellByPosition.get(`${row}:${col}`);
if (!cell) {
return '';
}
return escapeMarkdownCell(cell.dataValue !== undefined ? cell.dataValue : cell.value);
}),
);

return [header, separator, ...body].map((line) => `| ${line.join(' | ')} |`).join('\n');
};
1 change: 1 addition & 0 deletions chat2db-community-client/src/i18n/en-US/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export default {
'common.button.tabularSeparatedValues': 'TAB delimited (data)',
'common.button.tabularSeparatedValuesFieldName': 'TAB delimited (field name)',
'common.button.tabularSeparatedValuesFieldNameAndData': 'Tab-separated (field names and data)',
'common.button.markdownTable': 'Markdown table',
'common.button.cloneRow': 'Clone row',
'common.button.deleteRow': 'Delete row',
'common.button.setNull': 'Set NULL',
Expand Down
1 change: 1 addition & 0 deletions chat2db-community-client/src/i18n/es-ES/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export default {
'common.button.tabularSeparatedValues': 'Delimitado por tabuladores (datos)',
'common.button.tabularSeparatedValuesFieldName': 'Delimitado por tabuladores (nombre de campo)',
'common.button.tabularSeparatedValuesFieldNameAndData': 'Separado por tabuladores (nombres de campo y datos)',
'common.button.markdownTable': 'Tabla Markdown',
'common.button.cloneRow': 'Clonar fila',
'common.button.deleteRow': 'Eliminar fila',
'common.button.setNull': 'Establecer NULL',
Expand Down
1 change: 1 addition & 0 deletions chat2db-community-client/src/i18n/ja-JP/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export default {
'common.button.tabularSeparatedValues': 'タブ区切り値',
'common.button.tabularSeparatedValuesFieldName': 'タブ区切り値(フィールド名)',
'common.button.tabularSeparatedValuesFieldNameAndData': 'タブ区切り値(フィールド名とデータ)',
'common.button.markdownTable': 'Markdown テーブル',
'common.button.cloneRow': '行を複製',
'common.button.deleteRow': '行を削除',
'common.button.setNull': 'NULLに設定',
Expand Down
1 change: 1 addition & 0 deletions chat2db-community-client/src/i18n/ko-KR/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export default {
'common.button.tabularSeparatedValues': '탭으로 구분(데이터)',
'common.button.tabularSeparatedValuesFieldName': '탭으로 구분(필드 이름)',
'common.button.tabularSeparatedValuesFieldNameAndData': '탭으로 구분(필드 이름 및 데이터)',
'common.button.markdownTable': 'Markdown 표',
'common.button.cloneRow': '행 복제',
'common.button.deleteRow': '행 삭제',
'common.button.setNull': 'NULL로 설정',
Expand Down
1 change: 1 addition & 0 deletions chat2db-community-client/src/i18n/zh-CN/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export default {
'common.button.tabularSeparatedValues': '制表符分隔值',
'common.button.tabularSeparatedValuesFieldName': '制表符分隔值(字段名)',
'common.button.tabularSeparatedValuesFieldNameAndData': '制表符分隔值(字段名和数据)',
'common.button.markdownTable': 'Markdown 表格',
'common.button.cloneRow': '克隆行',
'common.button.deleteRow': '删除行',
'common.button.setNull': '设置为NULL',
Expand Down
Loading