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
- 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)
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

View File

@ -74,6 +74,7 @@ const checkOrder = order => {
.find('[role=gridcell]')
.first()
.next()
.next()
.contains(index);
}
});
@ -84,6 +85,7 @@ const checkOrder = order => {
.find('[role=gridcell]')
.first()
.next()
.next()
.contains(22 - index);
}
});
@ -231,7 +233,7 @@ export const checkPagination = () => {
cy.wait(3000);
// Check if the page changed
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');
cy.get('.-pageJump > input').should('have.value', '2');
cy.get('.-previous > button').click();

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@import "../Common.scss";
@import '../Common.scss';
.container {
padding: 0;
@ -50,7 +50,7 @@
label.radioLabel {
padding-top: 0;
input[type="radio"] {
input[type='radio'] {
margin-top: 10px;
}
}
@ -64,7 +64,7 @@
width: auto;
tr:nth-child(even) {
background: #f4f4f4
background: #f4f4f4;
}
th {
@ -183,3 +183,21 @@ a.expanded {
.tableCellExpanded {
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 {
padding-top: 0;
input[type="radio"] {
input[type='radio'] {
margin-top: 10px;
}
}
@ -104,7 +104,7 @@ a.expanded {
i:hover {
cursor: pointer;
color: #B85C27;
color: #b85c27;
transition: 0.2s;
}
}
@ -127,5 +127,5 @@ a.expanded {
.relEditButtons {
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';
import dataHeaders from '../Common/Headers';
import { getConfirmation } from '../../../Common/utils/jsUtils';
import { getBulkDeleteQuery } from '../../../Common/utils/v1QueryUtils';
/* ****************** View actions *************/
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) => {
return dispatch => {
// Modify the query (UI will automatically change)
@ -560,6 +602,7 @@ export {
vCollapseRow,
V_SET_ACTIVE,
deleteItem,
deleteItems,
UPDATE_TRIGGER_ROW,
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 '../../../Common/TableCommon/ReactTableOverrides.css';
import DragFoldTable from '../../../Common/TableCommon/DragFoldTable';
@ -11,6 +11,7 @@ import {
vExpandRel,
vCloseRel,
V_SET_ACTIVE,
deleteItems,
deleteItem,
vExpandRow,
vCollapseRow,
@ -73,8 +74,12 @@ const ViewRows = ({
location,
readOnlyMode,
}) => {
const [selectedRows, setSelectedRows] = useState([]);
const styles = require('../../../Common/TableCommon/Table.scss');
const NO_PRIMARY_KEY_MSG = 'No primary key to identify row';
// Invoke manual trigger status
const invokeTrigger = (trigger, row) => {
updateInvocationRow(row);
@ -86,6 +91,14 @@ const ViewRows = ({
updateInvocationFunction(null);
};
const handleAllCheckboxChange = e => {
if (e.target.checked) {
setSelectedRows(curRows);
} else {
setSelectedRows([]);
}
};
const checkIfSingleRow = _curRelName => {
let _isSingleRow = false;
@ -116,7 +129,7 @@ const ViewRows = ({
);
};
const getGridHeadings = (_columns, _relationships) => {
const getGridHeadings = (_columns, _relationships, _disableBulkSelect) => {
const _gridHeadings = [];
const getColWidth = (header, contentRows = []) => {
@ -184,6 +197,26 @@ const ViewRows = ({
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 => {
const columnName = col.column_name;
@ -232,7 +265,56 @@ const ViewRows = ({
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 = [];
curRows.forEach((row, rowIndex) => {
@ -241,22 +323,6 @@ const ViewRows = ({
const rowCellIndex = `${curTableName}-${rowIndex}`;
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 = () => {
let editButton;
let cloneButton;
@ -441,7 +507,7 @@ const ViewRows = ({
const showActionBtns = !readOnlyMode && !_isSingleRow && !isView;
if (showActionBtns) {
const pkClause = getPKClause();
const pkClause = getPKClause(row, _hasPrimaryKey, _tableSchema);
editButton = getEditButton(pkClause);
deleteButton = getDeleteButton(pkClause);
@ -467,6 +533,24 @@ const ViewRows = ({
// Insert Edit, Delete, Clone in a cell
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
_tableSchema.columns.forEach(col => {
const columnName = col.column_name;
@ -556,7 +640,7 @@ const ViewRows = ({
cellValue = <i>NULL</i>;
} else {
// can be expanded
const pkClause = getPKClause();
const pkClause = getPKClause(row, _hasPrimaryKey, _tableSchema);
const handleViewClick = e => {
e.preventDefault();
@ -604,9 +688,20 @@ const ViewRows = ({
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 = () => {
let _filterQuery = null;
@ -647,6 +742,36 @@ const ViewRows = ({
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
const getChildComponent = () => {
let _childComponent = null;
@ -871,6 +996,7 @@ const ViewRows = ({
<div className={isVisible ? '' : 'hide '}>
{getFilterQuery()}
<div className={`row ${styles.add_mar_top}`}>
{getSelectedRowsSection()}
<div className="col-xs-12">
<div className={styles.tableContainer}>{renderTableBody()}</div>
<br />