console: add multi select in browse rows to allow bulk delete (close #1739) (#3735)

This commit is contained in:
Aleksandra Sikora 2020-03-11 14:25:36 +01:00 committed by GitHub
parent 71240f310d
commit 36c92db991
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 248 additions and 31 deletions

View File

@ -6,9 +6,13 @@
- Introducing Actions: https://docs.hasura.io/1.0/graphql/manual/actions/index.html - Introducing Actions: https://docs.hasura.io/1.0/graphql/manual/actions/index.html
- Downgrade command: https://hasura.io/docs/1.0/graphql/manual/deployment/downgrading.html#downgrading-hasura-graphql-engine - Downgrade command: https://hasura.io/docs/1.0/graphql/manual/deployment/downgrading.html#downgrading-hasura-graphql-engine
- console: add multi select to data table and bulk delete (#3735)
Added a checkbox to each row on Browse Rows view that allows selecting one or more rows from the table and bulk delete them.
- console: allow setting check constraints during table create (#3881) - console: allow setting check constraints during table create (#3881)
There was added a component that allows adding check constraints while creating a new table in the same way as it can be done on the `Modify` view. Added a component that allows adding check constraints while creating a new table in the same way as it can be done on the `Modify` view.
### Other changes ### Other changes

View File

@ -74,6 +74,7 @@ const checkOrder = order => {
.find('[role=gridcell]') .find('[role=gridcell]')
.first() .first()
.next() .next()
.next()
.contains(index); .contains(index);
} }
}); });
@ -84,6 +85,7 @@ const checkOrder = order => {
.find('[role=gridcell]') .find('[role=gridcell]')
.first() .first()
.next() .next()
.next()
.contains(22 - index); .contains(22 - index);
} }
}); });
@ -231,7 +233,7 @@ export const checkPagination = () => {
cy.wait(3000); cy.wait(3000);
// Check if the page changed // Check if the page changed
cy.get( cy.get(
'.rt-tbody > div:nth-child(1) > div > div:nth-child(2) > div' '.rt-tbody > div:nth-child(1) > div > div:nth-child(3) > div'
).contains('11'); ).contains('11');
cy.get('.-pageJump > input').should('have.value', '2'); cy.get('.-pageJump > input').should('have.value', '2');
cy.get('.-previous > button').click(); cy.get('.-previous > button').click();

View File

@ -318,6 +318,7 @@ const checkOrder = order => {
.find('[role=gridcell]') .find('[role=gridcell]')
.first() .first()
.next() .next()
.next()
.contains(2); .contains(2);
} }
if (index === 2) { if (index === 2) {
@ -325,6 +326,7 @@ const checkOrder = order => {
.find('[role=gridcell]') .find('[role=gridcell]')
.first() .first()
.next() .next()
.next()
.contains(userId); .contains(userId);
} }
}); });
@ -335,6 +337,7 @@ const checkOrder = order => {
.find('[role=gridcell]') .find('[role=gridcell]')
.first() .first()
.next() .next()
.next()
.contains(2); .contains(2);
} }
if (index === 1) { if (index === 1) {
@ -342,6 +345,7 @@ const checkOrder = order => {
.find('[role=gridcell]') .find('[role=gridcell]')
.first() .first()
.next() .next()
.next()
.contains(userId); .contains(userId);
} }
}); });

View File

@ -522,6 +522,10 @@ input {
padding-left: 15px; padding-left: 15px;
} }
.add_padd_left_18 {
padding-left: 18px;
}
.add_mar_top_small { .add_mar_top_small {
margin-top: 10px; margin-top: 10px;
} }

View File

