mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
This commit is contained in:
parent
71240f310d
commit
36c92db991
@ -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
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -522,6 +522,10 @@ input {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.add_padd_left_18 {
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.add_mar_top_small {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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 />
|
||||
|
Loading…
Reference in New Issue
Block a user