mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-05-01 02:43:59 +00:00
autofill
This commit is contained in:
@@ -115,6 +115,54 @@ export function setChangeSetValue(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export function batchUpdateChangeSet(
|
||||||
|
// changeSet: ChangeSet,
|
||||||
|
// rowDefinitions: ChangeSetRowDefinition[],
|
||||||
|
// dataRows: []
|
||||||
|
// ): ChangeSet {
|
||||||
|
// const res = {
|
||||||
|
// updates: [...changeSet.updates],
|
||||||
|
// deletes: [...changeSet.deletes],
|
||||||
|
// inserts: [...changeSet.inserts],
|
||||||
|
// };
|
||||||
|
// const rowItems: ChangeSetItem[] = rowDefinitions.map(definition => {
|
||||||
|
// let [field, item] = findExistingChangeSetItem(res, definition);
|
||||||
|
// let createUpdate = false;
|
||||||
|
// if (field == 'deletes') {
|
||||||
|
// res.deletes = res.deletes.filter(x => x != item);
|
||||||
|
// createUpdate = true;
|
||||||
|
// }
|
||||||
|
// if (field == 'updates' && item == null) {
|
||||||
|
// item = {
|
||||||
|
// ...definition,
|
||||||
|
// fields: {},
|
||||||
|
// };
|
||||||
|
// res.updates.push(item);
|
||||||
|
// }
|
||||||
|
// return item;
|
||||||
|
// });
|
||||||
|
// for (const tuple in _.zip(rowItems, dataRows)) {
|
||||||
|
// const [definition, dataRow] = tuple;
|
||||||
|
// for
|
||||||
|
// }
|
||||||
|
// return res;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function batchUpdateChangeSet(
|
||||||
|
changeSet: ChangeSet,
|
||||||
|
rowDefinitions: ChangeSetRowDefinition[],
|
||||||
|
dataRows: []
|
||||||
|
): ChangeSet {
|
||||||
|
// console.log('batchUpdateChangeSet', changeSet, rowDefinitions, dataRows);
|
||||||
|
for (const tuple of _.zip(rowDefinitions, dataRows)) {
|
||||||
|
const [definition, dataRow] = tuple;
|
||||||
|
for (const key of _.keys(dataRow)) {
|
||||||
|
changeSet = setChangeSetValue(changeSet, { ...definition, columnName: key, uniqueName: key }, dataRow[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changeSet;
|
||||||
|
}
|
||||||
|
|
||||||
function extractFields(item: ChangeSetItem): UpdateField[] {
|
function extractFields(item: ChangeSetItem): UpdateField[] {
|
||||||
return _.keys(item.fields).map(targetColumn => ({
|
return _.keys(item.fields).map(targetColumn => ({
|
||||||
targetColumn,
|
targetColumn,
|
||||||
@@ -194,9 +242,9 @@ export function changeSetToSql(changeSet: ChangeSet): Command[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function revertChangeSetRowChanges(changeSet: ChangeSet, definition: ChangeSetRowDefinition): ChangeSet {
|
export function revertChangeSetRowChanges(changeSet: ChangeSet, definition: ChangeSetRowDefinition): ChangeSet {
|
||||||
console.log('definition', definition)
|
console.log('definition', definition);
|
||||||
const [field, item] = findExistingChangeSetItem(changeSet, definition);
|
const [field, item] = findExistingChangeSetItem(changeSet, definition);
|
||||||
console.log('field, item', field, item)
|
console.log('field, item', field, item);
|
||||||
if (item)
|
if (item)
|
||||||
return {
|
return {
|
||||||
...changeSet,
|
...changeSet,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
getChangeSetInsertedRows,
|
getChangeSetInsertedRows,
|
||||||
changeSetInsertNewRow,
|
changeSetInsertNewRow,
|
||||||
deleteChangeSetRows,
|
deleteChangeSetRows,
|
||||||
|
batchUpdateChangeSet,
|
||||||
} from '@dbgate/datalib';
|
} from '@dbgate/datalib';
|
||||||
import { scriptToSql } from '@dbgate/sqltree';
|
import { scriptToSql } from '@dbgate/sqltree';
|
||||||
import { sleep } from '../utility/common';
|
import { sleep } from '../utility/common';
|
||||||
@@ -111,6 +112,8 @@ export default function DataGridCore(props) {
|
|||||||
const [selectedCells, setSelectedCells] = React.useState(emptyCellArray);
|
const [selectedCells, setSelectedCells] = React.useState(emptyCellArray);
|
||||||
const [dragStartCell, setDragStartCell] = React.useState(nullCell);
|
const [dragStartCell, setDragStartCell] = React.useState(nullCell);
|
||||||
const [shiftDragStartCell, setShiftDragStartCell] = React.useState(nullCell);
|
const [shiftDragStartCell, setShiftDragStartCell] = React.useState(nullCell);
|
||||||
|
const [autofillDragStartCell, setAutofillDragStartCell] = React.useState(nullCell);
|
||||||
|
const [autofillSelectedCells, setAutofillSelectedCells] = React.useState(emptyCellArray);
|
||||||
|
|
||||||
// const [inplaceEditorCell, setInplaceEditorCell] = React.useState(nullCell);
|
// const [inplaceEditorCell, setInplaceEditorCell] = React.useState(nullCell);
|
||||||
// const [inplaceEditorInitText, setInplaceEditorInitText] = React.useState('');
|
// const [inplaceEditorInitText, setInplaceEditorInitText] = React.useState('');
|
||||||
@@ -121,6 +124,14 @@ export default function DataGridCore(props) {
|
|||||||
|
|
||||||
changeSetRef.current = changeSet;
|
changeSetRef.current = changeSet;
|
||||||
|
|
||||||
|
const autofillMarkerCell = React.useMemo(
|
||||||
|
() =>
|
||||||
|
selectedCells && selectedCells.length > 0 && _.uniq(selectedCells.map(x => x[0])).length == 1
|
||||||
|
? [_.max(selectedCells.map(x => x[0])), _.max(selectedCells.map(x => x[1]))]
|
||||||
|
: null,
|
||||||
|
[selectedCells]
|
||||||
|
);
|
||||||
|
|
||||||
const loadNextData = async () => {
|
const loadNextData = async () => {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
setLoadProps({
|
setLoadProps({
|
||||||
@@ -280,21 +291,6 @@ export default function DataGridCore(props) {
|
|||||||
[columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns]
|
[columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cellIsSelected = React.useCallback(
|
|
||||||
(row, col) => {
|
|
||||||
const [currentRow, currentCol] = currentCell;
|
|
||||||
if (row == currentRow && col == currentCol) return true;
|
|
||||||
for (const [selectedRow, selectedCol] of selectedCells) {
|
|
||||||
if (row == selectedRow && col == selectedCol) return true;
|
|
||||||
if (selectedRow == 'header' && col == selectedCol) return true;
|
|
||||||
if (row == selectedRow && selectedCol == 'header') return true;
|
|
||||||
if (selectedRow == 'header' && selectedCol == 'header') return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
[currentCell, selectedCells]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!loadedRows || !columns) return null;
|
if (!loadedRows || !columns) return null;
|
||||||
const insertedRows = getChangeSetInsertedRows(changeSet, display.baseTable);
|
const insertedRows = getChangeSetInsertedRows(changeSet, display.baseTable);
|
||||||
const rowCountNewIncluded = loadedRows.length + insertedRows.length;
|
const rowCountNewIncluded = loadedRows.length + insertedRows.length;
|
||||||
@@ -310,6 +306,10 @@ export default function DataGridCore(props) {
|
|||||||
function handleGridMouseDown(event) {
|
function handleGridMouseDown(event) {
|
||||||
event.target.closest('table').focus();
|
event.target.closest('table').focus();
|
||||||
const cell = cellFromEvent(event);
|
const cell = cellFromEvent(event);
|
||||||
|
const autofill = event.target.closest('div.autofillHandleMarker');
|
||||||
|
if (autofill) {
|
||||||
|
setAutofillDragStartCell(cell);
|
||||||
|
} else {
|
||||||
setCurrentCell(cell);
|
setCurrentCell(cell);
|
||||||
setSelectedCells(getCellRange(cell, cell));
|
setSelectedCells(getCellRange(cell, cell));
|
||||||
setDragStartCell(cell);
|
setDragStartCell(cell);
|
||||||
@@ -322,9 +322,17 @@ export default function DataGridCore(props) {
|
|||||||
dispatchInsplaceEditor({ type: 'close' });
|
dispatchInsplaceEditor({ type: 'close' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleGridMouseMove(event) {
|
function handleGridMouseMove(event) {
|
||||||
if (dragStartCell) {
|
if (autofillDragStartCell) {
|
||||||
|
const cell = cellFromEvent(event);
|
||||||
|
if (isRegularCell(cell) && (cell[0] == autofillDragStartCell[0] || cell[1] == autofillDragStartCell[1])) {
|
||||||
|
const autoFillStart = [selectedCells[0][0], _.min(selectedCells.map(x => x[1]))];
|
||||||
|
// @ts-ignore
|
||||||
|
setAutofillSelectedCells(getCellRange(autoFillStart, cell));
|
||||||
|
}
|
||||||
|
} else if (dragStartCell) {
|
||||||
const cell = cellFromEvent(event);
|
const cell = cellFromEvent(event);
|
||||||
setCurrentCell(cell);
|
setCurrentCell(cell);
|
||||||
setSelectedCells(getCellRange(dragStartCell, cell));
|
setSelectedCells(getCellRange(dragStartCell, cell));
|
||||||
@@ -338,24 +346,44 @@ export default function DataGridCore(props) {
|
|||||||
setSelectedCells(getCellRange(dragStartCell, cell));
|
setSelectedCells(getCellRange(dragStartCell, cell));
|
||||||
setDragStartCell(null);
|
setDragStartCell(null);
|
||||||
}
|
}
|
||||||
|
if (autofillDragStartCell) {
|
||||||
|
const currentRowNumber = currentCell[0];
|
||||||
|
if (_.isNumber(currentRowNumber)) {
|
||||||
|
const rowIndexes = _.uniq((autofillSelectedCells || []).map(x => x[0])).filter(x => x != currentRowNumber);
|
||||||
|
// @ts-ignore
|
||||||
|
const colNames = selectedCells.map(cell => columns[columnSizes.realToModel(cell[1])].uniqueName);
|
||||||
|
const changeObject = _.pick(loadedAndInsertedRows[currentRowNumber], colNames);
|
||||||
|
setChangeSet(
|
||||||
|
batchUpdateChangeSet(
|
||||||
|
changeSet,
|
||||||
|
getRowDefinitions(rowIndexes),
|
||||||
|
// @ts-ignore
|
||||||
|
rowIndexes.map(() => changeObject)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedRowDefinitions() {
|
setAutofillDragStartCell(null);
|
||||||
|
setAutofillSelectedCells([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowDefinitions(rowIndexes) {
|
||||||
const res = [];
|
const res = [];
|
||||||
if (!loadedAndInsertedRows) return res;
|
if (!loadedAndInsertedRows) return res;
|
||||||
const rowIndexes = _.uniq((selectedCells || []).map(x => x[0]));
|
|
||||||
for (const index of rowIndexes) {
|
for (const index of rowIndexes) {
|
||||||
if (loadedAndInsertedRows[index] && _.isNumber(index)) {
|
if (loadedAndInsertedRows[index] && _.isNumber(index)) {
|
||||||
const insertedRowIndex =
|
const insertedRowIndex = index >= loadedRows.length ? index - loadedRows.length : null;
|
||||||
firstVisibleRowScrollIndex + index >= loadedRows.length
|
|
||||||
? firstVisibleRowScrollIndex + index - loadedRows.length
|
|
||||||
: null;
|
|
||||||
res.push(display.getChangeSetRow(loadedAndInsertedRows[index], insertedRowIndex));
|
res.push(display.getChangeSetRow(loadedAndInsertedRows[index], insertedRowIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSelectedRowDefinitions() {
|
||||||
|
return getRowDefinitions(_.uniq((selectedCells || []).map(x => x[0])));
|
||||||
|
}
|
||||||
|
|
||||||
function revertRowChanges() {
|
function revertRowChanges() {
|
||||||
const updatedChangeSet = getSelectedRowDefinitions().reduce(
|
const updatedChangeSet = getSelectedRowDefinitions().reduce(
|
||||||
(chs, row) => revertChangeSetRowChanges(chs, row),
|
(chs, row) => revertChangeSetRowChanges(chs, row),
|
||||||
@@ -365,10 +393,7 @@ export default function DataGridCore(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteCurrentRow() {
|
function deleteCurrentRow() {
|
||||||
const updatedChangeSet = getSelectedRowDefinitions().reduce(
|
const updatedChangeSet = getSelectedRowDefinitions().reduce((chs, row) => deleteChangeSetRows(chs, row), changeSet);
|
||||||
(chs, row) => deleteChangeSetRows(chs, row),
|
|
||||||
changeSet
|
|
||||||
);
|
|
||||||
setChangeSet(updatedChangeSet);
|
setChangeSet(updatedChangeSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,12 +724,14 @@ export default function DataGridCore(props) {
|
|||||||
visibleRealColumns={visibleRealColumns}
|
visibleRealColumns={visibleRealColumns}
|
||||||
inplaceEditorState={inplaceEditorState}
|
inplaceEditorState={inplaceEditorState}
|
||||||
dispatchInsplaceEditor={dispatchInsplaceEditor}
|
dispatchInsplaceEditor={dispatchInsplaceEditor}
|
||||||
cellIsSelected={cellIsSelected}
|
autofillSelectedCells={autofillSelectedCells}
|
||||||
|
selectedCells={selectedCells}
|
||||||
insertedRowIndex={
|
insertedRowIndex={
|
||||||
firstVisibleRowScrollIndex + index >= loadedRows.length
|
firstVisibleRowScrollIndex + index >= loadedRows.length
|
||||||
? firstVisibleRowScrollIndex + index - loadedRows.length
|
? firstVisibleRowScrollIndex + index - loadedRows.length
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
autofillMarkerCell={autofillMarkerCell}
|
||||||
changeSet={changeSet}
|
changeSet={changeSet}
|
||||||
setChangeSet={setChangeSet}
|
setChangeSet={setChangeSet}
|
||||||
display={display}
|
display={display}
|
||||||
|
|||||||
@@ -22,22 +22,34 @@ const TableBodyCell = styled.td`
|
|||||||
// border-collapse: collapse;
|
// border-collapse: collapse;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
${props =>
|
${props =>
|
||||||
props.isSelected &&
|
props.isSelected &&
|
||||||
|
!props.isAutofillSelected &&
|
||||||
`
|
`
|
||||||
background: initial;
|
background: initial;
|
||||||
background-color: deepskyblue;
|
background-color: deepskyblue;
|
||||||
color: white;`}
|
color: white;`}
|
||||||
|
|
||||||
|
${props =>
|
||||||
|
props.isAutofillSelected &&
|
||||||
|
`
|
||||||
|
background: initial;
|
||||||
|
background-color: magenta;
|
||||||
|
color: white;`}
|
||||||
|
|
||||||
${props =>
|
${props =>
|
||||||
props.isModifiedRow &&
|
props.isModifiedRow &&
|
||||||
!props.isInsertedRow &&
|
!props.isInsertedRow &&
|
||||||
!props.isSelected &&
|
!props.isSelected &&
|
||||||
|
!props.isAutofillSelected &&
|
||||||
!props.isModifiedCell &&
|
!props.isModifiedCell &&
|
||||||
`
|
`
|
||||||
background-color: #FFFFDB;`}
|
background-color: #FFFFDB;`}
|
||||||
${props =>
|
${props =>
|
||||||
!props.isSelected &&
|
!props.isSelected &&
|
||||||
|
!props.isAutofillSelected &&
|
||||||
!props.isInsertedRow &&
|
!props.isInsertedRow &&
|
||||||
props.isModifiedCell &&
|
props.isModifiedCell &&
|
||||||
`
|
`
|
||||||
@@ -45,15 +57,22 @@ const TableBodyCell = styled.td`
|
|||||||
|
|
||||||
${props =>
|
${props =>
|
||||||
!props.isSelected &&
|
!props.isSelected &&
|
||||||
|
!props.isAutofillSelected &&
|
||||||
props.isInsertedRow &&
|
props.isInsertedRow &&
|
||||||
`
|
`
|
||||||
background-color: #DBFFDB;`}
|
background-color: #DBFFDB;`}
|
||||||
|
|
||||||
${props =>
|
${props =>
|
||||||
!props.isSelected &&
|
!props.isSelected &&
|
||||||
|
!props.isAutofillSelected &&
|
||||||
props.isDeletedRow &&
|
props.isDeletedRow &&
|
||||||
`
|
`
|
||||||
background-color: #FFDBFF;
|
background-color: #FFDBFF;
|
||||||
|
`}
|
||||||
|
|
||||||
|
${props =>
|
||||||
|
props.isDeletedRow &&
|
||||||
|
`
|
||||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
|
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
|
||||||
// from http://www.patternify.com/
|
// from http://www.patternify.com/
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
@@ -89,24 +108,47 @@ const TableHeaderCell = styled.td`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const AutoFillPoint = styled.div`
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #1a73e8;
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
overflow: visible;
|
||||||
|
cursor: crosshair;
|
||||||
|
`;
|
||||||
|
|
||||||
function CellFormattedValue({ value }) {
|
function CellFormattedValue({ value }) {
|
||||||
if (value == null) return <NullSpan>(NULL)</NullSpan>;
|
if (value == null) return <NullSpan>(NULL)</NullSpan>;
|
||||||
if (_.isDate(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
if (_.isDate(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cellIsSelected(row, col, selectedCells) {
|
||||||
|
for (const [selectedRow, selectedCol] of selectedCells) {
|
||||||
|
if (row == selectedRow && col == selectedCol) return true;
|
||||||
|
if (selectedRow == 'header' && col == selectedCol) return true;
|
||||||
|
if (row == selectedRow && selectedCol == 'header') return true;
|
||||||
|
if (selectedRow == 'header' && selectedCol == 'header') return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export default function DataGridRow({
|
export default function DataGridRow({
|
||||||
rowHeight,
|
rowHeight,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
visibleRealColumns,
|
visibleRealColumns,
|
||||||
inplaceEditorState,
|
inplaceEditorState,
|
||||||
dispatchInsplaceEditor,
|
dispatchInsplaceEditor,
|
||||||
cellIsSelected,
|
|
||||||
row,
|
row,
|
||||||
display,
|
display,
|
||||||
changeSet,
|
changeSet,
|
||||||
setChangeSet,
|
setChangeSet,
|
||||||
insertedRowIndex,
|
insertedRowIndex,
|
||||||
|
autofillMarkerCell,
|
||||||
|
selectedCells,
|
||||||
|
autofillSelectedCells,
|
||||||
}) {
|
}) {
|
||||||
// console.log('RENDER ROW', rowIndex);
|
// console.log('RENDER ROW', rowIndex);
|
||||||
const rowDefinition = display.getChangeSetRow(row, insertedRowIndex);
|
const rowDefinition = display.getChangeSetRow(row, insertedRowIndex);
|
||||||
@@ -120,6 +162,7 @@ export default function DataGridRow({
|
|||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map(col => col.uniqueName);
|
.map(col => col.uniqueName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableBodyRow style={{ height: `${rowHeight}px` }}>
|
<TableBodyRow style={{ height: `${rowHeight}px` }}>
|
||||||
<TableHeaderCell data-row={rowIndex} data-col="header">
|
<TableHeaderCell data-row={rowIndex} data-col="header">
|
||||||
@@ -135,8 +178,8 @@ export default function DataGridRow({
|
|||||||
}}
|
}}
|
||||||
data-row={rowIndex}
|
data-row={rowIndex}
|
||||||
data-col={col.colIndex}
|
data-col={col.colIndex}
|
||||||
// @ts-ignore
|
isSelected={cellIsSelected(rowIndex, col.colIndex, selectedCells)}
|
||||||
isSelected={cellIsSelected(rowIndex, col.colIndex)}
|
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
|
||||||
isModifiedRow={!!matchedChangeSetItem}
|
isModifiedRow={!!matchedChangeSetItem}
|
||||||
isModifiedCell={
|
isModifiedCell={
|
||||||
matchedChangeSetItem && matchedField == 'updates' && col.uniqueName in matchedChangeSetItem.fields
|
matchedChangeSetItem && matchedField == 'updates' && col.uniqueName in matchedChangeSetItem.fields
|
||||||
@@ -163,6 +206,9 @@ export default function DataGridRow({
|
|||||||
{hintFieldsAllowed.includes(col.uniqueName) && <HintSpan>{row[col.hintColumnName]}</HintSpan>}
|
{hintFieldsAllowed.includes(col.uniqueName) && <HintSpan>{row[col.hintColumnName]}</HintSpan>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{autofillMarkerCell && autofillMarkerCell[1] == col.colIndex && autofillMarkerCell[0] == rowIndex && (
|
||||||
|
<AutoFillPoint className="autofillHandleMarker"></AutoFillPoint>
|
||||||
|
)}
|
||||||
</TableBodyCell>
|
</TableBodyCell>
|
||||||
))}
|
))}
|
||||||
</TableBodyRow>
|
</TableBodyRow>
|
||||||
|
|||||||
Reference in New Issue
Block a user