@ -1,4 +1,4 @@
@import "../Common.scss"; @import '../Common.scss';
.container { .container {
padding: 0; padding: 0;
@ -50,7 +50,7 @@
label.radioLabel { label.radioLabel {
padding-top: 0; padding-top: 0;
input[type="radio"] { input[type='radio'] {
margin-top: 10px; margin-top: 10px;
} }
} }
@ -64,7 +64,7 @@
width: auto; width: auto;
tr:nth-child(even) { tr:nth-child(even) {
background: #f4f4f4 background: #f4f4f4;
} }
th { th {
@ -183,3 +183,21 @@ a.expanded {
.tableCellExpanded { .tableCellExpanded {
white-space: pre-wrap; white-space: pre-wrap;
} }
.tableCenterContent {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.bulkDeleteButton {
margin: 10px;
font-size: 16px;
padding: 0 6px;
}
.headerInputCheckbox {
height: 16px;
margin-bottom: 2px;
}

View File

@ -35,7 +35,7 @@
label.radioLabel { label.radioLabel {
padding-top: 0; padding-top: 0;
input[type="radio"] { input[type='radio'] {
margin-top: 10px; margin-top: 10px;
} }
} }
@ -104,7 +104,7 @@ a.expanded {
i:hover { i:hover {
cursor: pointer; cursor: pointer;
color: #B85C27; color: #b85c27;
transition: 0.2s; transition: 0.2s;
} }
} }
@ -127,5 +127,5 @@ a.expanded {
.relEditButtons { .relEditButtons {
display: flex; display: flex;
flex-direction:row; flex-direction: row;
} }

View File

@ -209,3 +209,19 @@ export const getDropComputedFieldQuery = (tableDef, computedFieldName) => {
}, },
}; };
}; };
export const getDeleteQuery = (pkClause, tableName, schemaName) => {
return {
type: 'delete',
args: {
table: {
name: tableName,
schema: schemaName,
},
where: pkClause,
},
};
};
export const getBulkDeleteQuery = (pkClauses, tableName, schemaName) =>
pkClauses.map(pkClause => getDeleteQuery(pkClause, tableName, schemaName));

View File

@ -9,6 +9,7 @@ import {
} from '../../Common/Notification'; } from '../../Common/Notification';
import dataHeaders from '../Common/Headers'; import dataHeaders from '../Common/Headers';
import { getConfirmation } from '../../../Common/utils/jsUtils'; import { getConfirmation } from '../../../Common/utils/jsUtils';
import { getBulkDeleteQuery } from '../../../Common/utils/v1QueryUtils';
/* ****************** View actions *************/ /* ****************** View actions *************/
const V_SET_DEFAULTS = 'ViewTable/V_SET_DEFAULTS'; const V_SET_DEFAULTS = 'ViewTable/V_SET_DEFAULTS';
@ -210,6 +211,47 @@ const deleteItem = pkClause => {
}; };
}; };
const deleteItems = pkClauses => {
return (dispatch, getState) => {
const confirmMessage = 'This will permanently delete rows from this table';
const isOk = getConfirmation(confirmMessage);
if (!isOk) {
return;
}
const state = getState();
const reqBody = {
type: 'bulk',
args: getBulkDeleteQuery(
pkClauses,
state.tables.currentTable,
state.tables.currentSchema
),
};
const options = {
method: 'POST',
body: JSON.stringify(reqBody),
headers: dataHeaders(getState),
credentials: globalCookiePolicy,
};
dispatch(requestAction(Endpoints.query, options)).then(
data => {
const affected = data.reduce((acc, d) => acc + d.affected_rows, 0);
dispatch(vMakeRequest());
dispatch(
showSuccessNotification('Rows deleted!', 'Affected rows: ' + affected)
);
},
err => {
dispatch(
showErrorNotification('Deleting rows failed!', err.error, err)
);
}
);
};
};
const vExpandRel = (path, relname, pk) => { const vExpandRel = (path, relname, pk) => {
return dispatch => { return dispatch => {
// Modify the query (UI will automatically change) // Modify the query (UI will automatically change)
@ -560,6 +602,7 @@ export {
vCollapseRow, vCollapseRow,
V_SET_ACTIVE, V_SET_ACTIVE,
deleteItem, deleteItem,
deleteItems,
UPDATE_TRIGGER_ROW, UPDATE_TRIGGER_ROW,
UPDATE_TRIGGER_FUNCTION, UPDATE_TRIGGER_FUNCTION,
}; };

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import 'react-table/react-table.css'; import 'react-table/react-table.css';
import '../../../Common/TableCommon/ReactTableOverrides.css'; import '../../../Common/TableCommon/ReactTableOverrides.css';
import DragFoldTable from '../../../Common/TableCommon/DragFoldTable'; import DragFoldTable from '../../../Common/TableCommon/DragFoldTable';
@ -11,6 +11,7 @@ import {
vExpandRel, vExpandRel,
vCloseRel, vCloseRel,
V_SET_ACTIVE, V_SET_ACTIVE,
deleteItems,
deleteItem, deleteItem,
vExpandRow, vExpandRow,
vCollapseRow, vCollapseRow,
@ -73,8 +74,12 @@ const ViewRows = ({
location, location,
readOnlyMode, readOnlyMode,
}) => { }) => {
const [selectedRows, setSelectedRows] = useState([]);
const styles = require('../../../Common/TableCommon/Table.scss'); const styles = require('../../../Common/TableCommon/Table.scss');
const NO_PRIMARY_KEY_MSG = 'No primary key to identify row';
// Invoke manual trigger status // Invoke manual trigger status
const invokeTrigger = (trigger, row) => { const invokeTrigger = (trigger, row) => {
updateInvocationRow(row); updateInvocationRow(row);
@ -86,6 +91,14 @@ const ViewRows = ({
updateInvocationFunction(null); updateInvocationFunction(null);
}; };
const handleAllCheckboxChange = e => {
if (e.target.checked) {
setSelectedRows(curRows);
} else {
setSelectedRows([]);
}
};
const checkIfSingleRow = _curRelName => { const checkIfSingleRow = _curRelName => {
let _isSingleRow = false; let _isSingleRow = false;
@ -116,7 +129,7 @@ const ViewRows = ({
); );
}; };
const getGridHeadings = (_columns, _relationships) => { const getGridHeadings = (_columns, _relationships, _disableBulkSelect) => {
const _gridHeadings = []; const _gridHeadings = [];
const getColWidth = (header, contentRows = []) => { const getColWidth = (header, contentRows = []) => {
@ -184,6 +197,26 @@ const ViewRows = ({
width: 152, width: 152,
}); });
_gridHeadings.push({
Header: (
<div className={styles.tableCenterContent}>
<input
className={`${styles.inputCheckbox} ${styles.headerInputCheckbox}`}
checked={
curRows.length > 0 && selectedRows.length === curRows.length
}
disabled={_disableBulkSelect}
title={_disableBulkSelect ? 'No primary key to identify row' : ''}
type="checkbox"
onChange={handleAllCheckboxChange}
/>
</div>
),
accessor: 'tableRowSelectAction',
id: 'tableRowSelectAction',
width: 60,
});
_columns.map(col => { _columns.map(col => {
const columnName = col.column_name; const columnName = col.column_name;
@ -232,7 +265,56 @@ const ViewRows = ({
return _gridHeadings; return _gridHeadings;
}; };
const getGridRows = (_tableSchema, _hasPrimaryKey, _isSingleRow) => { const compareRows = (row1, row2, _tableSchema, _hasPrimaryKey) => {
let same = true;
if (!isView && _hasPrimaryKey) {
_tableSchema.primary_key.columns.map(pk => {
if (row1[pk] !== row2[pk]) {
same = false;
}
});
return same;
}
_tableSchema.columns.map(k => {
if (row1[k.column_name] !== row2[k.column_name]) {
return false;
}
});
return same;
};
const handleCheckboxChange = (row, e, ...rest) => {
if (e.target.checked) {
setSelectedRows(prev => [...prev, row]);
} else {
setSelectedRows(prev =>
prev.filter(prevRow => !compareRows(prevRow, row, ...rest))
);
}
};
const getPKClause = (row, hasPrimaryKey, tableSchema) => {
const pkClause = {};
if (!isView && hasPrimaryKey) {
tableSchema.primary_key.columns.map(pk => {
pkClause[pk] = row[pk];
});
} else {
tableSchema.columns.map(k => {
pkClause[k.column_name] = row[k.column_name];
});
}
return pkClause;
};
const getGridRows = (
_tableSchema,
_hasPrimaryKey,
_isSingleRow,
_disableBulkSelect
) => {
const _gridRows = []; const _gridRows = [];
curRows.forEach((row, rowIndex) => { curRows.forEach((row, rowIndex) => {
@ -241,22 +323,6 @@ const ViewRows = ({
const rowCellIndex = `${curTableName}-${rowIndex}`; const rowCellIndex = `${curTableName}-${rowIndex}`;
const isExpanded = expandedRow === rowCellIndex; const isExpanded = expandedRow === rowCellIndex;
const getPKClause = () => {
const pkClause = {};
if (!isView && _hasPrimaryKey) {
_tableSchema.primary_key.columns.map(pk => {
pkClause[pk] = row[pk];
});
} else {
_tableSchema.columns.map(k => {
pkClause[k.column_name] = row[k.column_name];
});
}
return pkClause;
};
const getActionButtons = () => { const getActionButtons = () => {
let editButton; let editButton;
let cloneButton; let cloneButton;
@ -441,7 +507,7 @@ const ViewRows = ({
const showActionBtns = !readOnlyMode && !_isSingleRow && !isView; const showActionBtns = !readOnlyMode && !_isSingleRow && !isView;
if (showActionBtns) { if (showActionBtns) {
const pkClause = getPKClause(); const pkClause = getPKClause(row, _hasPrimaryKey, _tableSchema);
editButton = getEditButton(pkClause); editButton = getEditButton(pkClause);
deleteButton = getDeleteButton(pkClause); deleteButton = getDeleteButton(pkClause);
@ -467,6 +533,24 @@ const ViewRows = ({
// Insert Edit, Delete, Clone in a cell // Insert Edit, Delete, Clone in a cell
newRow.tableRowActionButtons = getActionButtons(); newRow.tableRowActionButtons = getActionButtons();
// Check for bulk actions
newRow.tableRowSelectAction = (
<div className={styles.tableCenterContent}>
<input
className={styles.inputCheckbox}
type="checkbox"
disabled={_disableBulkSelect}
title={_disableBulkSelect ? NO_PRIMARY_KEY_MSG : ''}
checked={selectedRows.some(selectedRow =>
compareRows(selectedRow, row, _tableSchema, _hasPrimaryKey)
)}
onChange={e =>
handleCheckboxChange(row, e, _tableSchema, _hasPrimaryKey)
}
/>
</div>
);
// Insert column cells // Insert column cells
_tableSchema.columns.forEach(col => { _tableSchema.columns.forEach(col => {
const columnName = col.column_name; const columnName = col.column_name;
@ -556,7 +640,7 @@ const ViewRows = ({
cellValue = <i>NULL</i>; cellValue = <i>NULL</i>;
} else { } else {
// can be expanded // can be expanded
const pkClause = getPKClause(); const pkClause = getPKClause(row, _hasPrimaryKey, _tableSchema);
const handleViewClick = e => { const handleViewClick = e => {
e.preventDefault(); e.preventDefault();
@ -604,9 +688,20 @@ const ViewRows = ({
const isSingleRow = checkIfSingleRow(curRelName); const isSingleRow = checkIfSingleRow(curRelName);
const _gridHeadings = getGridHeadings(tableColumnsSorted, tableRelationships); const disableBulkSelect = !hasPrimaryKey;
const _gridRows = getGridRows(tableSchema, hasPrimaryKey, isSingleRow); const _gridHeadings = getGridHeadings(
tableColumnsSorted,
tableRelationships,
disableBulkSelect
);
const _gridRows = getGridRows(
tableSchema,
hasPrimaryKey,
isSingleRow,
disableBulkSelect
);
const getFilterQuery = () => { const getFilterQuery = () => {
let _filterQuery = null; let _filterQuery = null;
@ -647,6 +742,36 @@ const ViewRows = ({
return _filterQuery; return _filterQuery;
}; };
const getSelectedRowsSection = () => {
const handleDeleteItems = () => {
const pkClauses = selectedRows.map(row =>
getPKClause(row, hasPrimaryKey, tableSchema)
);
dispatch(deleteItems(pkClauses));
setSelectedRows([]);
};
let selectedRowsSection = null;
if (selectedRows.length > 0) {
selectedRowsSection = (
<div className={`${styles.display_flex} ${styles.add_padd_left_18}`}>
<b className={styles.padd_small_right}>Selected:</b>
{selectedRows.length}
<button
className={`${styles.add_mar_right_small} btn btn-xs btn-default ${styles.bulkDeleteButton}`}
title="Delete selected rows"
onClick={handleDeleteItems}
>
<i className="fa fa-trash" />
</button>
</div>
);
}
return selectedRowsSection;
};
// If query object has expanded columns // If query object has expanded columns
const getChildComponent = () => { const getChildComponent = () => {
let _childComponent = null; let _childComponent = null;
@ -871,6 +996,7 @@ const ViewRows = ({
<div className={isVisible ? '' : 'hide '}> <div className={isVisible ? '' : 'hide '}>
{getFilterQuery()} {getFilterQuery()}
<div className={`row ${styles.add_mar_top}`}> <div className={`row ${styles.add_mar_top}`}>
{getSelectedRowsSection()}
<div className="col-xs-12"> <div className="col-xs-12">
<div className={styles.tableContainer}>{renderTableBody()}</div> <div className={styles.tableContainer}>{renderTableBody()}</div>
<br /> <br />