mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-24 19:56:00 +00:00
join wizard
This commit is contained in:
189
packages/web/src/modals/InsertJoinModal.js
Normal file
189
packages/web/src/modals/InsertJoinModal.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import React from 'react';
|
||||
import ModalBase from './ModalBase';
|
||||
import { FormButtonRow } from '../utility/forms';
|
||||
import FormStyledButton from '../widgets/FormStyledButton';
|
||||
import SqlEditor from '../sqleditor/SqlEditor';
|
||||
import styled from 'styled-components';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import ModalHeader from './ModalHeader';
|
||||
import ModalContent from './ModalContent';
|
||||
import ModalFooter from './ModalFooter';
|
||||
import analyseQuerySources from '../sqleditor/analyseQuerySources';
|
||||
import TableControl, { TableColumn } from '../utility/TableControl';
|
||||
import { TextField } from '../utility/inputs';
|
||||
|
||||
const FlexLine = styled.div`
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
`;
|
||||
|
||||
const FlexColumn = styled.div`
|
||||
margin: 5px;
|
||||
`;
|
||||
|
||||
const Label = styled.div`
|
||||
margin: 5px;
|
||||
`;
|
||||
|
||||
const SqlWrapper = styled.div`
|
||||
position: relative;
|
||||
height: 80px;
|
||||
width: 40vw;
|
||||
`;
|
||||
|
||||
const JOIN_TYPES = ['INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN'];
|
||||
|
||||
export default function InsertJoinModal({ sql, modalState, engine, dbinfo, onInsert }) {
|
||||
const sources = React.useMemo(
|
||||
() => analyseQuerySources(sql, [...dbinfo.tables.map((x) => x.pureName), ...dbinfo.views.map((x) => x.pureName)]),
|
||||
[sql, dbinfo]
|
||||
);
|
||||
|
||||
const [sourceIndex, setSourceIndex] = React.useState(0);
|
||||
const [targetIndex, setTargetIndex] = React.useState(0);
|
||||
const [joinIndex, setJoinIndex] = React.useState(0);
|
||||
const [alias, setAlias] = React.useState('');
|
||||
const sourceRef = React.useRef(null);
|
||||
const targetRef = React.useRef(null);
|
||||
const aliasRef = React.useRef(null);
|
||||
const joinRef = React.useRef(null);
|
||||
|
||||
const targets = React.useMemo(() => {
|
||||
const source = sources[sourceIndex];
|
||||
if (!source) return [];
|
||||
/** @type {import('@dbgate/types').TableInfo} */
|
||||
const table = dbinfo.tables.find((x) => x.pureName == sources[sourceIndex].name);
|
||||
if (!table) return [];
|
||||
return [
|
||||
...table.foreignKeys.map((fk) => ({
|
||||
baseColumns: fk.columns.map((x) => x.columnName).join(', '),
|
||||
refTable: fk.refTableName,
|
||||
refColumns: fk.columns.map((x) => x.refColumnName).join(', '),
|
||||
constraintName: fk.constraintName,
|
||||
columnMap: fk.columns,
|
||||
})),
|
||||
...table.dependencies.map((fk) => ({
|
||||
baseColumns: fk.columns.map((x) => x.refColumnName).join(', '),
|
||||
refTable: fk.pureName,
|
||||
refColumns: fk.columns.map((x) => x.columnName).join(', '),
|
||||
constraintName: fk.constraintName,
|
||||
columnMap: fk.columns.map((x) => ({
|
||||
columnName: x.refColumnName,
|
||||
refColumnName: x.columnName,
|
||||
})),
|
||||
})),
|
||||
];
|
||||
}, [sourceIndex, sources]);
|
||||
|
||||
const sqlPreview = React.useMemo(() => {
|
||||
const source = sources[sourceIndex];
|
||||
const target = targets[targetIndex];
|
||||
if (source && target) {
|
||||
return `${JOIN_TYPES[joinIndex]} ${target.refTable}${alias ? ` ${alias}` : ''} ON ${target.columnMap
|
||||
.map((col) => `${source.name}.${col.columnName} = ${alias || target.refTable}.${col.refColumnName}`)
|
||||
.join(' AND ')}`;
|
||||
}
|
||||
return '';
|
||||
}, [joinIndex, sources, targets, sourceIndex, targetIndex, alias]);
|
||||
|
||||
const sourceKeyDown = React.useCallback((event) => {
|
||||
if (event.keyCode == keycodes.enter || event.keyCode == keycodes.rightArrow) {
|
||||
targetRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
const targetKeyDown = React.useCallback((event) => {
|
||||
if (event.keyCode == keycodes.leftArrow) {
|
||||
sourceRef.current.focus();
|
||||
}
|
||||
if (event.keyCode == keycodes.enter || event.keyCode == keycodes.rightArrow) {
|
||||
joinRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
const joinKeyDown = React.useCallback((event) => {
|
||||
if (event.keyCode == keycodes.leftArrow) {
|
||||
targetRef.current.focus();
|
||||
}
|
||||
if (event.keyCode == keycodes.enter || event.keyCode == keycodes.rightArrow) {
|
||||
aliasRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
const aliasKeyDown = React.useCallback((event) => {
|
||||
if (event.keyCode == keycodes.enter) {
|
||||
event.preventDefault();
|
||||
modalState.close();
|
||||
onInsert(sqlPreview);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ModalBase modalState={modalState}>
|
||||
<ModalHeader modalState={modalState}>Insert join</ModalHeader>
|
||||
<ModalContent>
|
||||
<FlexLine>
|
||||
<FlexColumn>
|
||||
<Label>Existing table</Label>
|
||||
<TableControl
|
||||
rows={sources}
|
||||
focusOnCreate
|
||||
selectedIndex={sourceIndex}
|
||||
setSelectedIndex={setSourceIndex}
|
||||
onKeyDown={sourceKeyDown}
|
||||
tableRef={sourceRef}
|
||||
>
|
||||
<TableColumn fieldName="alias" header="Alias" />
|
||||
<TableColumn fieldName="name" header="Name" />
|
||||
</TableControl>
|
||||
</FlexColumn>
|
||||
<FlexColumn>
|
||||
<Label>New table</Label>
|
||||
<TableControl
|
||||
rows={targets}
|
||||
selectedIndex={targetIndex}
|
||||
setSelectedIndex={setTargetIndex}
|
||||
tableRef={targetRef}
|
||||
onKeyDown={targetKeyDown}
|
||||
>
|
||||
<TableColumn fieldName="baseColumns" header="Column from" />
|
||||
<TableColumn fieldName="refTable" header="Table to" />
|
||||
<TableColumn fieldName="refColumns" header="Column to" />
|
||||
{/* <TableColumn fieldName="constraintName" header="Foreign key" /> */}
|
||||
</TableControl>
|
||||
</FlexColumn>
|
||||
<FlexColumn>
|
||||
<Label>Join</Label>
|
||||
<TableControl
|
||||
rows={JOIN_TYPES.map((name) => ({ name }))}
|
||||
selectedIndex={joinIndex}
|
||||
setSelectedIndex={setJoinIndex}
|
||||
tableRef={joinRef}
|
||||
onKeyDown={joinKeyDown}
|
||||
>
|
||||
<TableColumn fieldName="name" header="Join type" />
|
||||
</TableControl>
|
||||
<Label>Alias</Label>
|
||||
<TextField
|
||||
value={alias}
|
||||
onChange={(e) => setAlias(e.target.value)}
|
||||
editorRef={aliasRef}
|
||||
onKeyDown={aliasKeyDown}
|
||||
/>
|
||||
</FlexColumn>
|
||||
</FlexLine>
|
||||
<SqlWrapper>
|
||||
<SqlEditor value={sqlPreview} engine={engine} readOnly />
|
||||
</SqlWrapper>
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
<FormStyledButton
|
||||
value="OK"
|
||||
onClick={() => {
|
||||
modalState.close();
|
||||
onInsert(sqlPreview);
|
||||
}}
|
||||
/>
|
||||
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
|
||||
</ModalFooter>
|
||||
</ModalBase>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,12 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import AceEditor from 'react-ace';
|
||||
import useDimensions from '../utility/useDimensions';
|
||||
import { addCompleter, setCompleters } from 'ace-builds/src-noconflict/ext-language_tools';
|
||||
import { getDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import analyseQuerySources from './analyseQuerySources';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import useCodeCompletion from './useCodeCompletion';
|
||||
import showModal from '../modals/showModal';
|
||||
import InsertJoinModal from '../modals/InsertJoinModal';
|
||||
import { getDatabaseInfo } from '../utility/metadataLoaders';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: absolute;
|
||||
@@ -55,118 +58,50 @@ export default function SqlEditor({
|
||||
|
||||
const currentEditorRef = editorRef || ownEditorRef;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!tabVisible) return;
|
||||
|
||||
setCompleters([]);
|
||||
addCompleter({
|
||||
getCompletions: async function (editor, session, pos, prefix, callback) {
|
||||
const cursor = session.selection.cursor;
|
||||
const line = session.getLine(cursor.row).slice(0, cursor.column);
|
||||
const dbinfo = await getDatabaseInfo({ conid, database });
|
||||
|
||||
let list = COMMON_KEYWORDS.map((word) => ({
|
||||
name: word,
|
||||
value: word,
|
||||
caption: word,
|
||||
meta: 'keyword',
|
||||
score: 800,
|
||||
}));
|
||||
|
||||
if (/from\s*([a-zA-Z0-9_]*)?$/i.test(line)) {
|
||||
if (dbinfo) {
|
||||
list = [
|
||||
...list,
|
||||
...dbinfo.tables.map((x) => ({
|
||||
name: x.pureName,
|
||||
value: x.pureName,
|
||||
caption: x.pureName,
|
||||
meta: 'table',
|
||||
score: 1000,
|
||||
})),
|
||||
...dbinfo.views.map((x) => ({
|
||||
name: x.pureName,
|
||||
value: x.pureName,
|
||||
caption: x.pureName,
|
||||
meta: 'view',
|
||||
score: 1000,
|
||||
})),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const colMatch = line.match(/([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)?$/);
|
||||
if (colMatch && dbinfo) {
|
||||
const table = colMatch[1];
|
||||
const sources = analyseQuerySources(editor.getValue(), [
|
||||
...dbinfo.tables.map((x) => x.pureName),
|
||||
...dbinfo.views.map((x) => x.pureName),
|
||||
]);
|
||||
const source = sources.find((x) => (x.alias || x.name) == table);
|
||||
if (source) {
|
||||
const table = dbinfo.tables.find((x) => x.pureName == source.name);
|
||||
if (table) {
|
||||
list = [
|
||||
...list,
|
||||
...table.columns.map((x) => ({
|
||||
name: x.columnName,
|
||||
value: x.columnName,
|
||||
caption: x.columnName,
|
||||
meta: 'column',
|
||||
score: 1000,
|
||||
})),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, list);
|
||||
},
|
||||
});
|
||||
|
||||
const doLiveAutocomplete = function (e) {
|
||||
const editor = e.editor;
|
||||
var hasCompleter = editor.completer && editor.completer.activated;
|
||||
const session = editor.session;
|
||||
const cursor = session.selection.cursor;
|
||||
const line = session.getLine(cursor.row).slice(0, cursor.column);
|
||||
|
||||
// We don't want to autocomplete with no prefix
|
||||
if (e.command.name === 'backspace') {
|
||||
// do not hide after backspace
|
||||
} else if (e.command.name === 'insertstring') {
|
||||
if (!hasCompleter || e.args == '.') {
|
||||
editor.execCommand('startAutocomplete');
|
||||
}
|
||||
|
||||
// if (e.args == ' ' || e.args == '.') {
|
||||
// if (/from\s*$/i.test(line)) {
|
||||
// currentEditorRef.current.editor.execCommand('startAutocomplete');
|
||||
// }
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
currentEditorRef.current.editor.commands.on('afterExec', doLiveAutocomplete);
|
||||
|
||||
return () => {
|
||||
currentEditorRef.current.editor.commands.removeListener('afterExec', doLiveAutocomplete);
|
||||
};
|
||||
}, [tabVisible, conid, database, currentEditorRef.current]);
|
||||
useCodeCompletion({
|
||||
conid,
|
||||
database,
|
||||
tabVisible,
|
||||
currentEditorRef,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if ((tabVisible || focusOnCreate) && currentEditorRef.current && currentEditorRef.current.editor)
|
||||
currentEditorRef.current.editor.focus();
|
||||
}, [tabVisible, focusOnCreate]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
async (data, hash, keyString, keyCode, event) => {
|
||||
if (keyCode == keycodes.j && event.ctrlKey && !readOnly && tabVisible) {
|
||||
event.preventDefault();
|
||||
const dbinfo = await getDatabaseInfo({ conid, database });
|
||||
showModal((modalState) => (
|
||||
<InsertJoinModal
|
||||
sql={currentEditorRef.current.editor.getValue()}
|
||||
modalState={modalState}
|
||||
engine={engine}
|
||||
dbinfo={dbinfo}
|
||||
onInsert={(text) => {
|
||||
const editor = currentEditorRef.current.editor;
|
||||
editor.session.insert(editor.getCursorPosition(), text);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
if (onKeyDown) onKeyDown(data, hash, keyString, keyCode, event);
|
||||
},
|
||||
[onKeyDown]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (onKeyDown && currentEditorRef.current) {
|
||||
currentEditorRef.current.editor.keyBinding.addKeyboardHandler(onKeyDown);
|
||||
if ((onKeyDown || !readOnly) && currentEditorRef.current) {
|
||||
currentEditorRef.current.editor.keyBinding.addKeyboardHandler(handleKeyDown);
|
||||
}
|
||||
return () => {
|
||||
currentEditorRef.current.editor.keyBinding.removeKeyboardHandler(onKeyDown);
|
||||
currentEditorRef.current.editor.keyBinding.removeKeyboardHandler(handleKeyDown);
|
||||
};
|
||||
}, [onKeyDown]);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
return (
|
||||
<Wrapper ref={containerRef}>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function analyseQuerySources(sql, sourceNames) {
|
||||
name: word,
|
||||
});
|
||||
}
|
||||
if (/^(where)|(inner)|(left)|(right)$/i.test(postWord)) {
|
||||
if (/^(where)|(inner)|(left)|(right)|(on)$/i.test(postWord)) {
|
||||
continue;
|
||||
}
|
||||
res.push({
|
||||
|
||||
123
packages/web/src/sqleditor/useCodeCompletion.js
Normal file
123
packages/web/src/sqleditor/useCodeCompletion.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import { addCompleter, setCompleters } from 'ace-builds/src-noconflict/ext-language_tools';
|
||||
import { getDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import analyseQuerySources from './analyseQuerySources';
|
||||
|
||||
const COMMON_KEYWORDS = [
|
||||
'select',
|
||||
'where',
|
||||
'update',
|
||||
'delete',
|
||||
'group',
|
||||
'order',
|
||||
'from',
|
||||
'by',
|
||||
'create',
|
||||
'table',
|
||||
'drop',
|
||||
'alter',
|
||||
'view',
|
||||
'execute',
|
||||
'procedure',
|
||||
];
|
||||
|
||||
export default function useCodeCompletion({ conid, database, tabVisible, currentEditorRef }) {
|
||||
React.useEffect(() => {
|
||||
if (!tabVisible) return;
|
||||
|
||||
setCompleters([]);
|
||||
addCompleter({
|
||||
getCompletions: async function (editor, session, pos, prefix, callback) {
|
||||
const cursor = session.selection.cursor;
|
||||
const line = session.getLine(cursor.row).slice(0, cursor.column);
|
||||
const dbinfo = await getDatabaseInfo({ conid, database });
|
||||
|
||||
let list = COMMON_KEYWORDS.map((word) => ({
|
||||
name: word,
|
||||
value: word,
|
||||
caption: word,
|
||||
meta: 'keyword',
|
||||
score: 800,
|
||||
}));
|
||||
|
||||
if (/(join)|(from)|(update)|(delete)|(insert)\s*([a-zA-Z0-9_]*)?$/i.test(line)) {
|
||||
if (dbinfo) {
|
||||
list = [
|
||||
...list,
|
||||
...dbinfo.tables.map((x) => ({
|
||||
name: x.pureName,
|
||||
value: x.pureName,
|
||||
caption: x.pureName,
|
||||
meta: 'table',
|
||||
score: 1000,
|
||||
})),
|
||||
...dbinfo.views.map((x) => ({
|
||||
name: x.pureName,
|
||||
value: x.pureName,
|
||||
caption: x.pureName,
|
||||
meta: 'view',
|
||||
score: 1000,
|
||||
})),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const colMatch = line.match(/([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)?$/);
|
||||
if (colMatch && dbinfo) {
|
||||
const table = colMatch[1];
|
||||
const sources = analyseQuerySources(editor.getValue(), [
|
||||
...dbinfo.tables.map((x) => x.pureName),
|
||||
...dbinfo.views.map((x) => x.pureName),
|
||||
]);
|
||||
const source = sources.find((x) => (x.alias || x.name) == table);
|
||||
if (source) {
|
||||
const table = dbinfo.tables.find((x) => x.pureName == source.name);
|
||||
if (table) {
|
||||
list = [
|
||||
...list,
|
||||
...table.columns.map((x) => ({
|
||||
name: x.columnName,
|
||||
value: x.columnName,
|
||||
caption: x.columnName,
|
||||
meta: 'column',
|
||||
score: 1000,
|
||||
})),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, list);
|
||||
},
|
||||
});
|
||||
|
||||
const doLiveAutocomplete = function (e) {
|
||||
const editor = e.editor;
|
||||
var hasCompleter = editor.completer && editor.completer.activated;
|
||||
const session = editor.session;
|
||||
const cursor = session.selection.cursor;
|
||||
const line = session.getLine(cursor.row).slice(0, cursor.column);
|
||||
|
||||
// We don't want to autocomplete with no prefix
|
||||
if (e.command.name === 'backspace') {
|
||||
// do not hide after backspace
|
||||
} else if (e.command.name === 'insertstring') {
|
||||
if (!hasCompleter || e.args == '.') {
|
||||
editor.execCommand('startAutocomplete');
|
||||
}
|
||||
|
||||
// if (e.args == ' ' || e.args == '.') {
|
||||
// if (/from\s*$/i.test(line)) {
|
||||
// currentEditorRef.current.editor.execCommand('startAutocomplete');
|
||||
// }
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
currentEditorRef.current.editor.commands.on('afterExec', doLiveAutocomplete);
|
||||
|
||||
return () => {
|
||||
currentEditorRef.current.editor.commands.removeListener('afterExec', doLiveAutocomplete);
|
||||
};
|
||||
}, [tabVisible, conid, database, currentEditorRef.current]);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export default function ObjectListControl({ collection = [], title, showIfEmpty
|
||||
<TableColumn
|
||||
fieldName="displayName"
|
||||
header="Name"
|
||||
formatter={col => <AppObjectControl data={col} makeAppObj={makeAppObj} component="span" />}
|
||||
formatter={(col) => <AppObjectControl data={col} makeAppObj={makeAppObj} component="span" />}
|
||||
/>
|
||||
{children}
|
||||
</TableControl>
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import keycodes from './keycodes';
|
||||
|
||||
const Table = styled.table`
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
user-select: ${(props) =>
|
||||
// @ts-ignore
|
||||
props.focusable ? 'none' : ''};
|
||||
// outline: none;
|
||||
`;
|
||||
const TableHead = styled.thead``;
|
||||
const TableBody = styled.tbody``;
|
||||
const TableHeaderRow = styled.tr``;
|
||||
const TableBodyRow = styled.tr``;
|
||||
const TableBodyRow = styled.tr`
|
||||
background-color: ${(props) =>
|
||||
// @ts-ignore
|
||||
props.isSelected ? '#ccccff' : ''};
|
||||
`;
|
||||
const TableHeaderCell = styled.td`
|
||||
border: 1px solid #e8eef4;
|
||||
background-color: #e8eef4;
|
||||
@@ -30,26 +39,70 @@ function format(row, col) {
|
||||
return row[fieldName];
|
||||
}
|
||||
|
||||
export default function TableControl({ rows = [], children }) {
|
||||
console.log('children', children);
|
||||
|
||||
export default function TableControl({
|
||||
rows = [],
|
||||
children,
|
||||
focusOnCreate = false,
|
||||
onKeyDown = undefined,
|
||||
tabIndex = -1,
|
||||
setSelectedIndex = undefined,
|
||||
selectedIndex = undefined,
|
||||
tableRef = undefined,
|
||||
}) {
|
||||
const columns = (children instanceof Array ? _.flatten(children) : [children])
|
||||
.filter(child => child && child.props && child.props.fieldName)
|
||||
.map(child => child.props);
|
||||
.filter((child) => child && child.props && child.props.fieldName)
|
||||
.map((child) => child.props);
|
||||
|
||||
const myTableRef = React.useRef(null);
|
||||
const currentTableRef = tableRef || myTableRef;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (focusOnCreate) {
|
||||
currentTableRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = React.useCallback((event) => {
|
||||
if (event.keyCode == keycodes.downArrow) {
|
||||
setSelectedIndex((i) => Math.min(i + 1, rows.length - 1));
|
||||
}
|
||||
if (event.keyCode == keycodes.upArrow) {
|
||||
setSelectedIndex((i) => Math.max(0, i - 1));
|
||||
}
|
||||
if (onKeyDown) onKeyDown(event);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<Table
|
||||
ref={currentTableRef}
|
||||
onKeyDown={selectedIndex != null ? handleKeyDown : undefined}
|
||||
tabIndex={selectedIndex != null ? tabIndex : undefined}
|
||||
// @ts-ignore
|
||||
focusable={selectedIndex != null}
|
||||
>
|
||||
<TableHead>
|
||||
<TableHeaderRow>
|
||||
{columns.map(x => (
|
||||
{columns.map((x) => (
|
||||
<TableHeaderCell key={x.fieldName}>{x.header}</TableHeaderCell>
|
||||
))}
|
||||
</TableHeaderRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, index) => (
|
||||
<TableBodyRow key={index}>
|
||||
{columns.map(col => (
|
||||
<TableBodyRow
|
||||
key={index}
|
||||
// @ts-ignore
|
||||
isSelected={index == selectedIndex}
|
||||
onClick={
|
||||
selectedIndex != null
|
||||
? () => {
|
||||
setSelectedIndex(index);
|
||||
currentTableRef.current.focus();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{columns.map((col) => (
|
||||
<TableBodyCell key={col.fieldName}>{format(row, col)}</TableBodyCell>
|
||||
))}
|
||||
</TableBodyRow>
|
||||
|
||||
Reference in New Issue
Block a user